From 9fbd4a841197252e8065928bfafd3576babfc824 Mon Sep 17 00:00:00 2001 From: Ismayil Khayredinov Date: Thu, 12 Jan 2017 12:47:38 +0100 Subject: [PATCH] feat(router): adds routing based on URL templates Implements a new approach to routing using URL templates. Uses Symfony Routing Component for URL matching and URL generation based on predefined URL templates. Adds elgg_register_route() and elgg_unregister_route() Fixes #4820 --- composer.json | 3 +- composer.lock | 86 ++++++- docs/guides/context.rst | 25 +- docs/guides/hooks-list.rst | 1 + docs/guides/index.rst | 1 - docs/guides/pagehandler.rst | 27 -- docs/guides/routing.rst | 142 ++++++++--- docs/guides/walled-garden.rst | 6 +- docs/tutorials/blog.rst | 2 +- engine/classes/Elgg/Application.php | 74 ++++-- engine/classes/Elgg/BadRequestException.php | 24 ++ engine/classes/Elgg/BootService.php | 1 + engine/classes/Elgg/Database/EntityTable.php | 2 +- engine/classes/Elgg/Database/Plugins.php | 13 +- engine/classes/Elgg/Di/ServiceProvider.php | 31 ++- .../classes/Elgg/EntityNotFoundException.php | 24 ++ .../Elgg/EntityPermissionsException.php | 24 ++ engine/classes/Elgg/GatekeeperException.php | 24 ++ engine/classes/Elgg/Http/Input.php | 2 +- engine/classes/Elgg/Http/ResponseFactory.php | 1 + engine/classes/Elgg/HttpException.php | 10 + engine/classes/Elgg/PageNotFoundException.php | 24 ++ engine/classes/Elgg/Router.php | 232 ++++++++++++++++-- engine/classes/Elgg/Router/RequestContext.php | 10 + engine/classes/Elgg/Router/Route.php | 10 + .../classes/Elgg/Router/RouteCollection.php | 10 + engine/classes/Elgg/Router/UrlGenerator.php | 10 + engine/classes/Elgg/Router/UrlMatcher.php | 10 + engine/classes/ElggPlugin.php | 38 ++- engine/lib/actions.php | 14 +- engine/lib/constants.php | 5 + engine/lib/deprecated-3.0.php | 101 ++++++++ engine/lib/elgglib.php | 50 ++-- engine/lib/input.php | 33 --- engine/lib/pagehandler.php | 165 +++++-------- engine/lib/user_settings.php | 87 ++----- engine/lib/views.php | 11 +- engine/routes.php | 64 +++++ .../classes/Elgg/Plugins/StaticConfigTest.php | 28 ++- .../phpunit/unit/Elgg/RouteMatchingTest.php | 229 +++++++++++++++++ .../phpunit/unit/Elgg/RouterUnitTest.php | 16 +- .../tests/phpunit/unit/ElggPluginUnitTest.php | 7 + .../mod/test_plugin/elgg-plugin.php | 6 + .../views/default/resources/routes_match.php | 3 + languages/en.php | 6 + mod/blog/actions/blog/delete.php | 12 +- mod/blog/actions/blog/save.php | 6 +- mod/blog/elgg-plugin.php | 49 ++++ mod/blog/lib/blog.php | 9 +- mod/blog/start.php | 142 +++-------- mod/blog/views/default/blog/group_module.php | 9 +- .../views/default/blog/sidebar/revisions.php | 4 +- mod/blog/views/default/resources/blog/add.php | 7 +- mod/blog/views/default/resources/blog/all.php | 7 +- .../views/default/resources/blog/archive.php | 7 +- .../views/default/resources/blog/edit.php | 7 +- .../views/default/resources/blog/friends.php | 11 +- .../views/default/resources/blog/group.php | 9 +- .../views/default/resources/blog/owner.php | 9 +- .../views/default/resources/blog/view.php | 4 +- .../views/default/widgets/blog/content.php | 4 +- views/default/errors/400.php | 3 +- views/default/errors/403.php | 3 +- views/default/errors/404.php | 3 +- views/default/errors/default.php | 3 +- views/default/resources/favicon.ico.php | 13 + views/default/resources/livesearch.php | 16 ++ views/default/resources/manifest.json.php | 8 + views/default/resources/settings/account.php | 15 +- .../default/resources/settings/statistics.php | 20 +- views/default/resources/settings/tools.php | 21 +- views/json/resources/livesearch/users.php | 2 +- 72 files changed, 1545 insertions(+), 550 deletions(-) delete mode 100644 docs/guides/pagehandler.rst create mode 100644 engine/classes/Elgg/BadRequestException.php create mode 100644 engine/classes/Elgg/EntityNotFoundException.php create mode 100644 engine/classes/Elgg/EntityPermissionsException.php create mode 100644 engine/classes/Elgg/GatekeeperException.php create mode 100644 engine/classes/Elgg/HttpException.php create mode 100644 engine/classes/Elgg/PageNotFoundException.php create mode 100644 engine/classes/Elgg/Router/RequestContext.php create mode 100644 engine/classes/Elgg/Router/Route.php create mode 100644 engine/classes/Elgg/Router/RouteCollection.php create mode 100644 engine/classes/Elgg/Router/UrlGenerator.php create mode 100644 engine/classes/Elgg/Router/UrlMatcher.php create mode 100644 engine/routes.php create mode 100644 engine/tests/phpunit/unit/Elgg/RouteMatchingTest.php create mode 100644 engine/tests/test_files/views/default/resources/routes_match.php create mode 100644 views/default/resources/favicon.ico.php create mode 100644 views/default/resources/livesearch.php create mode 100644 views/default/resources/manifest.json.php diff --git a/composer.json b/composer.json index 62b944bdcef..b407c69f9b7 100644 --- a/composer.json +++ b/composer.json @@ -42,7 +42,8 @@ "symfony/var-dumper": "~3.3", "fzaninotto/faker": "^1.6", "peppeocchi/php-cron-scheduler": "2.*", - "bower-asset/normalize-css": "dev-master" + "bower-asset/normalize-css": "dev-master", + "symfony/routing": "^3.3" }, "config": { "process-timeout": 0, diff --git a/composer.lock b/composer.lock index f825613b541..08faff70b39 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "content-hash": "ad9876180ec3f027c668b66a86d4bd37", + "content-hash": "14543d3caf674292c68594e724fd3757", "packages": [ { "name": "bower-asset/jquery", @@ -213,7 +213,7 @@ "MIT" ], "description": "A modern alternative to CSS resets", - "time": "2017-05-02 18:14:41" + "time": "2017-05-02T18:14:41+00:00" }, { "name": "bower-asset/requirejs", @@ -331,7 +331,7 @@ "text", "wysiwyg" ], - "time": "2017-09-13 13:08:14" + "time": "2017-09-13T13:08:14+00:00" }, { "name": "composer/installers", @@ -2438,6 +2438,84 @@ ], "time": "2017-10-11T12:05:26+00:00" }, + { + "name": "symfony/routing", + "version": "v3.3.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/routing.git", + "reference": "4aee1a917fd4859ff8b51b9fd1dfb790a5ecfa26" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/routing/zipball/4aee1a917fd4859ff8b51b9fd1dfb790a5ecfa26", + "reference": "4aee1a917fd4859ff8b51b9fd1dfb790a5ecfa26", + "shasum": "" + }, + "require": { + "php": ">=5.5.9" + }, + "conflict": { + "symfony/config": "<2.8", + "symfony/dependency-injection": "<3.3", + "symfony/yaml": "<3.3" + }, + "require-dev": { + "doctrine/annotations": "~1.0", + "doctrine/common": "~2.2", + "psr/log": "~1.0", + "symfony/config": "~2.8|~3.0", + "symfony/dependency-injection": "~3.3", + "symfony/expression-language": "~2.8|~3.0", + "symfony/http-foundation": "~2.8|~3.0", + "symfony/yaml": "~3.3" + }, + "suggest": { + "doctrine/annotations": "For using the annotation loader", + "symfony/config": "For using the all-in-one router or any loader", + "symfony/dependency-injection": "For loading routes from a service", + "symfony/expression-language": "For using expression matching", + "symfony/http-foundation": "For using a Symfony Request object", + "symfony/yaml": "For using the YAML loader" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.3-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Routing\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Routing Component", + "homepage": "https://symfony.com", + "keywords": [ + "router", + "routing", + "uri", + "url" + ], + "time": "2017-07-21T17:43:13+00:00" + }, { "name": "symfony/var-dumper", "version": "v3.3.6", @@ -3012,7 +3090,7 @@ }, "notification-url": "https://packagist.org/downloads/", "description": "Elgg coding standards", - "time": "2017-11-02 13:08:36" + "time": "2017-11-02T13:08:36+00:00" }, { "name": "myclabs/deep-copy", diff --git a/docs/guides/context.rst b/docs/guides/context.rst index 15361139205..fbea96214d1 100644 --- a/docs/guides/context.rst +++ b/docs/guides/context.rst @@ -1,11 +1,26 @@ Context ======= -Within the Elgg framework, context can be used to by your plugin's functions to determine if they should run or not. You will be registering callbacks to be executed when particular :doc:`events are triggered `. Sometimes the events are generic and you only want to run your callback when your plugin caused the event to be triggered. In that case, you can use the page's context. +.. warning:: -You can explicitly set the context with ``set_context()``. The context is a string and typically you set it to the name of your plugin. You can retrieve the context with the function ``get_context()``. -It's however better to use ``elgg_push_context($string)`` to add a context to the stack. You can check if the context you want in in the current stack by calling ``elgg_in_context($context)``. Don't forget to pop (with ``elgg_pop_context()``) the context after you push one and don't need it anymore. + The contents of this page are outdated. While the functionality is still in place, using global context to + determine your business logic is bad practice, and will make your code less testable and succeptive to bugs. -If you don't set it, Elgg tries to guess the context. If the page was called through the page handler, the context is set to the name of the handler which was set in ``elgg_register_page_handler()``. If the page wasn't called through the page handler, it uses the name of your plugin directory. If it cannot determine that, it returns main as the default context. -Sometimes a view will return different HTML depending on the context. A plugin can take advantage of that by setting the context before calling ``elgg_view()`` on the view and then setting the context back. This is frequently done with the search context. \ No newline at end of file +Within the Elgg framework, context can be used by your plugin's functions to determine if they should run or not. +You will be registering callbacks to be executed when particular :doc:`events are triggered `. +Sometimes the events are generic and you only want to run your callback when your plugin caused the event to be triggered. +In that case, you can use the page's context. + +You can explicitly set the context with ``set_context()``. The context is a string and typically you set it to the name of your plugin. +You can retrieve the context with the function ``get_context()``. +It's however better to use ``elgg_push_context($string)`` to add a context to the stack. +You can check if the context you want in in the current stack by calling ``elgg_in_context($context)``. +Don't forget to pop (with ``elgg_pop_context()``) the context after you push one and don't need it anymore. + +If you don't set it, Elgg tries to guess the context. If the page was called through the router, +the context is set to the first segment of the current route, e.g. ``profile`` in ``profile/username``. + +Sometimes a view will return different HTML depending on the context. +A plugin can take advantage of that by setting the context before calling ``elgg_view()`` on the view and then setting the context back. +This is frequently done with the search context. \ No newline at end of file diff --git a/docs/guides/hooks-list.rst b/docs/guides/hooks-list.rst index f070171e8d4..b28a1f87e38 100644 --- a/docs/guides/hooks-list.rst +++ b/docs/guides/hooks-list.rst @@ -250,6 +250,7 @@ Action hooks **forward, ** Filter the URL to forward a user to when ``forward($url, $reason)`` is called. + In certain cases, the ``params`` array will contain an instance of ``HttpException`` that triggered the error. **response, action:** Filter an instance of ``\Elgg\Http\ResponseBuilder`` before it is sent to the client. diff --git a/docs/guides/index.rst b/docs/guides/index.rst index 65bcc79d1ca..372f5b17985 100644 --- a/docs/guides/index.rst +++ b/docs/guides/index.rst @@ -23,7 +23,6 @@ Customize Elgg's behavior with plugins. javascript menus notifications - pagehandler routing services search diff --git a/docs/guides/pagehandler.rst b/docs/guides/pagehandler.rst deleted file mode 100644 index 983deb84852..00000000000 --- a/docs/guides/pagehandler.rst +++ /dev/null @@ -1,27 +0,0 @@ -Page handler -============ - -Elgg offers a facility to manage your plugin pages via a page handler, enabling custom urls like ``http://yoursite/your_plugin/section``. To add a page handler to a plugin, a handler function needs to be registered in the plugin's ``start.php`` file with ``elgg_register_page_handler()``: - -.. code-block:: php - - elgg_register_page_handler('your_plugin', 'your_plugin_page_handler'); - -The plugin's page handler is passed two parameters: - -- an array containing the sections of the URL exploded by '/'. With this information the handler will be able to apply any logic necessary, for example loading the appropriate view and returning its contents. -- the handler, this is the handler that is currently used (in our example ``your_plugin``). If you don't register multiple page handlers to the same function you'll never need this. - -Code flow ---------- - -Pages in plugins should be rendered via page handlers (not by using ``Elgg\Application``). Generally the rendering is done by views with names starting with ``resources/``. The program flow is something like this: - -1. A user requests ``/plugin_name/section/entity`` -2. Elgg checks if ``plugin_name`` is registered to a page handler and calls that function, passing ``array('section', 'entity')`` as the first argument -3. The page handler function determines which resource view will display the page. -4. The handler uses ``elgg_view_resource()`` to render the page, also passing in any relevant info to the view via the ``$vars`` argument. -5. The resource view combines many separate views, calls formatting functions like ``elgg_view_layout()`` and ``elgg_view_page()``, and then echos the final output -6. The user sees a fully rendered page - -There is no syntax enforced on the URLs, but Elgg's coding standards suggests a certain format. \ No newline at end of file diff --git a/docs/guides/routing.rst b/docs/guides/routing.rst index fd00b3aced1..f4092a4b538 100644 --- a/docs/guides/routing.rst +++ b/docs/guides/routing.rst @@ -20,47 +20,120 @@ an empty segments array. .. warning:: URL identifier/segments should be considered potentially dangerous user input. Elgg uses ``htmlspecialchars`` to escapes HTML entities in them. -Page Handler -============ +Page Handling +============= -To handle all URLs that begin with a particular identifier, you can register a function to -act as a :doc:`/guides/pagehandler`. When the handler is called, the segments array is -passed in as the first argument. - -The following code registers a page handler for "blog" URLs and shows how one might route -the request to a resource view. +Elgg offers a facility to manage your plugin pages via custom routes, enabling URLs like ``http://yoursite/my_plugin/section``. +You can register a new route using ``elgg_register_route()`, or via ``routes`` config in ``elgg-plugin.php``. +Routes map to resource views, where you can render page contents. .. code-block:: php - elgg_register_page_handler('blog', 'blog_page_handler'); + // in your 'init', 'system' handler + elgg_register_route('my_plugin:section' [ + 'path' => 'my_plugin/section/{guid}/{subsection?}', + 'resource' => 'my_plugin/section', + 'requirements' => [ + 'guid' => '\d+', + 'subsection' => '\w+', + ], + ]); - function blog_page_handler(array $segments) { - // if the URL is http://example.com/elgg/blog/view/123/my-blog-post - // $segments contains: ['view', '123', 'my-blog-post'] + // in my_plugin/views/default/resources/my_plugin/section.php + $guid = elgg_extract('guid', $vars); + $subsection = elgg_extract('subsection', $vars); - $subpage = elgg_extract(0, $segments); - if ($subpage === 'view') { + // render content - // use a view for the page logic to allow other plugins to easily change it - $resource = elgg_view_resource('blog/view', [ - 'guid' => (int)elgg_extract(1, $segments); - ]); +In the example above, we have registered a new route that is accessible via ``http://yoursite/my_plugin/section//``. +Whenever that route is accessed with a required ``guid`` segment and an optional ``subsection`` segment, the router +will render the specified ``my_plugin/section`` resource view and pass the parameters extracted from the URL to your +resource view with ``$vars``. - return elgg_ok_response($resource); - } - // redirect to a different location - if ($subpage === '') { - return elgg_redirect_response('blog/all'); - } +Routes names +------------ - // send an error page - if ($subpage === 'owner' && !elgg_entity_exists($segments[1])) { - return elgg_error_response('User not found', 'blog/all', ELGG_HTTP_NOT_FOUND); - } - - // ... handle other subpages - } +Route names can then be used to generate a URL: + +.. code::php + + $url = elgg_generate_url('my_plugin:section', [ + 'guid' => $entity->guid, + 'subsection' => 'assets', + ]); + + +The route names are unique across all plugins and core, so another plugin can override the route by registering different +parameters to the same route name. + +Route names follow a certain convention and in certain cases will be used to automatically resolve URLs, e.g. to display an entity. + +The following conventions are used in core and recommended for plugins: + +**view::** + Maps to the entity profile page, e.g. ``view:user:user`` or ``view:object:blog`` + The path must contain a ``guid``, or ``username`` for users + +**edit::** + Maps to the form to edit the entity, e.g. ``edit:user:user`` or ``edit:object:blog`` + The path must contain a ``guid``, or ``username`` for users + If you need to add subresources, use suffixes, e.g. ``edit:object:blog:images``, keeping at least one subresource as a default without suffix. + +**add::** + Maps to the form to add a new entity of a given type, e.g. ``add:object:blog`` + The path, as a rule, contains ``container_guid`` parameter + +**collection:::** + Maps to listing pages. Common route names used in core are, as follows: + + - ``collection:object:blog:all``: list all blogs + - ``collection:object:blog:owner``: list blogs owned by a user with a given username + - ``collection:object:blog:friends``: list blogs owned by friends of the logged in user (or user with a given username) + - ``collection:object:blog:group``: list blogs in a group + + +Route configuration +------------------- + +Segments can be defined using wildcards, e.g. ``profile/{username}``, which will match all URLs that contain ``profile/`` followed by +and arbitrary username. + +To make a segment optional you can add a ``?`` (question mark) to the wildcard name, e.g. ``profile/{username}/{section?}``. +In this case the URL will be matched even if the ``section`` segment is not provided. + +You can further constrain segments using regex requirements: + +.. php::code + + // elgg-plugin.php + return [ + 'routes' => [ + 'profile' => [ + 'path' => 'profile/{username}/{section?}', + 'resource' => 'profile', + 'requirements' => [ + 'username' => '[\p{L}\p{Nd}._-]+', // only allow valid usernames + 'section' => '\w+', // can only contain alphanumeric characters + ], + 'defaults' => [ + 'section' => 'index', + ], + ], + ] + ]; + +By default, Elgg will set the following requirements for named URL segments: + +.. php::code + + $patterns = [ + 'guid' => '\d+', // only digits + 'group_guid' => '\d+', // only digits + 'container_guid' => '\d+', // only digits + 'owner_guid' => '\d+', // only digits + 'username' => '[\p{L}\p{Nd}._-]+', // letters, digits, underscores, dashes + ]; The ``route`` Plugin Hook @@ -130,13 +203,10 @@ For regular pages, Elgg's program flow is something like this: #. Elgg parses the URL to identifier ``news`` and segments ``['owner', 'jane']``. #. Elgg triggers the plugin hook ``route:rewrite, news`` (see above). #. Elgg triggers the plugin hook ``route, blog`` (was rewritten in the rewrite hook). -#. Elgg finds a registered page handler (see above) for ``blog``, and calls the function, passing in - the segments. -#. The page handler function determines it needs to render a single user's blog. It calls - ``elgg_view_resource('blog/owner', $vars)`` where ``$vars`` contains the username. +#. Elgg finds a registered route that matches the final route path, and renders a resource view associated with it. + It calls ``elgg_view_resource('blog/owner', $vars)`` where ``$vars`` contains the username. #. The ``resources/blog/owner`` view gets the username via ``$vars['username']``, and uses many other views and formatting functions like ``elgg_view_layout()`` and ``elgg_view_page()`` to create the entire HTML page. -#. The page handler echos the view HTML and returns ``true`` to indicate it handled the request. #. PHP invokes Elgg's shutdown sequence. #. The user receives a fully rendered page. diff --git a/docs/guides/walled-garden.rst b/docs/guides/walled-garden.rst index 300299b38ee..e4f1ef2f111 100644 --- a/docs/guides/walled-garden.rst +++ b/docs/guides/walled-garden.rst @@ -13,13 +13,15 @@ From the Advanced Settings page, find the option labelled "Restrict pages to log Exposing pages through Walled Gardens ------------------------------------- -Many plugins extend Elgg by adding pages. Walled Garden mode will prevent these pages from being viewed by logged out users. Elgg uses :ref:`plugin hook ` to manage which pages are visible through the Walled Garden. +Many plugins extend Elgg by adding pages. Walled Garden mode will prevent these pages from being viewed by logged out users. +Elgg uses :ref:`plugin hook ` to manage which pages are visible through the Walled Garden. Plugin authors must register pages as public if they should be viewable through Walled Gardens by responding to the ``public_pages``, ``walled_garden`` plugin hook. The returned value is an array of regexp expressions for public pages. -The following code shows how to expose http://example.org/my_plugin/public_page through a Walled Garden. This assumes the plugin has registered a :doc:`pagehandler` for ``my_plugin``. +The following code shows how to expose http://example.org/my_plugin/public_page through a Walled Garden. +This assumes the plugin has registered a :doc:`route ` for ``my_plugin/public_page``. .. code-block:: php diff --git a/docs/tutorials/blog.rst b/docs/tutorials/blog.rst index f3389135cb3..c24afecc614 100644 --- a/docs/tutorials/blog.rst +++ b/docs/tutorials/blog.rst @@ -276,7 +276,7 @@ forwarded to the site's 404 page (requested page does not exist or not found). In this particular example, the URL must contain either ``/my_blog/add`` or ``/my_blog/view/id`` where id is a valid ID of an entity with the ``my_blog`` subtype. More information about page handling is at -:doc:`Page handler`. +:doc:`Page handler`. .. _tutorials/blog#view: diff --git a/engine/classes/Elgg/Application.php b/engine/classes/Elgg/Application.php index 8106f75b06e..c4c20c926b6 100644 --- a/engine/classes/Elgg/Application.php +++ b/engine/classes/Elgg/Application.php @@ -8,10 +8,13 @@ use Elgg\Di\ServiceProvider; use Elgg\Filesystem\Directory; use Elgg\Filesystem\Directory\Local; +use Elgg\Http\ErrorResponse; use Elgg\Http\Request; use Elgg\Project\Paths; use InstallationException; use InvalidArgumentException; +use InvalidParameterException; +use SecurityException; /** * Load, boot, and implement a front controller for an Elgg application @@ -441,40 +444,63 @@ public static function index() { * Routes the request, booting core if not yet booted * * @return bool False if Elgg wants the PHP CLI server to handle the request + * @throws InstallationException + * @throws InvalidParameterException + * @throws SecurityException */ public function run() { - $config = $this->_services->config; - $request = $this->_services->request; + try { + $config = $this->_services->config; + $request = $this->_services->request; + + if ($request->isCliServer()) { + if ($request->isCliServable(Paths::project())) { + return false; + } - if ($request->isCliServer()) { - if ($request->isCliServable(Paths::project())) { - return false; + // overwrite value from settings + $www_root = rtrim($request->getSchemeAndHttpHost() . $request->getBaseUrl(), '/') . '/'; + $config->wwwroot = $www_root; + $config->wwwroot_cli_server = $www_root; } - // overwrite value from settings - $www_root = rtrim($request->getSchemeAndHttpHost() . $request->getBaseUrl(), '/') . '/'; - $config->wwwroot = $www_root; - $config->wwwroot_cli_server = $www_root; - } + if (0 === strpos($request->getElggPath(), '/cache/')) { + $this->_services->cacheHandler->handleRequest($request, $this)->prepare($request)->send(); - if (0 === strpos($request->getElggPath(), '/cache/')) { - $this->_services->cacheHandler->handleRequest($request, $this)->prepare($request)->send(); - return true; - } + return true; + } - if (0 === strpos($request->getElggPath(), '/serve-file/')) { - $this->_services->serveFileHandler->getResponse($request)->send(); - return true; - } + if (0 === strpos($request->getElggPath(), '/serve-file/')) { + $this->_services->serveFileHandler->getResponse($request)->send(); - $this->bootCore(); + return true; + } - // re-fetch new request from services in case it was replaced by route:rewrite - $request = $this->_services->request; + $this->bootCore(); - if (!$this->_services->router->route($request)) { - forward('', '404'); + // re-fetch new request from services in case it was replaced by route:rewrite + $request = $this->_services->request; + + if (!$this->_services->router->route($request)) { + throw new PageNotFoundException(); + } + } catch (HttpException $ex) { + $forward_url = REFERRER; + if ($ex instanceof GatekeeperException) { + $forward_url = elgg_is_logged_in() ? '' : '/login'; + } + + $hook_params = [ + 'exception' => $ex, + ]; + + $this->_services->hooks->trigger('forward', $ex->getCode(), $hook_params, $forward_url); + + $response = new ErrorResponse($ex->getMessage(), $ex->getCode(), $forward_url); + $this->_services->responseFactory->respond($response); } + + return true; } /** @@ -518,7 +544,7 @@ public static function install() { _elgg_services()->responseFactory->respond($response); return headers_sent(); - } catch (\InvalidParameterException $ex) { + } catch (InvalidParameterException $ex) { throw new InstallationException($ex->getMessage()); } } diff --git a/engine/classes/Elgg/BadRequestException.php b/engine/classes/Elgg/BadRequestException.php new file mode 100644 index 00000000000..c1ad2c71984 --- /dev/null +++ b/engine/classes/Elgg/BadRequestException.php @@ -0,0 +1,24 @@ +guids = $guid; + $where->guids = (int) $guid; $where->viewer_guid = $user_guid; $select = Select::fromTable('entities'); diff --git a/engine/classes/Elgg/Database/Plugins.php b/engine/classes/Elgg/Database/Plugins.php index a9e2844d339..deef7f54071 100644 --- a/engine/classes/Elgg/Database/Plugins.php +++ b/engine/classes/Elgg/Database/Plugins.php @@ -380,12 +380,13 @@ function load() { $plugins_path = elgg_get_plugins_path(); $start_flags = ELGG_PLUGIN_INCLUDE_START | - ELGG_PLUGIN_REGISTER_VIEWS | - ELGG_PLUGIN_REGISTER_ACTIONS | - ELGG_PLUGIN_REGISTER_LANGUAGES | - ELGG_PLUGIN_REGISTER_WIDGETS | - ELGG_PLUGIN_REGISTER_CLASSES; - + ELGG_PLUGIN_REGISTER_VIEWS | + ELGG_PLUGIN_REGISTER_ACTIONS | + ELGG_PLUGIN_REGISTER_ROUTES | + ELGG_PLUGIN_REGISTER_LANGUAGES | + ELGG_PLUGIN_REGISTER_WIDGETS | + ELGG_PLUGIN_REGISTER_CLASSES; + if (!$plugins_path) { return false; } diff --git a/engine/classes/Elgg/Di/ServiceProvider.php b/engine/classes/Elgg/Di/ServiceProvider.php index 2c523d01c47..771f911c8e2 100644 --- a/engine/classes/Elgg/Di/ServiceProvider.php +++ b/engine/classes/Elgg/Di/ServiceProvider.php @@ -75,8 +75,10 @@ * @property-read \Elgg\Database\QueryCounter $queryCounter * @property-read \Elgg\RedirectService $redirects * @property-read \Elgg\Http\Request $request + * @property-read \Elgg\Router\RequestContext $requestContext * @property-read \Elgg\Http\ResponseFactory $responseFactory * @property-read \Elgg\Database\RelationshipsTable $relationshipsTable + * @property-read \Elgg\Router\RouteCollection $routeCollection * @property-read \Elgg\Router $router * @property-read \Elgg\Database\Seeder $seeder * @property-read \Elgg\Application\ServeFileHandler $serveFileHandler @@ -95,6 +97,8 @@ * @property-read \Elgg\Security\UrlSigner $urlSigner * @property-read \Elgg\UpgradeService $upgrades * @property-read \Elgg\Upgrade\Locator $upgradeLocator + * @property-read \Elgg\Router\UrlGenerator $urlGenerator + * @property-read \Elgg\Router\UrlMatcher $urlMatcher * @property-read \Elgg\UploadService $uploads * @property-read \Elgg\UserCapabilities $userCapabilities * @property-read \Elgg\Database\UsersTable $usersTable @@ -415,6 +419,12 @@ public function __construct(Config $config) { $this->setFactory('request', [\Elgg\Http\Request::class, 'createFromGlobals']); + $this->setFactory('requestContext', function(ServiceProvider $c) { + $context = new \Elgg\Router\RequestContext(); + $context->fromRequest($c->request); + return $context; + }); + $this->setFactory('responseFactory', function(ServiceProvider $c) { if (php_sapi_name() === 'cli') { $transport = new \Elgg\Http\OutputBufferTransport(); @@ -424,9 +434,12 @@ public function __construct(Config $config) { return new \Elgg\Http\ResponseFactory($c->request, $c->hooks, $c->ajax, $transport); }); + $this->setFactory('routeCollection', function(ServiceProvider $c) { + return new \Elgg\Router\RouteCollection(); + }); + $this->setFactory('router', function(ServiceProvider $c) { - // TODO(evan): Init routes from plugins or cache - $router = new \Elgg\Router($c->hooks); + $router = new \Elgg\Router($c->hooks, $c->routeCollection, $c->urlMatcher, $c->urlGenerator); if ($c->config->enable_profiling) { $router->setTimer($c->timer); } @@ -499,6 +512,20 @@ public function __construct(Config $config) { ); }); + $this->setFactory('urlGenerator', function(ServiceProvider $c) { + return new \Elgg\Router\UrlGenerator( + $c->routeCollection, + $c->requestContext + ); + }); + + $this->setFactory('urlMatcher', function(ServiceProvider $c) { + return new \Elgg\Router\UrlMatcher( + $c->routeCollection, + $c->requestContext + ); + }); + $this->setFactory('userCapabilities', function(ServiceProvider $c) { return new \Elgg\UserCapabilities($c->hooks, $c->entityTable, $c->session); }); diff --git a/engine/classes/Elgg/EntityNotFoundException.php b/engine/classes/Elgg/EntityNotFoundException.php new file mode 100644 index 00000000000..fcb6b31e5cf --- /dev/null +++ b/engine/classes/Elgg/EntityNotFoundException.php @@ -0,0 +1,24 @@ +context->push('input'); if (isset($this->data[$variable])) { // a plugin has already set this variable diff --git a/engine/classes/Elgg/Http/ResponseFactory.php b/engine/classes/Elgg/Http/ResponseFactory.php index 4a487dcaf6d..630abf30980 100644 --- a/engine/classes/Elgg/Http/ResponseFactory.php +++ b/engine/classes/Elgg/Http/ResponseFactory.php @@ -296,6 +296,7 @@ public function respondWithError($error, $status_code = ELGG_HTTP_BAD_REQUEST, a } $params['type'] = $forward_reason; + $params['params']['error'] = $error; $error_page = elgg_view_resource('error', $params); return $this->send($this->prepareResponse($error_page, $status_code)); } diff --git a/engine/classes/Elgg/HttpException.php b/engine/classes/Elgg/HttpException.php new file mode 100644 index 00000000000..03a98761dea --- /dev/null +++ b/engine/classes/Elgg/HttpException.php @@ -0,0 +1,10 @@ +hooks = $hooks; + $this->routes = $routes; + $this->matcher = $matcher; + $this->generator = $generator; } /** @@ -41,7 +78,10 @@ public function __construct(PluginHooksService $hooks) { * modify the routing or handle a request. * * @param Request $request The request to handle. + * * @return boolean Whether the request was routed successfully. + * @throws InvalidParameterException + * @throws SecurityException * @access private */ public function route(Request $request) { @@ -56,13 +96,14 @@ public function route(Request $request) { $is_walled_garden = _elgg_config()->walled_garden; $is_logged_in = _elgg_services()->session->isLoggedIn(); $url = elgg_normalize_url($identifier . '/' . implode('/', $segments)); - + if ($is_walled_garden && !$is_logged_in && !$this->isPublicPage($url)) { if (!elgg_is_xhr()) { _elgg_services()->session->set('last_forward_from', current_page_url()); } register_error(_elgg_services()->translator->translate('loggedinrequired')); _elgg_services()->responseFactory->redirect('', 'walled_garden'); + return false; } @@ -86,6 +127,8 @@ public function route(Request $request) { $output = ob_get_clean(); $response = elgg_ok_response($output); } else { + $response = false; + if ($result !== $old) { _elgg_services()->logger->warn('Use the route:rewrite hook to modify routes.'); } @@ -98,15 +141,41 @@ public function route(Request $request) { $segments = $result['segments']; - $response = false; + $path = '/'; + if ($identifier) { + $path .= $identifier; + if (!empty($segments)) { + $path .= '/' . implode('/', $segments); + } + } - if (isset($this->handlers[$identifier]) && is_callable($this->handlers[$identifier])) { - $function = $this->handlers[$identifier]; - $response = call_user_func($function, $segments, $identifier); + try { + $parameters = $this->matcher->match($path); + + $this->current_route = $this->routes->get($parameters['_route']); + + $resource = elgg_extract('_resource', $parameters); + unset($parameters['_resource']); + + $handler = elgg_extract('_handler', $parameters); + unset($parameters['_handler']); + + if ($handler) { + if (is_callable($handler)) { + $response = call_user_func($handler, $segments, $identifier); + } + } else { + $output = elgg_view_resource($resource, $parameters); + $response = elgg_ok_response($output); + } + } catch (ResourceNotFoundException $ex) { + // continue with the legacy logic + } catch (MethodNotAllowedException $ex) { + $response = elgg_error_response($ex->getMessage(), REFERRER, ELGG_HTTP_METHOD_NOT_ALLOWED); } - + $output = ob_get_clean(); - + if ($response === false) { return headers_sent(); } @@ -119,8 +188,9 @@ public function route(Request $request) { if (_elgg_services()->responseFactory->getSentResponse()) { return true; } - + _elgg_services()->responseFactory->respond($response); + return headers_sent(); } @@ -132,34 +202,143 @@ public function route(Request $request) { * @param string $function Your function name * * @return bool Depending on success + * @deprecated 3.0 */ public function registerPageHandler($identifier, $function) { - if (is_callable($function, true)) { - $this->handlers[$identifier] = $function; - return true; + if (!is_callable($function, true)) { + return false; } - return false; + $this->registerRoute($identifier, [ + 'path' => "/$identifier/{segments}", + 'handler' => $function, + 'defaults' => [ + 'segments' => '', + ], + 'requirements' => [ + 'segments' => '.+', + ], + ]); + + return true; } /** - * Unregister a page handler for an identifier + * Register a new route * - * @param string $identifier The page type identifier + * Route paths can contain wildcard segments, i.e. /blog/owner/{username} + * To make a certain wildcard segment optional, add ? to its name, + * i.e. /blog/owner/{username?} + * + * Wildcard requirements for common named variables such as 'guid' and 'username' + * will be set automatically. + * + * @param string $name Unique route name + * This name can later be used to generate route URLs + * @param array $params Route parameters + * - path : path of the route + * - resource : name of the resource view + * - defaults : default values of wildcard segments + * - requirements : regex patterns for wildcard segment requirements + * - methods : HTTP methods + * + * @return Route + * @throws InvalidParameterException + */ + public function registerRoute($name, array $params = []) { + + $path = elgg_extract('path', $params); + $resource = elgg_extract('resource', $params); + $handler = elgg_extract('handler', $params); + + if (!$path || (!$resource && !$handler)) { + throw new InvalidParameterException(__METHOD__ . ' requires "path" and "resource" parameters to be set'); + } + + $defaults = elgg_extract('defaults', $params, []); + $requirements = elgg_extract('requirements', $params, []); + $methods = elgg_extract('methods', $params, []); + + $patterns = [ + 'guid' => '\d+', + 'group_guid' => '\d+', + 'container_guid' => '\d+', + 'owner_guid' => '\d+', + 'username' => '[\p{L}\p{Nd}._-]+', + ]; + + $path = trim($path, '/'); + $segments = explode('/', $path); + foreach ($segments as &$segment) { + // look for segments that are defined as optional with added ? + // e.g. /blog/owner/{username?} + + if (!preg_match('/\{(\w*)(\?)?\}/i', $segment, $matches)) { + continue; + } + + $wildcard = $matches[1]; + if (!isset($defaults[$wildcard]) && isset($matches[2])) { + $defaults[$wildcard] = ''; // make it optional + } + + if (array_key_exists($wildcard, $patterns) && !isset($requirements[$wildcard])) { + $requirements[$wildcard] = $patterns[$wildcard]; + } + + $segment = '{' . $wildcard . '}'; + } + + $path = '/' . implode('/', $segments); + + $defaults['_resource'] = $resource; + $defaults['_handler'] = $handler; + + $route = new Route($path, $defaults, $requirements, [], '', [], $methods); + + $this->routes->add($name, $route); + + return $route; + } + + /** + * Unregister a route by its name + * + * @param string $name Name of the route * * @return void */ - public function unregisterPageHandler($identifier) { - unset($this->handlers[$identifier]); + public function unregisterRoute($name) { + $this->routes->remove($name); + } + + /** + * Generate a relative URL for a named route + * + * @param string $name Route name + * @param array $parameters Query parameters + * + * @return string + */ + public function generateUrl($name, array $parameters = []) { + try { + return $this->generator->generate($name, $parameters, UrlGenerator::ABSOLUTE_PATH); + } catch (RouteNotFoundException $exception) { + elgg_log($exception->getMessage(), 'ERROR'); + return ''; + } } /** - * Get page handlers as array of identifier => callback + * Unregister a page handler for an identifier + * + * @param string $identifier The page type identifier * - * @return array + * @return void + * @deprecated 3.0 */ - public function getPageHandlers() { - return $this->handlers; + public function unregisterPageHandler($identifier) { + $this->unregisterRoute($identifier); } /** @@ -195,6 +374,7 @@ public function allowRewrite(Request $request) { // rewrite request $segments = $new['segments']; array_unshift($segments, $new['identifier']); + return $request->setUrlSegments($segments); } @@ -252,7 +432,7 @@ public function isPublicPage($url = '') { ]; $public_routes = _elgg_services()->hooks->trigger('public_pages', 'walled_garden', $params, $defaults); - + $site_url = preg_quote($site_url); foreach ($public_routes as $public_route) { $pattern = "`^{$site_url}{$public_route}/*$`i"; diff --git a/engine/classes/Elgg/Router/RequestContext.php b/engine/classes/Elgg/Router/RequestContext.php new file mode 100644 index 00000000000..ddd4fd65b46 --- /dev/null +++ b/engine/classes/Elgg/Router/RequestContext.php @@ -0,0 +1,10 @@ +activateEntities(); if ($this->canReadFile('activate.php')) { - $flags = ELGG_PLUGIN_INCLUDE_START | ELGG_PLUGIN_REGISTER_CLASSES | - ELGG_PLUGIN_REGISTER_LANGUAGES | ELGG_PLUGIN_REGISTER_VIEWS | ELGG_PLUGIN_REGISTER_WIDGETS | ELGG_PLUGIN_REGISTER_ACTIONS; + _elgg_services()->hooks->getEvents()->trigger('cache:flush', 'system'); + + $flags = ELGG_PLUGIN_INCLUDE_START | + ELGG_PLUGIN_REGISTER_CLASSES | + ELGG_PLUGIN_REGISTER_LANGUAGES | + ELGG_PLUGIN_REGISTER_VIEWS | + ELGG_PLUGIN_REGISTER_WIDGETS | + ELGG_PLUGIN_REGISTER_ACTIONS | + ELGG_PLUGIN_REGISTER_ROUTES; $this->start($flags); @@ -632,8 +639,6 @@ public function activate() { $this->deactivate(); } - _elgg_services()->hooks->getEvents()->trigger('cache:flush', 'system'); - _elgg_services()->plugins->setBootPlugins(null); _elgg_services()->logger->notice("Plugin {$this->getID()} has been activated"); @@ -795,6 +800,11 @@ public function start($flags) { $this->registerActions(); } + // include routes + if ($flags & ELGG_PLUGIN_REGISTER_ROUTES) { + $this->registerRoutes(); + } + // include widgets if ($flags & ELGG_PLUGIN_REGISTER_WIDGETS) { // should load after views because those are used during registration @@ -972,6 +982,26 @@ public static function addActionsFromStaticConfig(array $spec, $root_path) { } } + /** + * Registers the plugin's routes provided in the plugin config file + * + * @throws PluginException + * @return void + */ + protected function registerRoutes() { + $router = _elgg_services()->router; + + $spec = (array) $this->getStaticConfig('routes', []); + + foreach ($spec as $name => $route_spec) { + if (!is_array($route_spec)) { + continue; + } + + $router->registerRoute($name, $route_spec); + } + } + /** * Registers the plugin's widgets provided in the plugin config file * diff --git a/engine/lib/actions.php b/engine/lib/actions.php index 130ed60ae37..70582713087 100644 --- a/engine/lib/actions.php +++ b/engine/lib/actions.php @@ -225,21 +225,9 @@ function _elgg_csrf_token_refresh() { return _elgg_services()->actions->handleTokenRefreshRequest(); } -/** - * Initialize some ajaxy actions features - * - * @return void - * - * @access private - */ -function actions_init() { - elgg_register_page_handler('action', '_elgg_action_handler'); - elgg_register_page_handler('refresh_token', '_elgg_csrf_token_refresh'); -} - /** * @see \Elgg\Application::loadCore Do not do work here. Just register for events. */ return function(\Elgg\EventsService $events, \Elgg\HooksRegistrationService $hooks) { - $events->registerHandler('init', 'system', 'actions_init'); + }; diff --git a/engine/lib/constants.php b/engine/lib/constants.php index fc95d62abde..ef04dfe7f30 100644 --- a/engine/lib/constants.php +++ b/engine/lib/constants.php @@ -155,6 +155,11 @@ */ define('ELGG_PLUGIN_IGNORE_MANIFEST', 64); +/** + * Tells \ElggPlugin::start() to automatically register the plugin's routes. + */ +define('ELGG_PLUGIN_REGISTER_ROUTES', 128); + /** * Prefix for plugin user setting names */ diff --git a/engine/lib/deprecated-3.0.php b/engine/lib/deprecated-3.0.php index 675f0fb197b..f7749ab3281 100644 --- a/engine/lib/deprecated-3.0.php +++ b/engine/lib/deprecated-3.0.php @@ -1079,4 +1079,105 @@ function sanitise_filepath($path, $append_slash = true) { elgg_deprecated_notice(__FUNCTION__ . ' is deprecated. Use \\Elgg\\Project\\Paths::sanitize().', 3.0); return \Elgg\Project\Paths::sanitize($path, $append_slash); +} + +/** + * Registers a page handler for a particular identifier + * + * For example, you can register a function called 'blog_page_handler' for the identifier 'blog' + * For all URLs http://yoururl/blog/*, the blog_page_handler() function will be called. + * The part of the URL marked with * above will be exploded on '/' characters and passed as an + * array to that function. + * For example, the URL http://yoururl/blog/username/friends/ would result in the call: + * blog_page_handler(array('username','friends'), blog); + * + * A request to register a page handler with the same identifier as previously registered + * handler will replace the previous one. + * + * The context is set to the identifier before the registered + * page handler function is called. For the above example, the context is set to 'blog'. + * + * Page handlers should return true to indicate that they handled the request. + * Requests not handled are forwarded to the front page with a reason of 404. + * Plugins can register for the 'forward', '404' plugin hook. @see forward() + * + * @param string $identifier The page type identifier + * @param string $function Your function name + * + * @return bool Depending on success + * @deprecated 3.0 + */ +function elgg_register_page_handler($identifier, callable $function) { + elgg_deprecated_notice( + __FUNCTION__ . ' has been deprecated. + Use elgg_register_route() to register a named route or define it in elgg-plugin.php', + '3.0' + ); + return _elgg_services()->router->registerPageHandler($identifier, $function); +} + +/** + * Unregister a page handler for an identifier + * + * Note: to replace a page handler, call elgg_register_page_handler() + * + * @param string $identifier The page type identifier + * + * @since 1.7.2 + * @return void + * @deprecated + */ +function elgg_unregister_page_handler($identifier) { + elgg_deprecated_notice( + __FUNCTION__ . ' has been deprecated. + Use new routing API to register and unregister routes.', + '3.0' + ); + _elgg_services()->router->unregisterPageHandler($identifier); +} + +/** + * Alias of elgg_gatekeeper() + * + * Used at the top of a page to mark it as logged in users only. + * + * @return void + * @throws \Elgg\GatekeeperException + * @deprecated 3.0 + */ +function gatekeeper() { + elgg_deprecated_notice(__FUNCTION__ . ' is deprecated. Use elgg_gatekeeper()', '3.0'); + elgg_gatekeeper(); +} + +/** + * Alias of elgg_admin_gatekeeper() + * + * Used at the top of a page to mark it as logged in admin or siteadmin only. + * + * @return void + * @throws \Elgg\GatekeeperException + * @deprecated 3.0 + */ +function admin_gatekeeper() { + elgg_deprecated_notice(__FUNCTION__ . ' is deprecated. Use elgg_admin_gatekeeper()', '3.0'); + elgg_admin_gatekeeper(); +} + +/** + * May the current user access item(s) on this page? If the page owner is a group, + * membership, visibility, and logged in status are taken into account. + * + * @param bool $forward If set to true (default), will forward the page; + * if set to false, will return true or false. + * + * @param int $page_owner_guid The current page owner guid. If not set, this + * will be pulled from elgg_get_page_owner_guid(). + * + * @return bool Will return if $forward is set to false. + * @deprecated 3.0 + */ +function group_gatekeeper($forward = true, $page_owner_guid = null) { + elgg_deprecated_notice(__FUNCTION__ . ' is deprecated. Use elgg_group_gatekeeper()', '3.0'); + return elgg_group_gatekeeper($forward, $page_owner_guid); } \ No newline at end of file diff --git a/engine/lib/elgglib.php b/engine/lib/elgglib.php index 4d1cf5c8f4b..45a0dbdd9ba 100644 --- a/engine/lib/elgglib.php +++ b/engine/lib/elgglib.php @@ -1437,12 +1437,12 @@ function _elgg_ajax_page_handler($segments) { $ajax_api = _elgg_services()->ajax; $allowed_views = $ajax_api->getViews(); - + // cacheable views are always allowed if (!in_array($view, $allowed_views) && !_elgg_services()->views->isCacheableView($view)) { return elgg_error_response("Ajax view '$view' was not registered", REFERRER, ELGG_HTTP_FORBIDDEN); } - + if (!elgg_view_exists($view)) { return elgg_error_response("Ajax view '$view' was not found", REFERRER, ELGG_HTTP_NOT_FOUND); } @@ -1488,7 +1488,7 @@ function _elgg_ajax_page_handler($segments) { if ($content_type) { elgg_set_http_header("Content-Type: $content_type"); } - + return elgg_ok_response($output); } @@ -1613,16 +1613,6 @@ function _elgg_is_valid_options_for_batch_operation($options, $type) { return false; } -/** - * Intercepts the index page when Walled Garden mode is enabled. - * - * @return ResponseBuilder - * @access private - */ -function _elgg_walled_garden_index() { - return elgg_ok_response(elgg_view_resource('walled_garden')); -} - /** * Checks the status of the Walled Garden and forwards to a login page * if required. @@ -1645,8 +1635,6 @@ function _elgg_walled_garden_init() { elgg_register_plugin_hook_handler('register', 'menu:walled_garden', '_elgg_walled_garden_menu'); - elgg_register_page_handler('walled_garden', '_elgg_walled_garden_ajax_handler'); - if (_elgg_config()->default_access == ACCESS_PUBLIC) { elgg_set_config('default_access', ACCESS_LOGGED_IN); } @@ -1655,7 +1643,10 @@ function _elgg_walled_garden_init() { if (!elgg_is_logged_in()) { // override the front page - elgg_register_page_handler('', '_elgg_walled_garden_index'); + elgg_register_route('index', [ + 'path' => '/', + 'resource' => 'walled_garden', + ]); } } @@ -1717,17 +1708,6 @@ function _elgg_walled_garden_remove_public_access($hook, $type, $accesses) { function _elgg_init() { elgg_register_action('entity/delete'); - elgg_register_page_handler('ajax', '_elgg_ajax_page_handler'); - elgg_register_page_handler('favicon.ico', '_elgg_favicon_page_handler'); - - elgg_register_page_handler('manifest.json', function() { - $site = elgg_get_site_entity(); - $resource = new \Elgg\Http\WebAppManifestResource($site); - header('Content-Type: application/json;charset=utf-8'); - echo json_encode($resource->get()); - return true; - }); - elgg_register_plugin_hook_handler('head', 'page', function($hook, $type, array $result) { $result['links']['manifest'] = [ 'rel' => 'manifest', @@ -1778,6 +1758,20 @@ function _elgg_delete_autoload_cache() { _elgg_services()->autoloadManager->deleteCache(); } +/** + * Register core routes + * @return void + * @internal + */ +function _elgg_register_routes() { + $conf = \Elgg\Project\Paths::elgg() . 'engine/routes.php'; + $routes = \Elgg\Includer::includeFile($conf); + + foreach ($routes as $name => $def) { + elgg_register_route($name, $def); + } +} + /** * Adds unit tests for the general API. * @@ -1804,6 +1798,8 @@ function _elgg_api_test($hook, $type, $value, $params) { */ return function(\Elgg\EventsService $events, \Elgg\HooksRegistrationService $hooks) { + _elgg_register_routes(); + elgg_set_entity_class('user', 'user', \ElggUser::class); elgg_set_entity_class('group', 'group', \ElggGroup::class); elgg_set_entity_class('site', 'site', \ElggSite::class); diff --git a/engine/lib/input.php b/engine/lib/input.php index 10a8e4c4726..776dfb9f895 100644 --- a/engine/lib/input.php +++ b/engine/lib/input.php @@ -189,37 +189,6 @@ function elgg_clear_sticky_value($form_name, $variable) { _elgg_services()->stickyForms->clearStickyValue($form_name, $variable); } -/** - * Page handler for autocomplete endpoint. - * - * @todo split this into functions/objects, this is way too big - * - * /livesearch?q= - * - * Other options include: - * match_on string all or array(groups|users|friends) - * match_owner int 0/1 - * limit int default is 10 - * name string default "members" - * - * @param array $page URL segments - * @return string JSON string is returned and then exit - * @access private - */ -function input_livesearch_page_handler($page) { - $match_on = array_shift($page); - if (!$match_on) { - $match_on = get_input('match_on'); - } - elgg_set_viewtype('json'); - if (elgg_view_exists("resources/livesearch/$match_on")) { - $output = elgg_view_resource("livesearch/$match_on"); - return elgg_ok_response($output); - } else { - return elgg_error_response('', REFERRER, ELGG_HTTP_NOT_FOUND); - } -} - /** * htmLawed filtering of data * @@ -396,8 +365,6 @@ function _elgg_disable_password_autocomplete($hook, $type, $return_value, $param * @access private */ function _elgg_input_init() { - // register an endpoint for live search / autocomplete. - elgg_register_page_handler('livesearch', 'input_livesearch_page_handler'); elgg_register_plugin_hook_handler('validate', 'input', '_elgg_htmlawed_filter_tags', 1); diff --git a/engine/lib/pagehandler.php b/engine/lib/pagehandler.php index f07d5e5e4b0..901f9de0f71 100644 --- a/engine/lib/pagehandler.php +++ b/engine/lib/pagehandler.php @@ -7,77 +7,78 @@ */ /** - * Registers a page handler for a particular identifier + * Register a new route * - * For example, you can register a function called 'blog_page_handler' for the identifier 'blog' - * For all URLs http://yoururl/blog/*, the blog_page_handler() function will be called. - * The part of the URL marked with * above will be exploded on '/' characters and passed as an - * array to that function. - * For example, the URL http://yoururl/blog/username/friends/ would result in the call: - * blog_page_handler(array('username','friends'), blog); + * Route paths can contain wildcard segments, i.e. /blog/owner/{username} + * To make a certain wildcard segment optional, add ? to its name, + * i.e. /blog/owner/{username?} * - * A request to register a page handler with the same identifier as previously registered - * handler will replace the previous one. + * Wildcard requirements for common named variables such as 'guid' and 'username' + * will be set automatically. * - * The context is set to the identifier before the registered - * page handler function is called. For the above example, the context is set to 'blog'. + * @warning If you are registering a route in the path of a route registered by + * deprecated {@link elgg_register_page_handler}, your registration must + * preceed the call to elgg_register_page_handler() in the boot sequence. * - * Page handlers should return true to indicate that they handled the request. - * Requests not handled are forwarded to the front page with a reason of 404. - * Plugins can register for the 'forward', '404' plugin hook. @see forward() + * @param string $name Unique route name + * This name can later be used to generate route URLs + * @param array $params Route parameters + * - path : path of the route + * - resource : name of the resource view + * - defaults : default values of wildcard segments + * - requirements : regex patterns for wildcard segment requirements + * - methods : HTTP methods * - * @param string $identifier The page type identifier - * @param string $function Your function name - * - * @return bool Depending on success + * @return \Elgg\Router\Route */ -function elgg_register_page_handler($identifier, $function) { - return _elgg_services()->router->registerPageHandler($identifier, $function); +function elgg_register_route($name, array $params = []) { + return _elgg_services()->router->registerRoute($name, $params); } /** - * Unregister a page handler for an identifier - * - * Note: to replace a page handler, call elgg_register_page_handler() + * Unregister a route by its name * - * @param string $identifier The page type identifier + * @param string $name Name of the route * - * @since 1.7.2 * @return void */ -function elgg_unregister_page_handler($identifier) { - _elgg_services()->router->unregisterPageHandler($identifier); +function elgg_unregister_route($name) { + _elgg_services()->router->unregisterRoute($name); } /** - * Used at the top of a page to mark it as logged in users only. + * Generate a URL for named route * - * @return void - * @since 1.9.0 + * @param string $name Route name + * @param array $parameters Parameters + * + * @return string */ -function elgg_gatekeeper() { - if (!elgg_is_logged_in()) { - _elgg_services()->redirects->setLastForwardFrom(); - system_message(elgg_echo('loggedinrequired')); - forward('/login', '403'); - } +function elgg_generate_url($name, array $parameters = []) { + return _elgg_services()->router->generateUrl($name, $parameters); } /** - * Alias of elgg_gatekeeper() - * * Used at the top of a page to mark it as logged in users only. * * @return void + * @throws \Elgg\GatekeeperException + * @since 1.9.0 */ -function gatekeeper() { - elgg_gatekeeper(); +function elgg_gatekeeper() { + if (!elgg_is_logged_in()) { + _elgg_services()->redirects->setLastForwardFrom(); + + $msg = elgg_echo('loggedinrequired'); + throw new \Elgg\GatekeeperException($msg); + } } /** * Used at the top of a page to mark it as admin only. * * @return void + * @throws \Elgg\GatekeeperException * @since 1.9.0 */ function elgg_admin_gatekeeper() { @@ -85,21 +86,12 @@ function elgg_admin_gatekeeper() { if (!elgg_is_admin_logged_in()) { _elgg_services()->redirects->setLastForwardFrom(); - register_error(elgg_echo('adminrequired')); - forward('', '403'); + + $msg = elgg_echo('adminrequired'); + throw new \Elgg\GatekeeperException($msg); } } -/** - * Alias of elgg_admin_gatekeeper() - * - * Used at the top of a page to mark it as logged in admin or siteadmin only. - * - * @return void - */ -function admin_gatekeeper() { - elgg_admin_gatekeeper(); -} /** * May the current user access item(s) on this page? If the page owner is a group, @@ -112,6 +104,8 @@ function admin_gatekeeper() { * will be pulled from elgg_get_page_owner_guid(). * * @return bool Will return if $forward is set to false. + * @throws InvalidParameterException + * @throws SecurityException * @since 1.9.0 */ function elgg_group_gatekeeper($forward = true, $group_guid = null) { @@ -153,22 +147,6 @@ function elgg_group_gatekeeper($forward = true, $group_guid = null) { return false; } -/** - * May the current user access item(s) on this page? If the page owner is a group, - * membership, visibility, and logged in status are taken into account. - * - * @param bool $forward If set to true (default), will forward the page; - * if set to false, will return true or false. - * - * @param int $page_owner_guid The current page owner guid. If not set, this - * will be pulled from elgg_get_page_owner_guid(). - * - * @return bool Will return if $forward is set to false. - */ -function group_gatekeeper($forward = true, $page_owner_guid = null) { - return elgg_group_gatekeeper($forward, $page_owner_guid); -} - /** * Can the viewer see this entity? * @@ -181,7 +159,12 @@ function group_gatekeeper($forward = true, $page_owner_guid = null) { * @param string $subtype Optional required entity subtype * @param bool $forward If set to true (default), will forward the page; * if set to false, will return true or false. + * * @return bool Will return if $forward is set to false. + * @throws \Elgg\BadRequestException + * @throws \Elgg\EntityNotFoundException + * @throws \Elgg\EntityPermissionsException + * @throws \Elgg\GatekeeperException * @since 1.9.0 */ function elgg_entity_gatekeeper($guid, $type = null, $subtype = null, $forward = true) { @@ -189,14 +172,14 @@ function elgg_entity_gatekeeper($guid, $type = null, $subtype = null, $forward = if (!$entity && $forward) { if (!elgg_entity_exists($guid)) { // entity doesn't exist - forward('', '404'); - } elseif (!elgg_is_logged_in()) { + throw new \Elgg\EntityNotFoundException(); + } else if (!elgg_is_logged_in()) { // entity requires at least a logged in user elgg_gatekeeper(); } else { // user is logged in but still does not have access to it - register_error(elgg_echo('limited_access')); - forward(); + $msg = elgg_echo('limited_access'); + throw new \Elgg\GatekeeperException($msg); } } else if (!$entity) { return false; @@ -205,7 +188,7 @@ function elgg_entity_gatekeeper($guid, $type = null, $subtype = null, $forward = if ($type && !elgg_instanceof($entity, $type, $subtype)) { // entity is of wrong type/subtype if ($forward) { - forward('', '404'); + throw new \Elgg\BadRequestException(); } else { return false; } @@ -218,7 +201,7 @@ function elgg_entity_gatekeeper($guid, $type = null, $subtype = null, $forward = ]; if (!elgg_trigger_plugin_hook('gatekeeper', $hook_type, $hook_params, true)) { if ($forward) { - forward('', '403'); + throw new \Elgg\EntityPermissionsException(); } else { return false; } @@ -232,24 +215,16 @@ function elgg_entity_gatekeeper($guid, $type = null, $subtype = null, $forward = * will end and a 400 response page will be sent. * * @return void + * @throws \Elgg\BadRequestException * @since 1.12.0 */ function elgg_ajax_gatekeeper() { if (!elgg_is_xhr()) { - register_error(_elgg_services()->translator->translate('ajax:not_is_xhr')); - forward(null, '400'); + $msg = elgg_echo('ajax:not_is_xhr'); + throw new \Elgg\BadRequestException($msg); } } -/** - * Front page handler - * - * @return bool - */ -function elgg_front_page_handler() { - return elgg_ok_response(elgg_view_resource('index')); -} - /** * Prepares a successful response to be returned by a page or an action handler * @@ -262,14 +237,16 @@ function elgg_front_page_handler() { * Can be used by handlers to redirect the client on non-ajax requests * @param int $status_code HTTP status code * Status code of the HTTP response (defaults to 200) + * * @return \Elgg\Http\OkResponse */ function elgg_ok_response($content = '', $message = '', $forward_url = null, $status_code = ELGG_HTTP_OK) { if ($message) { system_message($message); } + return new \Elgg\Http\OkResponse($content, $status_code, $forward_url); - + } /** @@ -285,12 +262,14 @@ function elgg_ok_response($content = '', $message = '', $forward_url = null, $st * For BC reasons and due to the logic in the client-side AJAX API, * this defaults to 200. Note that the Router and AJAX API will * treat these responses as error in spite of the HTTP code assigned + * * @return \Elgg\Http\ErrorResponse */ function elgg_error_response($error = '', $forward_url = REFERRER, $status_code = ELGG_HTTP_OK) { if ($error) { register_error($error); } + return new \Elgg\Http\ErrorResponse($error, $status_code, $forward_url); } @@ -304,6 +283,7 @@ function elgg_error_response($error = '', $forward_url = REFERRER, $status_code * Note that the Router and AJAX API will treat these responses * as redirection in spite of the HTTP code assigned * Note that non-redirection HTTP codes will throw an exception + * * @return \Elgg\Http\RedirectResponse * @throws \InvalidArgumentException */ @@ -311,19 +291,10 @@ function elgg_redirect_response($forward_url = REFERRER, $status_code = ELGG_HTT return new Elgg\Http\RedirectResponse($forward_url, $status_code); } -/** - * Initializes the page handler/routing system - * - * @return void - * @access private - */ -function _elgg_page_handler_init() { - elgg_register_page_handler('', 'elgg_front_page_handler'); -} /** * @see \Elgg\Application::loadCore Do not do work here. Just register for events. */ -return function(\Elgg\EventsService $events, \Elgg\HooksRegistrationService $hooks) { - $events->registerHandler('init', 'system', '_elgg_page_handler_init'); +return function (\Elgg\EventsService $events, \Elgg\HooksRegistrationService $hooks) { + }; diff --git a/engine/lib/user_settings.php b/engine/lib/user_settings.php index 89b73e7c212..3949f045f55 100644 --- a/engine/lib/user_settings.php +++ b/engine/lib/user_settings.php @@ -142,16 +142,16 @@ function _elgg_set_user_username() { if (!isset($username)) { return; } - + if (!elgg_is_admin_logged_in()) { return; } - + $user = get_user($user_guid); if (empty($user)) { return; } - + if ($user->username === $username) { return; } @@ -167,7 +167,7 @@ function _elgg_set_user_username() { } // restore access settings access_show_hidden_entities($hidden); - + if ($found) { register_error(elgg_echo('registration:userexists')); return false; @@ -177,7 +177,7 @@ function _elgg_set_user_username() { register_error($e->getMessage()); return false; } - + $user->username = $username; if ($user->save()) { // correctly forward after after a username change @@ -187,7 +187,7 @@ function _elgg_set_user_username() { system_message(elgg_echo('user:username:success')); return true; } - + register_error(elgg_echo('user:username:fail')); return false; } @@ -208,7 +208,7 @@ function _elgg_set_user_language() { if (!isset($language)) { return; } - + if ($user_guid) { $user = get_user($user_guid); } else { @@ -250,7 +250,7 @@ function _elgg_set_user_email() { if (!isset($email)) { return; } - + if ($user_guid) { $user = get_user($user_guid); } else { @@ -266,12 +266,12 @@ function _elgg_set_user_email() { register_error(elgg_echo('email:save:fail')); return false; } - + if (strcmp($email, $user->email) === 0) { // no change return; } - + if (_elgg_config()->security_email_require_password && ($user->getGUID() === elgg_get_logged_in_user_guid())) { // validate password $pwd = get_input('email_password'); @@ -281,7 +281,7 @@ function _elgg_set_user_email() { return false; } } - + if (!get_user_by_email($email)) { $user->email = $email; if ($user->save()) { @@ -293,7 +293,7 @@ function _elgg_set_user_email() { } else { register_error(elgg_echo('registration:dupeemail')); } - + return false; } @@ -363,7 +363,7 @@ function _elgg_user_settings_menu_register($hook, $type, $return, $params) { if (!elgg_in_context('settings')) { return; } - + $return[] = \ElggMenuItem::factory([ 'name' => '1_account', 'text' => elgg_echo('usersettings:user:opt:linktext'), @@ -387,7 +387,7 @@ function _elgg_user_settings_menu_register($hook, $type, $return, $params) { // register plugin user settings menu items $active_plugins = elgg_get_plugins(); - + foreach ($active_plugins as $plugin) { $plugin_id = $plugin->getID(); if (!elgg_view_exists("usersettings/$plugin_id/edit") && !elgg_view_exists("plugins/$plugin_id/usersettings")) { @@ -399,7 +399,7 @@ function _elgg_user_settings_menu_register($hook, $type, $return, $params) { } else { $title = $plugin->getDisplayName(); } - + $return[] = \ElggMenuItem::factory([ 'name' => $plugin_id, 'text' => $title, @@ -408,7 +408,7 @@ function _elgg_user_settings_menu_register($hook, $type, $return, $params) { 'section' => 'configure', ]); } - + return $return; } @@ -427,21 +427,21 @@ function _elgg_user_settings_menu_prepare($hook, $type, $value, $params) { if (empty($value)) { return $value; } - + if (!elgg_in_context("settings")) { return $value; } - + $configure = elgg_extract("configure", $value); if (empty($configure)) { return $value; } - + foreach ($configure as $index => $menu_item) { if (!($menu_item instanceof ElggMenuItem)) { continue; } - + if ($menu_item->getName() == "1_plugins") { if (!$menu_item->getChildren()) { // no need for this menu item if it has no children @@ -449,50 +449,8 @@ function _elgg_user_settings_menu_prepare($hook, $type, $value, $params) { } } } - - return $value; -} -/** - * Page handler for user settings - * - * @param array $page Pages array - * - * @return bool - * @access private - */ -function _elgg_user_settings_page_handler($page) { - if (!isset($page[0])) { - $page[0] = 'user'; - } - - if (isset($page[1])) { - $user = get_user_by_username($page[1]); - elgg_set_page_owner_guid($user->guid); - } else { - $user = elgg_get_logged_in_user_entity(); - elgg_set_page_owner_guid($user->guid); - } - - $vars['username'] = $user->username; - - switch ($page[0]) { - case 'statistics': - echo elgg_view_resource('settings/statistics', $vars); - return true; - case 'plugins': - if (isset($page[2])) { - $vars['plugin_id'] = $page[2]; - echo elgg_view_resource('settings/tools', $vars); - return true; - } - break; - case 'user': - echo elgg_view_resource("settings/account", $vars); - return true; - } - - return false; + return $value; } /** @@ -502,7 +460,6 @@ function _elgg_user_settings_page_handler($page) { * @access private */ function _elgg_user_settings_init() { - elgg_register_page_handler('settings', '_elgg_user_settings_page_handler'); elgg_register_plugin_hook_handler('register', 'menu:page', '_elgg_user_settings_menu_register'); elgg_register_plugin_hook_handler('prepare', 'menu:page', '_elgg_user_settings_menu_prepare'); @@ -513,7 +470,7 @@ function _elgg_user_settings_init() { elgg_register_plugin_hook_handler('usersettings:save', 'user', '_elgg_set_user_name'); elgg_register_plugin_hook_handler('usersettings:save', 'user', '_elgg_set_user_username'); elgg_register_plugin_hook_handler('usersettings:save', 'user', '_elgg_set_user_email'); - + elgg_register_action("usersettings/save"); // extend the account settings form diff --git a/engine/lib/views.php b/engine/lib/views.php index 615d25e7b45..859559824f7 100644 --- a/engine/lib/views.php +++ b/engine/lib/views.php @@ -419,7 +419,7 @@ function elgg_view_page($title, $body, $page_shell = 'default', $vars = []) { * @param array $vars Arguments passed to the view * * @return string - * @throws SecurityException + * @throws \Elgg\PageNotFoundException */ function elgg_view_resource($name, array $vars = []) { $view = "resources/$name"; @@ -434,13 +434,8 @@ function elgg_view_resource($name, array $vars = []) { _elgg_services()->logger->error("The view $view is missing."); - if (elgg_get_viewtype() === 'default') { - // only works for default viewtype - forward('', '404'); - } else { - register_error(elgg_echo('error:404:content')); - forward(''); - } + // only works for default viewtype + throw new \Elgg\PageNotFoundException(); } /** diff --git a/engine/routes.php b/engine/routes.php new file mode 100644 index 00000000000..bee8965d2fc --- /dev/null +++ b/engine/routes.php @@ -0,0 +1,64 @@ + [ + 'path' => '/', + 'resource' => 'index', + ], + 'ajax' => [ + 'path' => '/ajax/{segments}', + 'handler' => '_elgg_ajax_page_handler', + 'requirements' => [ + 'segments' => '.+', + ], + ], + 'favicon.ico' => [ + 'path' => '/favicon.ico', + 'resource' => 'favicon.ico', + ], + 'manifest.json' => [ + 'path' => '/manifest.json', + 'resource' => 'manifest.json', + ], + 'action' => [ + 'path' => '/action/{segments}', + 'handler' => '_elgg_action_handler', + 'requirements' => [ + 'segments' => '.+', + ], + ], + 'action' => [ + 'path' => '/action/{segments}', + 'handler' => '_elgg_action_handler', + 'requirements' => [ + 'segments' => '.+', + ], + ], + 'action:token' => [ + 'path' => '/refresh_token', + 'handler' => '_elgg_csrf_token_refresh', + ], + 'livesearch' => [ + 'path' => '/livesearch/{match_on?}', + 'resource' => 'livesearch', + 'requirements' => [ + 'match_on' => '\w+', + ], + ], + 'settings:index' => [ + 'path' => '/settings', + 'resource' => 'settings/account', + ], + 'settings:account' => [ + 'path' => '/settings/user/{username?}', + 'resource' => 'settings/account', + ], + 'settings:statistics' => [ + 'path' => '/settings/statistics/{username?}', + 'resource' => 'settings/statistics', + ], + 'settings:tools' => [ + 'path' => '/settings/tools/{username?}/{plugin_id}', + 'resource' => 'settings/tools', + ], +]; \ No newline at end of file diff --git a/engine/tests/classes/Elgg/Plugins/StaticConfigTest.php b/engine/tests/classes/Elgg/Plugins/StaticConfigTest.php index 81fbe510064..242fc6bbb7c 100644 --- a/engine/tests/classes/Elgg/Plugins/StaticConfigTest.php +++ b/engine/tests/classes/Elgg/Plugins/StaticConfigTest.php @@ -14,10 +14,12 @@ class StaticConfigTest extends UnitTestCase { private $plugin; public function up() { - $this->plugin = $this->startPlugin( - ELGG_PLUGIN_INCLUDE_START | - ELGG_PLUGIN_IGNORE_MANIFEST | - ELGG_PLUGIN_REGISTER_CLASSES); + $this->plugin = $this->startPlugin( + ELGG_PLUGIN_INCLUDE_START | + ELGG_PLUGIN_IGNORE_MANIFEST | + ELGG_PLUGIN_REGISTER_CLASSES | + ELGG_PLUGIN_REGISTER_VIEWS + ); } public function down() { @@ -51,4 +53,22 @@ public function testActionsRegistration() { } } + /** + * @group Routing + */ + public function testRouteRegistrations() { + + $routes = $this->plugin->getStaticConfig('routes', []); + + foreach ($routes as $name => $conf) { + if (elgg_extract('handler', $conf)) { + $this->assertTrue(is_callable($conf['handler'])); + } else if (elgg_extract('resource', $conf)) { + $this->assertTrue(elgg_view_exists("resources/{$conf['resource']}")); + } + + elgg_register_route($name, $conf); + elgg_unregister_route($name); + } + } } \ No newline at end of file diff --git a/engine/tests/phpunit/unit/Elgg/RouteMatchingTest.php b/engine/tests/phpunit/unit/Elgg/RouteMatchingTest.php new file mode 100644 index 00000000000..f928904ddcf --- /dev/null +++ b/engine/tests/phpunit/unit/Elgg/RouteMatchingTest.php @@ -0,0 +1,229 @@ + '/foo/{bar}', + 'requirements' => ['bar' => '\w+'], + 'handler' => function () use (&$route_calls) { + $route_calls++; + } + ]); + + elgg_register_page_handler('foo', function () use (&$page_handler_calls) { + $page_handler_calls++; + }); + + $request = $this->prepareHttpRequest('foo'); + _elgg_services()->router->route($request); + + $request = $this->prepareHttpRequest('foo/baz'); + _elgg_services()->router->route($request); + + $request = $this->prepareHttpRequest('foo/baz/bar'); + _elgg_services()->router->route($request); + + $this->assertEquals(1, $route_calls); + $this->assertEquals(2, $page_handler_calls); + + elgg_unregister_route('foo:bar'); + elgg_unregister_page_handler('foo'); + } + + /** + * @dataProvider patternProvider + */ + public function testPatterns($route, $match_path, $is_match) { + + $calls = 0; + + $route['handler'] = function () use (&$calls) { + $calls++; + }; + + elgg_register_route('foo', $route); + + $request = $this->prepareHttpRequest($match_path); + _elgg_services()->router->route($request); + + if ($is_match) { + $this->assertEquals(1, $calls); + } else { + $this->assertEquals(0, $calls); + } + + elgg_unregister_route('foo'); + } + + public function patternProvider() { + + $config = [ + [ + 'path' => '/foo/{segments?}', + 'defaults' => [], + 'requirements' => ['segments' => '.+'], + 'matches' => [ + '/foo' => true, + '/foo/bar' => true, + '/foo/bar/baz' => true, + '/bar' => false, + ], + ], + [ + 'path' => '/foo/{guid}/{bar?}', + 'defaults' => [], + 'requirements' => [ + 'bar' => '\w+', + ], + 'matches' => [ + '/foo' => false, + '/foo/123' => true, + '/foo/abc' => false, + '/foo/123/abc3' => true, + ], + ], + [ + 'path' => '/foo/{username}/{bar?}', + 'defaults' => [], + 'requirements' => [ + 'bar' => '\w+', + ], + 'matches' => [ + '/foo' => false, + '/foo/123' => true, + '/foo/abc123' => true, + '/foo/abc123_abc/abc3' => true, + ], + ], + [ + 'path' => '/foo/{_underscore}/{bar?}', + 'defaults' => [], + 'requirements' => [ + '_underscore' => '\w+', + 'bar' => '\w+', + ], + 'matches' => [ + '/foo' => false, + '/foo/123' => true, + '/foo/abc123' => true, + '/foo/abc123_abc/abc3' => true, + ], + ], + ]; + + $provides = []; + + foreach ($config as $conf) { + foreach ($conf['matches'] as $request_path => $is_match) { + $route = [ + 'path' => $conf['path'], + 'defaults' => $conf['defaults'], + 'requirements' => $conf['requirements'], + ]; + $provides[] = [$route, $request_path, $is_match]; + } + } + + return $provides; + } + + public function testCanGenerateURL() { + + elgg_register_route('foo', [ + 'path' => '/hello/{guid}/{bar?}', + 'handler' => function() {}, + ]); + + $this->assertEquals('/hello/123?baz=x', elgg_generate_url('foo', ['guid' => '123', 'baz' => 'x'])); + $this->assertEquals('/hello/123/x?baz=y', elgg_generate_url('foo', ['guid' => '123', 'bar' => 'x', 'baz' => 'y'])); + + elgg_unregister_route('foo'); + } + + public function testResourceParameterIsNotReplaceableByQueryElements() { + + $this->viewsDir = $this->normalizeTestFilePath('views'); + _elgg_services()->views->autoregisterViews('', "$this->viewsDir/default", 'default'); + + elgg_register_route('foo:bar', [ + 'path' => '/foo/{bar}', + 'requirements' => ['bar' => '\w+'], + 'resource' => 'routes_match', + ]); + + $request = $this->prepareHttpRequest('foo/baz', 'GET', [ + '_resource' => 'custom_resource', + ]); + + ob_start(); + _elgg_services()->router->route($request); + ob_get_clean(); + + $response = _elgg_services()->responseFactory->getSentResponse(); + /* @var $response Response */ + + $this->assertInstanceOf(Response::class, $response); + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals(serialize([ + 'bar' => 'baz', + '_route' => 'foo:bar', + ]), $response->getContent()); + + } + + public function testHandlerParameterIsNotReplaceableByQueryElements() { + + $this->viewsDir = $this->normalizeTestFilePath('views'); + _elgg_services()->views->autoregisterViews('', "$this->viewsDir/default", 'default'); + + $calls = 0; + elgg_register_route('foo:bar', [ + 'path' => '/foo/{bar}', + 'requirements' => ['bar' => '\w+'], + 'handler' => function() use (&$calls) { + $calls++; + } + ]); + + $request = $this->prepareHttpRequest('foo/baz', 'GET', [ + '_handler' => '_elgg_init', + ]); + + ob_start(); + _elgg_services()->router->route($request); + ob_get_clean(); + + $response = _elgg_services()->responseFactory->getSentResponse(); + /* @var $response Response */ + + $this->assertInstanceOf(Response::class, $response); + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals(1, $calls); + + } + +} diff --git a/engine/tests/phpunit/unit/Elgg/RouterUnitTest.php b/engine/tests/phpunit/unit/Elgg/RouterUnitTest.php index 2340794712b..b9a2c295289 100644 --- a/engine/tests/phpunit/unit/Elgg/RouterUnitTest.php +++ b/engine/tests/phpunit/unit/Elgg/RouterUnitTest.php @@ -76,7 +76,7 @@ public function up() { $this->translator->addTranslation('en', ['__test__' => 'Test']); $this->hooks = new PluginHooksService(); - $this->router = new Router($this->hooks); + $this->router = new Router($this->hooks, _elgg_services()->routeCollection, _elgg_services()->urlMatcher, _elgg_services()->urlGenerator); $this->system_messages = new SystemMessagesService(elgg_get_session()); @@ -98,7 +98,7 @@ function createService() { _elgg_services()->setValue('hooks', $this->hooks); _elgg_services()->setValue('request', $this->request); _elgg_services()->setValue('translator', $this->translator); - _elgg_services()->setValue('router', new Router($this->hooks)); + _elgg_services()->setValue('router', new Router($this->hooks, _elgg_services()->routeCollection, _elgg_services()->urlMatcher, _elgg_services()->urlGenerator)); $this->amd_config = _elgg_services()->amdConfig; $this->ajax = new Service($this->hooks, $this->system_messages, $this->input, $this->amd_config); _elgg_services()->setValue('ajax', $this->ajax); @@ -107,7 +107,13 @@ function createService() { $this->response_factory = new ResponseFactory($this->request, $this->hooks, $this->ajax, $transport); _elgg_services()->setValue('responseFactory', $this->response_factory); - elgg_register_page_handler('ajax', '_elgg_ajax_page_handler'); + elgg_register_route('ajax', [ + 'path' => '/ajax/{segments}', + 'handler' => '_elgg_ajax_page_handler', + 'requirements' => [ + 'segments' => '.+', + ], + ]); _elgg_services()->views->autoregisterViews('', "$this->viewsDir/default", 'default'); _elgg_services()->views->autoregisterViews('', "$this->viewsDir/json", 'json'); @@ -142,10 +148,6 @@ function testCanRegisterFunctionsAsPageHandlers() { $this->assertInstanceOf(Response::class, $response); $this->assertEquals($path, $response->getContent()); - - $this->assertEquals(array( - 'hello' => array($this, 'hello_page_handler') - ), $this->router->getPageHandlers()); } function testFailToRegisterInvalidCallback() { diff --git a/engine/tests/phpunit/unit/ElggPluginUnitTest.php b/engine/tests/phpunit/unit/ElggPluginUnitTest.php index 4bf021d8e78..219e6535a87 100644 --- a/engine/tests/phpunit/unit/ElggPluginUnitTest.php +++ b/engine/tests/phpunit/unit/ElggPluginUnitTest.php @@ -88,11 +88,18 @@ public function testCanLoadStaticConfig() { 'context' => ['profile', 'dashboard'], ], ], + 'routes' => [ + 'plugin:foo' => [ + 'path' => '/plugin/{foo?}', + 'resource' => 'plugin/foo', + ], + ] ]; $this->assertEquals($config['entities'], $plugin->getStaticConfig('entities')); $this->assertEquals($config['actions'], $plugin->getStaticConfig('actions')); $this->assertEquals($config['widgets'], $plugin->getStaticConfig('widgets')); + $this->assertEquals($config['routes'], $plugin->getStaticConfig('routes')); $plugin->delete(); } diff --git a/engine/tests/test_files/mod/test_plugin/elgg-plugin.php b/engine/tests/test_files/mod/test_plugin/elgg-plugin.php index c91b0b0f3a0..1e3779fdf98 100644 --- a/engine/tests/test_files/mod/test_plugin/elgg-plugin.php +++ b/engine/tests/test_files/mod/test_plugin/elgg-plugin.php @@ -24,5 +24,11 @@ ], 'user_settings' => [ 'user_default1' => 'set1', + ], + 'routes' => [ + 'plugin:foo' => [ + 'path' => '/plugin/{foo?}', + 'resource' => 'plugin/foo', + ], ] ]; diff --git a/engine/tests/test_files/views/default/resources/routes_match.php b/engine/tests/test_files/views/default/resources/routes_match.php new file mode 100644 index 00000000000..2be82b84b26 --- /dev/null +++ b/engine/tests/test_files/views/default/resources/routes_match.php @@ -0,0 +1,3 @@ + 'Cannot check permission for user_guid [%s] as the user does not exist.', + 'PageNotFoundException' => 'The page you are trying to view does not exist or you do not have permissions to view it', + 'EntityNotFoundException' => 'The content you were trying to access has been removed or you do not have permissions to access it.', + 'EntityPermissionsException' => 'You do not have sufficient permissions for this action.', + 'GatekeeperException' => 'You do not have permissions to view the page you are trying to access', + 'BadRequestException' => 'Bad request', + 'deprecatedfunction' => 'Warning: This code uses the deprecated function \'%s\' and is not compatible with this version of Elgg', 'pageownerunavailable' => 'Warning: The page owner %d is not accessible!', diff --git a/mod/blog/actions/blog/delete.php b/mod/blog/actions/blog/delete.php index 26e5c789ab3..baacf5afa37 100644 --- a/mod/blog/actions/blog/delete.php +++ b/mod/blog/actions/blog/delete.php @@ -18,9 +18,17 @@ } if ($container instanceof \ElggGroup) { - $forward_url = "blog/group/{$container->guid}/all"; + $forward_url = elgg_generate_url('collection:object:blog:group', [ + 'group_guid' => $container->guid, + 'subpage' => 'all', + ]); } else { - $forward_url = "blog/owner/{$container->username}"; + $foward_url = elgg_generate_url('collection:object:blog:owner', [ + 'username' => $container->username, + ]); } return elgg_ok_response('', elgg_echo('blog:message:deleted_post'), $forward_url); + +$message = elgg_echo('blog:message:deleted_post'); +return elgg_ok_response($data, $message, $forward_url); diff --git a/mod/blog/actions/blog/save.php b/mod/blog/actions/blog/save.php index 90fc8db4a2d..24d1d1681b8 100644 --- a/mod/blog/actions/blog/save.php +++ b/mod/blog/actions/blog/save.php @@ -114,7 +114,7 @@ foreach ($values as $name => $value) { $blog->$name = $value; } - + if (!$blog->save()) { return elgg_error_response(elgg_echo('blog:error:cannot_save')); } @@ -163,7 +163,9 @@ if ($blog->status == 'published' || $save == false) { $forward_url = $blog->getURL(); } else { - $forward_url = "blog/edit/{$blog->guid}"; + $forward_url = elgg_generate_url('edit:object:blog', [ + 'guid' => $blog->guid, + ]); } return elgg_ok_response('', elgg_echo('blog:message:saved'), $forward_url); diff --git a/mod/blog/elgg-plugin.php b/mod/blog/elgg-plugin.php index a9b0c434b7e..3bb16675427 100644 --- a/mod/blog/elgg-plugin.php +++ b/mod/blog/elgg-plugin.php @@ -14,6 +14,55 @@ 'blog/auto_save_revision' => [], 'blog/delete' => [], ], + 'routes' => [ + 'collection:object:blog:owner' => [ + 'path' => '/blog/owner/{username?}', + 'resource' => 'blog/owner', + ], + 'collection:object:blog:friends' => [ + 'path' => '/blog/friends/{username?}', + 'resource' => 'blog/friends', + ], + 'collection:object:blog:archive' => [ + 'path' => '/blog/archive/{username?}/{lower?}/{upper?}', + 'resource' => 'blog/archive', + 'requirements' => [ + 'lower' => '\d+', + 'upper' => '\d+', + ], + ], + 'view:object:blog' => [ + 'path' => '/blog/view/{guid}/{title?}', + 'resource' => 'blog/view', + ], + 'add:object:blog' => [ + 'path' => '/blog/add/{guid?}', + 'resource' => 'blog/add', + ], + 'edit:object:blog' => [ + 'path' => '/blog/edit/{guid}/{revision?}', + 'resource' => 'blog/edit', + 'requirements' => [ + 'revision' => '\d+', + ], + ], + 'collection:object:blog:group' => [ + 'path' => '/blog/group/{group_guid}/{subpage?}/{lower?}/{upper?}', + 'resource' => 'blog/group', + 'defaults' => [ + 'subpage' => 'all', + ], + 'requirements' => [ + 'subpage' => 'all|archive', + 'lower' => '\d+', + 'upper' => '\d+', + ], + ], + 'collection:object:blog:all' => [ + 'path' => '/blog/all', + 'resource' => 'blog/all', + ], + ], 'widgets' => [ 'blog' => [ 'description' => elgg_echo('blog:widget:description'), diff --git a/mod/blog/lib/blog.php b/mod/blog/lib/blog.php index d06d891761d..67884750284 100644 --- a/mod/blog/lib/blog.php +++ b/mod/blog/lib/blog.php @@ -81,9 +81,14 @@ function blog_get_page_content_archive($owner_guid, $lower = 0, $upper = 0) { $crumbs_title = $owner->name; if ($owner instanceof ElggUser) { - $url = "blog/owner/{$owner->username}"; + $url = elgg_generate_url('collection:object:blog:owner', [ + 'username' => $owner->username, + ]); } else { - $url = "blog/group/$owner->guid/all"; + $url = elgg_generate_url('collection:object:blog:group', [ + 'group_guid' => $owner->guid, + 'subpage' => 'all', + ]); } elgg_push_breadcrumb($crumbs_title, $url); elgg_push_breadcrumb(elgg_echo('blog:archives')); diff --git a/mod/blog/start.php b/mod/blog/start.php index edf9f8d8158..42ef6f750db 100644 --- a/mod/blog/start.php +++ b/mod/blog/start.php @@ -18,14 +18,11 @@ function blog_init() { elgg_register_menu_item('site', [ 'name' => 'blog', 'text' => elgg_echo('blog:blogs'), - 'href' => 'blog/all', + 'href' => elgg_generate_url('collection:object:blog:all'), ]); elgg_extend_view('object/elements/imprint/contents', 'blog/imprint/status'); - // routing of urls - elgg_register_page_handler('blog', 'blog_page_handler'); - // override the default url to view a blog object elgg_register_plugin_hook_handler('entity:url', 'object', 'blog_set_url'); @@ -53,90 +50,6 @@ function blog_init() { elgg_register_plugin_hook_handler('seeds', 'database', 'blog_register_db_seeds'); } -/** - * Dispatches blog pages. - * URLs take the form of - * All blogs: blog/all - * User's blogs: blog/owner/ - * Friends' blog: blog/friends/ - * User's archives: blog/archives/// - * Blog post: blog/view// - * New post: blog/add/<guid> - * Edit post: blog/edit/<guid>/<revision> - * Preview post: blog/preview/<guid> - * Group blog: blog/group/<guid>/all - * - * Title is ignored - * - * @todo no archives for all blogs or friends - * - * @param array $page URL segments - * @return bool - */ -function blog_page_handler($page) { - - elgg_load_library('elgg:blog'); - - // push all blogs breadcrumb - elgg_push_breadcrumb(elgg_echo('blog:blogs'), 'blog/all'); - - $page_type = elgg_extract(0, $page, 'all'); - $resource_vars = [ - 'page_type' => $page_type, - ]; - - switch ($page_type) { - case 'owner': - $resource_vars['username'] = elgg_extract(1, $page); - - echo elgg_view_resource('blog/owner', $resource_vars); - break; - case 'friends': - $resource_vars['username'] = elgg_extract(1, $page); - - echo elgg_view_resource('blog/friends', $resource_vars); - break; - case 'archive': - $resource_vars['username'] = elgg_extract(1, $page); - $resource_vars['lower'] = elgg_extract(2, $page); - $resource_vars['upper'] = elgg_extract(3, $page); - - echo elgg_view_resource('blog/archive', $resource_vars); - break; - case 'view': - $resource_vars['guid'] = elgg_extract(1, $page); - - echo elgg_view_resource('blog/view', $resource_vars); - break; - case 'add': - $resource_vars['guid'] = elgg_extract(1, $page); - - echo elgg_view_resource('blog/add', $resource_vars); - break; - case 'edit': - $resource_vars['guid'] = elgg_extract(1, $page); - $resource_vars['revision'] = elgg_extract(2, $page); - - echo elgg_view_resource('blog/edit', $resource_vars); - break; - case 'group': - $resource_vars['group_guid'] = elgg_extract(1, $page); - $resource_vars['subpage'] = elgg_extract(2, $page); - $resource_vars['lower'] = elgg_extract(3, $page); - $resource_vars['upper'] = elgg_extract(4, $page); - - echo elgg_view_resource('blog/group', $resource_vars); - break; - case 'all': - echo elgg_view_resource('blog/all', $resource_vars); - break; - default: - return false; - } - - return true; -} - /** * Format and return the URL for blogs. * @@ -152,9 +65,11 @@ function blog_set_url($hook, $type, $url, $params) { if (!$entity instanceof ElggBlog) { return; } - - $friendly_title = elgg_get_friendly_title($entity->title); - return "blog/view/{$entity->guid}/$friendly_title"; + + return elgg_generate_url('view:object:blog', [ + 'guid' => $entity->guid, + 'title' => elgg_get_friendly_title($entity->title), + ]); } /** @@ -171,16 +86,21 @@ function blog_owner_block_menu($hook, $type, $return, $params) { $entity = elgg_extract('entity', $params); if ($entity instanceof ElggUser) { $return[] = ElggMenuItem::factory([ - 'name' => 'blog', - 'text' => elgg_echo('blog'), - 'href' => "blog/owner/{$entity->username}", + 'name' => 'blog', + 'text' => elgg_echo('blog'), + 'href' => elgg_generate_url('collection:object:blog:owner', [ + 'username' => $entity->username, + ]), ]); } elseif ($entity instanceof ElggGroup) { if ($entity->isToolEnabled('blog')) { $return[] = ElggMenuItem::factory([ - 'name' => 'blog', - 'text' => elgg_echo('blog:group'), - 'href' => "blog/group/{$entity->guid}/all", + 'name' => 'blog', + 'text' => elgg_echo('blog:group'), + 'href' => elgg_generate_url('collection:object:blog:group', [ + 'group_guid' => $entity->guid, + 'subpage' => 'all', + ]), ]); } } @@ -211,13 +131,26 @@ function blog_archive_menu_setup($hook, $type, $return, $params) { } $dates = array_reverse($dates); + + $generate_url = function($lower = null, $upper = null) use ($page_owner) { + if ($page_owner instanceof ElggUser) { + $url_segment = elgg_generate_url('collection:object:blog:archive', [ + 'username' => $page_owner->username, + 'lower' => $lower, + 'upper' => $upper, + ]); + } else { + $url_segment = elgg_generate_url('collection:object:blog:group', [ + 'group_guid' => $page_owner->guid, + 'subpage' => 'archive', + 'lower' => $lower, + 'upper' => $upper, + ]); + } - if ($page_owner instanceof ElggUser) { - $url_segment = 'blog/archive/' . $page_owner->username; - } else { - $url_segment = 'blog/group/' . $page_owner->getGUID() . '/archive'; - } - + return $url_segment; + }; + $years = []; foreach ($dates as $date) { $timestamplow = mktime(0, 0, 0, substr($date, 4, 2), 1, substr($date, 0, 4)); @@ -235,13 +168,12 @@ function blog_archive_menu_setup($hook, $type, $return, $params) { ]); } - $link = $url_segment . '/' . $timestamplow . '/' . $timestamphigh; $month = trim(elgg_echo('date:month:' . substr($date, 4, 2), [''])); $return[] = ElggMenuItem::factory([ 'name' => $date, 'text' => $month, - 'href' => $link, + 'href' => $generate_url($timestamplow, $timestamphigh), 'parent_name' => $year, ]); } diff --git a/mod/blog/views/default/blog/group_module.php b/mod/blog/views/default/blog/group_module.php index 2040ac4e0a9..a9e25c91b8c 100644 --- a/mod/blog/views/default/blog/group_module.php +++ b/mod/blog/views/default/blog/group_module.php @@ -13,7 +13,10 @@ } $all_link = elgg_view('output/url', [ - 'href' => "blog/group/$group->guid/all", + 'href' => elgg_generate_url('collection:object:blog:group', [ + 'group_guid' => $group->guid, + 'subpage' => 'all', + ]), 'text' => elgg_echo('link:view:all'), 'is_trusted' => true, ]); @@ -33,7 +36,9 @@ elgg_pop_context(); $new_link = elgg_view('output/url', [ - 'href' => "blog/add/$group->guid", + 'href' => elgg_generate_url('add:object:blog', [ + 'guid' => $group->guid, + ]), 'text' => elgg_echo('blog:write'), 'is_trusted' => true, ]); diff --git a/mod/blog/views/default/blog/sidebar/revisions.php b/mod/blog/views/default/blog/sidebar/revisions.php index b1e13b87d49..e812b4e5934 100644 --- a/mod/blog/views/default/blog/sidebar/revisions.php +++ b/mod/blog/views/default/blog/sidebar/revisions.php @@ -40,7 +40,9 @@ return; } -$load_base_url = "blog/edit/{$blog->getGUID()}"; +$load_base_url = elgg_generate_url('edit:object:blog', [ + 'guid' => $blog->guid, +]); // show the "published revision" $published_item = ''; diff --git a/mod/blog/views/default/resources/blog/add.php b/mod/blog/views/default/resources/blog/add.php index 10292cab7b1..01e3da9e220 100644 --- a/mod/blog/views/default/resources/blog/add.php +++ b/mod/blog/views/default/resources/blog/add.php @@ -2,7 +2,12 @@ elgg_gatekeeper(); -$page_type = elgg_extract('page_type', $vars); +elgg_load_library('elgg:blog'); + +// push all blogs breadcrumb +elgg_push_breadcrumb(elgg_echo('blog:blogs'), elgg_generate_url('collection:object:blog:all')); + +$page_type = 'add'; $guid = elgg_extract('guid', $vars); $params = blog_get_page_content_edit('add', $guid); diff --git a/mod/blog/views/default/resources/blog/all.php b/mod/blog/views/default/resources/blog/all.php index 8502e289037..137162dc730 100644 --- a/mod/blog/views/default/resources/blog/all.php +++ b/mod/blog/views/default/resources/blog/all.php @@ -1,6 +1,11 @@ <?php -$page_type = elgg_extract('page_type', $vars); +$page_type = 'all'; + +elgg_load_library('elgg:blog'); + +// push all blogs breadcrumb +elgg_push_breadcrumb(elgg_echo('blog:blogs'), elgg_generate_url('collection:object:blog:all')); $params = blog_get_page_content_list(); diff --git a/mod/blog/views/default/resources/blog/archive.php b/mod/blog/views/default/resources/blog/archive.php index 7dafac86170..51a4a6a07e6 100644 --- a/mod/blog/views/default/resources/blog/archive.php +++ b/mod/blog/views/default/resources/blog/archive.php @@ -1,6 +1,11 @@ <?php -$page_type = elgg_extract('page_type', $vars); +elgg_load_library('elgg:blog'); + +// push all blogs breadcrumb +elgg_push_breadcrumb(elgg_echo('blog:blogs'), elgg_generate_url('collection:object:blog:all')); + +$page_type = 'archive'; $username = elgg_extract('username', $vars); $lower = elgg_extract('lower', $vars); $upper = elgg_extract('upper', $vars); diff --git a/mod/blog/views/default/resources/blog/edit.php b/mod/blog/views/default/resources/blog/edit.php index 0ac39675ba3..fbdd9613bcb 100644 --- a/mod/blog/views/default/resources/blog/edit.php +++ b/mod/blog/views/default/resources/blog/edit.php @@ -2,7 +2,12 @@ elgg_gatekeeper(); -$page_type = elgg_extract('page_type', $vars); +elgg_load_library('elgg:blog'); + +// push all blogs breadcrumb +elgg_push_breadcrumb(elgg_echo('blog:blogs'), elgg_generate_url('collection:object:blog:all')); + +$page_type = 'edit'; $guid = elgg_extract('guid', $vars); $revision = elgg_extract('revision', $vars); diff --git a/mod/blog/views/default/resources/blog/friends.php b/mod/blog/views/default/resources/blog/friends.php index ab11ce95342..5bd039c17ae 100644 --- a/mod/blog/views/default/resources/blog/friends.php +++ b/mod/blog/views/default/resources/blog/friends.php @@ -1,11 +1,16 @@ <?php -$page_type = elgg_extract('page_type', $vars); +elgg_load_library('elgg:blog'); + +// push all blogs breadcrumb +elgg_push_breadcrumb(elgg_echo('blog:blogs'), elgg_generate_url('collection:object:blog:all')); + +$page_type = 'friends'; $username = elgg_extract('username', $vars); $user = get_user_by_username($username); if (!$user) { - forward('', '404'); + throw new \Elgg\EntityNotFoundException(); } $params = [ @@ -14,7 +19,7 @@ ]; $crumbs_title = $user->name; -elgg_push_breadcrumb($crumbs_title, "blog/owner/{$user->username}"); +elgg_push_breadcrumb($crumbs_title, elgg_generate_url('collection:object:blog:owner', ['username' => $user->username])); elgg_push_breadcrumb(elgg_echo('friends')); elgg_register_title_button('blog', 'add', 'object', 'blog'); diff --git a/mod/blog/views/default/resources/blog/group.php b/mod/blog/views/default/resources/blog/group.php index 98221324631..49638dc620b 100644 --- a/mod/blog/views/default/resources/blog/group.php +++ b/mod/blog/views/default/resources/blog/group.php @@ -1,7 +1,12 @@ <?php +elgg_load_library('elgg:blog'); + +// push all blogs breadcrumb +elgg_push_breadcrumb(elgg_echo('blog:blogs'), elgg_generate_url('collection:object:blog:all')); + $subpage = elgg_extract('subpage', $vars); -$page_type = elgg_extract('page_type', $vars); +$page_type = 'group'; $group_guid = elgg_extract('group_guid', $vars); $lower = elgg_extract('lower', $vars); $upper = elgg_extract('upper', $vars); @@ -9,7 +14,7 @@ $group = get_entity($group_guid); if (!$group instanceof ElggGroup) { - forward('', '404'); + throw new \Elgg\EntityNotFoundException(); } if (!isset($subpage) || $subpage == 'all') { diff --git a/mod/blog/views/default/resources/blog/owner.php b/mod/blog/views/default/resources/blog/owner.php index d9ec13e9495..2e32b0a805b 100644 --- a/mod/blog/views/default/resources/blog/owner.php +++ b/mod/blog/views/default/resources/blog/owner.php @@ -1,11 +1,16 @@ <?php -$page_type = elgg_extract('page_type', $vars); +elgg_load_library('elgg:blog'); + +// push all blogs breadcrumb +elgg_push_breadcrumb(elgg_echo('blog:blogs'), elgg_generate_url('collection:object:blog:all')); + +$page_type = 'owner'; $username = elgg_extract('username', $vars); $user = get_user_by_username($username); if (!$user) { - forward('', '404'); + throw new \Elgg\EntityNotFoundException(); } $params = blog_get_page_content_list($user->guid); diff --git a/mod/blog/views/default/resources/blog/view.php b/mod/blog/views/default/resources/blog/view.php index 9d676fb784f..fdd68f95ec8 100644 --- a/mod/blog/views/default/resources/blog/view.php +++ b/mod/blog/views/default/resources/blog/view.php @@ -20,9 +20,9 @@ $crumbs_title = $container->name; if ($container instanceof ElggGroup) { - elgg_push_breadcrumb($crumbs_title, "blog/group/$container->guid/all"); + elgg_push_breadcrumb($crumbs_title, elgg_generate_url('collection:object:blog:group', ['group_guid' => $container->guid])); } else { - elgg_push_breadcrumb($crumbs_title, "blog/owner/$container->username"); + elgg_push_breadcrumb($crumbs_title, elgg_generate_url('collection:object:blog:owner', ['username' => $container->username])); } elgg_push_breadcrumb($blog->title); diff --git a/mod/blog/views/default/widgets/blog/content.php b/mod/blog/views/default/widgets/blog/content.php index 564387e8895..9f963303236 100644 --- a/mod/blog/views/default/widgets/blog/content.php +++ b/mod/blog/views/default/widgets/blog/content.php @@ -24,7 +24,9 @@ echo $content; $more_link = elgg_view('output/url', [ - 'href' => 'blog/owner/' . $widget->getOwnerEntity()->username, + 'href' => elgg_generate_url('collection:object:blog:owner', [ + 'username' => $widget->getOwnerEntity()->username, + ]), 'text' => elgg_echo('blog:moreblogs'), 'is_trusted' => true, ]); diff --git a/views/default/errors/400.php b/views/default/errors/400.php index 03e2ef0a6ee..3c5b27bca4a 100644 --- a/views/default/errors/400.php +++ b/views/default/errors/400.php @@ -3,4 +3,5 @@ * Bad request error */ -echo elgg_view_message('error', elgg_echo('error:400:content')); +$error = elgg_extract('error', $vars, elgg_echo('error:400:content')); +echo elgg_view_message('error', $error); diff --git a/views/default/errors/403.php b/views/default/errors/403.php index b22d6d2880d..9339c5117f1 100644 --- a/views/default/errors/403.php +++ b/views/default/errors/403.php @@ -3,4 +3,5 @@ * Forbidden error */ -echo elgg_view_message('error', elgg_echo('error:403:content')); +$error = elgg_extract('error', $vars, elgg_echo('error:403:content')); +echo elgg_view_message('error', $error); diff --git a/views/default/errors/404.php b/views/default/errors/404.php index e6ead53ba9b..2d677493c2c 100644 --- a/views/default/errors/404.php +++ b/views/default/errors/404.php @@ -3,4 +3,5 @@ * Page not found error */ -echo elgg_view_message('error', elgg_echo('error:404:content')); +$error = elgg_extract('error', $vars, elgg_echo('error:404:content')); +echo elgg_view_message('error', $error); diff --git a/views/default/errors/default.php b/views/default/errors/default.php index abc98247e9d..bb5b818cf2f 100644 --- a/views/default/errors/default.php +++ b/views/default/errors/default.php @@ -3,4 +3,5 @@ * General error */ -echo elgg_view_message('error', elgg_echo('error:default:content')); +$error = elgg_extract('error', $vars, elgg_echo('error:default:content')); +echo elgg_view_message('error', $error); diff --git a/views/default/resources/favicon.ico.php b/views/default/resources/favicon.ico.php new file mode 100644 index 00000000000..cdbe71bb21f --- /dev/null +++ b/views/default/resources/favicon.ico.php @@ -0,0 +1,13 @@ +<?php + +/** + * Handle requests for /favicon.ico + */ + +elgg_set_http_header('Expires: ' . gmdate('D, d M Y H:i:s \G\M\T', strtotime("+1 week")), true); +elgg_set_http_header("Pragma: public", true); +elgg_set_http_header("Cache-Control: public", true); + +elgg_set_http_header('Content-Type: image/x-icon'); + +echo elgg_view('graphics/favicon.ico'); diff --git a/views/default/resources/livesearch.php b/views/default/resources/livesearch.php new file mode 100644 index 00000000000..8bc6933fda6 --- /dev/null +++ b/views/default/resources/livesearch.php @@ -0,0 +1,16 @@ +<?php + +/** + * Page handler for autocomplete endpoint + * + * /livesearch/<match_on>?q=<query> + */ + +$match_on = elgg_extract('match_on', $vars, get_input('match_on')); + +if (!elgg_view_exists("resources/livesearch/$match_on")) { + throw new \Elgg\PageNotFoundException(); +} + +elgg_set_viewtype('json'); +echo elgg_view("resources/livesearch/$match_on", $vars); diff --git a/views/default/resources/manifest.json.php b/views/default/resources/manifest.json.php new file mode 100644 index 00000000000..272cfb37a10 --- /dev/null +++ b/views/default/resources/manifest.json.php @@ -0,0 +1,8 @@ +<?php + +elgg_set_http_header('Content-Type: application/json;charset=utf-8'); + +$site = elgg_get_site_entity(); +$resource = new \Elgg\Http\WebAppManifestResource($site); + +echo json_encode($resource->get()); diff --git a/views/default/resources/settings/account.php b/views/default/resources/settings/account.php index fba504bc689..f28d49c44a4 100644 --- a/views/default/resources/settings/account.php +++ b/views/default/resources/settings/account.php @@ -6,16 +6,19 @@ * @subpackage Core */ -// Only logged in users elgg_gatekeeper(); -// Make sure we don't open a security hole ... -if ((!elgg_get_page_owner_entity()) || (!elgg_get_page_owner_entity()->canEdit())) { - register_error(elgg_echo('noaccess')); - forward('/'); +$username = elgg_extract('username', $vars); +if (!$username) { + $username = elgg_get_logged_in_user_entity()->username; } -$username = elgg_extract('username', $vars); +$user = get_user_by_username($username); +if (!$user || !$user->canEdit()) { + throw new \Elgg\EntityPermissionsException(); +} + +elgg_set_page_owner_guid($user->guid); elgg_push_breadcrumb(elgg_echo('settings'), "settings/user/$username"); diff --git a/views/default/resources/settings/statistics.php b/views/default/resources/settings/statistics.php index 4ff989b4674..2f8d254998b 100644 --- a/views/default/resources/settings/statistics.php +++ b/views/default/resources/settings/statistics.php @@ -6,18 +6,24 @@ * @subpackage Core */ -// Only logged in users elgg_gatekeeper(); -// Make sure we don't open a security hole ... -if ((!elgg_get_page_owner_entity()) || (!elgg_get_page_owner_entity()->canEdit())) { - register_error(elgg_echo('noaccess')); - forward('/'); +$username = elgg_extract('username', $vars); +if (!$username) { + $username = elgg_get_logged_in_user_entity()->username; } -$username = elgg_extract('username', $vars); +$user = get_user_by_username($username); +if (!$user || !$user->canEdit()) { + throw new \Elgg\EntityPermissionsException(); +} + +elgg_set_page_owner_guid($user->guid); + +elgg_push_breadcrumb(elgg_echo('settings'), elgg_generate_url('settings:account', [ + 'username' => $user->username, +])); -elgg_push_breadcrumb(elgg_echo('settings'), "settings/user/$username"); elgg_push_breadcrumb(elgg_echo('usersettings:statistics:opt:linktext')); $title = elgg_echo("usersettings:statistics"); diff --git a/views/default/resources/settings/tools.php b/views/default/resources/settings/tools.php index 24cdb6796fb..95f3075727b 100644 --- a/views/default/resources/settings/tools.php +++ b/views/default/resources/settings/tools.php @@ -6,27 +6,30 @@ * @subpackage Core */ -// Only logged in users elgg_gatekeeper(); -// Make sure we don't open a security hole ... -if ((!elgg_get_page_owner_entity()) || (!elgg_get_page_owner_entity()->canEdit())) { - register_error(elgg_echo('noaccess')); - forward('/'); +$username = elgg_extract('username', $vars); +if (!$username) { + $username = elgg_get_logged_in_user_entity()->username; +} + +$user = get_user_by_username($username); +if (!$user || !$user->canEdit()) { + throw new \Elgg\EntityPermissionsException(); } +elgg_set_page_owner_guid($user->guid); + $plugin_id = elgg_extract("plugin_id", $vars); if (empty($plugin_id)) { - register_error(elgg_echo('ElggPlugin:MissingID')); - forward(REFERER); + throw new \Elgg\PageNotFoundException(elgg_echo('ElggPlugin:MissingID')); } $plugin = elgg_get_plugin_from_id($plugin_id); if (!$plugin) { - register_error(elgg_echo('PluginException:InvalidID', [$plugin_id])); - forward(REFERER); + throw new \Elgg\PageNotFoundException(elgg_echo('PluginException:InvalidID', [$plugin_id])); } if (elgg_language_key_exists($plugin_id . ':usersettings:title')) { diff --git a/views/json/resources/livesearch/users.php b/views/json/resources/livesearch/users.php index dfafe429355..aef32b06a92 100644 --- a/views/json/resources/livesearch/users.php +++ b/views/json/resources/livesearch/users.php @@ -28,7 +28,7 @@ } if (!$target || !$target->canEdit()) { - forward('', '403'); + throw new \Elgg\EntityPermissionsException(); } $dbprefix = elgg_get_config('dbprefix');