diff options
Diffstat (limited to 'phpBB/phpbb/event')
-rw-r--r-- | phpBB/phpbb/event/data.php | 10 | ||||
-rw-r--r-- | phpBB/phpbb/event/dispatcher.php | 50 | ||||
-rw-r--r-- | phpBB/phpbb/event/dispatcher_interface.php | 50 | ||||
-rw-r--r-- | phpBB/phpbb/event/extension_subscriber_loader.php | 35 | ||||
-rw-r--r-- | phpBB/phpbb/event/kernel_exception_subscriber.php | 87 | ||||
-rw-r--r-- | phpBB/phpbb/event/kernel_request_subscriber.php | 77 | ||||
-rw-r--r-- | phpBB/phpbb/event/kernel_terminate_subscriber.php | 14 | ||||
-rw-r--r-- | phpBB/phpbb/event/md_exporter.php | 560 | ||||
-rw-r--r-- | phpBB/phpbb/event/php_exporter.php | 718 | ||||
-rw-r--r-- | phpBB/phpbb/event/recursive_event_filter_iterator.php | 71 |
10 files changed, 1524 insertions, 148 deletions
diff --git a/phpBB/phpbb/event/data.php b/phpBB/phpbb/event/data.php index fbb16574ed..c7365aee35 100644 --- a/phpBB/phpbb/event/data.php +++ b/phpBB/phpbb/event/data.php @@ -1,9 +1,13 @@ <?php /** * -* @package phpBB3 -* @copyright (c) 2012 phpBB Group -* @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License v2 +* This file is part of the phpBB Forum Software package. +* +* @copyright (c) phpBB Limited <https://www.phpbb.com> +* @license GNU General Public License, version 2 (GPL-2.0) +* +* For full copyright and license information, please see +* the docs/CREDITS.txt file. * */ diff --git a/phpBB/phpbb/event/dispatcher.php b/phpBB/phpbb/event/dispatcher.php index 74b35eb78d..1c4abeb108 100644 --- a/phpBB/phpbb/event/dispatcher.php +++ b/phpBB/phpbb/event/dispatcher.php @@ -1,15 +1,20 @@ <?php /** * -* @package phpBB3 -* @copyright (c) 2012 phpBB Group -* @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License v2 +* This file is part of the phpBB Forum Software package. +* +* @copyright (c) phpBB Limited <https://www.phpbb.com> +* @license GNU General Public License, version 2 (GPL-2.0) +* +* For full copyright and license information, please see +* the docs/CREDITS.txt file. * */ namespace phpbb\event; use Symfony\Component\EventDispatcher\ContainerAwareEventDispatcher; +use Symfony\Component\EventDispatcher\Event; /** * Extension of the Symfony2 EventDispatcher @@ -25,12 +30,49 @@ use Symfony\Component\EventDispatcher\ContainerAwareEventDispatcher; * extract($phpbb_dispatcher->trigger_event('core.index', compact($vars))); * */ -class dispatcher extends ContainerAwareEventDispatcher +class dispatcher extends ContainerAwareEventDispatcher implements dispatcher_interface { + /** + * @var bool + */ + protected $disabled = false; + + /** + * {@inheritdoc} + */ public function trigger_event($eventName, $data = array()) { $event = new \phpbb\event\data($data); $this->dispatch($eventName, $event); return $event->get_data_filtered(array_keys($data)); } + + /** + * {@inheritdoc} + */ + public function dispatch($eventName, Event $event = null) + { + if ($this->disabled) + { + return $event; + } + + return parent::dispatch($eventName, $event); + } + + /** + * {@inheritdoc} + */ + public function disable() + { + $this->disabled = true; + } + + /** + * {@inheritdoc} + */ + public function enable() + { + $this->disabled = false; + } } diff --git a/phpBB/phpbb/event/dispatcher_interface.php b/phpBB/phpbb/event/dispatcher_interface.php new file mode 100644 index 0000000000..c66aa98260 --- /dev/null +++ b/phpBB/phpbb/event/dispatcher_interface.php @@ -0,0 +1,50 @@ +<?php +/** +* +* This file is part of the phpBB Forum Software package. +* +* @copyright (c) phpBB Limited <https://www.phpbb.com> +* @license GNU General Public License, version 2 (GPL-2.0) +* +* For full copyright and license information, please see +* the docs/CREDITS.txt file. +* +*/ + +namespace phpbb\event; + +/** +* Extension of the Symfony2 EventDispatcher +* +* It provides an additional `trigger_event` method, which +* gives some syntactic sugar for dispatching events. Instead +* of creating the event object, the method will do that for +* you. +* +* Example: +* +* $vars = array('page_title'); +* extract($phpbb_dispatcher->trigger_event('core.index', compact($vars))); +* +*/ +interface dispatcher_interface extends \Symfony\Component\EventDispatcher\EventDispatcherInterface +{ + /** + * Construct and dispatch an event + * + * @param string $eventName The event name + * @param array $data An array containing the variables sending with the event + * @return mixed + */ + public function trigger_event($eventName, $data = array()); + + /** + * Disable the event dispatcher. + */ + public function disable(); + + /** + * Enable the event dispatcher. + */ + public function enable(); +} diff --git a/phpBB/phpbb/event/extension_subscriber_loader.php b/phpBB/phpbb/event/extension_subscriber_loader.php deleted file mode 100644 index 6408f93e2a..0000000000 --- a/phpBB/phpbb/event/extension_subscriber_loader.php +++ /dev/null @@ -1,35 +0,0 @@ -<?php -/** -* -* @package phpBB3 -* @copyright (c) 2012 phpBB Group -* @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License v2 -* -*/ - -namespace phpbb\event; - -use Symfony\Component\EventDispatcher\EventDispatcherInterface; - -class extension_subscriber_loader -{ - private $dispatcher; - private $listener_collection; - - public function __construct(EventDispatcherInterface $dispatcher, \phpbb\di\service_collection $listener_collection) - { - $this->dispatcher = $dispatcher; - $this->listener_collection = $listener_collection; - } - - public function load() - { - if (!empty($this->listener_collection)) - { - foreach ($this->listener_collection as $listener) - { - $this->dispatcher->addSubscriber($listener); - } - } - } -} diff --git a/phpBB/phpbb/event/kernel_exception_subscriber.php b/phpBB/phpbb/event/kernel_exception_subscriber.php index 8a4de1fbad..e427abf5e3 100644 --- a/phpBB/phpbb/event/kernel_exception_subscriber.php +++ b/phpBB/phpbb/event/kernel_exception_subscriber.php @@ -1,44 +1,51 @@ <?php /** * -* @package phpBB3 -* @copyright (c) 2012 phpBB Group -* @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License v2 +* This file is part of the phpBB Forum Software package. +* +* @copyright (c) phpBB Limited <https://www.phpbb.com> +* @license GNU General Public License, version 2 (GPL-2.0) +* +* For full copyright and license information, please see +* the docs/CREDITS.txt file. * */ namespace phpbb\event; use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\HttpFoundation\JsonResponse; +use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface; use Symfony\Component\HttpKernel\KernelEvents; use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent; -use Symfony\Component\HttpKernel\Exception\HttpException; use Symfony\Component\HttpFoundation\Response; class kernel_exception_subscriber implements EventSubscriberInterface { /** * Template object + * * @var \phpbb\template\template */ protected $template; /** - * User object - * @var \phpbb\user + * Language object + * + * @var \phpbb\language\language */ - protected $user; + protected $language; /** * Construct method * - * @param \phpbb\template\template $template Template object - * @param \phpbb\user $user User object + * @param \phpbb\template\template $template Template object + * @param \phpbb\language\language $language Language object */ - public function __construct(\phpbb\template\template $template, \phpbb\user $user) + public function __construct(\phpbb\template\template $template, \phpbb\language\language $language) { $this->template = $template; - $this->user = $user; + $this->language = $language; } /** @@ -49,27 +56,59 @@ class kernel_exception_subscriber implements EventSubscriberInterface */ public function on_kernel_exception(GetResponseForExceptionEvent $event) { - page_header($this->user->lang('INFORMATION')); - $exception = $event->getException(); - $this->template->assign_vars(array( - 'MESSAGE_TITLE' => $this->user->lang('INFORMATION'), - 'MESSAGE_TEXT' => $exception->getMessage(), - )); + $message = $exception->getMessage(); + + if ($exception instanceof \phpbb\exception\exception_interface) + { + $message = $this->language->lang_array($message, $exception->get_parameters()); + } + + if (!$event->getRequest()->isXmlHttpRequest()) + { + page_header($this->language->lang('INFORMATION')); + + $this->template->assign_vars(array( + 'MESSAGE_TITLE' => $this->language->lang('INFORMATION'), + 'MESSAGE_TEXT' => $message, + )); + + $this->template->set_filenames(array( + 'body' => 'message_body.html', + )); + + page_footer(true, false, false); + + $response = new Response($this->template->assign_display('body'), 500); + } + else + { + $data = array(); + + if (!empty($message)) + { + $data['message'] = $message; + } + + if (defined('DEBUG')) + { + $data['trace'] = $exception->getTrace(); + } - $this->template->set_filenames(array( - 'body' => 'message_body.html', - )); + $response = new JsonResponse($data, 500); + } - page_footer(true, false, false); + if ($exception instanceof HttpExceptionInterface) + { + $response->setStatusCode($exception->getStatusCode()); + $response->headers->add($exception->getHeaders()); + } - $status_code = $exception instanceof HttpException ? $exception->getStatusCode() : 500; - $response = new Response($this->template->assign_display('body'), $status_code); $event->setResponse($response); } - public static function getSubscribedEvents() + static public function getSubscribedEvents() { return array( KernelEvents::EXCEPTION => 'on_kernel_exception', diff --git a/phpBB/phpbb/event/kernel_request_subscriber.php b/phpBB/phpbb/event/kernel_request_subscriber.php deleted file mode 100644 index 7d5418498b..0000000000 --- a/phpBB/phpbb/event/kernel_request_subscriber.php +++ /dev/null @@ -1,77 +0,0 @@ -<?php -/** -* -* @package phpBB3 -* @copyright (c) 2012 phpBB Group -* @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License v2 -* -*/ - -namespace phpbb\event; - -use Symfony\Component\EventDispatcher\EventSubscriberInterface; -use Symfony\Component\HttpKernel\KernelEvents; -use Symfony\Component\HttpKernel\Event\GetResponseEvent; -use Symfony\Component\HttpKernel\EventListener\RouterListener; -use Symfony\Component\Routing\RequestContext; - -class kernel_request_subscriber implements EventSubscriberInterface -{ - /** - * Extension finder object - * @var \phpbb\extension\finder - */ - protected $finder; - - /** - * PHP extension - * @var string - */ - protected $php_ext; - - /** - * Root path - * @var string - */ - protected $root_path; - - /** - * Construct method - * - * @param \phpbb\extension\finder $finder Extension finder object - * @param string $root_path Root path - * @param string $php_ext PHP extension - */ - public function __construct(\phpbb\extension\finder $finder, $root_path, $php_ext) - { - $this->finder = $finder; - $this->root_path = $root_path; - $this->php_ext = $php_ext; - } - - /** - * This listener is run when the KernelEvents::REQUEST event is triggered - * - * This is responsible for setting up the routing information - * - * @param GetResponseEvent $event - * @return null - */ - public function on_kernel_request(GetResponseEvent $event) - { - $request = $event->getRequest(); - $context = new RequestContext(); - $context->fromRequest($request); - - $matcher = phpbb_get_url_matcher($this->finder, $context, $this->root_path, $this->php_ext); - $router_listener = new RouterListener($matcher, $context); - $router_listener->onKernelRequest($event); - } - - public static function getSubscribedEvents() - { - return array( - KernelEvents::REQUEST => 'on_kernel_request', - ); - } -} diff --git a/phpBB/phpbb/event/kernel_terminate_subscriber.php b/phpBB/phpbb/event/kernel_terminate_subscriber.php index 32dba322d1..f0d0a2f595 100644 --- a/phpBB/phpbb/event/kernel_terminate_subscriber.php +++ b/phpBB/phpbb/event/kernel_terminate_subscriber.php @@ -1,9 +1,13 @@ <?php /** * -* @package phpBB3 -* @copyright (c) 2012 phpBB Group -* @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License v2 +* This file is part of the phpBB Forum Software package. +* +* @copyright (c) phpBB Limited <https://www.phpbb.com> +* @license GNU General Public License, version 2 (GPL-2.0) +* +* For full copyright and license information, please see +* the docs/CREDITS.txt file. * */ @@ -28,10 +32,10 @@ class kernel_terminate_subscriber implements EventSubscriberInterface exit_handler(); } - public static function getSubscribedEvents() + static public function getSubscribedEvents() { return array( - KernelEvents::TERMINATE => 'on_kernel_terminate', + KernelEvents::TERMINATE => array('on_kernel_terminate', ~PHP_INT_MAX), ); } } diff --git a/phpBB/phpbb/event/md_exporter.php b/phpBB/phpbb/event/md_exporter.php new file mode 100644 index 0000000000..e042d0a5d1 --- /dev/null +++ b/phpBB/phpbb/event/md_exporter.php @@ -0,0 +1,560 @@ +<?php +/** +* +* This file is part of the phpBB Forum Software package. +* +* @copyright (c) phpBB Limited <https://www.phpbb.com> +* @license GNU General Public License, version 2 (GPL-2.0) +* +* For full copyright and license information, please see +* the docs/CREDITS.txt file. +* +*/ + +namespace phpbb\event; + +/** +* Crawls through a markdown file and grabs all events +*/ +class md_exporter +{ + /** @var string Path where we look for files*/ + protected $path; + + /** @var string phpBB Root Path */ + protected $root_path; + + /** @var string The minimum version for the events to return */ + protected $min_version; + + /** @var string The maximum version for the events to return */ + protected $max_version; + + /** @var string */ + protected $filter; + + /** @var string */ + protected $current_event; + + /** @var array */ + protected $events; + + /** + * @param string $phpbb_root_path + * @param mixed $extension String 'vendor/ext' to filter, null for phpBB core + * @param string $min_version + * @param string $max_version + */ + public function __construct($phpbb_root_path, $extension = null, $min_version = null, $max_version = null) + { + $this->root_path = $phpbb_root_path; + $this->path = $this->root_path; + if ($extension) + { + $this->path .= 'ext/' . $extension . '/'; + } + + $this->events = array(); + $this->events_by_file = array(); + $this->filter = $this->current_event = ''; + $this->min_version = $min_version; + $this->max_version = $max_version; + } + + /** + * Get the list of all events + * + * @return array Array with events: name => details + */ + public function get_events() + { + return $this->events; + } + + /** + * @param string $md_file Relative from phpBB root + * @return int Number of events found + * @throws \LogicException + */ + public function crawl_phpbb_directory_adm($md_file) + { + $this->crawl_eventsmd($md_file, 'adm'); + + $file_list = $this->get_recursive_file_list($this->path . 'adm/style/'); + foreach ($file_list as $file) + { + $file_name = 'adm/style/' . $file; + $this->validate_events_from_file($file_name, $this->crawl_file_for_events($file_name)); + } + + return sizeof($this->events); + } + + /** + * @param string $md_file Relative from phpBB root + * @return int Number of events found + * @throws \LogicException + */ + public function crawl_phpbb_directory_styles($md_file) + { + $this->crawl_eventsmd($md_file, 'styles'); + + $styles = array('prosilver'); + foreach ($styles as $style) + { + $file_list = $this->get_recursive_file_list( + $this->path . 'styles/' . $style . '/template/' + ); + + foreach ($file_list as $file) + { + $file_name = 'styles/' . $style . '/template/' . $file; + $this->validate_events_from_file($file_name, $this->crawl_file_for_events($file_name)); + } + } + + return sizeof($this->events); + } + + /** + * @param string $md_file Relative from phpBB root + * @param string $filter Should be 'styles' or 'adm' + * @return int Number of events found + * @throws \LogicException + */ + public function crawl_eventsmd($md_file, $filter) + { + if (!file_exists($this->path . $md_file)) + { + throw new \LogicException("The event docs file '{$md_file}' could not be found"); + } + + $file_content = file_get_contents($this->path . $md_file); + $this->filter = $filter; + + $events = explode("\n\n", $file_content); + foreach ($events as $event) + { + // Last row of the file + if (strpos($event, "\n===\n") === false) + { + continue; + } + + list($event_name, $details) = explode("\n===\n", $event, 2); + $this->validate_event_name($event_name); + $this->current_event = $event_name; + + if (isset($this->events[$this->current_event])) + { + throw new \LogicException("The event '{$this->current_event}' is defined multiple times"); + } + + if (($this->filter == 'adm' && strpos($this->current_event, 'acp_') !== 0) + || ($this->filter == 'styles' && strpos($this->current_event, 'acp_') === 0)) + { + continue; + } + + list($file_details, $details) = explode("\n* Since: ", $details, 2); + + $changed_versions = array(); + if (strpos($details, "\n* Changed: ") !== false) + { + list($since, $details) = explode("\n* Changed: ", $details, 2); + while (strpos($details, "\n* Changed: ") !== false) + { + list($changed, $details) = explode("\n* Changed: ", $details, 2); + $changed_versions[] = $changed; + } + list($changed, $description) = explode("\n* Purpose: ", $details, 2); + $changed_versions[] = $changed; + } + else + { + list($since, $description) = explode("\n* Purpose: ", $details, 2); + $changed_versions = array(); + } + + $files = $this->validate_file_list($file_details); + $since = $this->validate_since($since); + $changes = array(); + foreach ($changed_versions as $changed) + { + list($changed_version, $changed_description) = $this->validate_changed($changed); + + if (isset($changes[$changed_version])) + { + throw new \LogicException("Duplicate change information found for event '{$this->current_event}'"); + } + + $changes[$changed_version] = $changed_description; + } + $description = trim($description, "\n") . "\n"; + + if (!$this->version_is_filtered($since)) + { + $is_filtered = false; + foreach ($changes as $version => $null) + { + if ($this->version_is_filtered($version)) + { + $is_filtered = true; + break; + } + } + + if (!$is_filtered) + { + continue; + } + } + + $this->events[$event_name] = array( + 'event' => $this->current_event, + 'files' => $files, + 'since' => $since, + 'changed' => $changes, + 'description' => $description, + ); + } + + return sizeof($this->events); + } + + /** + * The version to check + * + * @param string $version + * @return bool + */ + protected function version_is_filtered($version) + { + return (!$this->min_version || phpbb_version_compare($this->min_version, $version, '<=')) + && (!$this->max_version || phpbb_version_compare($this->max_version, $version, '>=')); + } + + /** + * Format the php events as a wiki table + * + * @param string $action + * @return string Number of events found + */ + public function export_events_for_wiki($action = '') + { + if ($this->filter === 'adm') + { + if ($action === 'diff') + { + $wiki_page = '=== ACP Template Events ===' . "\n"; + } + else + { + $wiki_page = '= ACP Template Events =' . "\n"; + } + $wiki_page .= '{| class="zebra sortable" cellspacing="0" cellpadding="5"' . "\n"; + $wiki_page .= '! Identifier !! Placement !! Added in Release !! Explanation' . "\n"; + } + else + { + if ($action === 'diff') + { + $wiki_page = '=== Template Events ===' . "\n"; + } + else + { + $wiki_page = '= Template Events =' . "\n"; + } + $wiki_page .= '{| class="zebra sortable" cellspacing="0" cellpadding="5"' . "\n"; + $wiki_page .= '! Identifier !! Prosilver Placement (If applicable) !! Added in Release !! Explanation' . "\n"; + } + + foreach ($this->events as $event_name => $event) + { + $wiki_page .= "|- id=\"{$event_name}\"\n"; + $wiki_page .= "| [[#{$event_name}|{$event_name}]] || "; + + if ($this->filter === 'adm') + { + $wiki_page .= implode(', ', $event['files']['adm']); + } + else + { + $wiki_page .= implode(', ', $event['files']['prosilver']); + } + + $wiki_page .= " || {$event['since']} || " . str_replace("\n", ' ', $event['description']) . "\n"; + } + $wiki_page .= '|}' . "\n"; + + return $wiki_page; + } + + /** + * Validates a template event name + * + * @param $event_name + * @return null + * @throws \LogicException + */ + public function validate_event_name($event_name) + { + if (!preg_match('#^([a-z][a-z0-9]*(?:_[a-z][a-z0-9]*)+)$#', $event_name)) + { + throw new \LogicException("Invalid event name '{$event_name}'"); + } + } + + /** + * Validate "Since" Information + * + * @param string $since + * @return string + * @throws \LogicException + */ + public function validate_since($since) + { + if (!$this->validate_version($since)) + { + throw new \LogicException("Invalid since information found for event '{$this->current_event}'"); + } + + return $since; + } + + /** + * Validate "Changed" Information + * + * @param string $changed + * @return string + * @throws \LogicException + */ + public function validate_changed($changed) + { + if (strpos($changed, ' ') !== false) + { + list($version, $description) = explode(' ', $changed, 2); + } + else + { + $version = $changed; + $description = ''; + } + + if (!$this->validate_version($version)) + { + throw new \LogicException("Invalid changed information found for event '{$this->current_event}'"); + } + + return array($version, $description); + } + + /** + * Validate "version" Information + * + * @param string $version + * @return bool True if valid, false otherwise + */ + public function validate_version($version) + { + return preg_match('#^\d+\.\d+\.\d+(?:-(?:a|b|RC|pl)\d+)?$#', $version); + } + + /** + * Validate the files list + * + * @param string $file_details + * @return array + * @throws \LogicException + */ + public function validate_file_list($file_details) + { + $files_list = array( + 'prosilver' => array(), + 'adm' => array(), + ); + + // Multi file list + if (strpos($file_details, "* Locations:\n + ") === 0) + { + $file_details = substr($file_details, strlen("* Locations:\n + ")); + $files = explode("\n + ", $file_details); + foreach ($files as $file) + { + if (!file_exists($this->path . $file) || substr($file, -5) !== '.html') + { + throw new \LogicException("Invalid file '{$file}' not found for event '{$this->current_event}'", 1); + } + + if (($this->filter !== 'adm') && strpos($file, 'styles/prosilver/template/') === 0) + { + $files_list['prosilver'][] = substr($file, strlen('styles/prosilver/template/')); + } + else if (($this->filter === 'adm') && strpos($file, 'adm/style/') === 0) + { + $files_list['adm'][] = substr($file, strlen('adm/style/')); + } + else + { + throw new \LogicException("Invalid file '{$file}' not found for event '{$this->current_event}'", 2); + } + + $this->events_by_file[$file][] = $this->current_event; + } + } + else if ($this->filter == 'adm') + { + $file = substr($file_details, strlen('* Location: ')); + if (!file_exists($this->path . $file) || substr($file, -5) !== '.html') + { + throw new \LogicException("Invalid file '{$file}' not found for event '{$this->current_event}'", 1); + } + + $files_list['adm'][] = substr($file, strlen('adm/style/')); + + $this->events_by_file[$file][] = $this->current_event; + } + else + { + throw new \LogicException("Invalid file list found for event '{$this->current_event}'", 2); + } + + return $files_list; + } + + /** + * Get all template events in a template file + * + * @param string $file + * @return array + * @throws \LogicException + */ + public function crawl_file_for_events($file) + { + if (!file_exists($this->path . $file)) + { + throw new \LogicException("File '{$file}' does not exist", 1); + } + + $event_list = array(); + $file_content = file_get_contents($this->path . $file); + + $events = explode('<!-- EVENT ', $file_content); + // Remove the code before the first event + array_shift($events); + foreach ($events as $event) + { + $event = explode(' -->', $event, 2); + $event_list[] = array_shift($event); + } + + return $event_list; + } + + /** + * Validates whether all events from $file are in the md file and vice-versa + * + * @param string $file + * @param array $events + * @return true + * @throws \LogicException + */ + public function validate_events_from_file($file, array $events) + { + if (empty($this->events_by_file[$file]) && empty($events)) + { + return true; + } + else if (empty($this->events_by_file[$file])) + { + $event_list = implode("', '", $events); + throw new \LogicException("File '{$file}' should not contain events, but contains: " + . "'{$event_list}'", 1); + } + else if (empty($events)) + { + $event_list = implode("', '", $this->events_by_file[$file]); + throw new \LogicException("File '{$file}' contains no events, but should contain: " + . "'{$event_list}'", 1); + } + + $missing_events_from_file = array(); + foreach ($this->events_by_file[$file] as $event) + { + if (!in_array($event, $events)) + { + $missing_events_from_file[] = $event; + } + } + + if (!empty($missing_events_from_file)) + { + $event_list = implode("', '", $missing_events_from_file); + throw new \LogicException("File '{$file}' does not contain events: '{$event_list}'", 2); + } + + $missing_events_from_md = array(); + foreach ($events as $event) + { + if (!in_array($event, $this->events_by_file[$file])) + { + $missing_events_from_md[] = $event; + } + } + + if (!empty($missing_events_from_md)) + { + $event_list = implode("', '", $missing_events_from_md); + throw new \LogicException("File '{$file}' contains additional events: '{$event_list}'", 3); + } + + return true; + } + + /** + * Returns a list of files in $dir + * + * Works recursive with any depth + * + * @param string $dir Directory to go through + * @return array List of files (including directories) + */ + public function get_recursive_file_list($dir) + { + try + { + $iterator = new \RecursiveIteratorIterator( + new \phpbb\recursive_dot_prefix_filter_iterator( + new \RecursiveDirectoryIterator( + $dir, + \FilesystemIterator::SKIP_DOTS + ) + ), + \RecursiveIteratorIterator::SELF_FIRST + ); + } + catch (\Exception $e) + { + return array(); + } + + $files = array(); + foreach ($iterator as $file_info) + { + /** @var \RecursiveDirectoryIterator $file_info */ + if ($file_info->isDir()) + { + continue; + } + + $relative_path = $iterator->getInnerIterator()->getSubPathname(); + + if (substr($relative_path, -5) == '.html') + { + $files[] = str_replace(DIRECTORY_SEPARATOR, '/', $relative_path); + } + } + + return $files; + } +} diff --git a/phpBB/phpbb/event/php_exporter.php b/phpBB/phpbb/event/php_exporter.php new file mode 100644 index 0000000000..d2ab0595c0 --- /dev/null +++ b/phpBB/phpbb/event/php_exporter.php @@ -0,0 +1,718 @@ +<?php +/** +* +* This file is part of the phpBB Forum Software package. +* +* @copyright (c) phpBB Limited <https://www.phpbb.com> +* @license GNU General Public License, version 2 (GPL-2.0) +* +* For full copyright and license information, please see +* the docs/CREDITS.txt file. +* +*/ + +namespace phpbb\event; + +/** +* Class php_exporter +* Crawls through a list of files and grabs all php-events +*/ +class php_exporter +{ + /** @var string Path where we look for files*/ + protected $path; + + /** @var string phpBB Root Path */ + protected $root_path; + + /** @var string The minimum version for the events to return */ + protected $min_version; + + /** @var string The maximum version for the events to return */ + protected $max_version; + + /** @var string */ + protected $current_file; + + /** @var string */ + protected $current_event; + + /** @var int */ + protected $current_event_line; + + /** @var array */ + protected $events; + + /** @var array */ + protected $file_lines; + + /** + * @param string $phpbb_root_path + * @param mixed $extension String 'vendor/ext' to filter, null for phpBB core + * @param string $min_version + * @param string $max_version + */ + public function __construct($phpbb_root_path, $extension = null, $min_version = null, $max_version = null) + { + $this->root_path = $phpbb_root_path; + $this->path = $phpbb_root_path; + $this->events = $this->file_lines = array(); + $this->current_file = $this->current_event = ''; + $this->current_event_line = 0; + $this->min_version = $min_version; + $this->max_version = $max_version; + + $this->path = $this->root_path; + if ($extension) + { + $this->path .= 'ext/' . $extension . '/'; + } + } + + /** + * Get the list of all events + * + * @return array Array with events: name => details + */ + public function get_events() + { + return $this->events; + } + + /** + * Set current event data + * + * @param string $name Name of the current event (used for error messages) + * @param int $line Line where the current event is placed in + * @return null + */ + public function set_current_event($name, $line) + { + $this->current_event = $name; + $this->current_event_line = $line; + } + + /** + * Set the content of this file + * + * @param array $content Array with the lines of the file + * @return null + */ + public function set_content($content) + { + $this->file_lines = $content; + } + + /** + * Crawl the phpBB/ directory for php events + * @return int The number of events found + */ + public function crawl_phpbb_directory_php() + { + $files = $this->get_recursive_file_list(); + $this->events = array(); + foreach ($files as $file) + { + $this->crawl_php_file($file); + } + ksort($this->events); + + return sizeof($this->events); + } + + /** + * Returns a list of files in $dir + * + * @return array List of files (including the path) + */ + public function get_recursive_file_list() + { + try + { + $iterator = new \RecursiveIteratorIterator( + new \phpbb\event\recursive_event_filter_iterator( + new \RecursiveDirectoryIterator( + $this->path, + \FilesystemIterator::SKIP_DOTS + ), + $this->path + ), + \RecursiveIteratorIterator::LEAVES_ONLY + ); + } + catch (\Exception $e) + { + return array(); + } + + $files = array(); + foreach ($iterator as $file_info) + { + /** @var \RecursiveDirectoryIterator $file_info */ + $relative_path = $iterator->getInnerIterator()->getSubPathname(); + $files[] = str_replace(DIRECTORY_SEPARATOR, '/', $relative_path); + } + + return $files; + } + + /** + * Format the php events as a wiki table + * + * @param string $action + * @return string + */ + public function export_events_for_wiki($action = '') + { + if ($action === 'diff') + { + $wiki_page = '=== PHP Events (Hook Locations) ===' . "\n"; + } + else + { + $wiki_page = '= PHP Events (Hook Locations) =' . "\n"; + } + $wiki_page .= '{| class="sortable zebra" cellspacing="0" cellpadding="5"' . "\n"; + $wiki_page .= '! Identifier !! Placement !! Arguments !! Added in Release !! Explanation' . "\n"; + foreach ($this->events as $event) + { + $wiki_page .= '|- id="' . $event['event'] . '"' . "\n"; + $wiki_page .= '| [[#' . $event['event'] . '|' . $event['event'] . ']] || ' . $event['file'] . ' || ' . implode(', ', $event['arguments']) . ' || ' . $event['since'] . ' || ' . $event['description'] . "\n"; + } + $wiki_page .= '|}' . "\n"; + + return $wiki_page; + } + + /** + * @param string $file + * @return int Number of events found in this file + * @throws \LogicException + */ + public function crawl_php_file($file) + { + $this->current_file = $file; + $this->file_lines = array(); + $content = file_get_contents($this->path . $this->current_file); + $num_events_found = 0; + + if (strpos($content, "dispatcher->trigger_event('") || strpos($content, "dispatcher->dispatch('")) + { + $this->set_content(explode("\n", $content)); + for ($i = 0, $num_lines = sizeof($this->file_lines); $i < $num_lines; $i++) + { + $event_line = false; + $found_trigger_event = strpos($this->file_lines[$i], "dispatcher->trigger_event('"); + $arguments = array(); + if ($found_trigger_event !== false) + { + $event_line = $i; + $this->set_current_event($this->get_event_name($event_line, false), $event_line); + + // Find variables of the event + $arguments = $this->get_vars_from_array(); + $doc_vars = $this->get_vars_from_docblock(); + $this->validate_vars_docblock_array($arguments, $doc_vars); + } + else + { + $found_dispatch = strpos($this->file_lines[$i], "dispatcher->dispatch('"); + if ($found_dispatch !== false) + { + $event_line = $i; + $this->set_current_event($this->get_event_name($event_line, true), $event_line); + } + } + + if ($event_line) + { + // Validate @event + $event_line_num = $this->find_event(); + $this->validate_event($this->current_event, $this->file_lines[$event_line_num]); + + // Validate @since + $since_line_num = $this->find_since(); + $since = $this->validate_since($this->file_lines[$since_line_num]); + + $changed_line_nums = $this->find_changed('changed'); + if (empty($changed_line_nums)) + { + $changed_line_nums = $this->find_changed('change'); + } + $changed_versions = array(); + if (!empty($changed_line_nums)) + { + foreach ($changed_line_nums as $changed_line_num) + { + $changed_versions[] = $this->validate_changed($this->file_lines[$changed_line_num]); + } + } + + if (!$this->version_is_filtered($since)) + { + $valid_version = false; + foreach ($changed_versions as $changed) + { + $valid_version = $valid_version || $this->version_is_filtered($changed); + } + + if (!$valid_version) + { + continue; + } + } + + // Find event description line + $description_line_num = $this->find_description(); + $description = substr(trim($this->file_lines[$description_line_num]), strlen('* ')); + + if (isset($this->events[$this->current_event])) + { + throw new \LogicException("The event '{$this->current_event}' from file " + . "'{$this->current_file}:{$event_line_num}' already exists in file " + . "'{$this->events[$this->current_event]['file']}'", 10); + } + + sort($arguments); + $this->events[$this->current_event] = array( + 'event' => $this->current_event, + 'file' => $this->current_file, + 'arguments' => $arguments, + 'since' => $since, + 'description' => $description, + ); + $num_events_found++; + } + } + } + + return $num_events_found; + } + + /** + * The version to check + * + * @param string $version + * @return bool + */ + protected function version_is_filtered($version) + { + return (!$this->min_version || phpbb_version_compare($this->min_version, $version, '<=')) + && (!$this->max_version || phpbb_version_compare($this->max_version, $version, '>=')); + } + + /** + * Find the name of the event inside the dispatch() line + * + * @param int $event_line + * @param bool $is_dispatch Do we look for dispatch() or trigger_event() ? + * @return string Name of the event + * @throws \LogicException + */ + public function get_event_name($event_line, $is_dispatch) + { + $event_text_line = $this->file_lines[$event_line]; + $event_text_line = ltrim($event_text_line, "\t "); + + if ($is_dispatch) + { + $regex = '#\$([a-z](?:[a-z0-9_]|->)*)'; + $regex .= '->dispatch\('; + $regex .= '\'' . $this->preg_match_event_name() . '\''; + $regex .= '\);#'; + } + else + { + $regex = '#extract\(\$([a-z](?:[a-z0-9_]|->)*)'; + $regex .= '->trigger_event\('; + $regex .= '\'' . $this->preg_match_event_name() . '\''; + $regex .= ', compact\(\$vars\)\)\);#'; + } + + $match = array(); + preg_match($regex, $event_text_line, $match); + if (!isset($match[2])) + { + throw new \LogicException("Can not find event name in line '{$event_text_line}' " + . "in file '{$this->current_file}:{$event_line}'", 1); + } + + return $match[2]; + } + + /** + * Returns a regex match for the event name + * + * @return string + */ + protected function preg_match_event_name() + { + return '([a-z][a-z0-9_]*(?:\.[a-z][a-z0-9_]*)+)'; + } + + /** + * Find the $vars array + * + * @return array List of variables + * @throws \LogicException + */ + public function get_vars_from_array() + { + $line = ltrim($this->file_lines[$this->current_event_line - 1], "\t"); + if ($line === ');') + { + $vars_array = $this->get_vars_from_multi_line_array(); + } + else + { + $vars_array = $this->get_vars_from_single_line_array($line); + } + + foreach ($vars_array as $var) + { + if (!preg_match('#^([a-zA-Z_][a-zA-Z0-9_]*)$#', $var)) + { + throw new \LogicException("Found invalid var '{$var}' in array for event '{$this->current_event}' in file '{$this->current_file}:{$this->current_event_line}'", 3); + } + } + + sort($vars_array); + return $vars_array; + } + + /** + * Find the variables in single line array + * + * @param string $line + * @param bool $throw_multiline Throw an exception when there are too + * many arguments in one line. + * @return array List of variables + * @throws \LogicException + */ + public function get_vars_from_single_line_array($line, $throw_multiline = true) + { + $match = array(); + preg_match('#^\$vars = array\(\'([a-zA-Z0-9_\' ,]+)\'\);$#', $line, $match); + + if (isset($match[1])) + { + $vars_array = explode("', '", $match[1]); + if ($throw_multiline && sizeof($vars_array) > 6) + { + throw new \LogicException('Should use multiple lines for $vars definition ' + . "for event '{$this->current_event}' in file '{$this->current_file}:{$this->current_event_line}'", 2); + } + return $vars_array; + } + else + { + throw new \LogicException("Can not find '\$vars = array();'-line for event '{$this->current_event}' in file '{$this->current_file}:{$this->current_event_line}'", 1); + } + } + + /** + * Find the variables in single line array + * + * @return array List of variables + * @throws \LogicException + */ + public function get_vars_from_multi_line_array() + { + $current_vars_line = 2; + $var_lines = array(); + while (ltrim($this->file_lines[$this->current_event_line - $current_vars_line], "\t") !== '$vars = array(') + { + $var_lines[] = substr(trim($this->file_lines[$this->current_event_line - $current_vars_line]), 0, -1); + + $current_vars_line++; + if ($current_vars_line > $this->current_event_line) + { + // Reached the start of the file + 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); + } + } + + return $this->get_vars_from_single_line_array('$vars = array(' . implode(", ", $var_lines) . ');', false); + } + + /** + * Find the $vars array + * + * @return array List of variables + * @throws \LogicException + */ + public function get_vars_from_docblock() + { + $doc_vars = array(); + $current_doc_line = 1; + $found_comment_end = false; + while (ltrim($this->file_lines[$this->current_event_line - $current_doc_line], "\t") !== '/**') + { + if (ltrim($this->file_lines[$this->current_event_line - $current_doc_line], "\t ") === '*/') + { + $found_comment_end = true; + } + + if ($found_comment_end) + { + $var_line = trim($this->file_lines[$this->current_event_line - $current_doc_line]); + $var_line = preg_replace('!\s+!', ' ', $var_line); + if (strpos($var_line, '* @var ') === 0) + { + $doc_line = explode(' ', $var_line, 5); + if (sizeof($doc_line) !== 5) + { + throw new \LogicException("Found invalid line '{$this->file_lines[$this->current_event_line - $current_doc_line]}' " + . "for event '{$this->current_event}' in file '{$this->current_file}:{$this->current_event_line}'", 1); + } + $doc_vars[] = $doc_line[3]; + } + } + + $current_doc_line++; + if ($current_doc_line > $this->current_event_line) + { + // Reached the start of the file + throw new \LogicException("Can not find end of docblock for event '{$this->current_event}' in file '{$this->current_file}:{$this->current_event_line}'", 2); + } + } + + if (empty($doc_vars)) + { + // Reached the start of the file + throw new \LogicException("Can not find @var lines for event '{$this->current_event}' in file '{$this->current_file}:{$this->current_event_line}'", 3); + } + + foreach ($doc_vars as $var) + { + if (!preg_match('#^([a-zA-Z_][a-zA-Z0-9_]*)$#', $var)) + { + throw new \LogicException("Found invalid @var '{$var}' in docblock for event " + . "'{$this->current_event}' in file '{$this->current_file}:{$this->current_event_line}'", 4); + } + } + + sort($doc_vars); + return $doc_vars; + } + + /** + * Find the "@since" Information line + * + * @return int Absolute line number + * @throws \LogicException + */ + public function find_since() + { + return $this->find_tag('since', array('event', 'var')); + } + + /** + * Find the "@changed" Information lines + * + * @param string $tag_name Should be 'changed' or 'change' + * @return array Absolute line numbers + * @throws \LogicException + */ + public function find_changed($tag_name) + { + $lines = array(); + $last_line = 0; + try + { + while ($line = $this->find_tag($tag_name, array('since'), $last_line)) + { + $lines[] = $line; + $last_line = $line; + } + } + catch (\LogicException $e) + { + // Not changed? No problem! + } + + return $lines; + } + + /** + * Find the "@event" Information line + * + * @return int Absolute line number + */ + public function find_event() + { + return $this->find_tag('event', array()); + } + + /** + * Find a "@*" Information line + * + * @param string $find_tag Name of the tag we are trying to find + * @param array $disallowed_tags List of tags that must not appear between + * the tag and the actual event + * @param int $skip_to_line Skip lines until this one + * @return int Absolute line number + * @throws \LogicException + */ + public function find_tag($find_tag, $disallowed_tags, $skip_to_line = 0) + { + $find_tag_line = $skip_to_line ? $this->current_event_line - $skip_to_line + 1 : 0; + $found_comment_end = ($skip_to_line) ? true : false; + while (strpos(ltrim($this->file_lines[$this->current_event_line - $find_tag_line], "\t "), '* @' . $find_tag . ' ') !== 0) + { + if ($found_comment_end && ltrim($this->file_lines[$this->current_event_line - $find_tag_line], "\t") === '/**') + { + // Reached the start of this doc block + throw new \LogicException("Can not find '@{$find_tag}' information for event " + . "'{$this->current_event}' in file '{$this->current_file}:{$this->current_event_line}'", 1); + } + + foreach ($disallowed_tags as $disallowed_tag) + { + if ($found_comment_end && strpos(ltrim($this->file_lines[$this->current_event_line - $find_tag_line], "\t "), '* @' . $disallowed_tag) === 0) + { + // Found @var after the @since + throw new \LogicException("Found '@{$disallowed_tag}' information after '@{$find_tag}' for event " + . "'{$this->current_event}' in file '{$this->current_file}:{$this->current_event_line}'", 3); + } + } + + if (ltrim($this->file_lines[$this->current_event_line - $find_tag_line], "\t ") === '*/') + { + $found_comment_end = true; + } + + $find_tag_line++; + if ($find_tag_line >= $this->current_event_line) + { + // Reached the start of the file + throw new \LogicException("Can not find '@{$find_tag}' information for event " + . "'{$this->current_event}' in file '{$this->current_file}:{$this->current_event_line}'", 2); + } + } + + return $this->current_event_line - $find_tag_line; + } + + /** + * Find a "@*" Information line + * + * @return int Absolute line number + * @throws \LogicException + */ + public function find_description() + { + $find_desc_line = 0; + while (ltrim($this->file_lines[$this->current_event_line - $find_desc_line], "\t") !== '/**') + { + $find_desc_line++; + if ($find_desc_line > $this->current_event_line) + { + // Reached the start of the file + throw new \LogicException("Can not find a description for event " + . "'{$this->current_event}' in file '{$this->current_file}:{$this->current_event_line}'", 1); + } + } + + $find_desc_line = $this->current_event_line - $find_desc_line + 1; + + $desc = trim($this->file_lines[$find_desc_line]); + if (strpos($desc, '* @') === 0 || $desc[0] !== '*' || substr($desc, 1) == '') + { + // First line of the doc block is a @-line, empty or only contains "*" + throw new \LogicException("Can not find a description for event " + . "'{$this->current_event}' in file '{$this->current_file}:{$this->current_event_line}'", 2); + } + + return $find_desc_line; + } + + /** + * Validate "@since" Information + * + * @param string $line + * @return string + * @throws \LogicException + */ + public function validate_since($line) + { + $match = array(); + preg_match('#^\* @since (\d+\.\d+\.\d+(?:-(?:a|b|RC|pl)\d+)?)$#', ltrim($line, "\t "), $match); + if (!isset($match[1])) + { + throw new \LogicException("Invalid '@since' information for event " + . "'{$this->current_event}' in file '{$this->current_file}:{$this->current_event_line}'"); + } + + return $match[1]; + } + + /** + * Validate "@changed" Information + * + * @param string $line + * @return string + * @throws \LogicException + */ + public function validate_changed($line) + { + $match = array(); + $line = str_replace("\t", ' ', ltrim($line, "\t ")); + preg_match('#^\* @change(d)? (\d+\.\d+\.\d+(?:-(?:a|b|RC|pl)\d+)?)( (?:.*))?$#', $line, $match); + if (!isset($match[2])) + { + throw new \LogicException("Invalid '@changed' information for event " + . "'{$this->current_event}' in file '{$this->current_file}:{$this->current_event_line}'"); + } + + return $match[2]; + } + + /** + * Validate "@event" Information + * + * @param string $event_name + * @param string $line + * @return string + * @throws \LogicException + */ + public function validate_event($event_name, $line) + { + $event = substr(ltrim($line, "\t "), strlen('* @event ')); + + if ($event !== trim($event)) + { + throw new \LogicException("Invalid '@event' information for event " + . "'{$this->current_event}' in file '{$this->current_file}:{$this->current_event_line}'", 1); + } + + if ($event !== $event_name) + { + throw new \LogicException("Event name does not match '@event' tag for event " + . "'{$this->current_event}' in file '{$this->current_file}:{$this->current_event_line}'", 2); + } + + return $event; + } + + /** + * Validates that two arrays contain the same strings + * + * @param array $vars_array Variables found in the array line + * @param array $vars_docblock Variables found in the doc block + * @return null + * @throws \LogicException + */ + public function validate_vars_docblock_array($vars_array, $vars_docblock) + { + $vars_array = array_unique($vars_array); + $vars_docblock = array_unique($vars_docblock); + $sizeof_vars_array = sizeof($vars_array); + + if ($sizeof_vars_array !== sizeof($vars_docblock) || $sizeof_vars_array !== sizeof(array_intersect($vars_array, $vars_docblock))) + { + throw new \LogicException("\$vars array does not match the list of '@var' tags for event " + . "'{$this->current_event}' in file '{$this->current_file}:{$this->current_event_line}'"); + } + } +} diff --git a/phpBB/phpbb/event/recursive_event_filter_iterator.php b/phpBB/phpbb/event/recursive_event_filter_iterator.php new file mode 100644 index 0000000000..64e2e56f6a --- /dev/null +++ b/phpBB/phpbb/event/recursive_event_filter_iterator.php @@ -0,0 +1,71 @@ +<?php +/** +* +* This file is part of the phpBB Forum Software package. +* +* @copyright (c) phpBB Limited <https://www.phpbb.com> +* @license GNU General Public License, version 2 (GPL-2.0) +* +* For full copyright and license information, please see +* the docs/CREDITS.txt file. +* +*/ + +namespace phpbb\event; + +/** +* This filter ignores directories and files starting with a dot. +* It also skips some directories that do not contain events anyway, +* such as e.g. files/, store/ and vendor/ +*/ +class recursive_event_filter_iterator extends \RecursiveFilterIterator +{ + protected $root_path; + + /** + * Construct + * + * @param \RecursiveIterator $iterator + * @param string $root_path + */ + public function __construct(\RecursiveIterator $iterator, $root_path) + { + $this->root_path = str_replace(DIRECTORY_SEPARATOR, '/', $root_path); + parent::__construct($iterator); + } + + /** + * Return the inner iterator's children contained in a recursive_event_filter_iterator + * + * @return recursive_event_filter_iterator + */ + public function getChildren() + { + return new self($this->getInnerIterator()->getChildren(), $this->root_path); + } + + /** + * {@inheritDoc} + */ + public function accept() + { + $relative_path = str_replace(DIRECTORY_SEPARATOR, '/', $this->current()); + $filename = $this->current()->getFilename(); + + return (substr($relative_path, -4) === '.php' || $this->current()->isDir()) + && $filename[0] !== '.' + && strpos($relative_path, $this->root_path . 'cache/') !== 0 + && strpos($relative_path, $this->root_path . 'develop/') !== 0 + && strpos($relative_path, $this->root_path . 'docs/') !== 0 + && strpos($relative_path, $this->root_path . 'ext/') !== 0 + && strpos($relative_path, $this->root_path . 'files/') !== 0 + && strpos($relative_path, $this->root_path . 'includes/utf/') !== 0 + && strpos($relative_path, $this->root_path . 'language/') !== 0 + && strpos($relative_path, $this->root_path . 'phpbb/db/migration/data/') !== 0 + && strpos($relative_path, $this->root_path . 'phpbb/event/') !== 0 + && strpos($relative_path, $this->root_path . 'store/') !== 0 + && strpos($relative_path, $this->root_path . 'tests/') !== 0 + && strpos($relative_path, $this->root_path . 'vendor/') !== 0 + ; + } +} |