├── .gitignore ├── README.md ├── composer.json ├── readme.txt ├── routemaster.php └── src ├── Exception ├── NoFallbackException.php └── RoutemasterException.php ├── Model └── Route.php ├── Oowp ├── OowpRouter.php ├── OowpRouterHelper.php ├── Response │ ├── ContainerViewResponse.php │ └── ViewResponse.php └── View │ ├── ContainerView.php │ ├── NotFoundView.php │ ├── RoutemasterOowpView.php │ └── SitemapView.php ├── Response ├── FileResponse.php ├── HtmlResponse.php ├── JsonResponse.php ├── RedirectResponse.php ├── RobotsResponse.php ├── RoutemasterResponse.php └── XmlResponse.php ├── Router.php ├── RouterHelper.php └── View ├── HtmlPage.php └── Renderable.php /.gitignore: -------------------------------------------------------------------------------- 1 | ## Composer ## 2 | vendor/ 3 | wp-content/mu-plugins/ 4 | # Locked versions won't be reflected in installations, so not having any 5 | # is probably less confusing. 6 | # https://getcomposer.org/doc/02-libraries.md#lock-file 7 | composer.lock 8 | 9 | ## IDEs ## 10 | .idea/ 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Routemaster WordPress plugin 2 | 3 | Replaces the built-in WordPress routing logic with one defined by URL patterns. 4 | 5 | ## Installation 6 | 7 | - Install and activate plugin 8 | - Create Router subclass that implements abstract `routes` function 9 | - add the following to your theme: 10 | 11 | ~~~~ 12 | $router = MyRouter::getInstance(); 13 | $router->setup(); 14 | ~~~~ 15 | 16 | ### Use with [OOWP](https://github.com/outlandishideas/oowp) 17 | 18 | To successfully extend `OowpRouter` and gain router awareness of post objects, 19 | you should also install OOWP. This is optional if you avoid the 20 | `Outlandish\Wordpress\Routemaster\Oowp` namespace. 21 | 22 | To install it: 23 | 24 | composer require outlandish/oowp 25 | 26 | ### Wordpress.org 27 | 28 | Unfortunately for _new_ projects Wordpress [are not accepting](https://make.wordpress.org/plugins/2016/03/01/please-do-not-submit-frameworks/) 29 | libraries as plugins which they host. The GitHub Actions support for this 30 | is now deleted since we tried and failed to have the project added there. 31 | 32 | See [this PR](https://github.com/outlandishideas/routemaster/pull/14/files#diff-2b7bfbec6c9ddad9e63030b179d67ece) if you'd like to refer back to the 33 | GitHub Actions setup for Wordpress.org publishing, for another plugin which meets the 34 | current guidelines. 35 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "outlandish/routemaster", 3 | "type": "wordpress-muplugin", 4 | "description": "A routing plugin for WordPress", 5 | "minimum-stability": "stable", 6 | "license": "GPL-3.0", 7 | "autoload": { 8 | "psr-4": { 9 | "Outlandish\\Wordpress\\Routemaster\\": "src/" 10 | } 11 | }, 12 | "require": { 13 | "php": ">=5.6", 14 | "ext-json": "*" 15 | }, 16 | "suggest": { 17 | "outlandish/oowp": "To optionally use a post-object-aware version of the router and corresponding views" 18 | }, 19 | "authors": [ 20 | { 21 | "name": "Rasmus Winter", 22 | "email": "rasmus@outlandish.com" 23 | }, 24 | { 25 | "name": "Tamlyn Rhodes", 26 | "email": "tam@outlandish.com" 27 | }, 28 | { 29 | "name": "Matthew Kendon", 30 | "email": "matt@outlandish.com" 31 | } 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /readme.txt: -------------------------------------------------------------------------------- 1 | === Routemaster === 2 | Contributors: outlandishcoop 3 | Tags: routing, routes 4 | Requires at least: 4.9.8 5 | Tested up to: 5.4.1 6 | Stable tag: 2.4.1 7 | Requires PHP: 5.6 8 | License: GPLv3 or later 9 | License URI: https://www.gnu.org/licenses/gpl-3.0.html 10 | 11 | Replaces the built-in WordPress routing logic with one defined by URL patterns. 12 | -------------------------------------------------------------------------------- /routemaster.php: -------------------------------------------------------------------------------- 1 | post_name = sanitize_title($post->post_name ? $post->post_name : $post->post_title, $post->ID); 22 | $link = $post->permalink(); 23 | $link .= '?' . $qs; 24 | } 25 | } 26 | 27 | return $link; 28 | }); 29 | } 30 | }); 31 | -------------------------------------------------------------------------------- /src/Exception/NoFallbackException.php: -------------------------------------------------------------------------------- 1 | allowFallback = false; 10 | } 11 | } -------------------------------------------------------------------------------- /src/Exception/RoutemasterException.php: -------------------------------------------------------------------------------- 1 | pattern = $pattern; 19 | $this->actionName = $actionName; 20 | $this->handler = $handler; 21 | } 22 | 23 | 24 | } -------------------------------------------------------------------------------- /src/Oowp/OowpRouter.php: -------------------------------------------------------------------------------- 1 | permalinkHook($postLink, $post, $leavename); 25 | }, 10, 4); 26 | } 27 | 28 | protected function getDefaultRoutePatterns() 29 | { 30 | return array_merge(parent::getDefaultRoutePatterns(), [ 31 | '|^sitemap.xml$|i' => 'sitemap', //xml sitemap for google etc 32 | '|^__preview__/([^/]+)/(\d+)/?$|' => 'previewPost', //__preview__/{post type}/{post id} 33 | '|([^/]+)/?$|' => 'defaultPost', //matches blah/blah/slug 34 | '|^$|' => 'frontPage' //matches empty string 35 | ]); 36 | } 37 | 38 | 39 | /** @var int|null Used in permalinkHook function, to prevent infinite recursion */ 40 | protected $permalinkHookPostId; 41 | 42 | /** 43 | * Overwrites the post_link with the post's permalink() 44 | * @param string $post_link 45 | * @param \WP_Post $post 46 | * @param boolean $leaveName 47 | * @return string|void 48 | */ 49 | public function permalinkHook($post_link, $post, $leaveName) 50 | { 51 | if ($post->post_name) { 52 | if ($post->ID != $this->permalinkHookPostId) { 53 | // prevent infinite recursion by saving the ID before calling permalink() (which may come back here again) 54 | $this->permalinkHookPostId = $post->ID; 55 | $post_link = WordpressPost::createWordpressPost($post)->permalink($leaveName); 56 | $this->permalinkHookPostId = null; 57 | } 58 | } elseif (in_array($post->post_status, ['draft', 'auto-draft', 'inherit'])) { 59 | $post_link = get_bloginfo('url') . '/__preview__/' . $post->post_type . '/' . $post->ID; 60 | } 61 | return $post_link; 62 | } 63 | 64 | /*********************************************** 65 | * 66 | * Methods for default routes (defined above) 67 | * 68 | ***********************************************/ 69 | 70 | /** 71 | * @route /sitemap.xml 72 | */ 73 | protected function sitemap() 74 | { 75 | $view = new SitemapView(new OowpQuery(array('post_type' => 'any', 'orderby' => 'date'))); 76 | return new XmlResponse($view); 77 | } 78 | 79 | /** 80 | * @route /any/unknown/route 81 | */ 82 | protected function show404() 83 | { 84 | global $post; 85 | $post = new FakePost(array('post_title' => 'Page not found')); 86 | return parent::show404(); 87 | } 88 | 89 | /** 90 | * @route /default/route/when/no/other/match 91 | * @param string $slug 92 | * @return array 93 | */ 94 | protected function defaultPost($slug) 95 | { 96 | $args = ['name' => $slug, 'post_type' => 'any']; 97 | return [ 98 | 'post' => $this->helper->querySingle($args, true) 99 | ]; 100 | } 101 | 102 | /** 103 | * @route /__preview__/{post type}/{post id} 104 | * @param string $postType 105 | * @param string $id 106 | * @return array 107 | */ 108 | protected function previewPost($postType, $id) 109 | { 110 | $args = ['id' => $id, 'post_type' => $postType]; // querySingle will apply the correct post status parameters 111 | return [ 112 | 'post' => $this->helper->querySingle($args, false) 113 | ]; 114 | } 115 | 116 | /** 117 | * @route / 118 | */ 119 | protected function frontPage() 120 | { 121 | $args = ['page_id' => get_option('page_on_front')]; 122 | return [ 123 | 'post' => $this->helper->querySingle($args, true) 124 | ]; 125 | } 126 | 127 | } 128 | -------------------------------------------------------------------------------- /src/Oowp/OowpRouterHelper.php: -------------------------------------------------------------------------------- 1 | permalink(); 46 | if ("$scheme://$_SERVER[HTTP_HOST]$path" !== $permalink) { 47 | wp_redirect($permalink); 48 | die; 49 | } 50 | } 51 | 52 | /** 53 | * Create a new query object and set the global $wp_query 54 | * @param $args 55 | * @return OowpQuery 56 | */ 57 | protected function query($args) { 58 | global $wp_query, $wp_the_query; 59 | $wp_the_query = $wp_query = new OowpQuery($args); 60 | return $wp_query; 61 | } 62 | 63 | /** 64 | * Select a single post, set globals and throw 404 exception if nothing matches 65 | * @param array $args 66 | * @param bool $redirectCanonical true if should redirect canonically after fetching the post 67 | * @throws RoutemasterException 68 | * @return WordpressPost 69 | */ 70 | public function querySingle($args, $redirectCanonical = false) { 71 | global $post; 72 | 73 | if (current_user_can('edit_posts') && (isset($_GET['preview']) && $_GET['preview'] === 'true')) { 74 | //currently published posts just need this to show the latest autosave instead 75 | $args['preview'] = 'true'; 76 | 77 | //for unpublished posts, override query entirely 78 | if (isset($_GET['p']) || isset($_GET['page_id'])) { 79 | $args = array_intersect_key($_GET, array_flip(array('preview', 'p', 'page_id'))); 80 | } 81 | 82 | //for unpublished posts and posts returned to draft, allow draft status 83 | //for being able to preview edits of existing pages, allow inherit 84 | $args['post_status'] = array( 85 | 'auto-draft', 86 | 'draft', 87 | 'inherit', 88 | 'private', 89 | 'publish', 90 | ); 91 | 92 | $redirectCanonical = false; 93 | } 94 | 95 | $query = $this->query( $args ); 96 | //no matched posts, so first check if this is a logged in user with a private post 97 | if ( ( ! count( $query ) ) && is_user_logged_in() ) { 98 | $args['author'] = get_current_user_id(); 99 | $args['post_status'] = 'private'; 100 | $query = $this->query( $args ); 101 | } 102 | 103 | //no matched posts so 404 104 | if (!count($query)) { 105 | $query->is_404 = true; 106 | throw new RoutemasterException('Not found', 404); 107 | } 108 | 109 | $oowpPost = $query[0]; 110 | $post = $oowpPost->get_post(); 111 | 112 | if ($redirectCanonical) { 113 | $this->redirectCanonical($oowpPost); 114 | } 115 | return $oowpPost; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/Oowp/Response/ContainerViewResponse.php: -------------------------------------------------------------------------------- 1 | view = $this->createContainerView($renderable); 21 | } 22 | 23 | protected function createContainerView($view) 24 | { 25 | return new ContainerView($view); 26 | } 27 | 28 | protected function render() 29 | { 30 | if ($this->view) { 31 | foreach ($this->outputArgs as $name=>$value) { 32 | $this->view->$name = $value; 33 | } 34 | $this->view->render(); 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /src/Oowp/Response/ViewResponse.php: -------------------------------------------------------------------------------- 1 | view) { 16 | foreach ($this->outputArgs as $key=>$value) { 17 | $this->view->$key = $value; 18 | } 19 | $this->view->render(); 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /src/Oowp/View/ContainerView.php: -------------------------------------------------------------------------------- 1 | content = $content; 19 | } 20 | 21 | 22 | public function render($args = []) 23 | { 24 | ?> 25 | 26 | 27 |
28 | 29 | content) { 31 | $this->content->render($args); 32 | } 33 | ?> 34 | 35 | 36 | 13 |