GoogleCloudStorage.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383
  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\Json;
  17. class GoogleCloudStorage extends AbstractData
  18. {
  19. /**
  20. * GCS client
  21. *
  22. * @access private
  23. * @var StorageClient
  24. */
  25. private $_client = null;
  26. /**
  27. * GCS bucket
  28. *
  29. * @access private
  30. * @var Bucket
  31. */
  32. private $_bucket = null;
  33. /**
  34. * object prefix
  35. *
  36. * @access private
  37. * @var string
  38. */
  39. private $_prefix = 'pastes';
  40. /**
  41. * bucket acl type
  42. *
  43. * @access private
  44. * @var bool
  45. */
  46. private $_uniformacl = false;
  47. /**
  48. * instantiantes a new Google Cloud Storage data backend.
  49. *
  50. * @access public
  51. * @param array $options
  52. */
  53. public function __construct(array $options)
  54. {
  55. if (getenv('PRIVATEBIN_GCS_BUCKET')) {
  56. $bucket = getenv('PRIVATEBIN_GCS_BUCKET');
  57. }
  58. if (is_array($options) && array_key_exists('bucket', $options)) {
  59. $bucket = $options['bucket'];
  60. }
  61. if (is_array($options) && array_key_exists('prefix', $options)) {
  62. $this->_prefix = $options['prefix'];
  63. }
  64. if (is_array($options) && array_key_exists('uniformacl', $options)) {
  65. $this->_uniformacl = $options['uniformacl'];
  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. * 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. * Uploads the payload in the $this->_bucket under the specified key.
  90. * The entire payload is stored as a JSON document. The metadata is replicated
  91. * as the GCS object's metadata except for the fields attachment, attachmentname
  92. * and 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['attachment'], $metadata['attachmentname'], $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. $comment = JSON::decode($this->_bucket->object($key->name())->downloadAsString());
  204. $comment['id'] = basename($key->name());
  205. $slot = $this->getOpenSlot($comments, (int) $comment['meta']['created']);
  206. $comments[$slot] = $comment;
  207. }
  208. } catch (NotFoundException $e) {
  209. // no comments found
  210. }
  211. return $comments;
  212. }
  213. /**
  214. * @inheritDoc
  215. */
  216. public function existsComment($pasteid, $parentid, $commentid)
  217. {
  218. $name = $this->_getKey($pasteid) . '/discussion/' . $parentid . '/' . $commentid;
  219. $o = $this->_bucket->object($name);
  220. return $o->exists();
  221. }
  222. /**
  223. * @inheritDoc
  224. */
  225. public function purgeValues($namespace, $time)
  226. {
  227. $path = 'config/' . $namespace;
  228. try {
  229. foreach ($this->_bucket->objects(array('prefix' => $path)) as $object) {
  230. $name = $object->name();
  231. if (strlen($name) > strlen($path) && substr($name, strlen($path), 1) !== '/') {
  232. continue;
  233. }
  234. $info = $object->info();
  235. if (key_exists('metadata', $info) && key_exists('value', $info['metadata'])) {
  236. $value = $info['metadata']['value'];
  237. if (is_numeric($value) && intval($value) < $time) {
  238. try {
  239. $object->delete();
  240. } catch (NotFoundException $e) {
  241. // deleted by another instance.
  242. }
  243. }
  244. }
  245. }
  246. } catch (NotFoundException $e) {
  247. // no objects in the bucket yet
  248. }
  249. }
  250. /**
  251. * For GoogleCloudStorage, the value will also be stored in the metadata for the
  252. * namespaces traffic_limiter and purge_limiter.
  253. * @inheritDoc
  254. */
  255. public function setValue($value, $namespace, $key = '')
  256. {
  257. if ($key === '') {
  258. $key = 'config/' . $namespace;
  259. } else {
  260. $key = 'config/' . $namespace . '/' . $key;
  261. }
  262. $metadata = array('namespace' => $namespace);
  263. if ($namespace != 'salt') {
  264. $metadata['value'] = strval($value);
  265. }
  266. try {
  267. $data = array(
  268. 'name' => $key,
  269. 'chunkSize' => 262144,
  270. 'metadata' => array(
  271. 'content-type' => 'application/json',
  272. 'metadata' => $metadata,
  273. ),
  274. );
  275. if (!$this->_uniformacl) {
  276. $data['predefinedAcl'] = 'private';
  277. }
  278. $this->_bucket->upload($value, $data);
  279. } catch (Exception $e) {
  280. error_log('failed to set key ' . $key . ' to ' . $this->_bucket->name() . ', ' .
  281. trim(preg_replace('/\s\s+/', ' ', $e->getMessage())));
  282. return false;
  283. }
  284. return true;
  285. }
  286. /**
  287. * @inheritDoc
  288. */
  289. public function getValue($namespace, $key = '')
  290. {
  291. if ($key === '') {
  292. $key = 'config/' . $namespace;
  293. } else {
  294. $key = 'config/' . $namespace . '/' . $key;
  295. }
  296. try {
  297. $o = $this->_bucket->object($key);
  298. return $o->downloadAsString();
  299. } catch (NotFoundException $e) {
  300. return '';
  301. }
  302. }
  303. /**
  304. * @inheritDoc
  305. */
  306. protected function _getExpiredPastes($batchsize)
  307. {
  308. $expired = array();
  309. $now = time();
  310. $prefix = $this->_prefix;
  311. if ($prefix != '') {
  312. $prefix .= '/';
  313. }
  314. try {
  315. foreach ($this->_bucket->objects(array('prefix' => $prefix)) as $object) {
  316. $metadata = $object->info()['metadata'];
  317. if ($metadata != null && array_key_exists('expire_date', $metadata)) {
  318. $expire_at = intval($metadata['expire_date']);
  319. if ($expire_at != 0 && $expire_at < $now) {
  320. array_push($expired, basename($object->name()));
  321. }
  322. }
  323. if (count($expired) > $batchsize) {
  324. break;
  325. }
  326. }
  327. } catch (NotFoundException $e) {
  328. // no objects in the bucket yet
  329. }
  330. return $expired;
  331. }
  332. /**
  333. * @inheritDoc
  334. */
  335. public function getAllPastes()
  336. {
  337. $pastes = array();
  338. $prefix = $this->_prefix;
  339. if ($prefix != '') {
  340. $prefix .= '/';
  341. }
  342. try {
  343. foreach ($this->_bucket->objects(array('prefix' => $prefix)) as $object) {
  344. $candidate = substr($object->name(), strlen($prefix));
  345. if (!str_contains($candidate, '/')) {
  346. $pastes[] = $candidate;
  347. }
  348. }
  349. } catch (NotFoundException $e) {
  350. // no objects in the bucket yet
  351. }
  352. return $pastes;
  353. }
  354. }