Kaynağa Gözat

added Google Cloud Storage support

Mark van Holsteijn 5 yıl önce
ebeveyn
işleme
342270d6dd

+ 1 - 0
.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

+ 4 - 0
.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

+ 3 - 1
.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

+ 39 - 0
.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

+ 12 - 0
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.

+ 7 - 0
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

+ 4 - 1
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"
 	}
-}
+}

+ 125 - 7
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"
 }

+ 10 - 0
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

+ 251 - 0
lib/Data/GoogleCloudStorage.php

@@ -0,0 +1,251 @@
+<?php
+
+namespace PrivateBin\Data;
+
+use Exception;
+use Google\Cloud\Core\Exception\NotFoundException;
+use Google\Cloud\Storage\StorageClient;
+use PrivateBin\Json;
+
+class GoogleCloudStorage extends AbstractData
+{
+    /**
+     * returns a Google Cloud Storage data backend.
+     *
+     * @access public
+     * @static
+     * @param array $options
+     * @return GoogleCloudStorage
+     */
+    public static function getInstance(array $options)
+    {
+        $client = null;
+        $bucket = null;
+        $prefix = 'pastes';
+
+        if (getenv('PRIVATEBIN_GCS_BUCKET')) {
+            $bucket = getenv('PRIVATEBIN_GCS_BUCKET');
+        }
+        if (is_array($options) && array_key_exists('bucket', $options)) {
+            $bucket = $options['bucket'];
+        }
+        if (is_array($options) && array_key_exists('prefix', $options)) {
+            $prefix = $options['prefix'];
+        }
+        if (is_array($options) && array_key_exists('client', $options)) {
+            $client = $options['client'];
+        }
+
+        if (!(self::$_instance instanceof self)) {
+            self::$_instance = new self($bucket, $prefix, $client);
+        }
+        return self::$_instance;
+    }
+
+    protected $_client = null;
+    protected $_bucket = null;
+    protected $_prefix = 'pastes';
+
+    public function __construct($bucket, $prefix, $client = null)
+    {
+        parent::__construct();
+        if ($client == null) {
+            $this->_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;
+    }
+}

+ 2 - 2
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;
         }
 

+ 715 - 0
tst/Data/GoogleCloudStorageTest.php

@@ -0,0 +1,715 @@
+<?php
+
+use Google\Auth\HttpHandler\HttpHandlerFactory;
+use Google\Cloud\Core\Exception\BadRequestException;
+use Google\Cloud\Core\Exception\NotFoundException;
+use Google\Cloud\Storage\Bucket;
+use Google\Cloud\Storage\Connection\ConnectionInterface;
+use Google\Cloud\Storage\StorageClient;
+use Google\Cloud\Storage\StorageObject;
+use GuzzleHttp\Client;
+use PrivateBin\Data\GoogleCloudStorage;
+
+class GoogleCloudStorageTest extends PHPUnit_Framework_TestCase
+{
+    private static $_client;
+    private static $_bucket;
+
+    public static function setUpBeforeClass()
+    {
+        $httpClient = new Client(array('debug'=>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');
+    }
+}

+ 37 - 3
vendor/composer/ClassLoader.php

@@ -37,11 +37,13 @@ namespace Composer\Autoload;
  *
  * @author Fabien Potencier <fabien@symfony.com>
  * @author Jordi Boggiano <j.boggiano@seld.be>
- * @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

+ 326 - 0
vendor/composer/InstalledVersions.php

@@ -0,0 +1,326 @@
+<?php
+
+
+
+
+
+
+
+
+
+
+
+namespace Composer;
+
+use Composer\Autoload\ClassLoader;
+use Composer\Semver\VersionParser;
+
+
+
+
+
+
+
+
+class InstalledVersions
+{
+private static $installed = array (
+  'root' => 
+  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;
+}
+}

+ 2 - 0
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',

+ 4 - 2
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 {

+ 2 - 0
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',

+ 51 - 0
vendor/composer/installed.php

@@ -0,0 +1,51 @@
+<?php return array (
+  'root' => 
+  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',
+    ),
+  ),
+);

+ 26 - 0
vendor/composer/platform_check.php

@@ -0,0 +1,26 @@
+<?php
+
+// platform_check.php @generated by Composer
+
+$issues = array();
+
+if (!(PHP_VERSION_ID >= 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
+    );
+}