Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
79.14% |
110 / 139 |
|
40.00% |
6 / 15 |
CRAP | |
0.00% |
0 / 1 |
GoogleCloudStorage | |
79.14% |
110 / 139 |
|
40.00% |
6 / 15 |
105.56 | |
0.00% |
0 / 1 |
__construct | |
84.62% |
11 / 13 |
|
0.00% |
0 / 1 |
10.36 | |||
_getKey | |
66.67% |
2 / 3 |
|
0.00% |
0 / 1 |
2.15 | |||
_upload | |
100.00% |
20 / 20 |
|
100.00% |
1 / 1 |
5 | |||
create | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
read | |
55.56% |
5 / 9 |
|
0.00% |
0 / 1 |
3.79 | |||
delete | |
71.43% |
5 / 7 |
|
0.00% |
0 / 1 |
5.58 | |||
exists | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
createComment | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
readComments | |
88.89% |
8 / 9 |
|
0.00% |
0 / 1 |
3.01 | |||
existsComment | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
purgeValues | |
75.00% |
9 / 12 |
|
0.00% |
0 / 1 |
11.56 | |||
setValue | |
81.82% |
18 / 22 |
|
0.00% |
0 / 1 |
5.15 | |||
getValue | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
3 | |||
_getExpiredPastes | |
86.67% |
13 / 15 |
|
0.00% |
0 / 1 |
9.19 | |||
getAllPastes | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
30 |
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; |
16 | use Google\Cloud\Storage\Bucket; |
17 | use Google\Cloud\Storage\StorageClient; |
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 | public function __construct(array $options) |
61 | { |
62 | if (getenv('PRIVATEBIN_GCS_BUCKET')) { |
63 | $bucket = getenv('PRIVATEBIN_GCS_BUCKET'); |
64 | } |
65 | if (is_array($options) && array_key_exists('bucket', $options)) { |
66 | $bucket = $options['bucket']; |
67 | } |
68 | if (is_array($options) && array_key_exists('prefix', $options)) { |
69 | $this->_prefix = $options['prefix']; |
70 | } |
71 | if (is_array($options) && array_key_exists('uniformacl', $options)) { |
72 | $this->_uniformacl = $options['uniformacl']; |
73 | } |
74 | |
75 | $this->_client = class_exists('StorageClientStub', false) ? |
76 | new \StorageClientStub(array()) : |
77 | new StorageClient(array('suppressKeyFileNotice' => true)); |
78 | if (isset($bucket)) { |
79 | $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 | private function _getKey($pasteid) |
91 | { |
92 | if ($this->_prefix != '') { |
93 | 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 | private function _upload($key, $payload) |
109 | { |
110 | $metadata = array_key_exists('meta', $payload) ? $payload['meta'] : array(); |
111 | unset($metadata['attachment'], $metadata['attachmentname'], $metadata['salt']); |
112 | foreach ($metadata as $k => $v) { |
113 | $metadata[$k] = strval($v); |
114 | } |
115 | try { |
116 | $data = array( |
117 | 'name' => $key, |
118 | 'chunkSize' => 262144, |
119 | 'metadata' => array( |
120 | 'content-type' => 'application/json', |
121 | 'metadata' => $metadata, |
122 | ), |
123 | ); |
124 | if (!$this->_uniformacl) { |
125 | $data['predefinedAcl'] = 'private'; |
126 | } |
127 | $this->_bucket->upload(Json::encode($payload), $data); |
128 | } catch (Exception $e) { |
129 | error_log('failed to upload ' . $key . ' to ' . $this->_bucket->name() . ', ' . |
130 | trim(preg_replace('/\s\s+/', ' ', $e->getMessage()))); |
131 | return false; |
132 | } |
133 | return true; |
134 | } |
135 | |
136 | /** |
137 | * @inheritDoc |
138 | */ |
139 | public function create($pasteid, array $paste) |
140 | { |
141 | if ($this->exists($pasteid)) { |
142 | return false; |
143 | } |
144 | |
145 | return $this->_upload($this->_getKey($pasteid), $paste); |
146 | } |
147 | |
148 | /** |
149 | * @inheritDoc |
150 | */ |
151 | public function read($pasteid) |
152 | { |
153 | try { |
154 | $o = $this->_bucket->object($this->_getKey($pasteid)); |
155 | $data = $o->downloadAsString(); |
156 | return Json::decode($data); |
157 | } catch (NotFoundException $e) { |
158 | 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 | public function delete($pasteid) |
170 | { |
171 | $name = $this->_getKey($pasteid); |
172 | |
173 | try { |
174 | foreach ($this->_bucket->objects(array('prefix' => $name . '/discussion/')) as $comment) { |
175 | try { |
176 | $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 | $this->_bucket->object($name)->delete(); |
187 | } catch (NotFoundException $e) { |
188 | // ignore if already deleted |
189 | } |
190 | } |
191 | |
192 | /** |
193 | * @inheritDoc |
194 | */ |
195 | public function exists($pasteid) |
196 | { |
197 | $o = $this->_bucket->object($this->_getKey($pasteid)); |
198 | return $o->exists(); |
199 | } |
200 | |
201 | /** |
202 | * @inheritDoc |
203 | */ |
204 | public function createComment($pasteid, $parentid, $commentid, array $comment) |
205 | { |
206 | if ($this->existsComment($pasteid, $parentid, $commentid)) { |
207 | return false; |
208 | } |
209 | $key = $this->_getKey($pasteid) . '/discussion/' . $parentid . '/' . $commentid; |
210 | return $this->_upload($key, $comment); |
211 | } |
212 | |
213 | /** |
214 | * @inheritDoc |
215 | */ |
216 | public function readComments($pasteid) |
217 | { |
218 | $comments = array(); |
219 | $prefix = $this->_getKey($pasteid) . '/discussion/'; |
220 | try { |
221 | foreach ($this->_bucket->objects(array('prefix' => $prefix)) as $key) { |
222 | $comment = JSON::decode($this->_bucket->object($key->name())->downloadAsString()); |
223 | $comment['id'] = basename($key->name()); |
224 | $slot = $this->getOpenSlot($comments, (int) $comment['meta']['created']); |
225 | $comments[$slot] = $comment; |
226 | } |
227 | } catch (NotFoundException $e) { |
228 | // no comments found |
229 | } |
230 | return $comments; |
231 | } |
232 | |
233 | /** |
234 | * @inheritDoc |
235 | */ |
236 | public function existsComment($pasteid, $parentid, $commentid) |
237 | { |
238 | $name = $this->_getKey($pasteid) . '/discussion/' . $parentid . '/' . $commentid; |
239 | $o = $this->_bucket->object($name); |
240 | return $o->exists(); |
241 | } |
242 | |
243 | /** |
244 | * @inheritDoc |
245 | */ |
246 | public function purgeValues($namespace, $time) |
247 | { |
248 | $path = 'config/' . $namespace; |
249 | try { |
250 | foreach ($this->_bucket->objects(array('prefix' => $path)) as $object) { |
251 | $name = $object->name(); |
252 | if (strlen($name) > strlen($path) && substr($name, strlen($path), 1) !== '/') { |
253 | continue; |
254 | } |
255 | $info = $object->info(); |
256 | if (key_exists('metadata', $info) && key_exists('value', $info['metadata'])) { |
257 | $value = $info['metadata']['value']; |
258 | if (is_numeric($value) && intval($value) < $time) { |
259 | try { |
260 | $object->delete(); |
261 | } catch (NotFoundException $e) { |
262 | // deleted by another instance. |
263 | } |
264 | } |
265 | } |
266 | } |
267 | } catch (NotFoundException $e) { |
268 | // no objects in the bucket yet |
269 | } |
270 | } |
271 | |
272 | /** |
273 | * For GoogleCloudStorage, the value will also be stored in the metadata for the |
274 | * namespaces traffic_limiter and purge_limiter. |
275 | * @inheritDoc |
276 | */ |
277 | public function setValue($value, $namespace, $key = '') |
278 | { |
279 | if ($key === '') { |
280 | $key = 'config/' . $namespace; |
281 | } else { |
282 | $key = 'config/' . $namespace . '/' . $key; |
283 | } |
284 | |
285 | $metadata = array('namespace' => $namespace); |
286 | if ($namespace != 'salt') { |
287 | $metadata['value'] = strval($value); |
288 | } |
289 | try { |
290 | $data = array( |
291 | 'name' => $key, |
292 | 'chunkSize' => 262144, |
293 | 'metadata' => array( |
294 | 'content-type' => 'application/json', |
295 | 'metadata' => $metadata, |
296 | ), |
297 | ); |
298 | if (!$this->_uniformacl) { |
299 | $data['predefinedAcl'] = 'private'; |
300 | } |
301 | $this->_bucket->upload($value, $data); |
302 | } catch (Exception $e) { |
303 | error_log('failed to set key ' . $key . ' to ' . $this->_bucket->name() . ', ' . |
304 | trim(preg_replace('/\s\s+/', ' ', $e->getMessage()))); |
305 | return false; |
306 | } |
307 | return true; |
308 | } |
309 | |
310 | /** |
311 | * @inheritDoc |
312 | */ |
313 | public function getValue($namespace, $key = '') |
314 | { |
315 | if ($key === '') { |
316 | $key = 'config/' . $namespace; |
317 | } else { |
318 | $key = 'config/' . $namespace . '/' . $key; |
319 | } |
320 | try { |
321 | $o = $this->_bucket->object($key); |
322 | return $o->downloadAsString(); |
323 | } catch (NotFoundException $e) { |
324 | return ''; |
325 | } |
326 | } |
327 | |
328 | /** |
329 | * @inheritDoc |
330 | */ |
331 | protected function _getExpiredPastes($batchsize) |
332 | { |
333 | $expired = array(); |
334 | |
335 | $now = time(); |
336 | $prefix = $this->_prefix; |
337 | if ($prefix != '') { |
338 | $prefix .= '/'; |
339 | } |
340 | try { |
341 | foreach ($this->_bucket->objects(array('prefix' => $prefix)) as $object) { |
342 | $metadata = $object->info()['metadata']; |
343 | if ($metadata != null && array_key_exists('expire_date', $metadata)) { |
344 | $expire_at = intval($metadata['expire_date']); |
345 | if ($expire_at != 0 && $expire_at < $now) { |
346 | array_push($expired, basename($object->name())); |
347 | } |
348 | } |
349 | |
350 | if (count($expired) > $batchsize) { |
351 | break; |
352 | } |
353 | } |
354 | } catch (NotFoundException $e) { |
355 | // no objects in the bucket yet |
356 | } |
357 | return $expired; |
358 | } |
359 | |
360 | /** |
361 | * @inheritDoc |
362 | */ |
363 | public function getAllPastes() |
364 | { |
365 | $pastes = array(); |
366 | $prefix = $this->_prefix; |
367 | if ($prefix != '') { |
368 | $prefix .= '/'; |
369 | } |
370 | |
371 | try { |
372 | foreach ($this->_bucket->objects(array('prefix' => $prefix)) as $object) { |
373 | $candidate = substr($object->name(), strlen($prefix)); |
374 | if (!str_contains($candidate, '/')) { |
375 | $pastes[] = $candidate; |
376 | } |
377 | } |
378 | } catch (NotFoundException $e) { |
379 | // no objects in the bucket yet |
380 | } |
381 | return $pastes; |
382 | } |
383 | } |