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