1
0

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 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 (!empty($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 = $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. $value = $object->info()['metadata']['value'] ?? '';
  241. if (is_numeric($value) && intval($value) < $time) {
  242. try {
  243. $object->delete();
  244. } catch (NotFoundException $e) {
  245. // deleted by another instance.
  246. }
  247. }
  248. }
  249. } catch (NotFoundException $e) {
  250. // no objects in the bucket yet
  251. }
  252. }
  253. /**
  254. * For GoogleCloudStorage, the value will also be stored in the metadata for the
  255. * namespaces traffic_limiter and purge_limiter.
  256. * @inheritDoc
  257. */
  258. public function setValue($value, $namespace, $key = '')
  259. {
  260. if (empty($key)) {
  261. $key = 'config/' . $namespace;
  262. } else {
  263. $key = 'config/' . $namespace . '/' . $key;
  264. }
  265. $metadata = array('namespace' => $namespace);
  266. if ($namespace !== 'salt') {
  267. $metadata['value'] = strval($value);
  268. }
  269. try {
  270. $data = array(
  271. 'name' => $key,
  272. 'chunkSize' => 262144,
  273. 'metadata' => array(
  274. 'content-type' => 'application/json',
  275. 'metadata' => $metadata,
  276. ),
  277. );
  278. if (!$this->_uniformacl) {
  279. $data['predefinedAcl'] = 'private';
  280. }
  281. $this->_bucket->upload($value, $data);
  282. } catch (Exception $e) {
  283. error_log('failed to set key ' . $key . ' to ' . $this->_bucket->name() . ', ' .
  284. trim(preg_replace('/\s\s+/', ' ', $e->getMessage())));
  285. return false;
  286. }
  287. return true;
  288. }
  289. /**
  290. * @inheritDoc
  291. */
  292. public function getValue($namespace, $key = '')
  293. {
  294. if ($key === '') {
  295. $key = 'config/' . $namespace;
  296. } else {
  297. $key = 'config/' . $namespace . '/' . $key;
  298. }
  299. try {
  300. $o = $this->_bucket->object($key);
  301. return $o->downloadAsString();
  302. } catch (NotFoundException $e) {
  303. return '';
  304. }
  305. }
  306. /**
  307. * @inheritDoc
  308. */
  309. protected function _getExpiredPastes($batchsize)
  310. {
  311. $expired = array();
  312. $now = time();
  313. $prefix = $this->_prefix;
  314. if (!empty($prefix)) {
  315. $prefix .= '/';
  316. }
  317. try {
  318. foreach ($this->_bucket->objects(array('prefix' => $prefix)) as $object) {
  319. $expire_at = $object->info()['metadata']['expire_date'] ?? '';
  320. if (is_numeric($expire_at) && intval($expire_at) < $now) {
  321. array_push($expired, basename($object->name()));
  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 (!empty($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. }