| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383 |
- <?php declare(strict_types=1);
- /**
- * PrivateBin
- *
- * a zero-knowledge paste bin
- *
- * @link https://github.com/PrivateBin/PrivateBin
- * @copyright 2012 Sébastien SAUVAGE (sebsauvage.net)
- * @license https://www.opensource.org/licenses/zlib-license.php The zlib/libpng License
- */
- namespace PrivateBin\Data;
- use Exception;
- use Google\Cloud\Core\Exception\NotFoundException;
- use Google\Cloud\Storage\Bucket;
- use Google\Cloud\Storage\StorageClient;
- use PrivateBin\Exception\JsonException;
- use PrivateBin\Json;
- class GoogleCloudStorage extends AbstractData
- {
- /**
- * GCS client
- *
- * @access private
- * @var StorageClient
- */
- private $_client = null;
- /**
- * GCS bucket
- *
- * @access private
- * @var Bucket
- */
- private $_bucket = null;
- /**
- * object prefix
- *
- * @access private
- * @var string
- */
- private $_prefix = 'pastes';
- /**
- * bucket acl type
- *
- * @access private
- * @var bool
- */
- private $_uniformacl = false;
- /**
- * instantiantes a new Google Cloud Storage data backend.
- *
- * @access public
- * @param array $options
- */
- public function __construct(array $options)
- {
- if (getenv('PRIVATEBIN_GCS_BUCKET')) {
- $bucket = getenv('PRIVATEBIN_GCS_BUCKET');
- }
- if (array_key_exists('bucket', $options)) {
- $bucket = $options['bucket'];
- }
- if (array_key_exists('prefix', $options)) {
- $this->_prefix = $options['prefix'];
- }
- if (array_key_exists('uniformacl', $options)) {
- $this->_uniformacl = $options['uniformacl'];
- }
- $this->_client = class_exists('StorageClientStub', false) ?
- new \StorageClientStub(array()) :
- new StorageClient(array('suppressKeyFileNotice' => true));
- if (isset($bucket)) {
- $this->_bucket = $this->_client->bucket($bucket);
- }
- }
- /**
- * returns the google storage object key for $pasteid in $this->_bucket.
- *
- * @access private
- * @param $pasteid string to get the key for
- * @return string
- */
- private function _getKey($pasteid)
- {
- if (!empty($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 field 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 = $payload['meta'] ?? array();
- unset($metadata['salt']);
- foreach ($metadata as $k => $v) {
- $metadata[$k] = strval($v);
- }
- try {
- $data = array(
- 'name' => $key,
- 'chunkSize' => 262144,
- 'metadata' => array(
- 'content-type' => 'application/json',
- 'metadata' => $metadata,
- ),
- );
- if (!$this->_uniformacl) {
- $data['predefinedAcl'] = 'private';
- }
- $this->_bucket->upload(Json::encode($payload), $data);
- } 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) {
- $data = $this->_bucket->object($key->name())->downloadAsString();
- try {
- $comment = Json::decode($data);
- } catch (JsonException $e) {
- error_log('failed to read comment from ' . $key->name() . ', ' . $e->getMessage());
- $comment = array();
- }
- $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
- */
- public function purgeValues($namespace, $time)
- {
- $path = 'config/' . $namespace;
- try {
- foreach ($this->_bucket->objects(array('prefix' => $path)) as $object) {
- $name = $object->name();
- if (strlen($name) > strlen($path) && substr($name, strlen($path), 1) !== '/') {
- continue;
- }
- $value = $object->info()['metadata']['value'] ?? '';
- if (is_numeric($value) && intval($value) < $time) {
- try {
- $object->delete();
- } catch (NotFoundException $e) {
- // deleted by another instance.
- }
- }
- }
- } catch (NotFoundException $e) {
- // no objects in the bucket yet
- }
- }
- /**
- * For GoogleCloudStorage, the value will also be stored in the metadata for the
- * namespaces traffic_limiter and purge_limiter.
- * @inheritDoc
- */
- public function setValue($value, $namespace, $key = '')
- {
- if (empty($key)) {
- $key = 'config/' . $namespace;
- } else {
- $key = 'config/' . $namespace . '/' . $key;
- }
- $metadata = array('namespace' => $namespace);
- if ($namespace !== 'salt') {
- $metadata['value'] = strval($value);
- }
- try {
- $data = array(
- 'name' => $key,
- 'chunkSize' => 262144,
- 'metadata' => array(
- 'content-type' => 'application/json',
- 'metadata' => $metadata,
- ),
- );
- if (!$this->_uniformacl) {
- $data['predefinedAcl'] = 'private';
- }
- $this->_bucket->upload($value, $data);
- } catch (Exception $e) {
- error_log('failed to set key ' . $key . ' to ' . $this->_bucket->name() . ', ' .
- trim(preg_replace('/\s\s+/', ' ', $e->getMessage())));
- return false;
- }
- return true;
- }
- /**
- * @inheritDoc
- */
- public function getValue($namespace, $key = '')
- {
- if ($key === '') {
- $key = 'config/' . $namespace;
- } else {
- $key = 'config/' . $namespace . '/' . $key;
- }
- try {
- $o = $this->_bucket->object($key);
- return $o->downloadAsString();
- } catch (NotFoundException $e) {
- return '';
- }
- }
- /**
- * @inheritDoc
- */
- protected function _getExpiredPastes($batchsize)
- {
- $expired = array();
- $now = time();
- $prefix = $this->_prefix;
- if (!empty($prefix)) {
- $prefix .= '/';
- }
- try {
- foreach ($this->_bucket->objects(array('prefix' => $prefix)) as $object) {
- $expire_at = $object->info()['metadata']['expire_date'] ?? '';
- if (is_numeric($expire_at) && intval($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;
- }
- /**
- * @inheritDoc
- */
- public function getAllPastes()
- {
- $pastes = array();
- $prefix = $this->_prefix;
- if (!empty($prefix)) {
- $prefix .= '/';
- }
- try {
- foreach ($this->_bucket->objects(array('prefix' => $prefix)) as $object) {
- $candidate = substr($object->name(), strlen($prefix));
- if (!str_contains($candidate, '/')) {
- $pastes[] = $candidate;
- }
- }
- } catch (NotFoundException $e) {
- // no objects in the bucket yet
- }
- return $pastes;
- }
- }
|