From 342270d6dd82e956313cb93008a1bd37576321c5 Mon Sep 17 00:00:00 2001 From: Mark van Holsteijn Date: Fri, 28 May 2021 22:39:50 +0200 Subject: [PATCH 01/15] added Google Cloud Storage support --- .gitattributes | 1 + .github/workflows/tests.yml | 4 + .gitignore | 4 +- .scrutinizer.yml | 39 ++ INSTALL.md | 12 + cfg/conf.sample.php | 7 + composer.json | 5 +- composer.lock | 132 ++++- lib/Configuration.php | 10 + lib/Data/GoogleCloudStorage.php | 251 +++++++++ lib/Persistence/TrafficLimiter.php | 4 +- tst/Data/GoogleCloudStorageTest.php | 715 ++++++++++++++++++++++++++ vendor/composer/ClassLoader.php | 40 +- vendor/composer/InstalledVersions.php | 326 ++++++++++++ vendor/composer/autoload_classmap.php | 2 + vendor/composer/autoload_real.php | 6 +- vendor/composer/autoload_static.php | 2 + vendor/composer/installed.php | 51 ++ vendor/composer/platform_check.php | 26 + 19 files changed, 1621 insertions(+), 16 deletions(-) create mode 100644 .scrutinizer.yml create mode 100644 lib/Data/GoogleCloudStorage.php create mode 100644 tst/Data/GoogleCloudStorageTest.php create mode 100644 vendor/composer/InstalledVersions.php create mode 100644 vendor/composer/installed.php create mode 100644 vendor/composer/platform_check.php diff --git a/.gitattributes b/.gitattributes index ef060615..28b68d13 100644 --- a/.gitattributes +++ b/.gitattributes @@ -16,6 +16,7 @@ js/test/ export-ignore .jshintrc export-ignore .nsprc export-ignore .php_cs export-ignore +.scrutinizer.yml export-ignore .styleci.yml export-ignore .travis.yml export-ignore composer.json export-ignore diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c1ac3d9c..c4bfae67 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -11,6 +11,8 @@ jobs: run: composer validate - name: Install dependencies run: /usr/bin/php7.4 $(which composer) install --prefer-dist --no-suggest + - name: Install Google Cloud Storage + run: /usr/bin/php7.4 $(which composer) require google/cloud-storage PHPunit: runs-on: ubuntu-latest strategy: @@ -29,6 +31,8 @@ jobs: run: rm composer.lock - name: Setup PHPunit run: composer install -n + - name: Install Google Cloud Storage + run: composer require google/cloud-storage - name: Run unit tests run: ../vendor/bin/phpunit --no-coverage working-directory: tst diff --git a/.gitignore b/.gitignore index a4cd2bb6..65ef7189 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,7 @@ cfg/* !cfg/.htaccess # Ignore data/ -data/ +/data/ # Ignore PhpDoc doc/* @@ -36,3 +36,5 @@ tst/ConfigurationCombinationsTest.php .project .externalToolBuilders .c9 +/.idea/ +*.iml diff --git a/.scrutinizer.yml b/.scrutinizer.yml new file mode 100644 index 00000000..e6957a51 --- /dev/null +++ b/.scrutinizer.yml @@ -0,0 +1,39 @@ +checks: + php: true + javascript: true +filter: + paths: + - "css/privatebin.css" + - "css/bootstrap/privatebin.css" + - "js/privatebin.js" + - "lib/*.php" + - "index.php" + excluded_paths: + - lib/Data/GoogleCloudStorage.php + - tst/Data/GoogleCloudStorageTest.php +coding_style: + php: + spaces: + around_operators: + additive: false + concatenation: true +build: + environment: + php: + version: '7.2' + tests: + override: + - + command: 'composer require google/cloud-storage && cd tst && ../vendor/bin/phpunit' + coverage: + file: 'tst/log/coverage-clover.xml' + format: 'clover' + nodes: + tests: true + analysis: + tests: + override: + - + command: phpcs-run + use_website_config: true + - php-scrutinizer-run diff --git a/INSTALL.md b/INSTALL.md index df0cac23..ec0d7f48 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -191,3 +191,15 @@ INSERT INTO prefix_config VALUES('VERSION', '1.3.5'); ``` In **PostgreSQL**, the data, attachment, nickname and vizhash columns needs to be TEXT and not BLOB or MEDIUMBLOB. + +### Using Google Cloud Storage +If you want to deploy PrivateBin in a serverless manner, you can choose the `GoogleCloudStorage` as backend. +To use this backend, you create a GCS bucket and specify the name as the model option `bucket`. Alternatively, +you can set the name through the environment variable PASTEBIN_GCS_BUCKET. + +The default prefix for pastes stored in the bucket is `pastes`. To change the prefix, specify the option `prefix`. + +Google Cloud Storage buckets may be significantly slower than a `FileSystem` or `Database` backend. The big advantage +is that the deployment on Google Cloud Platform using Google Cloud Run is easy and cheap. + +To use the Google Cloud Storage backend you have to install the suggested google/cloud-storage library. diff --git a/cfg/conf.sample.php b/cfg/conf.sample.php index e9e500b0..a4b7f6b5 100644 --- a/cfg/conf.sample.php +++ b/cfg/conf.sample.php @@ -167,6 +167,13 @@ class = Filesystem [model_options] dir = PATH "data" +[model] +; example of a Google Cloud Storage configuration +;class = GoogleCloudStorage +;[model_options] +;bucket = "my-private-bin" +;prefix = "pastes" + ;[model] ; example of DB configuration for MySQL ;class = Database diff --git a/composer.json b/composer.json index a8e98aaa..6f507778 100644 --- a/composer.json +++ b/composer.json @@ -29,6 +29,9 @@ "yzalis/identicon" : "2.0.0", "mlocati/ip-lib" : "1.14.0" }, + "suggest": { + "google/cloud-storage": "1.23.1" + }, "require-dev" : { "phpunit/phpunit" : "^4.6 || ^5.0" }, @@ -40,4 +43,4 @@ "config" : { "autoloader-suffix" : "DontChange" } -} \ No newline at end of file +} diff --git a/composer.lock b/composer.lock index 931c92fb..96183d1e 100644 --- a/composer.lock +++ b/composer.lock @@ -62,6 +62,10 @@ "range", "subnet" ], + "support": { + "issues": "https://github.com/mlocati/ip-lib/issues", + "source": "https://github.com/mlocati/ip-lib/tree/1.14.0" + }, "funding": [ { "url": "https://github.com/sponsors/mlocati", @@ -121,6 +125,11 @@ "pseudorandom", "random" ], + "support": { + "email": "info@paragonie.com", + "issues": "https://github.com/paragonie/random_compat/issues", + "source": "https://github.com/paragonie/random_compat" + }, "time": "2021-04-17T09:33:01+00:00" }, { @@ -173,6 +182,10 @@ "identicon", "image" ], + "support": { + "issues": "https://github.com/yzalis/Identicon/issues", + "source": "https://github.com/yzalis/Identicon/tree/master" + }, "abandoned": true, "time": "2019-10-14T09:30:57+00:00" } @@ -227,6 +240,10 @@ "constructor", "instantiate" ], + "support": { + "issues": "https://github.com/doctrine/instantiator/issues", + "source": "https://github.com/doctrine/instantiator/tree/1.4.0" + }, "funding": [ { "url": "https://www.doctrine-project.org/sponsorship.html", @@ -289,6 +306,10 @@ "object", "object graph" ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.10.2" + }, "funding": [ { "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", @@ -344,6 +365,10 @@ "reflection", "static analysis" ], + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues", + "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x" + }, "time": "2020-06-27T09:03:43+00:00" }, { @@ -396,6 +421,10 @@ } ], "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/master" + }, "time": "2020-09-03T19:13:55+00:00" }, { @@ -441,6 +470,10 @@ } ], "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", + "support": { + "issues": "https://github.com/phpDocumentor/TypeResolver/issues", + "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.4.0" + }, "time": "2020-09-17T18:55:26+00:00" }, { @@ -504,6 +537,10 @@ "spy", "stub" ], + "support": { + "issues": "https://github.com/phpspec/prophecy/issues", + "source": "https://github.com/phpspec/prophecy/tree/v1.10.3" + }, "time": "2020-03-05T15:02:03+00:00" }, { @@ -567,6 +604,11 @@ "testing", "xunit" ], + "support": { + "irc": "irc://irc.freenode.net/phpunit", + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/4.0" + }, "time": "2017-04-02T07:44:40+00:00" }, { @@ -614,6 +656,11 @@ "filesystem", "iterator" ], + "support": { + "irc": "irc://irc.freenode.net/phpunit", + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/1.4.5" + }, "time": "2017-11-27T13:52:08+00:00" }, { @@ -655,6 +702,10 @@ "keywords": [ "template" ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/1.2.1" + }, "time": "2015-06-21T13:50:34+00:00" }, { @@ -704,6 +755,10 @@ "keywords": [ "timer" ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "source": "https://github.com/sebastianbergmann/php-timer/tree/master" + }, "time": "2017-02-26T11:10:40+00:00" }, { @@ -753,6 +808,10 @@ "keywords": [ "tokenizer" ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-token-stream/issues", + "source": "https://github.com/sebastianbergmann/php-token-stream/tree/master" + }, "abandoned": true, "time": "2017-11-27T05:48:46+00:00" }, @@ -836,6 +895,10 @@ "testing", "xunit" ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "source": "https://github.com/sebastianbergmann/phpunit/tree/5.7.27" + }, "time": "2018-02-01T05:50:59+00:00" }, { @@ -895,6 +958,11 @@ "mock", "xunit" ], + "support": { + "irc": "irc://irc.freenode.net/phpunit", + "issues": "https://github.com/sebastianbergmann/phpunit-mock-objects/issues", + "source": "https://github.com/sebastianbergmann/phpunit-mock-objects/tree/3.4" + }, "abandoned": true, "time": "2017-06-30T09:13:00+00:00" }, @@ -941,6 +1009,10 @@ ], "description": "Looks up which function or method a line of code belongs to", "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/1.0.2" + }, "funding": [ { "url": "https://github.com/sebastianbergmann", @@ -1011,6 +1083,10 @@ "compare", "equality" ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "source": "https://github.com/sebastianbergmann/comparator/tree/1.2" + }, "time": "2017-01-29T09:50:25+00:00" }, { @@ -1063,6 +1139,10 @@ "keywords": [ "diff" ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "source": "https://github.com/sebastianbergmann/diff/tree/1.4" + }, "time": "2017-05-22T07:24:03+00:00" }, { @@ -1113,6 +1193,10 @@ "environment", "hhvm" ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "source": "https://github.com/sebastianbergmann/environment/tree/master" + }, "time": "2016-11-26T07:53:53+00:00" }, { @@ -1180,6 +1264,10 @@ "export", "exporter" ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "source": "https://github.com/sebastianbergmann/exporter/tree/master" + }, "time": "2016-11-19T08:54:04+00:00" }, { @@ -1231,6 +1319,10 @@ "keywords": [ "global state" ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "source": "https://github.com/sebastianbergmann/global-state/tree/1.1.1" + }, "time": "2015-10-12T03:26:01+00:00" }, { @@ -1277,6 +1369,10 @@ ], "description": "Traverses array structures and object graphs to enumerate all referenced objects", "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/master" + }, "time": "2017-02-18T15:18:39+00:00" }, { @@ -1330,6 +1426,10 @@ ], "description": "Provides functionality to recursively process PHP variables", "homepage": "http://www.github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/master" + }, "time": "2016-11-19T07:33:16+00:00" }, { @@ -1372,6 +1472,10 @@ ], "description": "Provides a list of PHP built-in functions that operate on resources", "homepage": "https://www.github.com/sebastianbergmann/resource-operations", + "support": { + "issues": "https://github.com/sebastianbergmann/resource-operations/issues", + "source": "https://github.com/sebastianbergmann/resource-operations/tree/master" + }, "time": "2015-07-28T20:34:47+00:00" }, { @@ -1415,20 +1519,24 @@ ], "description": "Library that helps with managing the version number of Git-hosted PHP projects", "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "source": "https://github.com/sebastianbergmann/version/tree/master" + }, "time": "2016-10-03T07:35:21+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.22.1", + "version": "v1.23.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "c6c942b1ac76c82448322025e084cadc56048b4e" + "reference": "46cd95797e9df938fdd2b03693b5fca5e64b01ce" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/c6c942b1ac76c82448322025e084cadc56048b4e", - "reference": "c6c942b1ac76c82448322025e084cadc56048b4e", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/46cd95797e9df938fdd2b03693b5fca5e64b01ce", + "reference": "46cd95797e9df938fdd2b03693b5fca5e64b01ce", "shasum": "" }, "require": { @@ -1440,7 +1548,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.22-dev" + "dev-main": "1.23-dev" }, "thanks": { "name": "symfony/polyfill", @@ -1477,6 +1585,9 @@ "polyfill", "portable" ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.23.0" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -1491,7 +1602,7 @@ "type": "tidelift" } ], - "time": "2021-01-07T16:49:33+00:00" + "time": "2021-02-19T12:13:01+00:00" }, { "name": "symfony/yaml", @@ -1545,6 +1656,9 @@ ], "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/yaml/tree/v4.4.24" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -1613,6 +1727,10 @@ "check", "validate" ], + "support": { + "issues": "https://github.com/webmozarts/assert/issues", + "source": "https://github.com/webmozarts/assert/tree/1.10.0" + }, "time": "2021-03-09T10:59:23+00:00" } ], @@ -1625,5 +1743,5 @@ "php": "^5.6.0 || ^7.0 || ^8.0" }, "platform-dev": [], - "plugin-api-version": "1.1.0" + "plugin-api-version": "2.0.0" } diff --git a/lib/Configuration.php b/lib/Configuration.php index 5de6de3a..1e92fc9b 100644 --- a/lib/Configuration.php +++ b/lib/Configuration.php @@ -153,6 +153,16 @@ class Configuration 'pwd' => null, 'opt' => array(PDO::ATTR_PERSISTENT => true), ); + } elseif ( + $section == 'model_options' && in_array( + $this->_configuration['model']['class'], + array('GoogleCloudStorage') + ) + ) { + $values = array( + 'bucket' => getenv('PRIVATEBIN_GCS_BUCKET') ? getenv('PRIVATEBIN_GCS_BUCKET') : null, + 'prefix' => 'pastes', + ); } // "*_options" sections don't require all defaults to be set diff --git a/lib/Data/GoogleCloudStorage.php b/lib/Data/GoogleCloudStorage.php new file mode 100644 index 00000000..1a1d8bf5 --- /dev/null +++ b/lib/Data/GoogleCloudStorage.php @@ -0,0 +1,251 @@ +_client = new StorageClient(array('suppressKeyFileNotice' => true)); + } else { + // use given client for test purposes + $this->_client = $client; + } + + $this->_bucket = $this->_client->bucket($bucket); + if ($prefix != null) { + $this->_prefix = $prefix; + } + } + + /** + * returns the google storage object key for $pasteid in $this->_bucket. + * @param $pasteid string to get the key for + * @return string + */ + private function _getKey($pasteid) + { + if ($this->_prefix != '') { + return $this->_prefix . '/' . $pasteid; + } + return $pasteid; + } + + /** + * Uploads the payload in the $this->_bucket under the specified key. + * The entire payload is stored as a JSON document. The metadata is replicated + * as the GCS object's metadata except for the fields attachment, attachmentname + * and salt. + * + * @param $key string to store the payload under + * @param $payload array to store + * @return bool true if successful, otherwise false. + */ + private function upload($key, $payload) + { + $metadata = array_key_exists('meta', $payload) ? $payload['meta'] : array(); + unset($metadata['attachment'], $metadata['attachmentname'], $metadata['salt']); + foreach ($metadata as $k => $v) { + $metadata[$k] = strval($v); + } + try { + $this->_bucket->upload(Json::encode($payload), array( + 'name' => $key, + 'chunkSize' => 262144, + 'predefinedAcl' => 'private', + 'metadata' => array( + 'content-type' => 'application/json', + 'metadata' => $metadata, + ), + )); + } catch (Exception $e) { + error_log('failed to upload ' . $key . ' to ' . $this->_bucket->name() . ', ' . + trim(preg_replace('/\s\s+/', ' ', $e->getMessage()))); + return false; + } + return true; + } + + /** + * @inheritDoc + */ + public function create($pasteid, array $paste) + { + if ($this->exists($pasteid)) { + return false; + } + + return $this->upload($this->_getKey($pasteid), $paste); + } + + /** + * @inheritDoc + */ + public function read($pasteid) + { + try { + $o = $this->_bucket->object($this->_getKey($pasteid)); + $data = $o->downloadAsString(); + return Json::decode($data); + } catch (NotFoundException $e) { + return false; + } catch (Exception $e) { + error_log('failed to read ' . $pasteid . ' from ' . $this->_bucket->name() . ', ' . + trim(preg_replace('/\s\s+/', ' ', $e->getMessage()))); + return false; + } + } + + /** + * @inheritDoc + */ + public function delete($pasteid) + { + $name = $this->_getKey($pasteid); + + try { + foreach ($this->_bucket->objects(array('prefix' => $name . '/discussion/')) as $comment) { + try { + $this->_bucket->object($comment->name())->delete(); + } catch (NotFoundException $e) { + // ignore if already deleted. + } + } + } catch (NotFoundException $e) { + // there are no discussions associated with the paste + } + + try { + $this->_bucket->object($name)->delete(); + } catch (NotFoundException $e) { + // ignore if already deleted + } + } + + /** + * @inheritDoc + */ + public function exists($pasteid) + { + $o = $this->_bucket->object($this->_getKey($pasteid)); + return $o->exists(); + } + + /** + * @inheritDoc + */ + public function createComment($pasteid, $parentid, $commentid, array $comment) + { + if ($this->existsComment($pasteid, $parentid, $commentid)) { + return false; + } + $key = $this->_getKey($pasteid) . '/discussion/' . $parentid . '/' . $commentid; + return $this->upload($key, $comment); + } + + /** + * @inheritDoc + */ + public function readComments($pasteid) + { + $comments = array(); + $prefix = $this->_getKey($pasteid) . '/discussion/'; + try { + foreach ($this->_bucket->objects(array('prefix' => $prefix)) as $key) { + $comment = JSON::decode($this->_bucket->object($key->name())->downloadAsString()); + $comment['id'] = basename($key->name()); + $slot = $this->getOpenSlot($comments, (int) $comment['meta']['created']); + $comments[$slot] = $comment; + } + } catch (NotFoundException $e) { + // no comments found + } + return $comments; + } + + /** + * @inheritDoc + */ + public function existsComment($pasteid, $parentid, $commentid) + { + $name = $this->_getKey($pasteid) . '/discussion/' . $parentid . '/' . $commentid; + $o = $this->_bucket->object($name); + return $o->exists(); + } + + /** + * @inheritDoc + */ + protected function _getExpiredPastes($batchsize) + { + $expired = array(); + + $now = time(); + $prefix = $this->_prefix; + if ($prefix != '') { + $prefix = $prefix . '/'; + } + try { + foreach ($this->_bucket->objects(array('prefix' => $prefix)) as $object) { + $metadata = $object->info()['metadata']; + if ($metadata != null && array_key_exists('expire_date', $metadata)) { + $expire_at = intval($metadata['expire_date']); + if ($expire_at != 0 && $expire_at < $now) { + array_push($expired, basename($object->name())); + } + } + + if (count($expired) > $batchsize) { + break; + } + } + } catch (NotFoundException $e) { + // no objects in the bucket yet + } + return $expired; + } +} diff --git a/lib/Persistence/TrafficLimiter.php b/lib/Persistence/TrafficLimiter.php index b45dcf81..be76b8cd 100644 --- a/lib/Persistence/TrafficLimiter.php +++ b/lib/Persistence/TrafficLimiter.php @@ -126,11 +126,11 @@ class TrafficLimiter extends AbstractPersistence $range = Factory::rangeFromString($ipRange); // address could not be parsed, we might not be in IP space and try a string comparison instead - if ($address == null) { + if (is_null($address)) { return $_SERVER[self::$_ipKey] === $ipRange; } // range could not be parsed, possibly an invalid ip range given in config - if ($range == null) { + if (is_null($range)) { return false; } diff --git a/tst/Data/GoogleCloudStorageTest.php b/tst/Data/GoogleCloudStorageTest.php new file mode 100644 index 00000000..91a5ca99 --- /dev/null +++ b/tst/Data/GoogleCloudStorageTest.php @@ -0,0 +1,715 @@ +false)); + $handler = HttpHandlerFactory::build($httpClient); + + $name = 'pb-'; + $alphabet = 'abcdefghijklmnopqrstuvwxyz'; + for ($i = 0; $i < 29; ++$i) { + $name .= $alphabet[rand(0, strlen($alphabet) - 1)]; + } + self::$_client = new StorageClientStub(array()); + self::$_bucket = self::$_client->createBucket($name); + } + + public function setUp() + { + // do not report E_NOTICE as fsouza/fake-gcs-server does not return a `generation` value in the response + // which the Google Cloud Storage PHP library expects. + error_reporting(E_ERROR | E_WARNING | E_PARSE); + $this->_model = GoogleCloudStorage::getInstance(array( + 'bucket' => self::$_bucket->name(), + 'prefix' => 'pastes', + 'client' => self::$_client, )); + } + + public function tearDown() + { + foreach (self::$_bucket->objects() as $object) { + $object->delete(); + } + error_reporting(E_ALL); + } + + public static function tearDownAfterClass() + { + self::$_bucket->delete(); + } + + public function testFileBasedDataStoreWorks() + { + $this->_model->delete(Helper::getPasteId()); + + // storing pastes + $paste = Helper::getPaste(2, array('expire_date' => 1344803344)); + $this->assertFalse($this->_model->exists(Helper::getPasteId()), 'paste does not yet exist'); + $this->assertTrue($this->_model->create(Helper::getPasteId(), $paste), 'store new paste'); + $this->assertTrue($this->_model->exists(Helper::getPasteId()), 'paste exists after storing it'); + $this->assertFalse($this->_model->create(Helper::getPasteId(), $paste), 'unable to store the same paste twice'); + $this->assertEquals($paste, $this->_model->read(Helper::getPasteId())); + + // storing comments + $this->assertFalse($this->_model->existsComment(Helper::getPasteId(), Helper::getPasteId(), Helper::getCommentId()), 'comment does not yet exist'); + $this->assertTrue($this->_model->createComment(Helper::getPasteId(), Helper::getPasteId(), Helper::getCommentId(), Helper::getComment()), 'store comment'); + $this->assertTrue($this->_model->existsComment(Helper::getPasteId(), Helper::getPasteId(), Helper::getCommentId()), 'comment exists after storing it'); + $this->assertFalse($this->_model->createComment(Helper::getPasteId(), Helper::getPasteId(), Helper::getCommentId(), Helper::getComment()), 'unable to store the same comment twice'); + $comment = Helper::getComment(); + $comment['id'] = Helper::getCommentId(); + $comment['parentid'] = Helper::getPasteId(); + $this->assertEquals( + array($comment['meta']['created'] => $comment), + $this->_model->readComments(Helper::getPasteId()) + ); + + // deleting pastes + $this->_model->delete(Helper::getPasteId()); + $this->assertFalse($this->_model->exists(Helper::getPasteId()), 'paste successfully deleted'); + $this->assertFalse($this->_model->existsComment(Helper::getPasteId(), Helper::getPasteId(), Helper::getCommentId()), 'comment was deleted with paste'); + $this->assertFalse($this->_model->read(Helper::getPasteId()), 'paste can no longer be found'); + } + + /** + * pastes a-g are expired and should get deleted, x never expires and y-z expire in an hour + */ + public function testPurge() + { + $expired = Helper::getPaste(2, array('expire_date' => 1344803344)); + $paste = Helper::getPaste(2, array('expire_date' => time() + 3600)); + $keys = array('a', 'b', 'c', 'd', 'e', 'f', 'g', 'x', 'y', 'z'); + $ids = array(); + foreach ($keys as $key) { + $ids[$key] = hash('fnv164', $key); + $this->assertFalse($this->_model->exists($ids[$key]), "paste $key does not yet exist"); + if (in_array($key, array('x', 'y', 'z'))) { + $this->assertTrue($this->_model->create($ids[$key], $paste), "store $key paste"); + } elseif ($key === 'x') { + $this->assertTrue($this->_model->create($ids[$key], Helper::getPaste()), "store $key paste"); + } else { + $this->assertTrue($this->_model->create($ids[$key], $expired), "store $key paste"); + } + $this->assertTrue($this->_model->exists($ids[$key]), "paste $key exists after storing it"); + } + $this->_model->purge(10); + foreach ($ids as $key => $id) { + if (in_array($key, array('x', 'y', 'z'))) { + $this->assertTrue($this->_model->exists($id), "paste $key exists after purge"); + $this->_model->delete($id); + } else { + $this->assertFalse($this->_model->exists($id), "paste $key was purged"); + } + } + } + + public function testErrorDetection() + { + $this->_model->delete(Helper::getPasteId()); + $paste = Helper::getPaste(2, array('expire' => "Invalid UTF-8 sequence: \xB1\x31")); + $this->assertFalse($this->_model->exists(Helper::getPasteId()), 'paste does not yet exist'); + $this->assertFalse($this->_model->create(Helper::getPasteId(), $paste), 'unable to store broken paste'); + $this->assertFalse($this->_model->exists(Helper::getPasteId()), 'paste does still not exist'); + } + + public function testCommentErrorDetection() + { + $this->_model->delete(Helper::getPasteId()); + $comment = Helper::getComment(1, array('nickname' => "Invalid UTF-8 sequence: \xB1\x31")); + $this->assertFalse($this->_model->exists(Helper::getPasteId()), 'paste does not yet exist'); + $this->assertTrue($this->_model->create(Helper::getPasteId(), Helper::getPaste()), 'store new paste'); + $this->assertTrue($this->_model->exists(Helper::getPasteId()), 'paste exists after storing it'); + $this->assertFalse($this->_model->existsComment(Helper::getPasteId(), Helper::getPasteId(), Helper::getCommentId()), 'comment does not yet exist'); + $this->assertFalse($this->_model->createComment(Helper::getPasteId(), Helper::getPasteId(), Helper::getCommentId(), $comment), 'unable to store broken comment'); + $this->assertFalse($this->_model->existsComment(Helper::getPasteId(), Helper::getPasteId(), Helper::getCommentId()), 'comment does still not exist'); + } +} + +/** + * Class StorageClientStub provides a limited stub for performing the unit test + */ +class StorageClientStub extends StorageClient +{ + private $_config = null; + private $_connection = null; + private $_buckets = array(); + + public function __construct(array $config = array()) + { + $this->_config = $config; + $this->_connection = new ConnectionInterfaceStub(); + } + + public function bucket($name, $userProject = false) + { + if (!key_exists($name, $this->_buckets)) { + $b = new BucketStub($this->_connection, $name, array(), $this); + $this->_buckets[$name] = $b; + } + return $this->_buckets[$name]; + } + + /** + * @throws \Google\Cloud\Core\Exception\NotFoundException + */ + public function deleteBucket($name) + { + if (key_exists($name, $this->_buckets)) { + unset($this->_buckets[$name]); + } else { + throw new NotFoundException(); + } + } + + public function buckets(array $options = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function registerStreamWrapper($protocol = null) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function unregisterStreamWrapper($protocol = null) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function signedUrlUploader($uri, $data, array $options = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function timestamp(\DateTimeInterface $timestamp, $nanoSeconds = null) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function getServiceAccount(array $options = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function hmacKeys(array $options = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function hmacKey($accessId, $projectId = null, array $metadata = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function createHmacKey($serviceAccountEmail, array $options = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function createBucket($name, array $options = array()) + { + if (key_exists($name, $this->_buckets)) { + throw new BadRequestException('already exists'); + } + $b = new BucketStub($this->_connection, $name, array(), $this); + $this->_buckets[$name] = $b; + return $b; + } +} + +/** + * Class BucketStub stubs a GCS bucket. + */ +class BucketStub extends Bucket +{ + public $_objects; + private $_name; + private $_info; + private $_connection; + private $_client; + + public function __construct(ConnectionInterface $connection, $name, array $info = array(), $client = null) + { + $this->_name = $name; + $this->_info = $info; + $this->_connection = $connection; + $this->_objects = array(); + $this->_client = $client; + } + + public function acl() + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function defaultAcl() + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function exists() + { + return true; + } + + public function upload($data, array $options = array()) + { + if (!is_string($data) || !key_exists('name', $options)) { + throw new BadMethodCallException('not supported by this stub'); + } + + $name = $options['name']; + $generation = '1'; + $o = new StorageObjectStub($this->_connection, $name, $this, $generation, $options); + $this->_objects[$options['name']] = $o; + $o->setData($data); + } + + public function uploadAsync($data, array $options = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function getResumableUploader($data, array $options = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function getStreamableUploader($data, array $options = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function object($name, array $options = array()) + { + if (key_exists($name, $this->_objects)) { + return $this->_objects[$name]; + } else { + return new StorageObjectStub($this->_connection, $name, $this, null, $options); + } + } + + public function objects(array $options = array()) + { + $prefix = key_exists('prefix', $options) ? $options['prefix'] : ''; + + return new CallbackFilterIterator( + new ArrayIterator($this->_objects), + function ($current, $key, $iterator) use ($prefix) { + return substr($key, 0, strlen($prefix)) == $prefix; + } + ); + } + + public function createNotification($topic, array $options = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function notification($id) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function notifications(array $options = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function delete(array $options = array()) + { + $this->_client->deleteBucket($this->_name); + } + + public function update(array $options = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function compose(array $sourceObjects, $name, array $options = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function info(array $options = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function reload(array $options = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function name() + { + return $this->_name; + } + + public static function lifecycle(array $lifecycle = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function currentLifecycle(array $options = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function isWritable($file = null) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function iam() + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function lockRetentionPolicy(array $options = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function signedUrl($expires, array $options = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function generateSignedPostPolicyV4($objectName, $expires, array $options = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } +} + +/** + * Class StorageObjectStub stubs a GCS storage object. + */ +class StorageObjectStub extends StorageObject +{ + private $_name; + private $_data; + private $_info; + private $_bucket; + private $_generation; + private $_exists = false; + private $_connection; + + public function __construct(ConnectionInterface $connection, $name, $bucket, $generation = null, array $info = array(), $encryptionKey = null, $encryptionKeySHA256 = null) + { + $this->_name = $name; + $this->_bucket = $bucket; + $this->_generation = $generation; + $this->_info = $info; + $this->_connection = $connection; + } + + public function acl() + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function exists(array $options = array()) + { + return key_exists($this->_name, $this->_bucket->_objects); + } + + /** + * @throws NotFoundException + */ + public function delete(array $options = array()) + { + if (key_exists($this->_name, $this->_bucket->_objects)) { + unset($this->_bucket->_objects[$this->_name]); + } else { + throw new NotFoundException('key ' . $this->_name . ' not found.'); + } + } + + /** + * @throws NotFoundException + */ + public function update(array $metadata, array $options = array()) + { + if (!$this->_exists) { + throw new NotFoundException('key ' . $this->_name . ' not found.'); + } + $this->_info = $metadata; + } + + public function copy($destination, array $options = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function rewrite($destination, array $options = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function rename($name, array $options = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + /** + * @throws NotFoundException + */ + public function downloadAsString(array $options = array()) + { + if (!$this->_exists) { + throw new NotFoundException('key ' . $this->_name . ' not found.'); + } + return $this->_data; + } + + public function downloadToFile($path, array $options = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function downloadAsStream(array $options = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function downloadAsStreamAsync(array $options = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function signedUrl($expires, array $options = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function signedUploadUrl($expires, array $options = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function beginSignedUploadSession(array $options = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function info(array $options = array()) + { + return key_exists('metadata',$this->_info) ? $this->_info['metadata'] : array(); + } + + public function reload(array $options = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function name() + { + return $this->_name; + } + + public function identity() + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function gcsUri() + { + return sprintf( + 'gs://%s/%s', + $this->_bucket->name(), + $this->_name + ); + } + + public function setData($data) + { + $this->_data = $data; + $this->_exists = true; + } +} + +/** + * Class ConnectionInterfaceStub required for the stubs. + */ +class ConnectionInterfaceStub implements ConnectionInterface +{ + public function deleteAcl(array $args = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function getAcl(array $args = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function listAcl(array $args = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function insertAcl(array $args = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function patchAcl(array $args = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function deleteBucket(array $args = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function getBucket(array $args = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function listBuckets(array $args = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function insertBucket(array $args = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function getBucketIamPolicy(array $args) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function setBucketIamPolicy(array $args) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function testBucketIamPermissions(array $args) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function patchBucket(array $args = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function deleteObject(array $args = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function copyObject(array $args = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function rewriteObject(array $args = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function composeObject(array $args = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function getObject(array $args = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function listObjects(array $args = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function patchObject(array $args = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function downloadObject(array $args = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function insertObject(array $args = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function getNotification(array $args = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function deleteNotification(array $args = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function insertNotification(array $args = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function listNotifications(array $args = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function getServiceAccount(array $args = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function lockRetentionPolicy(array $args = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function createHmacKey(array $args = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function deleteHmacKey(array $args = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function getHmacKey(array $args = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function updateHmacKey(array $args = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } + + public function listHmacKeys(array $args = array()) + { + throw new BadMethodCallException('not supported by this stub'); + } +} diff --git a/vendor/composer/ClassLoader.php b/vendor/composer/ClassLoader.php index fce8549f..247294d6 100644 --- a/vendor/composer/ClassLoader.php +++ b/vendor/composer/ClassLoader.php @@ -37,11 +37,13 @@ namespace Composer\Autoload; * * @author Fabien Potencier * @author Jordi Boggiano - * @see http://www.php-fig.org/psr/psr-0/ - * @see http://www.php-fig.org/psr/psr-4/ + * @see https://www.php-fig.org/psr/psr-0/ + * @see https://www.php-fig.org/psr/psr-4/ */ class ClassLoader { + private $vendorDir; + // PSR-4 private $prefixLengthsPsr4 = array(); private $prefixDirsPsr4 = array(); @@ -57,10 +59,17 @@ class ClassLoader private $missingClasses = array(); private $apcuPrefix; + private static $registeredLoaders = array(); + + public function __construct($vendorDir = null) + { + $this->vendorDir = $vendorDir; + } + public function getPrefixes() { if (!empty($this->prefixesPsr0)) { - return call_user_func_array('array_merge', $this->prefixesPsr0); + return call_user_func_array('array_merge', array_values($this->prefixesPsr0)); } return array(); @@ -300,6 +309,17 @@ class ClassLoader public function register($prepend = false) { spl_autoload_register(array($this, 'loadClass'), true, $prepend); + + if (null === $this->vendorDir) { + return; + } + + if ($prepend) { + self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders; + } else { + unset(self::$registeredLoaders[$this->vendorDir]); + self::$registeredLoaders[$this->vendorDir] = $this; + } } /** @@ -308,6 +328,10 @@ class ClassLoader public function unregister() { spl_autoload_unregister(array($this, 'loadClass')); + + if (null !== $this->vendorDir) { + unset(self::$registeredLoaders[$this->vendorDir]); + } } /** @@ -367,6 +391,16 @@ class ClassLoader return $file; } + /** + * Returns the currently registered loaders indexed by their corresponding vendor directories. + * + * @return self[] + */ + public static function getRegisteredLoaders() + { + return self::$registeredLoaders; + } + private function findFileWithExtension($class, $ext) { // PSR-4 lookup diff --git a/vendor/composer/InstalledVersions.php b/vendor/composer/InstalledVersions.php new file mode 100644 index 00000000..9766d9b4 --- /dev/null +++ b/vendor/composer/InstalledVersions.php @@ -0,0 +1,326 @@ + + array ( + 'pretty_version' => 'dev-master', + 'version' => 'dev-master', + 'aliases' => + array ( + ), + 'reference' => 'b6460616baa2d7f0a940dffd3f5b5efb9ecd00fd', + 'name' => 'privatebin/privatebin', + ), + 'versions' => + array ( + 'mlocati/ip-lib' => + array ( + 'pretty_version' => '1.14.0', + 'version' => '1.14.0.0', + 'aliases' => + array ( + ), + 'reference' => '882bc0e115970a536b13bcfa59f312783fce08c8', + ), + 'paragonie/random_compat' => + array ( + 'pretty_version' => 'v2.0.20', + 'version' => '2.0.20.0', + 'aliases' => + array ( + ), + 'reference' => '0f1f60250fccffeaf5dda91eea1c018aed1adc2a', + ), + 'privatebin/privatebin' => + array ( + 'pretty_version' => 'dev-master', + 'version' => 'dev-master', + 'aliases' => + array ( + ), + 'reference' => 'b6460616baa2d7f0a940dffd3f5b5efb9ecd00fd', + ), + 'yzalis/identicon' => + array ( + 'pretty_version' => '2.0.0', + 'version' => '2.0.0.0', + 'aliases' => + array ( + ), + 'reference' => 'ff5ed090129cab9bfa2a322857d4a01d107aa0ae', + ), + ), +); +private static $canGetVendors; +private static $installedByVendor = array(); + + + + + + + +public static function getInstalledPackages() +{ +$packages = array(); +foreach (self::getInstalled() as $installed) { +$packages[] = array_keys($installed['versions']); +} + +if (1 === \count($packages)) { +return $packages[0]; +} + +return array_keys(array_flip(\call_user_func_array('array_merge', $packages))); +} + + + + + + + + + +public static function isInstalled($packageName) +{ +foreach (self::getInstalled() as $installed) { +if (isset($installed['versions'][$packageName])) { +return true; +} +} + +return false; +} + + + + + + + + + + + + + + +public static function satisfies(VersionParser $parser, $packageName, $constraint) +{ +$constraint = $parser->parseConstraints($constraint); +$provided = $parser->parseConstraints(self::getVersionRanges($packageName)); + +return $provided->matches($constraint); +} + + + + + + + + + + +public static function getVersionRanges($packageName) +{ +foreach (self::getInstalled() as $installed) { +if (!isset($installed['versions'][$packageName])) { +continue; +} + +$ranges = array(); +if (isset($installed['versions'][$packageName]['pretty_version'])) { +$ranges[] = $installed['versions'][$packageName]['pretty_version']; +} +if (array_key_exists('aliases', $installed['versions'][$packageName])) { +$ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']); +} +if (array_key_exists('replaced', $installed['versions'][$packageName])) { +$ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']); +} +if (array_key_exists('provided', $installed['versions'][$packageName])) { +$ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']); +} + +return implode(' || ', $ranges); +} + +throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); +} + + + + + +public static function getVersion($packageName) +{ +foreach (self::getInstalled() as $installed) { +if (!isset($installed['versions'][$packageName])) { +continue; +} + +if (!isset($installed['versions'][$packageName]['version'])) { +return null; +} + +return $installed['versions'][$packageName]['version']; +} + +throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); +} + + + + + +public static function getPrettyVersion($packageName) +{ +foreach (self::getInstalled() as $installed) { +if (!isset($installed['versions'][$packageName])) { +continue; +} + +if (!isset($installed['versions'][$packageName]['pretty_version'])) { +return null; +} + +return $installed['versions'][$packageName]['pretty_version']; +} + +throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); +} + + + + + +public static function getReference($packageName) +{ +foreach (self::getInstalled() as $installed) { +if (!isset($installed['versions'][$packageName])) { +continue; +} + +if (!isset($installed['versions'][$packageName]['reference'])) { +return null; +} + +return $installed['versions'][$packageName]['reference']; +} + +throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); +} + + + + + +public static function getRootPackage() +{ +$installed = self::getInstalled(); + +return $installed[0]['root']; +} + + + + + + + + +public static function getRawData() +{ +@trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED); + +return self::$installed; +} + + + + + + + +public static function getAllRawData() +{ +return self::getInstalled(); +} + + + + + + + + + + + + + + + + + + + +public static function reload($data) +{ +self::$installed = $data; +self::$installedByVendor = array(); +} + + + + + +private static function getInstalled() +{ +if (null === self::$canGetVendors) { +self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders'); +} + +$installed = array(); + +if (self::$canGetVendors) { +foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) { +if (isset(self::$installedByVendor[$vendorDir])) { +$installed[] = self::$installedByVendor[$vendorDir]; +} elseif (is_file($vendorDir.'/composer/installed.php')) { +$installed[] = self::$installedByVendor[$vendorDir] = require $vendorDir.'/composer/installed.php'; +} +} +} + +$installed[] = self::$installed; + +return $installed; +} +} diff --git a/vendor/composer/autoload_classmap.php b/vendor/composer/autoload_classmap.php index 81358f5f..2abf65ec 100644 --- a/vendor/composer/autoload_classmap.php +++ b/vendor/composer/autoload_classmap.php @@ -6,6 +6,7 @@ $vendorDir = dirname(dirname(__FILE__)); $baseDir = dirname($vendorDir); return array( + 'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php', 'IPLib\\Address\\AddressInterface' => $vendorDir . '/mlocati/ip-lib/src/Address/AddressInterface.php', 'IPLib\\Address\\AssignedRange' => $vendorDir . '/mlocati/ip-lib/src/Address/AssignedRange.php', 'IPLib\\Address\\IPv4' => $vendorDir . '/mlocati/ip-lib/src/Address/IPv4.php', @@ -31,6 +32,7 @@ return array( 'PrivateBin\\Data\\AbstractData' => $baseDir . '/lib/Data/AbstractData.php', 'PrivateBin\\Data\\Database' => $baseDir . '/lib/Data/Database.php', 'PrivateBin\\Data\\Filesystem' => $baseDir . '/lib/Data/Filesystem.php', + 'PrivateBin\\Data\\GoogleCloudStorage' => $baseDir . '/lib/Data/GoogleCloudStorage.php', 'PrivateBin\\Filter' => $baseDir . '/lib/Filter.php', 'PrivateBin\\FormatV2' => $baseDir . '/lib/FormatV2.php', 'PrivateBin\\I18n' => $baseDir . '/lib/I18n.php', diff --git a/vendor/composer/autoload_real.php b/vendor/composer/autoload_real.php index 7a6fd4c0..fe68b5cb 100644 --- a/vendor/composer/autoload_real.php +++ b/vendor/composer/autoload_real.php @@ -22,13 +22,15 @@ class ComposerAutoloaderInitDontChange return self::$loader; } + require __DIR__ . '/platform_check.php'; + spl_autoload_register(array('ComposerAutoloaderInitDontChange', 'loadClassLoader'), true, true); - self::$loader = $loader = new \Composer\Autoload\ClassLoader(); + self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(\dirname(__FILE__))); spl_autoload_unregister(array('ComposerAutoloaderInitDontChange', 'loadClassLoader')); $useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded()); if ($useStaticLoader) { - require_once __DIR__ . '/autoload_static.php'; + require __DIR__ . '/autoload_static.php'; call_user_func(\Composer\Autoload\ComposerStaticInitDontChange::getInitializer($loader)); } else { diff --git a/vendor/composer/autoload_static.php b/vendor/composer/autoload_static.php index 9197c946..cfe595dd 100644 --- a/vendor/composer/autoload_static.php +++ b/vendor/composer/autoload_static.php @@ -38,6 +38,7 @@ class ComposerStaticInitDontChange ); public static $classMap = array ( + 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php', 'IPLib\\Address\\AddressInterface' => __DIR__ . '/..' . '/mlocati/ip-lib/src/Address/AddressInterface.php', 'IPLib\\Address\\AssignedRange' => __DIR__ . '/..' . '/mlocati/ip-lib/src/Address/AssignedRange.php', 'IPLib\\Address\\IPv4' => __DIR__ . '/..' . '/mlocati/ip-lib/src/Address/IPv4.php', @@ -63,6 +64,7 @@ class ComposerStaticInitDontChange 'PrivateBin\\Data\\AbstractData' => __DIR__ . '/../..' . '/lib/Data/AbstractData.php', 'PrivateBin\\Data\\Database' => __DIR__ . '/../..' . '/lib/Data/Database.php', 'PrivateBin\\Data\\Filesystem' => __DIR__ . '/../..' . '/lib/Data/Filesystem.php', + 'PrivateBin\\Data\\GoogleCloudStorage' => __DIR__ . '/../..' . '/lib/Data/GoogleCloudStorage.php', 'PrivateBin\\Filter' => __DIR__ . '/../..' . '/lib/Filter.php', 'PrivateBin\\FormatV2' => __DIR__ . '/../..' . '/lib/FormatV2.php', 'PrivateBin\\I18n' => __DIR__ . '/../..' . '/lib/I18n.php', diff --git a/vendor/composer/installed.php b/vendor/composer/installed.php new file mode 100644 index 00000000..56f2a5e4 --- /dev/null +++ b/vendor/composer/installed.php @@ -0,0 +1,51 @@ + + array ( + 'pretty_version' => 'dev-master', + 'version' => 'dev-master', + 'aliases' => + array ( + ), + 'reference' => 'b6460616baa2d7f0a940dffd3f5b5efb9ecd00fd', + 'name' => 'privatebin/privatebin', + ), + 'versions' => + array ( + 'mlocati/ip-lib' => + array ( + 'pretty_version' => '1.14.0', + 'version' => '1.14.0.0', + 'aliases' => + array ( + ), + 'reference' => '882bc0e115970a536b13bcfa59f312783fce08c8', + ), + 'paragonie/random_compat' => + array ( + 'pretty_version' => 'v2.0.20', + 'version' => '2.0.20.0', + 'aliases' => + array ( + ), + 'reference' => '0f1f60250fccffeaf5dda91eea1c018aed1adc2a', + ), + 'privatebin/privatebin' => + array ( + 'pretty_version' => 'dev-master', + 'version' => 'dev-master', + 'aliases' => + array ( + ), + 'reference' => 'b6460616baa2d7f0a940dffd3f5b5efb9ecd00fd', + ), + 'yzalis/identicon' => + array ( + 'pretty_version' => '2.0.0', + 'version' => '2.0.0.0', + 'aliases' => + array ( + ), + 'reference' => 'ff5ed090129cab9bfa2a322857d4a01d107aa0ae', + ), + ), +); diff --git a/vendor/composer/platform_check.php b/vendor/composer/platform_check.php new file mode 100644 index 00000000..8b379f44 --- /dev/null +++ b/vendor/composer/platform_check.php @@ -0,0 +1,26 @@ += 50600)) { + $issues[] = 'Your Composer dependencies require a PHP version ">= 5.6.0". You are running ' . PHP_VERSION . '.'; +} + +if ($issues) { + if (!headers_sent()) { + header('HTTP/1.1 500 Internal Server Error'); + } + if (!ini_get('display_errors')) { + if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') { + fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL); + } elseif (!headers_sent()) { + echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL; + } + } + trigger_error( + 'Composer detected issues in your platform: ' . implode(' ', $issues), + E_USER_ERROR + ); +} From d355bb87e361787beb3258c9da5bc906ebc774db Mon Sep 17 00:00:00 2001 From: El RIDO Date: Sun, 30 May 2021 08:11:45 +0200 Subject: [PATCH 02/15] documenting changes --- CHANGELOG.md | 3 ++- CREDITS.md | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a8ed407..84f630e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,8 @@ * ADDED: new HTTP headers improving security (#765) * ADDED: Download button for paste text (#774) * ADDED: Opt-out of federated learning of cohorts (FLoC) (#776) - * ADDED: Configuration option to exempt ips from the rate-limiter (#787) + * ADDED: Configuration option to exempt IPs from the rate-limiter (#787) + * ADDED: Google Cloud Storage backend support (#795) * CHANGED: Language selection cookie only transmitted over HTTPS (#472) * CHANGED: Upgrading libraries to: random_compat 2.0.20 * **1.3.5 (2021-04-05)** diff --git a/CREDITS.md b/CREDITS.md index 338c2df6..612749c0 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -13,7 +13,7 @@ Sébastien Sauvage - original idea and main developer * Alexey Gladkov - syntax highlighting * Greg Knaddison - robots.txt * MrKooky - HTML5 markup, CSS cleanup -* Simon Rupf - WebCrypto, unit tests, current docker containers, MVC, configuration, i18n +* Simon Rupf - WebCrypto, unit tests, containers images, database backend, MVC, configuration, i18n * Hexalyse - Password protection * Viktor Stanchev - File upload support * azlux - Tab character input support @@ -28,6 +28,7 @@ Sébastien Sauvage - original idea and main developer * Haocen - lots of bugfixes and UI improvements * Lucas Savva - configurable config file location, NixOS packaging * rodehoed - option to exempt ips from the rate-limiter +* Mark van Holsteijn - Google Cloud Storage backend ## Translations * Hexalyse - French From 33587d54e4b56738c543487a999d8b6d27dc61ed Mon Sep 17 00:00:00 2001 From: El RIDO Date: Sun, 30 May 2021 09:17:23 +0200 Subject: [PATCH 03/15] fix composer test on PHP 8 --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 0d1e3549..a8c94ae1 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -10,7 +10,7 @@ jobs: - name: Validate composer.json and composer.lock run: composer validate - name: Install dependencies - run: composer install --prefer-dist --no-suggest + run: composer install --prefer-dist --no-dev - name: Install Google Cloud Storage run: composer require google/cloud-storage PHPunit: From fc5e380ccc249a619f60ad182a17c55a49140c42 Mon Sep 17 00:00:00 2001 From: El RIDO Date: Sun, 30 May 2021 09:18:56 +0200 Subject: [PATCH 04/15] fix composer test on PHP 8 --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a8c94ae1..aff27df1 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -12,7 +12,7 @@ jobs: - name: Install dependencies run: composer install --prefer-dist --no-dev - name: Install Google Cloud Storage - run: composer require google/cloud-storage + run: composer require google/cloud-storage --no-dev PHPunit: runs-on: ubuntu-latest strategy: From 93138cbbae6d1ada37f3dcb5cba6a4b77a71d0f4 Mon Sep 17 00:00:00 2001 From: El RIDO Date: Sun, 30 May 2021 09:26:13 +0200 Subject: [PATCH 05/15] we already test this via the regular unit tests --- .github/workflows/tests.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index aff27df1..73fa11aa 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -11,8 +11,6 @@ jobs: run: composer validate - name: Install dependencies run: composer install --prefer-dist --no-dev - - name: Install Google Cloud Storage - run: composer require google/cloud-storage --no-dev PHPunit: runs-on: ubuntu-latest strategy: From 8bc97517fb16499b0cd20a14cd01ad286349ded8 Mon Sep 17 00:00:00 2001 From: rugk Date: Fri, 4 Jun 2021 23:43:01 +0200 Subject: [PATCH 06/15] Add Snyk security scan for PHP After I found https://github.com/PrivateBin/docker-nginx-fpm-alpine/pull/44 I saw they also support PHP, so let's do it here (one level before container packaging), too. Also it complements the CodeQL analysis, which only covers the JS part. I added the API token to the PrivateBIn org now. --- .github/workflows/snyk-scan.yml | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 .github/workflows/snyk-scan.yml diff --git a/.github/workflows/snyk-scan.yml b/.github/workflows/snyk-scan.yml new file mode 100644 index 00000000..af0afbd7 --- /dev/null +++ b/.github/workflows/snyk-scan.yml @@ -0,0 +1,27 @@ +# This is a basic workflow to help you get started with Actions + +name: Snyk scan + +on: + # Triggers the workflow on push or pull request events but only for the master branch + push: + branches: [ master ] + pull_request: + branches: [ master ] +jobs: + # https://github.com/snyk/actions/tree/master/php + snyk-php: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@master + - name: Run Snyk to check for vulnerabilities + uses: snyk/actions/php@master + continue-on-error: true # To make sure that SARIF upload gets called + env: + SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} + with: + args: --sarif-file-output=snyk.sarif + - name: Upload result to GitHub Code Scanning + uses: github/codeql-action/upload-sarif@v1 + with: + sarif_file: snyk.sarif From ffe48092fecde86474a194b05c5611edfd5fa593 Mon Sep 17 00:00:00 2001 From: El RIDO Date: Sat, 5 Jun 2021 05:38:05 +0200 Subject: [PATCH 07/15] suppress error_log output of GoogleCloudStorage class in unit testing --- tst/Data/GoogleCloudStorageTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tst/Data/GoogleCloudStorageTest.php b/tst/Data/GoogleCloudStorageTest.php index 91a5ca99..6905f04b 100644 --- a/tst/Data/GoogleCloudStorageTest.php +++ b/tst/Data/GoogleCloudStorageTest.php @@ -34,6 +34,7 @@ class GoogleCloudStorageTest extends PHPUnit_Framework_TestCase // do not report E_NOTICE as fsouza/fake-gcs-server does not return a `generation` value in the response // which the Google Cloud Storage PHP library expects. error_reporting(E_ERROR | E_WARNING | E_PARSE); + ini_set('error_log', stream_get_meta_data(tmpfile())['uri']); $this->_model = GoogleCloudStorage::getInstance(array( 'bucket' => self::$_bucket->name(), 'prefix' => 'pastes', From edb8e5e078f2557716a17c8e92f91eb657d3b728 Mon Sep 17 00:00:00 2001 From: El RIDO Date: Sat, 5 Jun 2021 05:48:17 +0200 Subject: [PATCH 08/15] handle edge cases with file locking: file needs to exist before it can be locked, fixes #803 --- lib/Persistence/AbstractPersistence.php | 28 +++++++++++++++++-------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/lib/Persistence/AbstractPersistence.php b/lib/Persistence/AbstractPersistence.php index 0dcef50e..c96a0de3 100644 --- a/lib/Persistence/AbstractPersistence.php +++ b/lib/Persistence/AbstractPersistence.php @@ -90,12 +90,15 @@ abstract class AbstractPersistence } $file = self::$_path . DIRECTORY_SEPARATOR . '.htaccess'; if (!is_file($file)) { - $writtenBytes = @file_put_contents( - $file, - 'Require all denied' . PHP_EOL, - LOCK_EX - ); - if ($writtenBytes === false || $writtenBytes < 19) { + $writtenBytes = 0; + if ($fileCreated = @touch($file)) { + $writtenBytes = @file_put_contents( + $file, + 'Require all denied' . PHP_EOL, + LOCK_EX + ); + } + if ($fileCreated === false || $writtenBytes === false || $writtenBytes < 19) { throw new Exception('unable to write to file ' . $file, 11); } } @@ -114,9 +117,16 @@ abstract class AbstractPersistence protected static function _store($filename, $data) { self::_initialize(); - $file = self::$_path . DIRECTORY_SEPARATOR . $filename; - $writtenBytes = @file_put_contents($file, $data, LOCK_EX); - if ($writtenBytes === false || $writtenBytes < strlen($data)) { + $file = self::$_path . DIRECTORY_SEPARATOR . $filename; + $fileCreated = true; + $writtenBytes = 0; + if (!is_file($file)) { + $fileCreated = @touch($file); + } + if ($fileCreated) { + $writtenBytes = @file_put_contents($file, $data, LOCK_EX); + } + if ($fileCreated === false || $writtenBytes === false || $writtenBytes < strlen($data)) { throw new Exception('unable to write to file ' . $file, 13); } @chmod($file, 0640); // protect file access From abb2b90e9b13e09ee8853a6ff20f5617a4aadd94 Mon Sep 17 00:00:00 2001 From: El RIDO Date: Sat, 5 Jun 2021 05:52:13 +0200 Subject: [PATCH 09/15] make StyleCI happy --- lib/Persistence/AbstractPersistence.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Persistence/AbstractPersistence.php b/lib/Persistence/AbstractPersistence.php index c96a0de3..489836da 100644 --- a/lib/Persistence/AbstractPersistence.php +++ b/lib/Persistence/AbstractPersistence.php @@ -117,8 +117,8 @@ abstract class AbstractPersistence protected static function _store($filename, $data) { self::_initialize(); - $file = self::$_path . DIRECTORY_SEPARATOR . $filename; - $fileCreated = true; + $file = self::$_path . DIRECTORY_SEPARATOR . $filename; + $fileCreated = true; $writtenBytes = 0; if (!is_file($file)) { $fileCreated = @touch($file); From 371dca19868464f3adf6a52a54423bd4f3652125 Mon Sep 17 00:00:00 2001 From: El RIDO Date: Sat, 5 Jun 2021 08:10:12 +0200 Subject: [PATCH 10/15] ensure the GCS library and dependencies get included in the scan --- .github/workflows/snyk-scan.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/snyk-scan.yml b/.github/workflows/snyk-scan.yml index af0afbd7..15e2beec 100644 --- a/.github/workflows/snyk-scan.yml +++ b/.github/workflows/snyk-scan.yml @@ -14,6 +14,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@master + - name: Install Google Cloud Storage + run: composer require google/cloud-storage - name: Run Snyk to check for vulnerabilities uses: snyk/actions/php@master continue-on-error: true # To make sure that SARIF upload gets called From cbdcaf4c307b9894fae4a995d98924b3103a0623 Mon Sep 17 00:00:00 2001 From: El RIDO Date: Sat, 5 Jun 2021 08:14:04 +0200 Subject: [PATCH 11/15] fix snyk --- .github/workflows/snyk-scan.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/snyk-scan.yml b/.github/workflows/snyk-scan.yml index 15e2beec..3d3e3e7f 100644 --- a/.github/workflows/snyk-scan.yml +++ b/.github/workflows/snyk-scan.yml @@ -15,7 +15,7 @@ jobs: steps: - uses: actions/checkout@master - name: Install Google Cloud Storage - run: composer require google/cloud-storage + run: composer require --no-dev google/cloud-storage - name: Run Snyk to check for vulnerabilities uses: snyk/actions/php@master continue-on-error: true # To make sure that SARIF upload gets called From 7a3a306ddcbf0cef045444b7ecd7aaab671a5f0d Mon Sep 17 00:00:00 2001 From: El RIDO Date: Sat, 5 Jun 2021 08:22:50 +0200 Subject: [PATCH 12/15] fix snyk --- .github/workflows/snyk-scan.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/snyk-scan.yml b/.github/workflows/snyk-scan.yml index 3d3e3e7f..91ae0924 100644 --- a/.github/workflows/snyk-scan.yml +++ b/.github/workflows/snyk-scan.yml @@ -15,7 +15,7 @@ jobs: steps: - uses: actions/checkout@master - name: Install Google Cloud Storage - run: composer require --no-dev google/cloud-storage + run: composer install --no-dev google/cloud-storage - name: Run Snyk to check for vulnerabilities uses: snyk/actions/php@master continue-on-error: true # To make sure that SARIF upload gets called From 197c4a34e8a0e303cc46743834b65634cef27a3a Mon Sep 17 00:00:00 2001 From: El RIDO Date: Sat, 5 Jun 2021 08:25:19 +0200 Subject: [PATCH 13/15] fix snyk --- .github/workflows/snyk-scan.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/snyk-scan.yml b/.github/workflows/snyk-scan.yml index 91ae0924..4730eec0 100644 --- a/.github/workflows/snyk-scan.yml +++ b/.github/workflows/snyk-scan.yml @@ -15,7 +15,7 @@ jobs: steps: - uses: actions/checkout@master - name: Install Google Cloud Storage - run: composer install --no-dev google/cloud-storage + run: composer require --no-update google/cloud-storage && composer update --no-dev - name: Run Snyk to check for vulnerabilities uses: snyk/actions/php@master continue-on-error: true # To make sure that SARIF upload gets called From a2ffbafa136fb8db83e956c2a63f4974f9f6103f Mon Sep 17 00:00:00 2001 From: El RIDO Date: Sat, 5 Jun 2021 09:43:01 +0200 Subject: [PATCH 14/15] ensure npm's package.json version gets incremented --- Makefile | 2 +- js/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 691d72bf..83a6b6de 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ CURRENT_VERSION = 1.3.5 VERSION ?= 1.3.6 -VERSION_FILES = index.php cfg/ *.md css/ i18n/ img/ js/privatebin.js lib/ Makefile tpl/ tst/ +VERSION_FILES = index.php cfg/ *.md css/ i18n/ img/ js/package.json js/privatebin.js lib/ Makefile tpl/ tst/ REGEX_CURRENT_VERSION := $(shell echo $(CURRENT_VERSION) | sed "s/\./\\\./g") REGEX_VERSION := $(shell echo $(VERSION) | sed "s/\./\\\./g") diff --git a/js/package.json b/js/package.json index 489cc677..53e85dc3 100644 --- a/js/package.json +++ b/js/package.json @@ -1,6 +1,6 @@ { "name": "privatebin", - "version": "1.3.0", + "version": "1.3.5", "description": "PrivateBin is a minimalist, open source online pastebin where the server has zero knowledge of pasted data. Data is encrypted/decrypted in the browser using 256 bit AES in Galois Counter mode (GCM).", "main": "privatebin.js", "directories": { From 2bc54caa071f35bf2e2f68dbe5df579fc2f8f59a Mon Sep 17 00:00:00 2001 From: El RIDO Date: Sat, 5 Jun 2021 10:33:01 +0200 Subject: [PATCH 15/15] fix never matched condition, kudos @ShiftLeftSecurity, found via #807 --- lib/Request.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Request.php b/lib/Request.php index cfa883ad..5776cabe 100644 --- a/lib/Request.php +++ b/lib/Request.php @@ -288,7 +288,7 @@ class Request } krsort($mediaTypes); foreach ($mediaTypes as $acceptedQuality => $acceptedValues) { - if ($acceptedQuality === 0.0) { + if ($acceptedQuality === '0.0') { continue; } foreach ($acceptedValues as $acceptedValue) {