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; |
||
0 ignored issues
–
show
|
|||
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 | 43 | public function __construct(array $options) |
|
61 | { |
||
62 | 43 | if (getenv('PRIVATEBIN_GCS_BUCKET')) { |
|
63 | $bucket = getenv('PRIVATEBIN_GCS_BUCKET'); |
||
64 | } |
||
65 | 43 | if (is_array($options) && array_key_exists('bucket', $options)) { |
|
66 | 43 | $bucket = $options['bucket']; |
|
67 | } |
||
68 | 43 | if (is_array($options) && array_key_exists('prefix', $options)) { |
|
69 | 43 | $this->_prefix = $options['prefix']; |
|
70 | } |
||
71 | 43 | if (is_array($options) && array_key_exists('uniformacl', $options)) { |
|
72 | 32 | $this->_uniformacl = $options['uniformacl']; |
|
73 | } |
||
74 | |||
75 | 43 | $this->_client = class_exists('StorageClientStub', false) ? |
|
76 | 43 | new \StorageClientStub(array()) : |
|
77 | new StorageClient(array('suppressKeyFileNotice' => true)); |
||
78 | 43 | if (isset($bucket)) { |
|
79 | 43 | $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 | 41 | private function _getKey($pasteid) |
|
91 | { |
||
92 | 41 | if ($this->_prefix != '') { |
|
93 | 41 | 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 | 41 | public function exists($pasteid) |
|
196 | { |
||
197 | 41 | $o = $this->_bucket->object($this->_getKey($pasteid)); |
|
198 | 41 | 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 | $comment = JSON::decode($this->_bucket->object($key->name())->downloadAsString()); |
|
223 | 2 | $comment['id'] = basename($key->name()); |
|
224 | 2 | $slot = $this->getOpenSlot($comments, (int) $comment['meta']['created']); |
|
225 | 2 | $comments[$slot] = $comment; |
|
226 | } |
||
227 | } catch (NotFoundException $e) { |
||
228 | // no comments found |
||
229 | } |
||
230 | 12 | return $comments; |
|
231 | } |
||
232 | |||
233 | /** |
||
234 | * @inheritDoc |
||
235 | */ |
||
236 | 7 | public function existsComment($pasteid, $parentid, $commentid) |
|
237 | { |
||
238 | 7 | $name = $this->_getKey($pasteid) . '/discussion/' . $parentid . '/' . $commentid; |
|
239 | 7 | $o = $this->_bucket->object($name); |
|
240 | 7 | return $o->exists(); |
|
241 | } |
||
242 | |||
243 | /** |
||
244 | * @inheritDoc |
||
245 | */ |
||
246 | 6 | public function purgeValues($namespace, $time) |
|
247 | { |
||
248 | 6 | $path = 'config/' . $namespace; |
|
249 | try { |
||
250 | 6 | foreach ($this->_bucket->objects(array('prefix' => $path)) as $object) { |
|
251 | 3 | $name = $object->name(); |
|
252 | 3 | if (strlen($name) > strlen($path) && substr($name, strlen($path), 1) !== '/') { |
|
253 | continue; |
||
254 | } |
||
255 | 3 | $info = $object->info(); |
|
256 | 3 | if (key_exists('metadata', $info) && key_exists('value', $info['metadata'])) { |
|
257 | 3 | $value = $info['metadata']['value']; |
|
258 | 3 | if (is_numeric($value) && intval($value) < $time) { |
|
259 | try { |
||
260 | 2 | $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 | 18 | public function setValue($value, $namespace, $key = '') |
|
278 | { |
||
279 | 18 | if ($key === '') { |
|
280 | 17 | $key = 'config/' . $namespace; |
|
281 | } else { |
||
282 | 6 | $key = 'config/' . $namespace . '/' . $key; |
|
283 | } |
||
284 | |||
285 | 18 | $metadata = array('namespace' => $namespace); |
|
286 | 18 | if ($namespace != 'salt') { |
|
287 | 13 | $metadata['value'] = strval($value); |
|
288 | } |
||
289 | try { |
||
290 | 18 | $data = array( |
|
291 | 18 | 'name' => $key, |
|
292 | 18 | 'chunkSize' => 262144, |
|
293 | 18 | 'metadata' => array( |
|
294 | 18 | 'content-type' => 'application/json', |
|
295 | 18 | 'metadata' => $metadata, |
|
296 | 18 | ), |
|
297 | 18 | ); |
|
298 | 18 | if (!$this->_uniformacl) { |
|
299 | 18 | $data['predefinedAcl'] = 'private'; |
|
300 | } |
||
301 | 18 | $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 | 18 | return true; |
|
308 | } |
||
309 | |||
310 | /** |
||
311 | * @inheritDoc |
||
312 | */ |
||
313 | 18 | public function getValue($namespace, $key = '') |
|
314 | { |
||
315 | 18 | if ($key === '') { |
|
316 | 17 | $key = 'config/' . $namespace; |
|
317 | } else { |
||
318 | 6 | $key = 'config/' . $namespace . '/' . $key; |
|
319 | } |
||
320 | try { |
||
321 | 18 | $o = $this->_bucket->object($key); |
|
322 | 18 | return $o->downloadAsString(); |
|
323 | 18 | } catch (NotFoundException $e) { |
|
324 | 18 | return ''; |
|
325 | } |
||
326 | } |
||
327 | |||
328 | /** |
||
329 | * @inheritDoc |
||
330 | */ |
||
331 | 11 | protected function _getExpiredPastes($batchsize) |
|
332 | { |
||
333 | 11 | $expired = array(); |
|
334 | |||
335 | 11 | $now = time(); |
|
336 | 11 | $prefix = $this->_prefix; |
|
337 | 11 | if ($prefix != '') { |
|
338 | 11 | $prefix .= '/'; |
|
339 | } |
||
340 | try { |
||
341 | 11 | foreach ($this->_bucket->objects(array('prefix' => $prefix)) as $object) { |
|
342 | 2 | $metadata = $object->info()['metadata']; |
|
343 | 2 | if ($metadata != null && array_key_exists('expire_date', $metadata)) { |
|
344 | 1 | $expire_at = intval($metadata['expire_date']); |
|
345 | 1 | if ($expire_at != 0 && $expire_at < $now) { |
|
346 | 1 | array_push($expired, basename($object->name())); |
|
347 | } |
||
348 | } |
||
349 | |||
350 | 2 | if (count($expired) > $batchsize) { |
|
351 | break; |
||
352 | } |
||
353 | } |
||
354 | } catch (NotFoundException $e) { |
||
355 | // no objects in the bucket yet |
||
356 | } |
||
357 | 11 | 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 (strpos($candidate, '/') === false) { |
||
375 | $pastes[] = $candidate; |
||
376 | } |
||
377 | } |
||
378 | } catch (NotFoundException $e) { |
||
379 | // no objects in the bucket yet |
||
380 | } |
||
381 | return $pastes; |
||
382 | } |
||
383 | } |
||
384 |
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:For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths