PageRenderTime 25ms CodeModel.GetById 17ms RepoModel.GetById 0ms app.codeStats 0ms

/phpBB/phpbb/event/md_exporter.php

https://github.com/VSEphpbb/phpbb
PHP | 565 lines | 376 code | 68 blank | 121 comment | 66 complexity | f2a2cb2aa14e0585963cca246dd6e939 MD5 | raw file
  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. * Crawls through a markdown file and grabs all events
  16. */
  17. class md_exporter
  18. {
  19. /** @var string Path where we look for files*/
  20. protected $path;
  21. /** @var string phpBB Root Path */
  22. protected $root_path;
  23. /** @var string The minimum version for the events to return */
  24. protected $min_version;
  25. /** @var string The maximum version for the events to return */
  26. protected $max_version;
  27. /** @var string */
  28. protected $filter;
  29. /** @var string */
  30. protected $current_event;
  31. /** @var array */
  32. protected $events;
  33. /**
  34. * @param string $phpbb_root_path
  35. * @param mixed $extension String 'vendor/ext' to filter, null for phpBB core
  36. * @param string $min_version
  37. * @param string $max_version
  38. */
  39. public function __construct($phpbb_root_path, $extension = null, $min_version = null, $max_version = null)
  40. {
  41. $this->root_path = $phpbb_root_path;
  42. $this->path = $this->root_path;
  43. if ($extension)
  44. {
  45. $this->path .= 'ext/' . $extension . '/';
  46. }
  47. $this->events = array();
  48. $this->events_by_file = array();
  49. $this->filter = $this->current_event = '';
  50. $this->min_version = $min_version;
  51. $this->max_version = $max_version;
  52. }
  53. /**
  54. * Get the list of all events
  55. *
  56. * @return array Array with events: name => details
  57. */
  58. public function get_events()
  59. {
  60. return $this->events;
  61. }
  62. /**
  63. * @param string $md_file Relative from phpBB root
  64. * @return int Number of events found
  65. * @throws \LogicException
  66. */
  67. public function crawl_phpbb_directory_adm($md_file)
  68. {
  69. $this->crawl_eventsmd($md_file, 'adm');
  70. $file_list = $this->get_recursive_file_list($this->path . 'adm/style/');
  71. foreach ($file_list as $file)
  72. {
  73. $file_name = 'adm/style/' . $file;
  74. $this->validate_events_from_file($file_name, $this->crawl_file_for_events($file_name));
  75. }
  76. return sizeof($this->events);
  77. }
  78. /**
  79. * @param string $md_file Relative from phpBB root
  80. * @return int Number of events found
  81. * @throws \LogicException
  82. */
  83. public function crawl_phpbb_directory_styles($md_file)
  84. {
  85. $this->crawl_eventsmd($md_file, 'styles');
  86. $styles = array('prosilver', 'subsilver2');
  87. foreach ($styles as $style)
  88. {
  89. $file_list = $this->get_recursive_file_list(
  90. $this->path . 'styles/' . $style . '/template/'
  91. );
  92. foreach ($file_list as $file)
  93. {
  94. $file_name = 'styles/' . $style . '/template/' . $file;
  95. $this->validate_events_from_file($file_name, $this->crawl_file_for_events($file_name));
  96. }
  97. }
  98. return sizeof($this->events);
  99. }
  100. /**
  101. * @param string $md_file Relative from phpBB root
  102. * @param string $filter Should be 'styles' or 'adm'
  103. * @return int Number of events found
  104. * @throws \LogicException
  105. */
  106. public function crawl_eventsmd($md_file, $filter)
  107. {
  108. if (!file_exists($this->path . $md_file))
  109. {
  110. throw new \LogicException("The event docs file '{$md_file}' could not be found");
  111. }
  112. $file_content = file_get_contents($this->path . $md_file);
  113. $this->filter = $filter;
  114. $events = explode("\n\n", $file_content);
  115. foreach ($events as $event)
  116. {
  117. // Last row of the file
  118. if (strpos($event, "\n===\n") === false)
  119. {
  120. continue;
  121. }
  122. list($event_name, $details) = explode("\n===\n", $event, 2);
  123. $this->validate_event_name($event_name);
  124. $this->current_event = $event_name;
  125. if (isset($this->events[$this->current_event]))
  126. {
  127. throw new \LogicException("The event '{$this->current_event}' is defined multiple times");
  128. }
  129. if (($this->filter == 'adm' && strpos($this->current_event, 'acp_') !== 0)
  130. || ($this->filter == 'styles' && strpos($this->current_event, 'acp_') === 0))
  131. {
  132. continue;
  133. }
  134. list($file_details, $details) = explode("\n* Since: ", $details, 2);
  135. $changed_versions = array();
  136. if (strpos($details, "\n* Changed: ") !== false)
  137. {
  138. list($since, $details) = explode("\n* Changed: ", $details, 2);
  139. while (strpos($details, "\n* Changed: ") !== false)
  140. {
  141. list($changed, $details) = explode("\n* Changed: ", $details, 2);
  142. $changed_versions[] = $changed;
  143. }
  144. list($changed, $description) = explode("\n* Purpose: ", $details, 2);
  145. $changed_versions[] = $changed;
  146. }
  147. else
  148. {
  149. list($since, $description) = explode("\n* Purpose: ", $details, 2);
  150. $changed_versions = array();
  151. }
  152. $files = $this->validate_file_list($file_details);
  153. $since = $this->validate_since($since);
  154. $changes = array();
  155. foreach ($changed_versions as $changed)
  156. {
  157. list($changed_version, $changed_description) = $this->validate_changed($changed);
  158. if (isset($changes[$changed_version]))
  159. {
  160. throw new \LogicException("Duplicate change information found for event '{$this->current_event}'");
  161. }
  162. $changes[$changed_version] = $changed_description;
  163. }
  164. $description = trim($description, "\n") . "\n";
  165. if (!$this->version_is_filtered($since))
  166. {
  167. $is_filtered = false;
  168. foreach ($changes as $version => $null)
  169. {
  170. if ($this->version_is_filtered($version))
  171. {
  172. $is_filtered = true;
  173. break;
  174. }
  175. }
  176. if (!$is_filtered)
  177. {
  178. continue;
  179. }
  180. }
  181. $this->events[$event_name] = array(
  182. 'event' => $this->current_event,
  183. 'files' => $files,
  184. 'since' => $since,
  185. 'changed' => $changes,
  186. 'description' => $description,
  187. );
  188. }
  189. return sizeof($this->events);
  190. }
  191. /**
  192. * The version to check
  193. *
  194. * @param string $version
  195. * @return bool
  196. */
  197. protected function version_is_filtered($version)
  198. {
  199. return (!$this->min_version || phpbb_version_compare($this->min_version, $version, '<='))
  200. && (!$this->max_version || phpbb_version_compare($this->max_version, $version, '>='));
  201. }
  202. /**
  203. * Format the php events as a wiki table
  204. *
  205. * @param string $action
  206. * @return string Number of events found
  207. */
  208. public function export_events_for_wiki($action = '')
  209. {
  210. if ($this->filter === 'adm')
  211. {
  212. if ($action === 'diff')
  213. {
  214. $wiki_page = '=== ACP Template Events ===' . "\n";
  215. }
  216. else
  217. {
  218. $wiki_page = '= ACP Template Events =' . "\n";
  219. }
  220. $wiki_page .= '{| class="zebra sortable" cellspacing="0" cellpadding="5"' . "\n";
  221. $wiki_page .= '! Identifier !! Placement !! Added in Release !! Explanation' . "\n";
  222. }
  223. else
  224. {
  225. if ($action === 'diff')
  226. {
  227. $wiki_page = '=== Template Events ===' . "\n";
  228. }
  229. else
  230. {
  231. $wiki_page = '= Template Events =' . "\n";
  232. }
  233. $wiki_page .= '{| class="zebra sortable" cellspacing="0" cellpadding="5"' . "\n";
  234. $wiki_page .= '! Identifier !! Prosilver Placement (If applicable) !! Subsilver Placement (If applicable) !! Added in Release !! Explanation' . "\n";
  235. }
  236. foreach ($this->events as $event_name => $event)
  237. {
  238. $wiki_page .= "|- id=\"{$event_name}\"\n";
  239. $wiki_page .= "| [[#{$event_name}|{$event_name}]] || ";
  240. if ($this->filter === 'adm')
  241. {
  242. $wiki_page .= implode(', ', $event['files']['adm']);
  243. }
  244. else
  245. {
  246. $wiki_page .= implode(', ', $event['files']['prosilver']) . ' || ' . implode(', ', $event['files']['subsilver2']);
  247. }
  248. $wiki_page .= " || {$event['since']} || " . str_replace("\n", ' ', $event['description']) . "\n";
  249. }
  250. $wiki_page .= '|}' . "\n";
  251. return $wiki_page;
  252. }
  253. /**
  254. * Validates a template event name
  255. *
  256. * @param $event_name
  257. * @return null
  258. * @throws \LogicException
  259. */
  260. public function validate_event_name($event_name)
  261. {
  262. if (!preg_match('#^([a-z][a-z0-9]*(?:_[a-z][a-z0-9]*)+)$#', $event_name))
  263. {
  264. throw new \LogicException("Invalid event name '{$event_name}'");
  265. }
  266. }
  267. /**
  268. * Validate "Since" Information
  269. *
  270. * @param string $since
  271. * @return string
  272. * @throws \LogicException
  273. */
  274. public function validate_since($since)
  275. {
  276. if (!$this->validate_version($since))
  277. {
  278. throw new \LogicException("Invalid since information found for event '{$this->current_event}'");
  279. }
  280. return $since;
  281. }
  282. /**
  283. * Validate "Changed" Information
  284. *
  285. * @param string $changed
  286. * @return string
  287. * @throws \LogicException
  288. */
  289. public function validate_changed($changed)
  290. {
  291. if (strpos($changed, ' ') !== false)
  292. {
  293. list($version, $description) = explode(' ', $changed, 2);
  294. }
  295. else
  296. {
  297. $version = $changed;
  298. $description = '';
  299. }
  300. if (!$this->validate_version($version))
  301. {
  302. throw new \LogicException("Invalid changed information found for event '{$this->current_event}'");
  303. }
  304. return array($version, $description);
  305. }
  306. /**
  307. * Validate "version" Information
  308. *
  309. * @param string $version
  310. * @return bool True if valid, false otherwise
  311. */
  312. public function validate_version($version)
  313. {
  314. return preg_match('#^\d+\.\d+\.\d+(?:-(?:a|b|RC|pl)\d+)?$#', $version);
  315. }
  316. /**
  317. * Validate the files list
  318. *
  319. * @param string $file_details
  320. * @return array
  321. * @throws \LogicException
  322. */
  323. public function validate_file_list($file_details)
  324. {
  325. $files_list = array(
  326. 'prosilver' => array(),
  327. 'subsilver2' => array(),
  328. 'adm' => array(),
  329. );
  330. // Multi file list
  331. if (strpos($file_details, "* Locations:\n + ") === 0)
  332. {
  333. $file_details = substr($file_details, strlen("* Locations:\n + "));
  334. $files = explode("\n + ", $file_details);
  335. foreach ($files as $file)
  336. {
  337. if (!file_exists($this->path . $file) || substr($file, -5) !== '.html')
  338. {
  339. throw new \LogicException("Invalid file '{$file}' not found for event '{$this->current_event}'", 1);
  340. }
  341. if (($this->filter !== 'adm') && strpos($file, 'styles/prosilver/template/') === 0)
  342. {
  343. $files_list['prosilver'][] = substr($file, strlen('styles/prosilver/template/'));
  344. }
  345. else if (($this->filter !== 'adm') && strpos($file, 'styles/subsilver2/template/') === 0)
  346. {
  347. $files_list['subsilver2'][] = substr($file, strlen('styles/subsilver2/template/'));
  348. }
  349. else if (($this->filter === 'adm') && strpos($file, 'adm/style/') === 0)
  350. {
  351. $files_list['adm'][] = substr($file, strlen('adm/style/'));
  352. }
  353. else
  354. {
  355. throw new \LogicException("Invalid file '{$file}' not found for event '{$this->current_event}'", 2);
  356. }
  357. $this->events_by_file[$file][] = $this->current_event;
  358. }
  359. }
  360. else if ($this->filter == 'adm')
  361. {
  362. $file = substr($file_details, strlen('* Location: '));
  363. if (!file_exists($this->path . $file) || substr($file, -5) !== '.html')
  364. {
  365. throw new \LogicException("Invalid file '{$file}' not found for event '{$this->current_event}'", 1);
  366. }
  367. $files_list['adm'][] = substr($file, strlen('adm/style/'));
  368. $this->events_by_file[$file][] = $this->current_event;
  369. }
  370. else
  371. {
  372. throw new \LogicException("Invalid file list found for event '{$this->current_event}'", 2);
  373. }
  374. return $files_list;
  375. }
  376. /**
  377. * Get all template events in a template file
  378. *
  379. * @param string $file
  380. * @return array
  381. * @throws \LogicException
  382. */
  383. public function crawl_file_for_events($file)
  384. {
  385. if (!file_exists($this->path . $file))
  386. {
  387. throw new \LogicException("File '{$file}' does not exist", 1);
  388. }
  389. $event_list = array();
  390. $file_content = file_get_contents($this->path . $file);
  391. $events = explode('<!-- EVENT ', $file_content);
  392. // Remove the code before the first event
  393. array_shift($events);
  394. foreach ($events as $event)
  395. {
  396. $event = explode(' -->', $event, 2);
  397. $event_list[] = array_shift($event);
  398. }
  399. return $event_list;
  400. }
  401. /**
  402. * Validates whether all events from $file are in the md file and vice-versa
  403. *
  404. * @param string $file
  405. * @param array $events
  406. * @return true
  407. * @throws \LogicException
  408. */
  409. public function validate_events_from_file($file, array $events)
  410. {
  411. if (empty($this->events_by_file[$file]) && empty($events))
  412. {
  413. return true;
  414. }
  415. else if (empty($this->events_by_file[$file]))
  416. {
  417. $event_list = implode("', '", $events);
  418. throw new \LogicException("File '{$file}' should not contain events, but contains: "
  419. . "'{$event_list}'", 1);
  420. }
  421. else if (empty($events))
  422. {
  423. $event_list = implode("', '", $this->events_by_file[$file]);
  424. throw new \LogicException("File '{$file}' contains no events, but should contain: "
  425. . "'{$event_list}'", 1);
  426. }
  427. $missing_events_from_file = array();
  428. foreach ($this->events_by_file[$file] as $event)
  429. {
  430. if (!in_array($event, $events))
  431. {
  432. $missing_events_from_file[] = $event;
  433. }
  434. }
  435. if (!empty($missing_events_from_file))
  436. {
  437. $event_list = implode("', '", $missing_events_from_file);
  438. throw new \LogicException("File '{$file}' does not contain events: '{$event_list}'", 2);
  439. }
  440. $missing_events_from_md = array();
  441. foreach ($events as $event)
  442. {
  443. if (!in_array($event, $this->events_by_file[$file]))
  444. {
  445. $missing_events_from_md[] = $event;
  446. }
  447. }
  448. if (!empty($missing_events_from_md))
  449. {
  450. $event_list = implode("', '", $missing_events_from_md);
  451. throw new \LogicException("File '{$file}' contains additional events: '{$event_list}'", 3);
  452. }
  453. return true;
  454. }
  455. /**
  456. * Returns a list of files in $dir
  457. *
  458. * Works recursive with any depth
  459. *
  460. * @param string $dir Directory to go through
  461. * @return array List of files (including directories)
  462. */
  463. public function get_recursive_file_list($dir)
  464. {
  465. try
  466. {
  467. $iterator = new \RecursiveIteratorIterator(
  468. new \phpbb\recursive_dot_prefix_filter_iterator(
  469. new \RecursiveDirectoryIterator(
  470. $dir,
  471. \FilesystemIterator::SKIP_DOTS
  472. )
  473. ),
  474. \RecursiveIteratorIterator::SELF_FIRST
  475. );
  476. }
  477. catch (\Exception $e)
  478. {
  479. return array();
  480. }
  481. $files = array();
  482. foreach ($iterator as $file_info)
  483. {
  484. /** @var \RecursiveDirectoryIterator $file_info */
  485. if ($file_info->isDir())
  486. {
  487. continue;
  488. }
  489. $relative_path = $iterator->getInnerIterator()->getSubPathname();
  490. if (substr($relative_path, -5) == '.html')
  491. {
  492. $files[] = str_replace(DIRECTORY_SEPARATOR, '/', $relative_path);
  493. }
  494. }
  495. return $files;
  496. }
  497. }