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 | */ |
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 | } |