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 |
2 | |
3 | namespace PrivateBin\Data; |
4 | |
5 | use Exception; |
6 | use Google\Cloud\Core\Exception\NotFoundException; |
7 | use Google\Cloud\Storage\Bucket; |
8 | use Google\Cloud\Storage\StorageClient; |
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 | * @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 | } |