PageRenderTime 44ms CodeModel.GetById 15ms RepoModel.GetById 1ms app.codeStats 0ms

/phpBB/phpbb/event/php_exporter.php

https://github.com/Jipem/phpbb
PHP | 610 lines | 381 code | 66 blank | 163 comment | 56 complexity | 84eddd244823d7eb04855e4ec1f69c61 MD5 | raw file
Possible License(s): AGPL-1.0
  1. <?php
  2. /**
  3. *
  4. * This file is part of the phpBB Forum Software package.
  5. *
  6. * @copyright (c) phpBB Limited <https://www.phpbb.com>
  7. * @license GNU General Public License, version 2 (GPL-2.0)
  8. *
  9. * For full copyright and license information, please see
  10. * the docs/CREDITS.txt file.
  11. *
  12. */
  13. namespace phpbb\event;
  14. /**
  15. * Class php_exporter
  16. * Crawls through a list of files and grabs all php-events
  17. */
  18. class php_exporter
  19. {
  20. /** @var string Path where we look for files*/
  21. protected $path;
  22. /** @var string phpBB Root Path */
  23. protected $root_path;
  24. /** @var string */
  25. protected $current_file;
  26. /** @var string */
  27. protected $current_event;
  28. /** @var int */
  29. protected $current_event_line;
  30. /** @var array */
  31. protected $events;
  32. /** @var array */
  33. protected $file_lines;
  34. /**
  35. * @param string $phpbb_root_path
  36. * @param mixed $extension String 'vendor/ext' to filter, null for phpBB core
  37. */
  38. public function __construct($phpbb_root_path, $extension = null)
  39. {
  40. $this->root_path = $phpbb_root_path;
  41. $this->path = $phpbb_root_path;
  42. $this->events = $this->file_lines = array();
  43. $this->current_file = $this->current_event = '';
  44. $this->current_event_line = 0;
  45. $this->path = $this->root_path;
  46. if ($extension)
  47. {
  48. $this->path .= 'ext/' . $extension . '/';
  49. }
  50. }
  51. /**
  52. * Get the list of all events
  53. *
  54. * @return array Array with events: name => details
  55. */
  56. public function get_events()
  57. {
  58. return $this->events;
  59. }
  60. /**
  61. * Set current event data
  62. *
  63. * @param string $name Name of the current event (used for error messages)
  64. * @param int $line Line where the current event is placed in
  65. * @return null
  66. */
  67. public function set_current_event($name, $line)
  68. {
  69. $this->current_event = $name;
  70. $this->current_event_line = $line;
  71. }
  72. /**
  73. * Set the content of this file
  74. *
  75. * @param array $content Array with the lines of the file
  76. * @return null
  77. */
  78. public function set_content($content)
  79. {
  80. $this->file_lines = $content;
  81. }
  82. /**
  83. * Crawl the phpBB/ directory for php events
  84. * @return int The number of events found
  85. */
  86. public function crawl_phpbb_directory_php()
  87. {
  88. $files = $this->get_recursive_file_list();
  89. $this->events = array();
  90. foreach ($files as $file)
  91. {
  92. $this->crawl_php_file($file);
  93. }
  94. ksort($this->events);
  95. return sizeof($this->events);
  96. }
  97. /**
  98. * Returns a list of files in $dir
  99. *
  100. * @return array List of files (including the path)
  101. */
  102. public function get_recursive_file_list()
  103. {
  104. try
  105. {
  106. $iterator = new \RecursiveIteratorIterator(
  107. new \phpbb\event\recursive_event_filter_iterator(
  108. new \RecursiveDirectoryIterator(
  109. $this->path,
  110. \FilesystemIterator::SKIP_DOTS
  111. ),
  112. $this->path
  113. ),
  114. \RecursiveIteratorIterator::LEAVES_ONLY
  115. );
  116. }
  117. catch (\Exception $e)
  118. {
  119. return array();
  120. }
  121. $files = array();
  122. foreach ($iterator as $file_info)
  123. {
  124. /** @var \RecursiveDirectoryIterator $file_info */
  125. $relative_path = $iterator->getInnerIterator()->getSubPathname();
  126. $files[] = str_replace(DIRECTORY_SEPARATOR, '/', $relative_path);
  127. }
  128. return $files;
  129. }
  130. /**
  131. * Format the php events as a wiki table
  132. * @return string
  133. */
  134. public function export_events_for_wiki()
  135. {
  136. $wiki_page = '= PHP Events (Hook Locations) =' . "\n";
  137. $wiki_page .= '{| class="sortable zebra" cellspacing="0" cellpadding="5"' . "\n";
  138. $wiki_page .= '! Identifier !! Placement !! Arguments !! Added in Release !! Explanation' . "\n";
  139. foreach ($this->events as $event)
  140. {
  141. $wiki_page .= '|- id="' . $event['event'] . '"' . "\n";
  142. $wiki_page .= '| [[#' . $event['event'] . '|' . $event['event'] . ']] || ' . $event['file'] . ' || ' . implode(', ', $event['arguments']) . ' || ' . $event['since'] . ' || ' . $event['description'] . "\n";
  143. }
  144. $wiki_page .= '|}' . "\n";
  145. return $wiki_page;
  146. }
  147. /**
  148. * @param string $file
  149. * @return int Number of events found in this file
  150. * @throws \LogicException
  151. */
  152. public function crawl_php_file($file)
  153. {
  154. $this->current_file = $file;
  155. $this->file_lines = array();
  156. $content = file_get_contents($this->path . $this->current_file);
  157. $num_events_found = 0;
  158. if (strpos($content, "dispatcher->trigger_event('") || strpos($content, "dispatcher->dispatch('"))
  159. {
  160. $this->set_content(explode("\n", $content));
  161. for ($i = 0, $num_lines = sizeof($this->file_lines); $i < $num_lines; $i++)
  162. {
  163. $event_line = false;
  164. $found_trigger_event = strpos($this->file_lines[$i], "dispatcher->trigger_event('");
  165. $arguments = array();
  166. if ($found_trigger_event !== false)
  167. {
  168. $event_line = $i;
  169. $this->set_current_event($this->get_event_name($event_line, false), $event_line);
  170. // Find variables of the event
  171. $arguments = $this->get_vars_from_array();
  172. $doc_vars = $this->get_vars_from_docblock();
  173. $this->validate_vars_docblock_array($arguments, $doc_vars);
  174. }
  175. else
  176. {
  177. $found_dispatch = strpos($this->file_lines[$i], "dispatcher->dispatch('");
  178. if ($found_dispatch !== false)
  179. {
  180. $event_line = $i;
  181. $this->set_current_event($this->get_event_name($event_line, true), $event_line);
  182. }
  183. }
  184. if ($event_line)
  185. {
  186. // Validate @event
  187. $event_line_num = $this->find_event();
  188. $this->validate_event($this->current_event, $this->file_lines[$event_line_num]);
  189. // Validate @since
  190. $since_line_num = $this->find_since();
  191. $since = $this->validate_since($this->file_lines[$since_line_num]);
  192. // Find event description line
  193. $description_line_num = $this->find_description();
  194. $description = substr(trim($this->file_lines[$description_line_num]), strlen('* '));
  195. if (isset($this->events[$this->current_event]))
  196. {
  197. throw new \LogicException("The event '{$this->current_event}' from file "
  198. . "'{$this->current_file}:{$event_line_num}' already exists in file "
  199. . "'{$this->events[$this->current_event]['file']}'", 10);
  200. }
  201. sort($arguments);
  202. $this->events[$this->current_event] = array(
  203. 'event' => $this->current_event,
  204. 'file' => $this->current_file,
  205. 'arguments' => $arguments,
  206. 'since' => $since,
  207. 'description' => $description,
  208. );
  209. $num_events_found++;
  210. }
  211. }
  212. }
  213. return $num_events_found;
  214. }
  215. /**
  216. * Find the name of the event inside the dispatch() line
  217. *
  218. * @param int $event_line
  219. * @param bool $is_dispatch Do we look for dispatch() or trigger_event() ?
  220. * @return string Name of the event
  221. * @throws \LogicException
  222. */
  223. public function get_event_name($event_line, $is_dispatch)
  224. {
  225. $event_text_line = $this->file_lines[$event_line];
  226. $event_text_line = ltrim($event_text_line, "\t");
  227. if ($is_dispatch)
  228. {
  229. $regex = '#\$([a-z](?:[a-z0-9_]|->)*)';
  230. $regex .= '->dispatch\(';
  231. $regex .= '\'' . $this->preg_match_event_name() . '\'';
  232. $regex .= '\);#';
  233. }
  234. else
  235. {
  236. $regex = '#extract\(\$([a-z](?:[a-z0-9_]|->)*)';
  237. $regex .= '->trigger_event\(';
  238. $regex .= '\'' . $this->preg_match_event_name() . '\'';
  239. $regex .= ', compact\(\$vars\)\)\);#';
  240. }
  241. $match = array();
  242. preg_match($regex, $event_text_line, $match);
  243. if (!isset($match[2]))
  244. {
  245. throw new \LogicException("Can not find event name in line '{$event_text_line}' "
  246. . "in file '{$this->current_file}:{$event_line}'", 1);
  247. }
  248. return $match[2];
  249. }
  250. /**
  251. * Returns a regex match for the event name
  252. *
  253. * @return string
  254. */
  255. protected function preg_match_event_name()
  256. {
  257. return '([a-z][a-z0-9_]*(?:\.[a-z][a-z0-9_]*)+)';
  258. }
  259. /**
  260. * Find the $vars array
  261. *
  262. * @return array List of variables
  263. * @throws \LogicException
  264. */
  265. public function get_vars_from_array()
  266. {
  267. $line = ltrim($this->file_lines[$this->current_event_line - 1], "\t");
  268. if ($line === ');')
  269. {
  270. $vars_array = $this->get_vars_from_multi_line_array();
  271. }
  272. else
  273. {
  274. $vars_array = $this->get_vars_from_single_line_array($line);
  275. }
  276. foreach ($vars_array as $var)
  277. {
  278. if (!preg_match('#^([a-zA-Z_][a-zA-Z0-9_]*)$#', $var))
  279. {
  280. throw new \LogicException("Found invalid var '{$var}' in array for event '{$this->current_event}' in file '{$this->current_file}:{$this->current_event_line}'", 3);
  281. }
  282. }
  283. sort($vars_array);
  284. return $vars_array;
  285. }
  286. /**
  287. * Find the variables in single line array
  288. *
  289. * @param string $line
  290. * @param bool $throw_multiline Throw an exception when there are too
  291. * many arguments in one line.
  292. * @return array List of variables
  293. * @throws \LogicException
  294. */
  295. public function get_vars_from_single_line_array($line, $throw_multiline = true)
  296. {
  297. $match = array();
  298. preg_match('#^\$vars = array\(\'([a-zA-Z0-9_\' ,]+)\'\);$#', $line, $match);
  299. if (isset($match[1]))
  300. {
  301. $vars_array = explode("', '", $match[1]);
  302. if ($throw_multiline && sizeof($vars_array) > 6)
  303. {
  304. throw new \LogicException('Should use multiple lines for $vars definition '
  305. . "for event '{$this->current_event}' in file '{$this->current_file}:{$this->current_event_line}'", 2);
  306. }
  307. return $vars_array;
  308. }
  309. else
  310. {
  311. throw new \LogicException("Can not find '\$vars = array();'-line for event '{$this->current_event}' in file '{$this->current_file}:{$this->current_event_line}'", 1);
  312. }
  313. }
  314. /**
  315. * Find the variables in single line array
  316. *
  317. * @return array List of variables
  318. * @throws \LogicException
  319. */
  320. public function get_vars_from_multi_line_array()
  321. {
  322. $current_vars_line = 2;
  323. $var_lines = array();
  324. while (ltrim($this->file_lines[$this->current_event_line - $current_vars_line], "\t") !== '$vars = array(')
  325. {
  326. $var_lines[] = substr(trim($this->file_lines[$this->current_event_line - $current_vars_line]), 0, -1);
  327. $current_vars_line++;
  328. if ($current_vars_line > $this->current_event_line)
  329. {
  330. // Reached the start of the file
  331. throw new \LogicException("Can not find end of \$vars array for event '{$this->current_event}' in file '{$this->current_file}:{$this->current_event_line}'", 2);
  332. }
  333. }
  334. return $this->get_vars_from_single_line_array('$vars = array(' . implode(", ", $var_lines) . ');', false);
  335. }
  336. /**
  337. * Find the $vars array
  338. *
  339. * @return array List of variables
  340. * @throws \LogicException
  341. */
  342. public function get_vars_from_docblock()
  343. {
  344. $doc_vars = array();
  345. $current_doc_line = 1;
  346. $found_comment_end = false;
  347. while (ltrim($this->file_lines[$this->current_event_line - $current_doc_line], "\t") !== '/**')
  348. {
  349. if (ltrim($this->file_lines[$this->current_event_line - $current_doc_line], "\t") === '*/')
  350. {
  351. $found_comment_end = true;
  352. }
  353. if ($found_comment_end)
  354. {
  355. $var_line = trim($this->file_lines[$this->current_event_line - $current_doc_line]);
  356. $var_line = preg_replace('!\s+!', ' ', $var_line);
  357. if (strpos($var_line, '* @var ') === 0)
  358. {
  359. $doc_line = explode(' ', $var_line, 5);
  360. if (sizeof($doc_line) !== 5)
  361. {
  362. throw new \LogicException("Found invalid line '{$this->file_lines[$this->current_event_line - $current_doc_line]}' "
  363. . "for event '{$this->current_event}' in file '{$this->current_file}:{$this->current_event_line}'", 1);
  364. }
  365. $doc_vars[] = $doc_line[3];
  366. }
  367. }
  368. $current_doc_line++;
  369. if ($current_doc_line > $this->current_event_line)
  370. {
  371. // Reached the start of the file
  372. throw new \LogicException("Can not find end of docblock for event '{$this->current_event}' in file '{$this->current_file}:{$this->current_event_line}'", 2);
  373. }
  374. }
  375. if (empty($doc_vars))
  376. {
  377. // Reached the start of the file
  378. throw new \LogicException("Can not find @var lines for event '{$this->current_event}' in file '{$this->current_file}:{$this->current_event_line}'", 3);
  379. }
  380. foreach ($doc_vars as $var)
  381. {
  382. if (!preg_match('#^([a-zA-Z_][a-zA-Z0-9_]*)$#', $var))
  383. {
  384. throw new \LogicException("Found invalid @var '{$var}' in docblock for event "
  385. . "'{$this->current_event}' in file '{$this->current_file}:{$this->current_event_line}'", 4);
  386. }
  387. }
  388. sort($doc_vars);
  389. return $doc_vars;
  390. }
  391. /**
  392. * Find the "@since" Information line
  393. *
  394. * @return int Absolute line number
  395. * @throws \LogicException
  396. */
  397. public function find_since()
  398. {
  399. return $this->find_tag('since', array('event', 'var'));
  400. }
  401. /**
  402. * Find the "@event" Information line
  403. *
  404. * @return int Absolute line number
  405. */
  406. public function find_event()
  407. {
  408. return $this->find_tag('event', array());
  409. }
  410. /**
  411. * Find a "@*" Information line
  412. *
  413. * @param string $find_tag Name of the tag we are trying to find
  414. * @param array $disallowed_tags List of tags that must not appear between
  415. * the tag and the actual event
  416. * @return int Absolute line number
  417. * @throws \LogicException
  418. */
  419. public function find_tag($find_tag, $disallowed_tags)
  420. {
  421. $find_tag_line = 0;
  422. $found_comment_end = false;
  423. while (strpos(ltrim($this->file_lines[$this->current_event_line - $find_tag_line], "\t"), '* @' . $find_tag . ' ') !== 0)
  424. {
  425. if ($found_comment_end && ltrim($this->file_lines[$this->current_event_line - $find_tag_line], "\t") === '/**')
  426. {
  427. // Reached the start of this doc block
  428. throw new \LogicException("Can not find '@{$find_tag}' information for event "
  429. . "'{$this->current_event}' in file '{$this->current_file}:{$this->current_event_line}'", 1);
  430. }
  431. foreach ($disallowed_tags as $disallowed_tag)
  432. {
  433. if ($found_comment_end && strpos(ltrim($this->file_lines[$this->current_event_line - $find_tag_line], "\t"), '* @' . $disallowed_tag) === 0)
  434. {
  435. // Found @var after the @since
  436. throw new \LogicException("Found '@{$disallowed_tag}' information after '@{$find_tag}' for event "
  437. . "'{$this->current_event}' in file '{$this->current_file}:{$this->current_event_line}'", 3);
  438. }
  439. }
  440. if (ltrim($this->file_lines[$this->current_event_line - $find_tag_line], "\t") === '*/')
  441. {
  442. $found_comment_end = true;
  443. }
  444. $find_tag_line++;
  445. if ($find_tag_line >= $this->current_event_line)
  446. {
  447. // Reached the start of the file
  448. throw new \LogicException("Can not find '@{$find_tag}' information for event "
  449. . "'{$this->current_event}' in file '{$this->current_file}:{$this->current_event_line}'", 2);
  450. }
  451. }
  452. return $this->current_event_line - $find_tag_line;
  453. }
  454. /**
  455. * Find a "@*" Information line
  456. *
  457. * @return int Absolute line number
  458. * @throws \LogicException
  459. */
  460. public function find_description()
  461. {
  462. $find_desc_line = 0;
  463. while (ltrim($this->file_lines[$this->current_event_line - $find_desc_line], "\t") !== '/**')
  464. {
  465. $find_desc_line++;
  466. if ($find_desc_line > $this->current_event_line)
  467. {
  468. // Reached the start of the file
  469. throw new \LogicException("Can not find a description for event "
  470. . "'{$this->current_event}' in file '{$this->current_file}:{$this->current_event_line}'", 1);
  471. }
  472. }
  473. $find_desc_line = $this->current_event_line - $find_desc_line + 1;
  474. $desc = trim($this->file_lines[$find_desc_line]);
  475. if (strpos($desc, '* @') === 0 || $desc[0] !== '*' || substr($desc, 1) == '')
  476. {
  477. // First line of the doc block is a @-line, empty or only contains "*"
  478. throw new \LogicException("Can not find a description for event "
  479. . "'{$this->current_event}' in file '{$this->current_file}:{$this->current_event_line}'", 2);
  480. }
  481. return $find_desc_line;
  482. }
  483. /**
  484. * Validate "@since" Information
  485. *
  486. * @param string $line
  487. * @return string
  488. * @throws \LogicException
  489. */
  490. public function validate_since($line)
  491. {
  492. $match = array();
  493. preg_match('#^\* @since (\d+\.\d+\.\d+(?:-(?:a|b|rc|pl)\d+)?)$#', ltrim($line, "\t"), $match);
  494. if (!isset($match[1]))
  495. {
  496. throw new \LogicException("Invalid '@since' information for event "
  497. . "'{$this->current_event}' in file '{$this->current_file}:{$this->current_event_line}'");
  498. }
  499. return $match[1];
  500. }
  501. /**
  502. * Validate "@event" Information
  503. *
  504. * @param string $event_name
  505. * @param string $line
  506. * @return string
  507. * @throws \LogicException
  508. */
  509. public function validate_event($event_name, $line)
  510. {
  511. $event = substr(ltrim($line, "\t"), strlen('* @event '));
  512. if ($event !== trim($event))
  513. {
  514. throw new \LogicException("Invalid '@event' information for event "
  515. . "'{$this->current_event}' in file '{$this->current_file}:{$this->current_event_line}'", 1);
  516. }
  517. if ($event !== $event_name)
  518. {
  519. throw new \LogicException("Event name does not match '@event' tag for event "
  520. . "'{$this->current_event}' in file '{$this->current_file}:{$this->current_event_line}'", 2);
  521. }
  522. return $event;
  523. }
  524. /**
  525. * Validates that two arrays contain the same strings
  526. *
  527. * @param array $vars_array Variables found in the array line
  528. * @param array $vars_docblock Variables found in the doc block
  529. * @return null
  530. * @throws \LogicException
  531. */
  532. public function validate_vars_docblock_array($vars_array, $vars_docblock)
  533. {
  534. $vars_array = array_unique($vars_array);
  535. $vars_docblock = array_unique($vars_docblock);
  536. $sizeof_vars_array = sizeof($vars_array);
  537. if ($sizeof_vars_array !== sizeof($vars_docblock) || $sizeof_vars_array !== sizeof(array_intersect($vars_array, $vars_docblock)))
  538. {
  539. throw new \LogicException("\$vars array does not match the list of '@var' tags for event "
  540. . "'{$this->current_event}' in file '{$this->current_file}:{$this->current_event_line}'");
  541. }
  542. }
  543. }