administration 9.4 KB

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