summaryrefslogtreecommitdiff
path: root/inc/mailgun/clue
diff options
context:
space:
mode:
Diffstat (limited to 'inc/mailgun/clue')
-rw-r--r--inc/mailgun/clue/stream-filter/.gitignore2
-rw-r--r--inc/mailgun/clue/stream-filter/.travis.yml10
-rw-r--r--inc/mailgun/clue/stream-filter/CHANGELOG.md30
-rw-r--r--inc/mailgun/clue/stream-filter/LICENSE21
-rw-r--r--inc/mailgun/clue/stream-filter/README.md252
-rw-r--r--inc/mailgun/clue/stream-filter/composer.json20
-rw-r--r--inc/mailgun/clue/stream-filter/phpunit.xml.dist19
-rw-r--r--inc/mailgun/clue/stream-filter/src/CallbackFilter.php120
-rw-r--r--inc/mailgun/clue/stream-filter/src/functions.php133
-rw-r--r--inc/mailgun/clue/stream-filter/tests/FilterTest.php386
-rw-r--r--inc/mailgun/clue/stream-filter/tests/FunTest.php34
-rw-r--r--inc/mailgun/clue/stream-filter/tests/FunZlibTest.php79
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 [![Build Status](https://travis-ci.org/clue/php-stream-filter.svg?branch=master)](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);
+ }
+}