administration 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369
  1. #!/usr/bin/env php
  2. <?php declare(strict_types=1);
  3. /**
  4. * PrivateBin
  5. *
  6. * a zero-knowledge paste bin
  7. *
  8. * @link https://github.com/PrivateBin/PrivateBin
  9. * @copyright 2012 Sébastien SAUVAGE (sebsauvage.net)
  10. * @license https://www.opensource.org/licenses/zlib-license.php The zlib/libpng License
  11. */
  12. namespace PrivateBin;
  13. use Exception;
  14. use PrivateBin\Configuration;
  15. use PrivateBin\Data\AbstractData;
  16. use PrivateBin\Model\Paste;
  17. define('PATH', dirname(__FILE__) . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR);
  18. require PATH . 'vendor' . DIRECTORY_SEPARATOR . 'autoload.php';
  19. /**
  20. * Administration
  21. *
  22. * Command line utility for administrative tasks.
  23. */
  24. class Administration
  25. {
  26. /**
  27. * configuration
  28. *
  29. * @access private
  30. * @var Configuration
  31. */
  32. private $_conf;
  33. /**
  34. * options, parsed from the command line arguments
  35. *
  36. * @access private
  37. * @var array
  38. */
  39. private $_opts = array();
  40. /**
  41. * data storage model
  42. *
  43. * @access private
  44. * @var AbstractData
  45. */
  46. private $_store;
  47. /**
  48. * deletes the requested paste ID, if a valid ID and it exists
  49. *
  50. * @access private
  51. * @param string $pasteId
  52. */
  53. private function _delete($pasteId)
  54. {
  55. if (!Paste::isValidId($pasteId)) {
  56. self::_error('given ID is not a valid paste ID (16 hexadecimal digits)', 5);
  57. }
  58. if (!$this->_store->exists($pasteId)) {
  59. self::_error('given ID does not exist, has expired or was already deleted', 6);
  60. }
  61. $this->_store->delete($pasteId);
  62. if ($this->_store->exists($pasteId)) {
  63. self::_error('paste ID exists after deletion, permission problem?', 7);
  64. }
  65. exit("paste $pasteId successfully deleted" . PHP_EOL);
  66. }
  67. /**
  68. * lists all stored paste IDs
  69. *
  70. * @access private
  71. */
  72. private function _list_ids()
  73. {
  74. $ids = $this->_store->getAllPastes();
  75. foreach ($ids as $pasteid) {
  76. echo $pasteid, PHP_EOL;
  77. }
  78. exit;
  79. }
  80. /**
  81. * deletes all stored pastes (regardless of expiration)
  82. *
  83. * @access private
  84. */
  85. private function _delete_all()
  86. {
  87. $ids = $this->_store->getAllPastes();
  88. foreach ($ids as $pasteid) {
  89. echo "Deleting paste ID: $pasteid" . PHP_EOL;
  90. $this->_store->delete($pasteid);
  91. }
  92. exit("All pastes successfully deleted" . PHP_EOL);
  93. }
  94. /**
  95. * removes empty directories, if current storage model uses Filesystem
  96. *
  97. * @access private
  98. */
  99. private function _empty_dirs()
  100. {
  101. if ($this->_conf->getKey('class', 'model') !== 'Filesystem') {
  102. self::_error('instance not using Filesystem storage, no directories to empty', 4);
  103. }
  104. $dir = $this->_conf->getKey('dir', 'model_options');
  105. passthru("find $dir -type d -empty -delete", $code);
  106. exit($code);
  107. }
  108. /**
  109. * display a message on STDERR and exits
  110. *
  111. * @access private
  112. * @static
  113. * @param string $message
  114. * @param int $code optional, defaults to 1
  115. */
  116. private static function _error($message, $code = 1)
  117. {
  118. self::_error_echo($message);
  119. exit($code);
  120. }
  121. /**
  122. * display a message on STDERR
  123. *
  124. * @access private
  125. * @static
  126. * @param string $message
  127. */
  128. private static function _error_echo($message)
  129. {
  130. fwrite(STDERR, 'Error: ' . $message . PHP_EOL);
  131. }
  132. /**
  133. * display usage help on STDOUT and exits
  134. *
  135. * @access private
  136. * @static
  137. * @param int $code optional, defaults to 0
  138. */
  139. private static function _help($code = 0)
  140. {
  141. echo <<<'EOT'
  142. Usage:
  143. administration [--delete <paste id> | --delete-all | --empty-dirs | --help | --list-ids | --purge | --statistics]
  144. Options:
  145. -d, --delete deletes the requested paste ID
  146. --delete-all deletes all paste IDs
  147. -e, --empty-dirs removes empty directories (only if Filesystem storage is
  148. configured)
  149. -h, --help displays this help message
  150. -l, --list-ids lists all paste IDs
  151. -p, --purge purge all expired pastes
  152. -s, --statistics reads all stored pastes and comments and reports statistics
  153. EOT, PHP_EOL;
  154. exit($code);
  155. }
  156. /**
  157. * return option for given short or long keyname, if it got set
  158. *
  159. * @access private
  160. * @static
  161. * @param string $short
  162. * @param string $long
  163. * @return string|null
  164. */
  165. private function _option($short, $long)
  166. {
  167. foreach (array($short, $long) as $key) {
  168. if (array_key_exists($key, $this->_opts)) {
  169. return $this->_opts[$key];
  170. }
  171. }
  172. return null;
  173. }
  174. /**
  175. * initialize options from given argument array
  176. *
  177. * @access private
  178. * @static
  179. * @param array $arguments
  180. */
  181. private function _options_initialize($arguments)
  182. {
  183. if ($arguments > 3) {
  184. self::_error_echo('too many arguments given');
  185. echo PHP_EOL;
  186. self::_help(1);
  187. }
  188. if ($arguments < 2) {
  189. self::_error_echo('missing arguments');
  190. echo PHP_EOL;
  191. self::_help(2);
  192. }
  193. $this->_opts = getopt('hd:epsl', array('help', 'delete:', 'empty-dirs', 'purge', 'statistics', 'list-ids', 'delete-all'));
  194. if (!$this->_opts) {
  195. self::_error_echo('unsupported arguments given');
  196. echo PHP_EOL;
  197. self::_help(3);
  198. }
  199. }
  200. /**
  201. * reads all stored pastes and comments and reports statistics
  202. *
  203. * @access public
  204. */
  205. private function _statistics()
  206. {
  207. $counters = array(
  208. 'burn' => 0,
  209. 'damaged' => 0,
  210. 'discussion' => 0,
  211. 'expired' => 0,
  212. 'md' => 0,
  213. 'percent' => 1,
  214. 'plain' => 0,
  215. 'progress' => 0,
  216. 'syntax' => 0,
  217. 'total' => 0,
  218. 'unknown' => 0,
  219. );
  220. $time = time();
  221. $ids = $this->_store->getAllPastes();
  222. $counters['total'] = count($ids);
  223. $dots = $counters['total'] < 100 ? 10 : (
  224. $counters['total'] < 1000 ? 50 : 100
  225. );
  226. $percentages = $counters['total'] < 100 ? 0 : (
  227. $counters['total'] < 1000 ? 4 : 10
  228. );
  229. echo "Total:\t\t\t{$counters['total']}", PHP_EOL;
  230. foreach ($ids as $pasteid) {
  231. try {
  232. $paste = $this->_store->read($pasteid);
  233. } catch (Exception $e) {
  234. echo "Error reading paste {$pasteid}: ", $e->getMessage(), PHP_EOL;
  235. ++$counters['damaged'];
  236. }
  237. ++$counters['progress'];
  238. if (
  239. array_key_exists('expire_date', $paste['meta']) &&
  240. $paste['meta']['expire_date'] < $time
  241. ) {
  242. ++$counters['expired'];
  243. }
  244. if (array_key_exists('adata', $paste)) {
  245. $format = $paste['adata'][1];
  246. $discussion = $paste['adata'][2];
  247. $burn = $paste['adata'][3];
  248. } else {
  249. $format = array_key_exists('formatter', $paste['meta']) ? $paste['meta']['formatter'] : 'plaintext';
  250. $discussion = array_key_exists('opendiscussion', $paste['meta']) ? $paste['meta']['opendiscussion'] : false;
  251. $burn = array_key_exists('burnafterreading', $paste['meta']) ? $paste['meta']['burnafterreading'] : false;
  252. }
  253. if ($format === 'plaintext') {
  254. ++$counters['plain'];
  255. } elseif ($format === 'syntaxhighlighting') {
  256. ++$counters['syntax'];
  257. } elseif ($format === 'markdown') {
  258. ++$counters['md'];
  259. } else {
  260. ++$counters['unknown'];
  261. }
  262. $counters['discussion'] += (int) $discussion;
  263. $counters['burn'] += (int) $burn;
  264. // display progress
  265. if ($counters['progress'] % $dots === 0) {
  266. echo '.';
  267. if ($percentages) {
  268. $progress = $percentages / $counters['total'] * $counters['progress'];
  269. if ($progress >= $counters['percent']) {
  270. printf(' %d%% ', 100 / $percentages * $progress);
  271. ++$counters['percent'];
  272. }
  273. }
  274. }
  275. }
  276. echo PHP_EOL, <<<EOT
  277. Expired:\t\t{$counters['expired']}
  278. Burn after reading:\t{$counters['burn']}
  279. Discussions:\t\t{$counters['discussion']}
  280. Plain Text:\t\t{$counters['plain']}
  281. Source Code:\t\t{$counters['syntax']}
  282. Markdown:\t\t{$counters['md']}
  283. EOT, PHP_EOL;
  284. if ($counters['damaged'] > 0) {
  285. echo "Damaged:\t\t{$counters['damaged']}", PHP_EOL;
  286. }
  287. if ($counters['unknown'] > 0) {
  288. echo "Unknown format:\t\t{$counters['unknown']}", PHP_EOL;
  289. }
  290. }
  291. /**
  292. * constructor
  293. *
  294. * initializes and runs administrative tasks
  295. *
  296. * @access public
  297. */
  298. public function __construct()
  299. {
  300. $this->_options_initialize($_SERVER['argc']);
  301. if ($this->_option('h', 'help') !== null) {
  302. self::_help();
  303. }
  304. $this->_conf = new Configuration;
  305. if ($this->_option('e', 'empty-dirs') !== null) {
  306. $this->_empty_dirs();
  307. }
  308. $class = 'PrivateBin\\Data\\' . $this->_conf->getKey('class', 'model');
  309. $this->_store = new $class($this->_conf->getSection('model_options'));
  310. if ($this->_option('l', 'list-ids') !== null) {
  311. $this->_list_ids();
  312. }
  313. if ($this->_option(null, 'delete-all') !== null) {
  314. $this->_delete_all();
  315. }
  316. if (($pasteId = $this->_option('d', 'delete')) !== null) {
  317. $this->_delete($pasteId);
  318. }
  319. if ($this->_option('p', 'purge') !== null) {
  320. try {
  321. $this->_store->purge(PHP_INT_MAX);
  322. } catch (Exception $e) {
  323. echo 'Error purging pastes: ', $e->getMessage(), PHP_EOL,
  324. 'Run the statistics to find damaged paste IDs and either delete them or restore them from backup.', PHP_EOL;
  325. }
  326. exit('purging of expired pastes concluded' . PHP_EOL);
  327. }
  328. if ($this->_option('s', 'statistics') !== null) {
  329. $this->_statistics();
  330. }
  331. }
  332. }
  333. new Administration();