administration 9.4 KB

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