Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
79.14% covered (warning)
79.14%
110 / 139
40.00% covered (danger)
40.00%
6 / 15
CRAP
0.00% covered (danger)
0.00%
0 / 1
GoogleCloudStorage
79.14% covered (warning)
79.14%
110 / 139
40.00% covered (danger)
40.00%
6 / 15
105.56
0.00% covered (danger)
0.00%
0 / 1
 __construct
84.62% covered (success)
84.62%
11 / 13
0.00% covered (danger)
0.00%
0 / 1
10.36
 _getKey
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 _upload
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
5
 create
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 read
55.56% covered (warning)
55.56%
5 / 9
0.00% covered (danger)
0.00%
0 / 1
3.79
 delete
71.43% covered (warning)
71.43%
5 / 7
0.00% covered (danger)
0.00%
0 / 1
5.58
 exists
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 createComment
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 readComments
88.89% covered (success)
88.89%
8 / 9
0.00% covered (danger)
0.00%
0 / 1
3.01
 existsComment
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 purgeValues
75.00% covered (warning)
75.00%
9 / 12
0.00% covered (danger)
0.00%
0 / 1
11.56
 setValue
81.82% covered (success)
81.82%
18 / 22
0.00% covered (danger)
0.00%
0 / 1
5.15
 getValue
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 _getExpiredPastes
86.67% covered (success)
86.67%
13 / 15
0.00% covered (danger)
0.00%
0 / 1
9.19
 getAllPastes
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
30
1<?php
2
3namespace PrivateBin\Data;
4
5use Exception;
6use Google\Cloud\Core\Exception\NotFoundException;
7use Google\Cloud\Storage\Bucket;
8use Google\Cloud\Storage\StorageClient;
9use PrivateBin\Json;
10
11class 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    public function __construct(array $options)
52    {
53        if (getenv('PRIVATEBIN_GCS_BUCKET')) {
54            $bucket = getenv('PRIVATEBIN_GCS_BUCKET');
55        }
56        if (is_array($options) && array_key_exists('bucket', $options)) {
57            $bucket = $options['bucket'];
58        }
59        if (is_array($options) && array_key_exists('prefix', $options)) {
60            $this->_prefix = $options['prefix'];
61        }
62        if (is_array($options) && array_key_exists('uniformacl', $options)) {
63            $this->_uniformacl = $options['uniformacl'];
64        }
65
66        $this->_client = class_exists('StorageClientStub', false) ?
67            new \StorageClientStub(array()) :
68            new StorageClient(array('suppressKeyFileNotice' => true));
69        if (isset($bucket)) {
70            $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    private function _getKey($pasteid)
82    {
83        if ($this->_prefix != '') {
84            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    private function _upload($key, $payload)
100    {
101        $metadata = array_key_exists('meta', $payload) ? $payload['meta'] : array();
102        unset($metadata['attachment'], $metadata['attachmentname'], $metadata['salt']);
103        foreach ($metadata as $k => $v) {
104            $metadata[$k] = strval($v);
105        }
106        try {
107            $data = array(
108                'name'          => $key,
109                'chunkSize'     => 262144,
110                'metadata'      => array(
111                    'content-type' => 'application/json',
112                    'metadata'     => $metadata,
113                ),
114            );
115            if (!$this->_uniformacl) {
116                $data['predefinedAcl'] = 'private';
117            }
118            $this->_bucket->upload(Json::encode($payload), $data);
119        } catch (Exception $e) {
120            error_log('failed to upload ' . $key . ' to ' . $this->_bucket->name() . ', ' .
121                trim(preg_replace('/\s\s+/', ' ', $e->getMessage())));
122            return false;
123        }
124        return true;
125    }
126
127    /**
128     * @inheritDoc
129     */
130    public function create($pasteid, array $paste)
131    {
132        if ($this->exists($pasteid)) {
133            return false;
134        }
135
136        return $this->_upload($this->_getKey($pasteid), $paste);
137    }
138
139    /**
140     * @inheritDoc
141     */
142    public function read($pasteid)
143    {
144        try {
145            $o    = $this->_bucket->object($this->_getKey($pasteid));
146            $data = $o->downloadAsString();
147            return Json::decode($data);
148        } catch (NotFoundException $e) {
149            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    public function delete($pasteid)
161    {
162        $name = $this->_getKey($pasteid);
163
164        try {
165            foreach ($this->_bucket->objects(array('prefix' => $name . '/discussion/')) as $comment) {
166                try {
167                    $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            $this->_bucket->object($name)->delete();
178        } catch (NotFoundException $e) {
179            // ignore if already deleted
180        }
181    }
182
183    /**
184     * @inheritDoc
185     */
186    public function exists($pasteid)
187    {
188        $o = $this->_bucket->object($this->_getKey($pasteid));
189        return $o->exists();
190    }
191
192    /**
193     * @inheritDoc
194     */
195    public function createComment($pasteid, $parentid, $commentid, array $comment)
196    {
197        if ($this->existsComment($pasteid, $parentid, $commentid)) {
198            return false;
199        }
200        $key = $this->_getKey($pasteid) . '/discussion/' . $parentid . '/' . $commentid;
201        return $this->_upload($key, $comment);
202    }
203
204    /**
205     * @inheritDoc
206     */
207    public function readComments($pasteid)
208    {
209        $comments = array();
210        $prefix   = $this->_getKey($pasteid) . '/discussion/';
211        try {
212            foreach ($this->_bucket->objects(array('prefix' => $prefix)) as $key) {
213                $comment         = JSON::decode($this->_bucket->object($key->name())->downloadAsString());
214                $comment['id']   = basename($key->name());
215                $slot            = $this->getOpenSlot($comments, (int) $comment['meta']['created']);
216                $comments[$slot] = $comment;
217            }
218        } catch (NotFoundException $e) {
219            // no comments found
220        }
221        return $comments;
222    }
223
224    /**
225     * @inheritDoc
226     */
227    public function existsComment($pasteid, $parentid, $commentid)
228    {
229        $name = $this->_getKey($pasteid) . '/discussion/' . $parentid . '/' . $commentid;
230        $o    = $this->_bucket->object($name);
231        return $o->exists();
232    }
233
234    /**
235     * @inheritDoc
236     */
237    public function purgeValues($namespace, $time)
238    {
239        $path = 'config/' . $namespace;
240        try {
241            foreach ($this->_bucket->objects(array('prefix' => $path)) as $object) {
242                $name = $object->name();
243                if (strlen($name) > strlen($path) && substr($name, strlen($path), 1) !== '/') {
244                    continue;
245                }
246                $info = $object->info();
247                if (key_exists('metadata', $info) && key_exists('value', $info['metadata'])) {
248                    $value = $info['metadata']['value'];
249                    if (is_numeric($value) && intval($value) < $time) {
250                        try {
251                            $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    public function setValue($value, $namespace, $key = '')
269    {
270        if ($key === '') {
271            $key = 'config/' . $namespace;
272        } else {
273            $key = 'config/' . $namespace . '/' . $key;
274        }
275
276        $metadata = array('namespace' => $namespace);
277        if ($namespace != 'salt') {
278            $metadata['value'] = strval($value);
279        }
280        try {
281            $data = array(
282                'name'          => $key,
283                'chunkSize'     => 262144,
284                'metadata'      => array(
285                    'content-type' => 'application/json',
286                    'metadata'     => $metadata,
287                ),
288            );
289            if (!$this->_uniformacl) {
290                $data['predefinedAcl'] = 'private';
291            }
292            $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        return true;
299    }
300
301    /**
302     * @inheritDoc
303     */
304    public function getValue($namespace, $key = '')
305    {
306        if ($key === '') {
307            $key = 'config/' . $namespace;
308        } else {
309            $key = 'config/' . $namespace . '/' . $key;
310        }
311        try {
312            $o = $this->_bucket->object($key);
313            return $o->downloadAsString();
314        } catch (NotFoundException $e) {
315            return '';
316        }
317    }
318
319    /**
320     * @inheritDoc
321     */
322    protected function _getExpiredPastes($batchsize)
323    {
324        $expired = array();
325
326        $now    = time();
327        $prefix = $this->_prefix;
328        if ($prefix != '') {
329            $prefix .= '/';
330        }
331        try {
332            foreach ($this->_bucket->objects(array('prefix' => $prefix)) as $object) {
333                $metadata = $object->info()['metadata'];
334                if ($metadata != null && array_key_exists('expire_date', $metadata)) {
335                    $expire_at = intval($metadata['expire_date']);
336                    if ($expire_at != 0 && $expire_at < $now) {
337                        array_push($expired, basename($object->name()));
338                    }
339                }
340
341                if (count($expired) > $batchsize) {
342                    break;
343                }
344            }
345        } catch (NotFoundException $e) {
346            // no objects in the bucket yet
347        }
348        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}