Issues (11)

lib/Data/GoogleCloudStorage.php (3 issues)

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