Source: components/class-router.php

<?php
/**
 * Router component.
 *
 * @package HivePress\Components
 */

namespace HivePress\Components;

use HivePress\Helpers as hp;

// Exit if accessed directly.
defined( 'ABSPATH' ) || exit;

/**
 * Handles URL routing.
 */
final class Router extends Component {

	/**
	 * All routes.
	 *
	 * @var array
	 */
	protected $routes = [];

	/**
	 * The current route.
	 *
	 * @var array
	 */
	protected $route;

	/**
	 * Class constructor.
	 *
	 * @param array $args Component arguments.
	 */
	public function __construct( $args = [] ) {

		// Register REST routes.
		add_action( 'rest_api_init', [ $this, 'register_rest_routes' ] );

		// Add rewrite rules.
		add_action( 'init', [ $this, 'add_rewrite_rules' ] );

		// Set rewrite slugs.
		add_filter( 'register_post_type_args', [ $this, 'set_rewrite_slug' ], 10, 2 );
		add_filter( 'register_taxonomy_args', [ $this, 'set_rewrite_slug' ], 10, 2 );

		// Flush rewrite rules.
		add_action( 'hivepress/v1/activate', [ $this, 'flush_rewrite_rules' ] );
		add_action( 'hivepress/v1/update', [ $this, 'flush_rewrite_rules' ] );
		add_action( 'hivepress/v1/deactivate', [ $this, 'flush_rewrite_rules' ] );

		if ( ! is_admin() ) {

			// Set page title.
			add_filter( 'document_title_parts', [ $this, 'set_page_title' ] );

			// Disable page title.
			add_filter( 'rank_math/frontend/title', [ $this, 'disable_page_title' ] );

			// Set page context.
			add_filter( 'hivepress/v1/templates/page', [ $this, 'set_page_context' ] );

			// Set page template.
			add_filter( 'template_include', [ $this, 'set_page_template' ], 10000 );

			// Disable page redirect.
			add_filter( 'redirect_canonical', [ $this, 'disable_page_redirect' ] );
		}

		parent::__construct( $args );
	}

	/**
	 * Gets routes.
	 *
	 * @return array
	 */
	protected function get_routes() {
		if ( empty( $this->routes ) ) {

			// Merge routes.
			foreach ( hivepress()->get_controllers() as $controller ) {
				$this->routes = hp\merge_arrays( $this->routes, $controller->get_routes() );
			}

			/**
			 * Filters URL routes registered by HivePress. If you customize the route URLs using this hook, don't forget to refresh permalinks afterwards.
			 *
			 * @hook hivepress/v1/routes
			 * @param {array} $routes Route configurations.
			 * @return {array} Route configurations.
			 */
			$this->routes = apply_filters( 'hivepress/v1/routes', $this->routes );
		}

		return $this->routes;
	}

	/**
	 * Gets route.
	 *
	 * @param string $name Route name.
	 * @return array
	 */
	public function get_route( $name ) {
		return hp\get_array_value( $this->get_routes(), $name );
	}

	/**
	 * Gets the current route.
	 *
	 * @return array
	 */
	public function get_current_route() {
		if ( ! isset( $this->route ) ) {
			$this->route = false;

			if ( hivepress()->request->get_param( 'route' ) ) {

				// Get route name.
				$name = hivepress()->request->get_param( 'route' );

				// Get route.
				$route = $this->get_route( $name );

				if ( $route && ! hp\get_array_value( $route, 'rest' ) ) {
					$this->route = array_merge( $route, [ 'name' => $name ] );
				}
			} else {

				// Match routes.
				foreach ( $this->get_routes() as $name => $route ) {
					if ( isset( $route['match'] ) && call_user_func( $route['match'] ) ) {
						$this->route = array_merge( $route, [ 'name' => $name ] );

						break;
					}
				}
			}
		}

		return $this->route;
	}

	/**
	 * Gets the current route name.
	 *
	 * @return string
	 */
	public function get_current_route_name() {
		return hp\get_array_value( $this->get_current_route(), 'name' );
	}

	/**
	 * Gets URL path.
	 *
	 * @param string $name Route name.
	 * @return string
	 */
	protected function get_url_path( $name ) {
		$path = '';

		// Get route.
		$route = $this->get_route( $name );

		// Merge paths.
		while ( $route ) {
			if ( isset( $route['path'] ) ) {
				$path = $route['path'] . $path;
			}

			if ( isset( $route['base'] ) ) {
				$route = $this->get_route( $route['base'] );
			} else {
				break;
			}
		}

		return $path;
	}

	/**
	 * Gets URL parameters.
	 *
	 * @param string $name Route name.
	 * @return array
	 */
	protected function get_url_params( $name ) {
		preg_match_all( '/<([a-z_]+)>/', $this->get_url_path( $name ), $params );

		array_shift( $params );

		return hp\get_first_array_value( $params );
	}

	/**
	 * Gets route URL.
	 *
	 * @param string $name Route name.
	 * @param array  $query URL query.
	 * @param bool   $filter Remove custom query parameters?
	 * @return string
	 */
	public function get_url( $name, $query = [], $filter = false ) {
		$url = '';

		// Get route.
		$route = $this->get_route( $name );

		if ( $route ) {
			if ( isset( $route['url'] ) ) {

				// Set URL.
				$url = call_user_func( $route['url'], $query );
			} else {

				// Get URL path.
				$path = $this->get_url_path( $name );

				if ( $path ) {

					// Get URL params.
					$params = $this->get_url_params( $name );

					// Get query variables.
					$vars = array_diff_key( $query, array_flip( $params ) );

					// Set URL query.
					$query = array_merge(
						array_fill_keys( $params, null ),
						array_diff_key( $query, $vars ),
						[
							'route' => $name,
						]
					);

					// Set URL path.
					if ( get_option( 'permalink_structure' ) || hp\get_array_value( $route, 'rest' ) ) {
						foreach ( $params as $param ) {
							$path = preg_replace( '/\(\?P<' . preg_quote( $param, '/' ) . '>[^\)]+\)\??/', $query[ $param ], $path );
						}

						$path = rtrim( str_replace( '/?', '/', $path ), '/' ) . '/';
					} else {
						$path = '/?' . http_build_query( array_combine( hp\prefix( array_keys( $query ) ), $query ) );
					}

					// Set URL.
					if ( hp\get_array_value( $route, 'rest' ) ) {
						$url = get_rest_url( null, 'hivepress/v1' . $path );
					} else {
						$url = home_url( $path );
					}

					// Add query variables.
					if ( $vars && ! $filter ) {
						$url = add_query_arg( array_map( 'rawurlencode', $vars ), $url );
					}
				}
			}
		}

		return $url;
	}

	/**
	 * Gets the current URL.
	 *
	 * @return string
	 */
	public function get_current_url() {
		global $wp;

		$path = rtrim( $wp->request, '/' ) . '/';

		if ( $_GET ) {
			$path .= '?' . http_build_query( $_GET );
		}

		return home_url( $path );
	}

	/**
	 * Gets return URL.
	 *
	 * @param string $name Route name.
	 * @return string
	 */
	public function get_return_url( $name ) {
		return $this->get_url(
			$name,
			[
				'redirect' => $this->get_current_url(),
			]
		);
	}

	/**
	 * Gets admin URL.
	 *
	 * @param string $type Object type.
	 * @param int    $id Object ID.
	 * @return string
	 */
	public function get_admin_url( $type, $id ) {
		$path = '';
		$args = [];

		switch ( $type ) {
			case 'user':
				$path = $type . '-edit.php';
				$args = [
					'user_id' => $id,
				];

				break;

			case 'post':
				$path = $type . '.php';
				$args = [
					'action' => 'edit',
					'post'   => $id,
				];

				break;

			case 'comment':
				$path = $type . '.php';
				$args = [
					'action' => 'editcomment',
					'c'      => $id,
				];

				break;
		}

		return admin_url( $path . '?' . http_build_query( $args ) );
	}

	/**
	 * Gets redirect URL.
	 *
	 * @return string
	 */
	public function get_redirect_url() {
		return wp_validate_redirect( hp\get_array_value( $_GET, 'redirect' ) );
	}

	/**
	 * Gets redirect callbacks.
	 *
	 * @param array $callbacks Callback arguments.
	 * @return array
	 */
	protected function get_redirect_callbacks( $callbacks ) {

		// Normalize callbacks.
		if ( count( $callbacks ) === 2 && is_object( hp\get_first_array_value( $callbacks ) ) ) {
			$callbacks = [
				[
					'callback' => $callbacks,
					'_order'   => 5,
				],
			];
		}

		// Sort callbacks.
		$callbacks = array_filter(
			array_map(
				function( $args ) {
					return hp\get_array_value( $args, 'callback' );
				},
				hp\sort_array( $callbacks )
			)
		);

		return $callbacks;
	}

	/**
	 * Registers REST routes.
	 */
	public function register_rest_routes() {
		foreach ( $this->get_routes() as $name => $route ) {
			if ( hp\get_array_value( $route, 'rest' ) && isset( $route['action'] ) ) {
				register_rest_route(
					'hivepress/v1',
					$this->get_url_path( $name ),
					[
						'methods'             => hp\get_array_value( $route, 'method', 'GET' ),
						'callback'            => $route['action'],
						'permission_callback' => '__return_true',
					]
				);
			}
		}
	}

	/**
	 * Adds rewrite rules.
	 */
	public function add_rewrite_rules() {

		// Set rewrite tags.
		$tags = [ 'route' ];

		foreach ( $this->get_routes() as $name => $route ) {
			if ( ! hp\get_array_value( $route, 'rest' ) && isset( $route['path'] ) && ( isset( $route['redirect'] ) || isset( $route['action'] ) ) ) {

				// Get URL path.
				$path = ltrim( $this->get_url_path( $name ), '/' );

				// Get URL params.
				$params = $this->get_url_params( $name );

				// Get query string.
				$query = ltrim(
					implode(
						'&',
						array_map(
							function( $index, $param ) {
								return hp\prefix( $param ) . '=$matches[' . ( $index + 1 ) . ']';
							},
							array_keys( $params ),
							$params
						)
					) . '&hp_route=' . rawurlencode( $name ),
					'&'
				);

				// Add rewrite rules.
				add_rewrite_rule( '^' . $path . '/?$', 'index.php?' . $query, 'top' );

				if ( hp\get_array_value( $route, 'paginated' ) ) {
					add_rewrite_rule( '^' . $path . '/page/(\d+)/?$', 'index.php?paged=$matches[' . ( count( $params ) + 1 ) . ']&' . $query, 'top' );
				}

				// Add rewrite tags.
				$tags = array_merge( $tags, $params );
			}
		}

		// Add rewrite tags.
		foreach ( array_unique( $tags ) as $tag ) {
			add_rewrite_tag( '%' . hp\prefix( $tag ) . '%', '([^&]+)' );
		}
	}

	/**
	 * Sets rewrite slug.
	 *
	 * @param array  $args Default arguments.
	 * @param string $type Post type or taxonomy.
	 * @return array
	 */
	public function set_rewrite_slug( $args, $type ) {

		// Check arguments.
		if ( strpos( $type, 'hp_' ) !== 0 || ! hp\get_array_value( $args, 'public', true ) ) {
			return $args;
		}

		// Get permalinks.
		$permalinks = (array) get_option( 'hp_permalinks', [] );

		if ( ! $permalinks ) {
			return $args;
		}

		// Set rewrite slug.
		$slug = hp\get_array_value( $permalinks, hp\unprefix( $type . '_slug' ) );

		if ( $slug ) {
			$args['rewrite']['slug'] = $slug;
		}

		return $args;
	}

	/**
	 * Flushes rewrite rules.
	 */
	public function flush_rewrite_rules() {
		delete_option( 'rewrite_rules' );
	}

	/**
	 * Sets page title.
	 *
	 * @param array $parts Title parts.
	 * @return array
	 */
	public function set_page_title( $parts ) {

		// Get the current route.
		$route = $this->get_current_route();

		if ( $route && isset( $route['title'] ) ) {

			// Remove query title.
			if ( count( $parts ) > 1 ) {
				array_shift( $parts );
			}

			// Add route title.
			array_unshift( $parts, $route['title'] );
		}

		return $parts;
	}

	/**
	 * Disables page title.
	 *
	 * @param string $title Page title.
	 * @return string
	 */
	public function disable_page_title( $title ) {
		if ( hp\get_array_value( hivepress()->router->get_current_route(), 'title' ) ) {
			return false;
		}

		return $title;
	}

	/**
	 * Sets page context.
	 *
	 * @param array $args Template arguments.
	 * @return array
	 */
	public function set_page_context( $args ) {
		$context = [];

		// Get title.
		$context['page_title'] = hp\get_array_value( $this->get_current_route(), 'title' );

		// @todo Remove theme-specific condition once fixed.
		if ( is_tax() && ( ! function_exists( 'hivetheme' ) || ! is_tax( 'hp_listing_category' ) ) ) {
			$term = get_queried_object();

			// Set title.
			$context['page_title'] = $term->name;

			// Set description.
			if ( $term->description ) {
				$context['page_description'] = apply_filters( 'the_content', $term->description );
			}
		}

		return hp\merge_arrays(
			$args,
			[
				'context' => $context,
			]
		);
	}

	/**
	 * Sets page template.
	 *
	 * @param array $template Template filepath.
	 * @return string
	 */
	public function set_page_template( $template ) {
		global $wp_query;

		// Get the current route.
		$route = $this->get_current_route();

		if ( $route ) {

			// Set query variables.
			if ( isset( $route['path'] ) ) {
				$wp_query->is_home = false;
				$wp_query->is_404  = false;
			}

			// Get menu redirect.
			$menu_redirect = home_url();

			foreach ( hivepress()->get_classes( 'menus' ) as $menu_class ) {
				if ( $menu_class::get_meta( 'chained' ) ) {

					// Create menu.
					$menu = hp\create_class_instance( $menu_class );

					if ( in_array( $route['name'], array_column( $menu->get_items(), 'route' ), true ) ) {

						// Get menu items.
						$menu_items      = $menu->get_items();
						$menu_item_names = array_keys( $menu_items );

						foreach ( $menu_items as $menu_item_name => $menu_item ) {
							if ( isset( $menu_item['route'] ) ) {

								// Get redirect URL.
								if ( $menu_item['route'] === $route['name'] ) {
									$next_menu_item = hp\get_array_value( $menu_items, hp\get_array_value( $menu_item_names, array_search( $menu_item_name, $menu_item_names, true ) + 1 ) );

									if ( $next_menu_item ) {
										$menu_redirect = $next_menu_item['url'];
									}

									break;
								}

								// Get menu route.
								$menu_route = $this->get_route( $menu_item['route'] );

								if ( $menu_route && isset( $menu_route['redirect'] ) ) {
									foreach ( $this->get_redirect_callbacks( $menu_route['redirect'] ) as $menu_route_redirect ) {
										if ( ! in_array( call_user_func( $menu_route_redirect ), [ null, true ], true ) ) {
											wp_safe_redirect( $menu_item['url'] );

											exit;
										}
									}
								}
							}
						}

						break;
					}
				}
			}

			// Set title.
			$title = hp\get_array_value( $route, 'title' );

			if ( is_callable( $title ) ) {
				$this->route['title'] = call_user_func( $title );
			}

			// Redirect page.
			if ( isset( $route['redirect'] ) ) {
				foreach ( $this->get_redirect_callbacks( $route['redirect'] ) as $route_redirect ) {
					$redirect = call_user_func( $route_redirect );

					if ( $redirect ) {
						if ( is_bool( $redirect ) ) {
							$redirect = $menu_redirect;
						}

						wp_safe_redirect( $redirect );

						exit;
					}
				}
			}

			// Render page.
			if ( isset( $route['action'] ) ) {
				echo call_user_func( $route['action'] );

				exit;
			}
		}

		return $template;
	}

	/**
	 * Disables page redirect.
	 *
	 * @param string $url Redirect URL.
	 * @return string
	 */
	public function disable_page_redirect( $url ) {
		foreach ( hivepress()->get_config( 'post_types' ) as $type => $args ) {
			if ( ! hp\get_array_value( $args, 'redirect_canonical', true ) && is_singular( hp\prefix( $type ) ) ) {
				$url = false;

				break;
			}
		}

		return $url;
	}
}