Issues (14)

lib/Data/GoogleCloudStorage.php (3 issues)

1
<?php declare(strict_types=1);
2
/**
3
 * PrivateBin
4
 *
5
 * a zero-knowledge paste bin
6
 *
7
 * @link      https://github.com/PrivateBin/PrivateBin
8
 * @copyright 2012 Sébastien SAUVAGE (sebsauvage.net)
9
 * @license   https://www.opensource.org/licenses/zlib-license.php The zlib/libpng License
10
 */
11
12
namespace PrivateBin\Data;
13
14
use Exception;
15
use Google\Cloud\Core\Exception\NotFoundException;
0 ignored issues
show
The type Google\Cloud\Core\Exception\NotFoundException was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
16
use Google\Cloud\Storage\Bucket;
0 ignored issues
show
The type Google\Cloud\Storage\Bucket was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
17
use Google\Cloud\Storage\StorageClient;
0 ignored issues
show
The type Google\Cloud\Storage\StorageClient was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
18
use PrivateBin\Json;
19
20
class GoogleCloudStorage extends AbstractData
21
{
22
    /**
23
     * GCS client
24
     *
25
     * @access private
26
     * @var    StorageClient
27
     */
28
    private $_client = null;
29
30
    /**
31
     * GCS bucket
32
     *
33
     * @access private
34
     * @var    Bucket
35
     */
36
    private $_bucket = null;
37
38
    /**
39
     * object prefix
40
     *
41
     * @access private
42
     * @var    string
43
     */
44
    private $_prefix = 'pastes';
45
46
    /**
47
     * bucket acl type
48
     *
49
     * @access private
50
     * @var    bool
51
     */
52
    private $_uniformacl = false;
53
54
    /**
55
     * instantiantes a new Google Cloud Storage data backend.
56
     *
57
     * @access public
58
     * @param array $options
59
     */
60 44
    public function __construct(array $options)
61
    {
62 44
        if (getenv('PRIVATEBIN_GCS_BUCKET')) {
63
            $bucket = getenv('PRIVATEBIN_GCS_BUCKET');
64
        }
65 44
        if (is_array($options) && array_key_exists('bucket', $options)) {
66 44
            $bucket = $options['bucket'];
67
        }
68 44
        if (is_array($options) && array_key_exists('prefix', $options)) {
69 44
            $this->_prefix = $options['prefix'];
70
        }
71 44
        if (is_array($options) && array_key_exists('uniformacl', $options)) {
72 32
            $this->_uniformacl = $options['uniformacl'];
73
        }
74
75 44
        $this->_client = class_exists('StorageClientStub', false) ?
76 44
            new \StorageClientStub(array()) :
77
            new StorageClient(array('suppressKeyFileNotice' => true));
78 44
        if (isset($bucket)) {
79 44
            $this->_bucket = $this->_client->bucket($bucket);
80
        }
81
    }
82
83
    /**
84
     * returns the google storage object key for $pasteid in $this->_bucket.
85
     *
86
     * @access private
87
     * @param $pasteid string to get the key for
88
     * @return string
89
     */
90 42
    private function _getKey($pasteid)
91
    {
92 42
        if ($this->_prefix != '') {
93 42
            return $this->_prefix . '/' . $pasteid;
94
        }
95
        return $pasteid;
96
    }
97
98
    /**
99
     * Uploads the payload in the $this->_bucket under the specified key.
100
     * The entire payload is stored as a JSON document. The metadata is replicated
101
     * as the GCS object's metadata except for the fields attachment, attachmentname
102
     * and salt.
103
     *
104
     * @param $key string to store the payload under
105
     * @param $payload array to store
106
     * @return bool true if successful, otherwise false.
107
     */
108 27
    private function _upload($key, &$payload)
109
    {
110 27
        $metadata = array_key_exists('meta', $payload) ? $payload['meta'] : array();
111 27
        unset($metadata['attachment'], $metadata['attachmentname'], $metadata['salt']);
112 27
        foreach ($metadata as $k => $v) {
113 27
            $metadata[$k] = strval($v);
114
        }
115
        try {
116 27
            $data = array(
117 27
                'name'          => $key,
118 27
                'chunkSize'     => 262144,
119 27
                'metadata'      => array(
120 27
                    'content-type' => 'application/json',
121 27
                    'metadata'     => $metadata,
122 27
                ),
123 27
            );
124 27
            if (!$this->_uniformacl) {
125 27
                $data['predefinedAcl'] = 'private';
126
            }
127 27
            $this->_bucket->upload(Json::encode($payload), $data);
128 2
        } catch (Exception $e) {
129 2
            error_log('failed to upload ' . $key . ' to ' . $this->_bucket->name() . ', ' .
130 2
                trim(preg_replace('/\s\s+/', ' ', $e->getMessage())));
131 2
            return false;
132
        }
133 26
        return true;
134
    }
135
136
    /**
137
     * @inheritDoc
138
     */
139 27
    public function create($pasteid, array &$paste)
140
    {
141 27
        if ($this->exists($pasteid)) {
142 1
            return false;
143
        }
144
145 27
        return $this->_upload($this->_getKey($pasteid), $paste);
146
    }
147
148
    /**
149
     * @inheritDoc
150
     */
151 20
    public function read($pasteid)
152
    {
153
        try {
154 20
            $o    = $this->_bucket->object($this->_getKey($pasteid));
155 20
            $data = $o->downloadAsString();
156 20
            return Json::decode($data);
157 1
        } catch (NotFoundException $e) {
158 1
            return false;
159
        } catch (Exception $e) {
160
            error_log('failed to read ' . $pasteid . ' from ' . $this->_bucket->name() . ', ' .
161
                trim(preg_replace('/\s\s+/', ' ', $e->getMessage())));
162
            return false;
163
        }
164
    }
165
166
    /**
167
     * @inheritDoc
168
     */
169 11
    public function delete($pasteid)
170
    {
171 11
        $name = $this->_getKey($pasteid);
172
173
        try {
174 11
            foreach ($this->_bucket->objects(array('prefix' => $name . '/discussion/')) as $comment) {
175
                try {
176 1
                    $this->_bucket->object($comment->name())->delete();
177
                } catch (NotFoundException $e) {
178
                    // ignore if already deleted.
179
                }
180
            }
181
        } catch (NotFoundException $e) {
182
            // there are no discussions associated with the paste
183
        }
184
185
        try {
186 11
            $this->_bucket->object($name)->delete();
187 3
        } catch (NotFoundException $e) {
188
            // ignore if already deleted
189
        }
190
    }
191
192
    /**
193
     * @inheritDoc
194
     */
195 42
    public function exists($pasteid)
196
    {
197 42
        $o = $this->_bucket->object($this->_getKey($pasteid));
198 42
        return $o->exists();
199
    }
200
201
    /**
202
     * @inheritDoc
203
     */
204 4
    public function createComment($pasteid, $parentid, $commentid, array &$comment)
205
    {
206 4
        if ($this->existsComment($pasteid, $parentid, $commentid)) {
207 1
            return false;
208
        }
209 4
        $key = $this->_getKey($pasteid) . '/discussion/' . $parentid . '/' . $commentid;
210 4
        return $this->_upload($key, $comment);
211
    }
212
213
    /**
214
     * @inheritDoc
215
     */
216 12
    public function readComments($pasteid)
217
    {
218 12
        $comments = array();
219 12
        $prefix   = $this->_getKey($pasteid) . '/discussion/';
220
        try {
221 12
            foreach ($this->_bucket->objects(array('prefix' => $prefix)) as $key) {
222 2
                $data            = $this->_bucket->object($key->name())->downloadAsString();
223 2
                $comment         = Json::decode($data);
224 2
                $comment['id']   = basename($key->name());
225 2
                $slot            = $this->getOpenSlot($comments, (int) $comment['meta']['created']);
226 2
                $comments[$slot] = $comment;
227
            }
228
        } catch (NotFoundException $e) {
229
            // no comments found
230
        }
231 12
        return $comments;
232
    }
233
234
    /**
235
     * @inheritDoc
236
     */
237 7
    public function existsComment($pasteid, $parentid, $commentid)
238
    {
239 7
        $name = $this->_getKey($pasteid) . '/discussion/' . $parentid . '/' . $commentid;
240 7
        $o    = $this->_bucket->object($name);
241 7
        return $o->exists();
242
    }
243
244
    /**
245
     * @inheritDoc
246
     */
247 6
    public function purgeValues($namespace, $time)
248
    {
249 6
        $path = 'config/' . $namespace;
250
        try {
251 6
            foreach ($this->_bucket->objects(array('prefix' => $path)) as $object) {
252 3
                $name = $object->name();
253 3
                if (strlen($name) > strlen($path) && substr($name, strlen($path), 1) !== '/') {
254
                    continue;
255
                }
256 3
                $info = $object->info();
257 3
                if (key_exists('metadata', $info) && key_exists('value', $info['metadata'])) {
258 3
                    $value = $info['metadata']['value'];
259 3
                    if (is_numeric($value) && intval($value) < $time) {
260
                        try {
261 2
                            $object->delete();
262
                        } catch (NotFoundException $e) {
263
                            // deleted by another instance.
264
                        }
265
                    }
266
                }
267
            }
268
        } catch (NotFoundException $e) {
269
            // no objects in the bucket yet
270
        }
271
    }
272
273
    /**
274
     * For GoogleCloudStorage, the value will also be stored in the metadata for the
275
     * namespaces traffic_limiter and purge_limiter.
276
     * @inheritDoc
277
     */
278 18
    public function setValue($value, $namespace, $key = '')
279
    {
280 18
        if ($key === '') {
281 17
            $key = 'config/' . $namespace;
282
        } else {
283 6
            $key = 'config/' . $namespace . '/' . $key;
284
        }
285
286 18
        $metadata = array('namespace' => $namespace);
287 18
        if ($namespace != 'salt') {
288 13
            $metadata['value'] = strval($value);
289
        }
290
        try {
291 18
            $data = array(
292 18
                'name'          => $key,
293 18
                'chunkSize'     => 262144,
294 18
                'metadata'      => array(
295 18
                    'content-type' => 'application/json',
296 18
                    'metadata'     => $metadata,
297 18
                ),
298 18
            );
299 18
            if (!$this->_uniformacl) {
300 18
                $data['predefinedAcl'] = 'private';
301
            }
302 18
            $this->_bucket->upload($value, $data);
303
        } catch (Exception $e) {
304
            error_log('failed to set key ' . $key . ' to ' . $this->_bucket->name() . ', ' .
305
                trim(preg_replace('/\s\s+/', ' ', $e->getMessage())));
306
            return false;
307
        }
308 18
        return true;
309
    }
310
311
    /**
312
     * @inheritDoc
313
     */
314 18
    public function getValue($namespace, $key = '')
315
    {
316 18
        if ($key === '') {
317 17
            $key = 'config/' . $namespace;
318
        } else {
319 6
            $key = 'config/' . $namespace . '/' . $key;
320
        }
321
        try {
322 18
            $o = $this->_bucket->object($key);
323 18
            return $o->downloadAsString();
324 18
        } catch (NotFoundException $e) {
325 18
            return '';
326
        }
327
    }
328
329
    /**
330
     * @inheritDoc
331
     */
332 11
    protected function _getExpiredPastes($batchsize)
333
    {
334 11
        $expired = array();
335
336 11
        $now    = time();
337 11
        $prefix = $this->_prefix;
338 11
        if ($prefix != '') {
339 11
            $prefix .= '/';
340
        }
341
        try {
342 11
            foreach ($this->_bucket->objects(array('prefix' => $prefix)) as $object) {
343 2
                $metadata = $object->info()['metadata'];
344 2
                if ($metadata != null && array_key_exists('expire_date', $metadata)) {
345 1
                    $expire_at = intval($metadata['expire_date']);
346 1
                    if ($expire_at != 0 && $expire_at < $now) {
347 1
                        array_push($expired, basename($object->name()));
348
                    }
349
                }
350
351 2
                if (count($expired) > $batchsize) {
352
                    break;
353
                }
354
            }
355
        } catch (NotFoundException $e) {
356
            // no objects in the bucket yet
357
        }
358 11
        return $expired;
359
    }
360
361
    /**
362
     * @inheritDoc
363
     */
364
    public function getAllPastes()
365
    {
366
        $pastes = array();
367
        $prefix = $this->_prefix;
368
        if ($prefix != '') {
369
            $prefix .= '/';
370
        }
371
372
        try {
373
            foreach ($this->_bucket->objects(array('prefix' => $prefix)) as $object) {
374
                $candidate = substr($object->name(), strlen($prefix));
375
                if (!str_contains($candidate, '/')) {
376
                    $pastes[] = $candidate;
377
                }
378
            }
379
        } catch (NotFoundException $e) {
380
            // no objects in the bucket yet
381
        }
382
        return $pastes;
383
    }
384
}
385