diff --git a/.github/workflows/snyk-scan.yml b/.github/workflows/snyk-scan.yml new file mode 100644 index 0000000..4730eec --- /dev/null +++ b/.github/workflows/snyk-scan.yml @@ -0,0 +1,29 @@ +# 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: Install 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 + 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 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c1ac3d9..73fa11a 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: /usr/bin/php7.4 $(which composer) install --prefer-dist --no-suggest + run: composer install --prefer-dist --no-dev PHPunit: runs-on: ubuntu-latest strategy: @@ -29,6 +29,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 a4cd2bb..65ef718 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 index e3c8fe1..cf69fbd 100644 --- a/.scrutinizer.yml +++ b/.scrutinizer.yml @@ -21,7 +21,7 @@ build: tests: override: - - command: 'cd tst && ../vendor/bin/phpunit' + command: 'composer require google/cloud-storage && cd tst && ../vendor/bin/phpunit' coverage: file: 'tst/log/coverage-clover.xml' format: 'clover' diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a8ed40..84f630e 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 338c2df..612749c 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 diff --git a/INSTALL.md b/INSTALL.md index df0cac2..70abf18 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -190,4 +190,21 @@ CREATE TABLE prefix_config ( 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. +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 in the Google Cloud, 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 +library using the command `composer require google/cloud-storage`. diff --git a/Makefile b/Makefile index 691d72b..83a6b6d 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/cfg/conf.sample.php b/cfg/conf.sample.php index e9e500b..a4b7f6b 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 a8e98aa..7b09fc3 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" }, diff --git a/composer.lock b/composer.lock index 931c92f..7a60520 100644 --- a/composer.lock +++ b/composer.lock @@ -1419,16 +1419,16 @@ }, { "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 +1440,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.22-dev" + "dev-main": "1.23-dev" }, "thanks": { "name": "symfony/polyfill", @@ -1491,7 +1491,7 @@ "type": "tidelift" } ], - "time": "2021-01-07T16:49:33+00:00" + "time": "2021-02-19T12:13:01+00:00" }, { "name": "symfony/yaml", diff --git a/js/package.json b/js/package.json index 489cc67..53e85dc 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": { diff --git a/lib/Configuration.php b/lib/Configuration.php index 5de6de3..1e92fc9 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 0000000..1a1d8bf --- /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/AbstractPersistence.php b/lib/Persistence/AbstractPersistence.php index 0dcef50..489836d 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); } } @@ -115,8 +118,15 @@ abstract class AbstractPersistence { self::_initialize(); $file = self::$_path . DIRECTORY_SEPARATOR . $filename; - $writtenBytes = @file_put_contents($file, $data, LOCK_EX); - if ($writtenBytes === false || $writtenBytes < strlen($data)) { + $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 diff --git a/lib/Request.php b/lib/Request.php index cfa883a..5776cab 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) { diff --git a/tst/Data/GoogleCloudStorageTest.php b/tst/Data/GoogleCloudStorageTest.php new file mode 100644 index 0000000..6905f04 --- /dev/null +++ b/tst/Data/GoogleCloudStorageTest.php @@ -0,0 +1,716 @@ +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); + ini_set('error_log', stream_get_meta_data(tmpfile())['uri']); + $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/autoload_classmap.php b/vendor/composer/autoload_classmap.php index 81358f5..2693674 100644 --- a/vendor/composer/autoload_classmap.php +++ b/vendor/composer/autoload_classmap.php @@ -31,6 +31,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_static.php b/vendor/composer/autoload_static.php index 9197c94..58f7a58 100644 --- a/vendor/composer/autoload_static.php +++ b/vendor/composer/autoload_static.php @@ -63,6 +63,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',