GoogleCloudStorage.php 11 KB

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