diff options
| author | Carson Fleming <cflems@cflems.net> | 2017-03-02 22:49:24 -0500 |
|---|---|---|
| committer | Carson Fleming <cflems@cflems.net> | 2017-03-02 22:49:24 -0500 |
| commit | b76e2ff898b23745d4c9aaee49eeb7d88f2896ab (patch) | |
| tree | 9b794be8db310a575d70165d9ebde0a183b61b01 /inc/mailgun/clue/stream-filter | |
| parent | bfcc9f7a7656a2db0c905b3c13114664f00f6c37 (diff) | |
| download | bulletin-b76e2ff898b23745d4c9aaee49eeb7d88f2896ab.tar.gz | |
Updated mailgun plugin
Diffstat (limited to 'inc/mailgun/clue/stream-filter')
| -rw-r--r-- | inc/mailgun/clue/stream-filter/.gitignore | 2 | ||||
| -rw-r--r-- | inc/mailgun/clue/stream-filter/.travis.yml | 10 | ||||
| -rw-r--r-- | inc/mailgun/clue/stream-filter/CHANGELOG.md | 30 | ||||
| -rw-r--r-- | inc/mailgun/clue/stream-filter/LICENSE | 21 | ||||
| -rw-r--r-- | inc/mailgun/clue/stream-filter/README.md | 252 | ||||
| -rw-r--r-- | inc/mailgun/clue/stream-filter/composer.json | 20 | ||||
| -rw-r--r-- | inc/mailgun/clue/stream-filter/phpunit.xml.dist | 19 | ||||
| -rw-r--r-- | inc/mailgun/clue/stream-filter/src/CallbackFilter.php | 120 | ||||
| -rw-r--r-- | inc/mailgun/clue/stream-filter/src/functions.php | 133 | ||||
| -rw-r--r-- | inc/mailgun/clue/stream-filter/tests/FilterTest.php | 386 | ||||
| -rw-r--r-- | inc/mailgun/clue/stream-filter/tests/FunTest.php | 34 | ||||
| -rw-r--r-- | inc/mailgun/clue/stream-filter/tests/FunZlibTest.php | 79 |
12 files changed, 1106 insertions, 0 deletions
diff --git a/inc/mailgun/clue/stream-filter/.gitignore b/inc/mailgun/clue/stream-filter/.gitignore new file mode 100644 index 0000000..de4a392 --- /dev/null +++ b/inc/mailgun/clue/stream-filter/.gitignore @@ -0,0 +1,2 @@ +/vendor +/composer.lock diff --git a/inc/mailgun/clue/stream-filter/.travis.yml b/inc/mailgun/clue/stream-filter/.travis.yml new file mode 100644 index 0000000..061b6ee --- /dev/null +++ b/inc/mailgun/clue/stream-filter/.travis.yml @@ -0,0 +1,10 @@ +language: php +php: + - 5.3 + - 5.6 + - 7 + - hhvm +install: + - composer install --prefer-source --no-interaction +script: + - phpunit --coverage-text diff --git a/inc/mailgun/clue/stream-filter/CHANGELOG.md b/inc/mailgun/clue/stream-filter/CHANGELOG.md new file mode 100644 index 0000000..a7ebf75 --- /dev/null +++ b/inc/mailgun/clue/stream-filter/CHANGELOG.md @@ -0,0 +1,30 @@ +# Changelog + +## 1.3.0 (2015-11-08) + +* Feature: Support accessing built-in filters as callbacks + (#5 by @clue) + + ```php +$fun = Filter\fun('zlib.deflate'); + +$ret = $fun('hello') . $fun('world') . $fun(); +assert('helloworld' === gzinflate($ret)); +``` + +## 1.2.0 (2015-10-23) + +* Feature: Invoke close event when closing filter (flush buffer) + (#9 by @clue) + +## 1.1.0 (2015-10-22) + +* Feature: Abort filter operation when catching an Exception + (#10 by @clue) + +* Feature: Additional safeguards to prevent filter state corruption + (#7 by @clue) + +## 1.0.0 (2015-10-18) + +* First tagged release diff --git a/inc/mailgun/clue/stream-filter/LICENSE b/inc/mailgun/clue/stream-filter/LICENSE new file mode 100644 index 0000000..dc09d1e --- /dev/null +++ b/inc/mailgun/clue/stream-filter/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015 Christian Lück + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/inc/mailgun/clue/stream-filter/README.md b/inc/mailgun/clue/stream-filter/README.md new file mode 100644 index 0000000..5881145 --- /dev/null +++ b/inc/mailgun/clue/stream-filter/README.md @@ -0,0 +1,252 @@ +# clue/stream-filter [](https://travis-ci.org/clue/php-stream-filter) + +A simple and modern approach to stream filtering in PHP + +**Table of contents** + +* [Why?](#why) +* [Usage](#usage) + * [append()](#append) + * [prepend()](#prepend) + * [fun()](#fun) + * [remove()](#remove) +* [Install](#install) +* [License](#license) + +## Why? + +PHP's stream filtering system is great! + +It offers very powerful stream filtering options and comes with a useful set of built-in filters. +These filters can be used to easily and efficiently perform various transformations on-the-fly, such as: + +* read from a gzip'ed input file, +* transcode from ISO-8859-1 (Latin1) to UTF-8, +* write to a bzip output file +* and much more. + +But let's face it: +Its API is [*difficult to work with*](http://php.net/manual/en/php-user-filter.filter.php) +and its documentation is [*subpar*](http://stackoverflow.com/questions/27103269/what-is-a-bucket-brigade). +This combined means its powerful features are often neglected. + +This project aims to make these features more accessible to a broader audience. +* **Lightweight, SOLID design** - + Provides a thin abstraction that is [*just good enough*](http://en.wikipedia.org/wiki/Principle_of_good_enough) + and does not get in your way. + Custom filters require trivial effort. +* **Good test coverage** - + Comes with an automated tests suite and is regularly tested in the *real world* + +## Usage + +This lightweight library consists only of a few simple functions. +All functions reside under the `Clue\StreamFilter` namespace. + +The below examples assume you use an import statement similar to this: + +```php +use Clue\StreamFilter as Filter; + +Filter\append(…); +``` + +Alternatively, you can also refer to them with their fully-qualified name: + +```php +\Clue\StreamFilter\append(…); +``` + +### append() + +The `append($stream, $callback, $read_write = STREAM_FILTER_ALL)` function can be used to +append a filter callback to the given stream. + +Each stream can have a list of filters attached. +This function appends a filter to the end of this list. + +This function returns a filter resource which can be passed to [`remove()`](#remove). +If the given filter can not be added, it throws an `Exception`. + +The `$stream` can be any valid stream resource, such as: + +```php +$stream = fopen('demo.txt', 'w+'); +``` + +The `$callback` should be a valid callable function which accepts an individual chunk of data +and should return the updated chunk: + +```php +$filter = Filter\append($stream, function ($chunk) { + // will be called each time you read or write a $chunk to/from the stream + return $chunk; +}); +``` + +As such, you can also use native PHP functions or any other `callable`: + +```php +Filter\append($stream, 'strtoupper'); + +// will write "HELLO" to the underlying stream +fwrite($stream, 'hello'); +``` + +If the `$callback` accepts invocation without parameters, then this signature +will be invoked once ending (flushing) the filter: + +```php +Filter\append($stream, function ($chunk = null) { + if ($chunk === null) { + // will be called once ending the filter + return 'end'; + } + // will be called each time you read or write a $chunk to/from the stream + return $chunk; +}); + +fclose($stream); +``` + +> Note: Legacy PHP versions (PHP < 5.4) do not support passing additional data +from the end signal handler if the stream is being closed. + +If your callback throws an `Exception`, then the filter process will be aborted. +In order to play nice with PHP's stream handling, the `Exception` will be +transformed to a PHP warning instead: + +```php +Filter\append($stream, function ($chunk) { + throw new \RuntimeException('Unexpected chunk'); +}); + +// raises an E_USER_WARNING with "Error invoking filter: Unexpected chunk" +fwrite($stream, 'hello'); +``` + +The optional `$read_write` parameter can be used to only invoke the `$callback` when either writing to the stream or only when reading from the stream: + +```php +Filter\append($stream, function ($chunk) { + // will be called each time you write to the stream + return $chunk; +}, STREAM_FILTER_WRITE); + +Filter\append($stream, function ($chunk) { + // will be called each time you read from the stream + return $chunk; +}, STREAM_FILTER_READ); +``` + +### prepend() + +The `prepend($stream, $callback, $read_write = STREAM_FILTER_ALL)` function can be used to +prepend a filter callback to the given stream. + +Each stream can have a list of filters attached. +This function prepends a filter to the start of this list. + +This function returns a filter resource which can be passed to [`remove()`](#remove). +If the given filter can not be added, it throws an `Exception`. + +```php +$filter = Filter\prepend($stream, function ($chunk) { + // will be called each time you read or write a $chunk to/from the stream + return $chunk; +}); +``` + +### fun() + +The `fun($filter, $parameters = null)` function can be used to +create a filter function which uses the given built-in `$filter`. + +PHP comes with a useful set of [built-in filters](http://php.net/manual/en/filters.php). +Using `fun()` makes accessing these as easy as passing an input string to filter +and getting the filtered output string. + +```php +$fun = Filter\fun('string.rot13'); + +assert('grfg' === $fun('test')); +assert('test' === $fun($fun('test')); +``` + +Please note that not all filter functions may be available depending on installed +PHP extensions and the PHP version in use. +In particular, [HHVM](http://hhvm.com/) may not offer the same filter functions +or parameters as Zend PHP. +Accessing an unknown filter function will result in a `RuntimeException`: + +```php +Filter\fun('unknown'); // throws RuntimeException +``` + +Some filters may accept or require additional filter parameters. +The optional `$parameters` argument will be passed to the filter handler as-is. +Please refer to the individual filter definition for more details. +For example, the `string.strip_tags` filter can be invoked like this: + +```php +$fun = Filter\fun('string.strip_tags', '<a><b>'); + +$ret = $fun('<b>h<br>i</b>'); +assert('<b>hi</b>' === $ret); +``` + +Under the hood, this function allocates a temporary memory stream, so it's +recommended to clean up the filter function after use. +Also, some filter functions (in particular the +[zlib compression filters](http://php.net/manual/en/filters.compression.php)) +may use internal buffers and may emit a final data chunk on close. +The filter function can be closed by invoking without any arguments: + +```php +$fun = Filter\fun('zlib.deflate'); + +$ret = $fun('hello') . $fun('world') . $fun(); +assert('helloworld' === gzinflate($ret)); +``` + +The filter function must not be used anymore after it has been closed. +Doing so will result in a `RuntimeException`: + +```php +$fun = Filter\fun('string.rot13'); +$fun(); + +$fun('test'); // throws RuntimeException +``` + +> Note: If you're using the zlib compression filters, then you should be wary +about engine inconsistencies between different PHP versions and HHVM. +These inconsistencies exist in the underlying PHP engines and there's little we +can do about this in this library. +[Our test suite](tests/) contains several test cases that exhibit these issues. +If you feel some test case is missing or outdated, we're happy to accept PRs! :) + +### remove() + +The `remove($filter)` function can be used to +remove a filter previously added via [`append()`](#append) or [`prepend()`](#prepend). + +```php +$filter = Filter\append($stream, function () { + // … +}); +Filter\remove($filter); +``` + +## Install + +The recommended way to install this library is [through composer](https://getcomposer.org). +[New to composer?](https://getcomposer.org/doc/00-intro.md) + +```bash +$ composer require clue/stream-filter:~1.3 +``` + +## License + +MIT diff --git a/inc/mailgun/clue/stream-filter/composer.json b/inc/mailgun/clue/stream-filter/composer.json new file mode 100644 index 0000000..3397dd8 --- /dev/null +++ b/inc/mailgun/clue/stream-filter/composer.json @@ -0,0 +1,20 @@ +{ + "name": "clue/stream-filter", + "description": "A simple and modern approach to stream filtering in PHP", + "keywords": ["stream", "callback", "filter", "php_user_filter", "stream_filter_append", "stream_filter_register", "bucket brigade"], + "homepage": "https://github.com/clue/php-stream-filter", + "license": "MIT", + "authors": [ + { + "name": "Christian Lück", + "email": "christian@lueck.tv" + } + ], + "require": { + "php": ">=5.3" + }, + "autoload": { + "psr-4": { "Clue\\StreamFilter\\": "src/" }, + "files": [ "src/functions.php" ] + } +} diff --git a/inc/mailgun/clue/stream-filter/phpunit.xml.dist b/inc/mailgun/clue/stream-filter/phpunit.xml.dist new file mode 100644 index 0000000..f373698 --- /dev/null +++ b/inc/mailgun/clue/stream-filter/phpunit.xml.dist @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<phpunit bootstrap="vendor/autoload.php" + colors="true" + convertErrorsToExceptions="true" + convertNoticesToExceptions="true" + convertWarningsToExceptions="true" +> + <testsuites> + <testsuite> + <directory>./tests/</directory> + </testsuite> + </testsuites> + <filter> + <whitelist> + <directory>./src/</directory> + </whitelist> + </filter> +</phpunit>
\ No newline at end of file diff --git a/inc/mailgun/clue/stream-filter/src/CallbackFilter.php b/inc/mailgun/clue/stream-filter/src/CallbackFilter.php new file mode 100644 index 0000000..710940b --- /dev/null +++ b/inc/mailgun/clue/stream-filter/src/CallbackFilter.php @@ -0,0 +1,120 @@ +<?php + +namespace Clue\StreamFilter; + +use php_user_filter; +use InvalidArgumentException; +use ReflectionFunction; +use Exception; + +/** + * + * @internal + * @see append() + * @see prepend() + */ +class CallbackFilter extends php_user_filter +{ + private $callback; + private $closed = true; + private $supportsClose = false; + + public function onCreate() + { + $this->closed = false; + + if (!is_callable($this->params)) { + throw new InvalidArgumentException('No valid callback parameter given to stream_filter_(append|prepend)'); + } + $this->callback = $this->params; + + // callback supports end event if it accepts invocation without arguments + $ref = new ReflectionFunction($this->callback); + $this->supportsClose = ($ref->getNumberOfRequiredParameters() === 0); + + return true; + } + + public function onClose() + { + $this->closed = true; + + // callback supports closing and is not already closed + if ($this->supportsClose) { + $this->supportsClose = false; + // invoke without argument to signal end and discard resulting buffer + try { + call_user_func($this->callback); + } catch (Exception $ignored) { + // this might be called during engine shutdown, so it's not safe + // to raise any errors or exceptions here + // trigger_error('Error closing filter: ' . $ignored->getMessage(), E_USER_WARNING); + } + } + + $this->callback = null; + } + + public function filter($in, $out, &$consumed, $closing) + { + // concatenate whole buffer from input brigade + $data = ''; + while ($bucket = stream_bucket_make_writeable($in)) { + $consumed += $bucket->datalen; + $data .= $bucket->data; + } + + // skip processing callback that already ended + if ($this->closed) { + return PSFS_FEED_ME; + } + + // only invoke filter function if buffer is not empty + // this may skip flushing a closing filter + if ($data !== '') { + try { + $data = call_user_func($this->callback, $data); + } catch (Exception $e) { + // exception should mark filter as closed + $this->onClose(); + trigger_error('Error invoking filter: ' . $e->getMessage(), E_USER_WARNING); + + return PSFS_ERR_FATAL; + } + } + + // mark filter as closed after processing closing chunk + if ($closing) { + $this->closed = true; + + // callback supports closing and is not already closed + if ($this->supportsClose) { + $this->supportsClose = false; + + // invoke without argument to signal end and append resulting buffer + try { + $data .= call_user_func($this->callback); + } catch (Exception $e) { + trigger_error('Error ending filter: ' . $e->getMessage(), E_USER_WARNING); + + return PSFS_ERR_FATAL; + } + } + } + + if ($data !== '') { + // create a new bucket for writing the resulting buffer to the output brigade + // reusing an existing bucket turned out to be bugged in some environments (ancient PHP versions and HHVM) + $bucket = @stream_bucket_new($this->stream, $data); + + // legacy PHP versions (PHP < 5.4) do not support passing data from the event signal handler + // because closing the stream invalidates the stream and its stream bucket brigade before + // invoking the filter close handler. + if ($bucket !== false) { + stream_bucket_append($out, $bucket); + } + } + + return PSFS_PASS_ON; + } +} diff --git a/inc/mailgun/clue/stream-filter/src/functions.php b/inc/mailgun/clue/stream-filter/src/functions.php new file mode 100644 index 0000000..6c8125b --- /dev/null +++ b/inc/mailgun/clue/stream-filter/src/functions.php @@ -0,0 +1,133 @@ +<?php + +namespace Clue\StreamFilter; + +use RuntimeException; + +/** + * append a callback filter to the given stream + * + * @param resource $stream + * @param callable $callback + * @param int $read_write + * @return resource filter resource which can be used for `remove()` + * @throws Exception on error + * @uses stream_filter_append() + */ +function append($stream, $callback, $read_write = STREAM_FILTER_ALL) +{ + $ret = @stream_filter_append($stream, register(), $read_write, $callback); + + if ($ret === false) { + $error = error_get_last() + array('message' => ''); + throw new RuntimeException('Unable to append filter: ' . $error['message']); + } + + return $ret; +} + +/** + * prepend a callback filter to the given stream + * + * @param resource $stream + * @param callable $callback + * @param int $read_write + * @return resource filter resource which can be used for `remove()` + * @throws Exception on error + * @uses stream_filter_prepend() + */ +function prepend($stream, $callback, $read_write = STREAM_FILTER_ALL) +{ + $ret = @stream_filter_prepend($stream, register(), $read_write, $callback); + + if ($ret === false) { + $error = error_get_last() + array('message' => ''); + throw new RuntimeException('Unable to prepend filter: ' . $error['message']); + } + + return $ret; +} + +/** + * Creates filter fun (function) which uses the given built-in $filter + * + * @param string $filter built-in filter name, see stream_get_filters() + * @param mixed $params additional parameters to pass to the built-in filter + * @return callable a filter callback which can be append()'ed or prepend()'ed + * @throws RuntimeException on error + * @see stream_get_filters() + * @see append() + */ +function fun($filter, $params = null) +{ + $fp = fopen('php://memory', 'w'); + $filter = @stream_filter_append($fp, $filter, STREAM_FILTER_WRITE, $params); + + if ($filter === false) { + fclose($fp); + $error = error_get_last() + array('message' => ''); + throw new RuntimeException('Unable to access built-in filter: ' . $error['message']); + } + + // append filter function which buffers internally + $buffer = ''; + append($fp, function ($chunk) use (&$buffer) { + $buffer .= $chunk; + + // always return empty string in order to skip actually writing to stream resource + return ''; + }, STREAM_FILTER_WRITE); + + $closed = false; + + return function ($chunk = null) use ($fp, $filter, &$buffer, &$closed) { + if ($closed) { + throw new \RuntimeException('Unable to perform operation on closed stream'); + } + if ($chunk === null) { + $closed = true; + $buffer = ''; + fclose($fp); + return $buffer; + } + // initialize buffer and invoke filters by attempting to write to stream + $buffer = ''; + fwrite($fp, $chunk); + + // buffer now contains everything the filter function returned + return $buffer; + }; +} + +/** + * remove a callback filter from the given stream + * + * @param resource $filter + * @return boolean true on success or false on error + * @throws Exception on error + * @uses stream_filter_remove() + */ +function remove($filter) +{ + if (@stream_filter_remove($filter) === false) { + throw new RuntimeException('Unable to remove given filter'); + } +} + +/** + * registers the callback filter and returns the resulting filter name + * + * There should be little reason to call this function manually. + * + * @return string filter name + * @uses CallbackFilter + */ +function register() +{ + static $registered = null; + if ($registered === null) { + $registered = 'stream-callback'; + stream_filter_register($registered, __NAMESPACE__ . '\CallbackFilter'); + } + return $registered; +} diff --git a/inc/mailgun/clue/stream-filter/tests/FilterTest.php b/inc/mailgun/clue/stream-filter/tests/FilterTest.php new file mode 100644 index 0000000..02aa3a4 --- /dev/null +++ b/inc/mailgun/clue/stream-filter/tests/FilterTest.php @@ -0,0 +1,386 @@ +<?php + +use Clue\StreamFilter; + +class FilterTest extends PHPUnit_Framework_TestCase +{ + public function testAppendSimpleCallback() + { + $stream = $this->createStream(); + + StreamFilter\append($stream, function ($chunk) { + return strtoupper($chunk); + }); + + fwrite($stream, 'hello'); + fwrite($stream, 'world'); + rewind($stream); + + $this->assertEquals('HELLOWORLD', stream_get_contents($stream)); + + fclose($stream); + } + + public function testAppendNativePhpFunction() + { + $stream = $this->createStream(); + + StreamFilter\append($stream, 'strtoupper'); + + fwrite($stream, 'hello'); + fwrite($stream, 'world'); + rewind($stream); + + $this->assertEquals('HELLOWORLD', stream_get_contents($stream)); + + fclose($stream); + } + + public function testAppendChangingChunkSize() + { + $stream = $this->createStream(); + + StreamFilter\append($stream, function ($chunk) { + return str_replace(array('a','e','i','o','u'), '', $chunk); + }); + + fwrite($stream, 'hello'); + fwrite($stream, 'world'); + rewind($stream); + + $this->assertEquals('hllwrld', stream_get_contents($stream)); + + fclose($stream); + } + + public function testAppendReturningEmptyStringWillNotPassThrough() + { + $stream = $this->createStream(); + + StreamFilter\append($stream, function ($chunk) { + return ''; + }); + + fwrite($stream, 'hello'); + fwrite($stream, 'world'); + rewind($stream); + + $this->assertEquals('', stream_get_contents($stream)); + + fclose($stream); + } + + public function testAppendEndEventCanBeBufferedOnClose() + { + if (PHP_VERSION < 5.4) $this->markTestSkipped('Not supported on legacy PHP'); + + $stream = $this->createStream(); + + StreamFilter\append($stream, function ($chunk = null) { + if ($chunk === null) { + // this signals the end event + return '!'; + } + return $chunk . ' '; + }, STREAM_FILTER_WRITE); + + $buffered = ''; + StreamFilter\append($stream, function ($chunk) use (&$buffered) { + $buffered .= $chunk; + return ''; + }); + + fwrite($stream, 'hello'); + fwrite($stream, 'world'); + + fclose($stream); + + $this->assertEquals('hello world !', $buffered); + } + + public function testAppendEndEventWillBeCalledOnRemove() + { + $stream = $this->createStream(); + + $ended = false; + $filter = StreamFilter\append($stream, function ($chunk = null) use (&$ended) { + if ($chunk === null) { + $ended = true; + } + return $chunk; + }, STREAM_FILTER_WRITE); + + $this->assertEquals(0, $ended); + StreamFilter\remove($filter); + $this->assertEquals(1, $ended); + } + + public function testAppendEndEventWillBeCalledOnClose() + { + $stream = $this->createStream(); + + $ended = false; + StreamFilter\append($stream, function ($chunk = null) use (&$ended) { + if ($chunk === null) { + $ended = true; + } + return $chunk; + }, STREAM_FILTER_WRITE); + + $this->assertEquals(0, $ended); + fclose($stream); + $this->assertEquals(1, $ended); + } + + public function testAppendWriteOnly() + { + $stream = $this->createStream(); + + $invoked = 0; + + StreamFilter\append($stream, function ($chunk) use (&$invoked) { + ++$invoked; + + return $chunk; + }, STREAM_FILTER_WRITE); + + fwrite($stream, 'a'); + fwrite($stream, 'b'); + fwrite($stream, 'c'); + rewind($stream); + + $this->assertEquals(3, $invoked); + $this->assertEquals('abc', stream_get_contents($stream)); + + fclose($stream); + } + + public function testAppendReadOnly() + { + $stream = $this->createStream(); + + $invoked = 0; + + StreamFilter\append($stream, function ($chunk) use (&$invoked) { + ++$invoked; + + return $chunk; + }, STREAM_FILTER_READ); + + fwrite($stream, 'a'); + fwrite($stream, 'b'); + fwrite($stream, 'c'); + rewind($stream); + + $this->assertEquals(0, $invoked); + $this->assertEquals('abc', stream_get_contents($stream)); + $this->assertEquals(1, $invoked); + + fclose($stream); + } + + public function testOrderCallingAppendAfterPrepend() + { + $stream = $this->createStream(); + + StreamFilter\append($stream, function ($chunk) { + return '[' . $chunk . ']'; + }, STREAM_FILTER_WRITE); + + StreamFilter\prepend($stream, function ($chunk) { + return '(' . $chunk . ')'; + }, STREAM_FILTER_WRITE); + + fwrite($stream, 'hello'); + rewind($stream); + + $this->assertEquals('[(hello)]', stream_get_contents($stream)); + + fclose($stream); + } + + public function testRemoveFilter() + { + $stream = $this->createStream(); + + $first = StreamFilter\append($stream, function ($chunk) { + return $chunk . '?'; + }, STREAM_FILTER_WRITE); + + StreamFilter\append($stream, function ($chunk) { + return $chunk . '!'; + }, STREAM_FILTER_WRITE); + + StreamFilter\remove($first); + + fwrite($stream, 'hello'); + rewind($stream); + + $this->assertEquals('hello!', stream_get_contents($stream)); + + fclose($stream); + } + + public function testAppendFunDechunk() + { + if (defined('HHVM_VERSION')) $this->markTestSkipped('Not supported on HHVM (dechunk filter does not exist)'); + + $stream = $this->createStream(); + + StreamFilter\append($stream, StreamFilter\fun('dechunk'), STREAM_FILTER_WRITE); + + fwrite($stream, "2\r\nhe\r\n"); + fwrite($stream, "3\r\nllo\r\n"); + fwrite($stream, "0\r\n\r\n"); + rewind($stream); + + $this->assertEquals('hello', stream_get_contents($stream)); + + fclose($stream); + } + + public function testAppendThrows() + { + $this->createErrorHandler($errors); + + $stream = $this->createStream(); + $this->createErrorHandler($errors); + + StreamFilter\append($stream, function ($chunk) { + throw new \DomainException($chunk); + }); + + fwrite($stream, 'test'); + + $this->removeErrorHandler(); + $this->assertCount(1, $errors); + $this->assertContains('test', $errors[0]); + } + + public function testAppendThrowsDuringEnd() + { + $stream = $this->createStream(); + $this->createErrorHandler($errors); + + StreamFilter\append($stream, function ($chunk = null) { + if ($chunk === null) { + throw new \DomainException('end'); + } + return $chunk; + }); + + fclose($stream); + + $this->removeErrorHandler(); + + // We can only assert we're not seeing an exception here… + // * php 5.3-5.6 sees one error here + // * php 7 does not see any error here + // * hhvm sees the same error twice + // + // If you're curious: + // + // var_dump($errors); + // $this->assertCount(1, $errors); + // $this->assertContains('end', $errors[0]); + } + + public function testAppendThrowsShouldTriggerEnd() + { + $stream = $this->createStream(); + $this->createErrorHandler($errors); + + $ended = false; + StreamFilter\append($stream, function ($chunk = null) use (&$ended) { + if ($chunk === null) { + $ended = true; + return ''; + } + throw new \DomainException($chunk); + }); + + $this->assertEquals(false, $ended); + fwrite($stream, 'test'); + $this->assertEquals(true, $ended); + + $this->removeErrorHandler(); + $this->assertCount(1, $errors); + $this->assertContains('test', $errors[0]); + } + + public function testAppendThrowsShouldTriggerEndButIgnoreExceptionDuringEnd() + { + //$this->markTestIncomplete(); + $stream = $this->createStream(); + $this->createErrorHandler($errors); + + StreamFilter\append($stream, function ($chunk = null) { + if ($chunk === null) { + $chunk = 'end'; + //return ''; + } + throw new \DomainException($chunk); + }); + + fwrite($stream, 'test'); + + $this->removeErrorHandler(); + $this->assertCount(1, $errors); + $this->assertContains('test', $errors[0]); + } + + /** + * @expectedException RuntimeException + */ + public function testAppendInvalidStreamIsRuntimeError() + { + if (defined('HHVM_VERSION')) $this->markTestSkipped('Not supported on HHVM (does not reject invalid stream)'); + StreamFilter\append(false, function () { }); + } + + /** + * @expectedException RuntimeException + */ + public function testPrependInvalidStreamIsRuntimeError() + { + if (defined('HHVM_VERSION')) $this->markTestSkipped('Not supported on HHVM (does not reject invalid stream)'); + StreamFilter\prepend(false, function () { }); + } + + /** + * @expectedException RuntimeException + */ + public function testRemoveInvalidFilterIsRuntimeError() + { + if (defined('HHVM_VERSION')) $this->markTestSkipped('Not supported on HHVM (does not reject invalid filters)'); + StreamFilter\remove(false); + } + + /** + * @expectedException InvalidArgumentException + */ + public function testInvalidCallbackIsInvalidArgument() + { + $stream = $this->createStream(); + + StreamFilter\append($stream, 'a-b-c'); + } + + private function createStream() + { + return fopen('php://memory', 'r+'); + } + + private function createErrorHandler(&$errors) + { + $errors = array(); + set_error_handler(function ($_, $message) use (&$errors) { + $errors []= $message; + }); + } + + private function removeErrorHandler() + { + restore_error_handler(); + } +} diff --git a/inc/mailgun/clue/stream-filter/tests/FunTest.php b/inc/mailgun/clue/stream-filter/tests/FunTest.php new file mode 100644 index 0000000..2eb1dd9 --- /dev/null +++ b/inc/mailgun/clue/stream-filter/tests/FunTest.php @@ -0,0 +1,34 @@ +<?php + +use Clue\StreamFilter as Filter; + +class FunTest extends PHPUnit_Framework_TestCase +{ + public function testFunInRot13() + { + $rot = Filter\fun('string.rot13'); + + $this->assertEquals('grfg', $rot('test')); + $this->assertEquals('test', $rot($rot('test'))); + $this->assertEquals(null, $rot()); + } + + /** + * @expectedException RuntimeException + */ + public function testFunWriteAfterCloseRot13() + { + $rot = Filter\fun('string.rot13'); + + $this->assertEquals(null, $rot()); + $rot('test'); + } + + /** + * @expectedException RuntimeException + */ + public function testFunInvalid() + { + Filter\fun('unknown'); + } +} diff --git a/inc/mailgun/clue/stream-filter/tests/FunZlibTest.php b/inc/mailgun/clue/stream-filter/tests/FunZlibTest.php new file mode 100644 index 0000000..752c8a2 --- /dev/null +++ b/inc/mailgun/clue/stream-filter/tests/FunZlibTest.php @@ -0,0 +1,79 @@ +<?php + +use Clue\StreamFilter; + +class BuiltInZlibTest extends PHPUnit_Framework_TestCase +{ + public function testFunZlibDeflateHelloWorld() + { + $deflate = StreamFilter\fun('zlib.deflate'); + + $data = $deflate('hello') . $deflate(' ') . $deflate('world') . $deflate(); + + $this->assertEquals(gzdeflate('hello world'), $data); + } + + public function testFunZlibDeflateEmpty() + { + if (PHP_VERSION >= 7) $this->markTestSkipped('Not supported on PHP7 (empty string does not invoke filter)'); + + $deflate = StreamFilter\fun('zlib.deflate'); + + //$data = gzdeflate(''); + $data = $deflate(); + + $this->assertEquals("\x03\x00", $data); + } + + public function testFunZlibDeflateBig() + { + $deflate = StreamFilter\fun('zlib.deflate'); + + $n = 1000; + $expected = str_repeat('hello', $n); + + $bytes = ''; + for ($i = 0; $i < $n; ++$i) { + $bytes .= $deflate('hello'); + } + $bytes .= $deflate(); + + $this->assertEquals($expected, gzinflate($bytes)); + } + + public function testFunZlibInflateHelloWorld() + { + $inflate = StreamFilter\fun('zlib.inflate'); + + $data = $inflate(gzdeflate('hello world')) . $inflate(); + + $this->assertEquals('hello world', $data); + } + + public function testFunZlibInflateEmpty() + { + $inflate = StreamFilter\fun('zlib.inflate'); + + $data = $inflate("\x03\x00") . $inflate(); + + $this->assertEquals('', $data); + } + + public function testFunZlibInflateBig() + { + if (defined('HHVM_VERSION')) $this->markTestSkipped('Not supported on HHVM (final chunk will not be emitted)'); + + $inflate = StreamFilter\fun('zlib.inflate'); + + $expected = str_repeat('hello', 10); + $bytes = gzdeflate($expected); + + $ret = ''; + foreach (str_split($bytes, 2) as $chunk) { + $ret .= $inflate($chunk); + } + $ret .= $inflate(); + + $this->assertEquals($expected, $ret); + } +} |
