Source: components/class-router.php

  1. <?php
  2. /**
  3. * Router component.
  4. *
  5. * @package HivePress\Components
  6. */
  7. namespace HivePress\Components;
  8. use HivePress\Helpers as hp;
  9. // Exit if accessed directly.
  10. defined( 'ABSPATH' ) || exit;
  11. /**
  12. * Handles URL routing.
  13. */
  14. final class Router extends Component {
  15. /**
  16. * All routes.
  17. *
  18. * @var array
  19. */
  20. protected $routes = [];
  21. /**
  22. * The current route.
  23. *
  24. * @var array
  25. */
  26. protected $route;
  27. /**
  28. * Class constructor.
  29. *
  30. * @param array $args Component arguments.
  31. */
  32. public function __construct( $args = [] ) {
  33. // Register REST routes.
  34. add_action( 'rest_api_init', [ $this, 'register_rest_routes' ] );
  35. // Add rewrite rules.
  36. add_action( 'init', [ $this, 'add_rewrite_rules' ] );
  37. // Set rewrite slugs.
  38. add_filter( 'register_post_type_args', [ $this, 'set_rewrite_slug' ], 10, 2 );
  39. add_filter( 'register_taxonomy_args', [ $this, 'set_rewrite_slug' ], 10, 2 );
  40. // Flush rewrite rules.
  41. add_action( 'hivepress/v1/activate', [ $this, 'flush_rewrite_rules' ] );
  42. add_action( 'hivepress/v1/update', [ $this, 'flush_rewrite_rules' ] );
  43. add_action( 'hivepress/v1/deactivate', [ $this, 'flush_rewrite_rules' ] );
  44. if ( ! is_admin() ) {
  45. // Set page title.
  46. add_filter( 'document_title_parts', [ $this, 'set_page_title' ] );
  47. // Disable page title.
  48. add_filter( 'rank_math/frontend/title', [ $this, 'disable_page_title' ] );
  49. // Set page context.
  50. add_filter( 'hivepress/v1/templates/page', [ $this, 'set_page_context' ] );
  51. // Set page template.
  52. add_filter( 'template_include', [ $this, 'set_page_template' ], 10000 );
  53. // Disable page redirect.
  54. add_filter( 'redirect_canonical', [ $this, 'disable_page_redirect' ] );
  55. }
  56. parent::__construct( $args );
  57. }
  58. /**
  59. * Gets routes.
  60. *
  61. * @return array
  62. */
  63. protected function get_routes() {
  64. if ( empty( $this->routes ) ) {
  65. // Merge routes.
  66. foreach ( hivepress()->get_controllers() as $controller ) {
  67. $this->routes = hp\merge_arrays( $this->routes, $controller->get_routes() );
  68. }
  69. /**
  70. * Filters URL routes registered by HivePress. If you customize the route URLs using this hook, don't forget to refresh permalinks afterwards.
  71. *
  72. * @hook hivepress/v1/routes
  73. * @param {array} $routes Route configurations.
  74. * @return {array} Route configurations.
  75. */
  76. $this->routes = apply_filters( 'hivepress/v1/routes', $this->routes );
  77. }
  78. return $this->routes;
  79. }
  80. /**
  81. * Gets route.
  82. *
  83. * @param string $name Route name.
  84. * @return array
  85. */
  86. public function get_route( $name ) {
  87. return hp\get_array_value( $this->get_routes(), $name );
  88. }
  89. /**
  90. * Gets the current route.
  91. *
  92. * @return array
  93. */
  94. public function get_current_route() {
  95. if ( ! isset( $this->route ) ) {
  96. $this->route = false;
  97. if ( hivepress()->request->get_param( 'route' ) ) {
  98. // Get route name.
  99. $name = hivepress()->request->get_param( 'route' );
  100. // Get route.
  101. $route = $this->get_route( $name );
  102. if ( $route && ! hp\get_array_value( $route, 'rest' ) ) {
  103. $this->route = array_merge( $route, [ 'name' => $name ] );
  104. }
  105. } else {
  106. // Match routes.
  107. foreach ( $this->get_routes() as $name => $route ) {
  108. if ( isset( $route['match'] ) && call_user_func( $route['match'] ) ) {
  109. $this->route = array_merge( $route, [ 'name' => $name ] );
  110. break;
  111. }
  112. }
  113. }
  114. }
  115. return $this->route;
  116. }
  117. /**
  118. * Gets the current route name.
  119. *
  120. * @return string
  121. */
  122. public function get_current_route_name() {
  123. return hp\get_array_value( $this->get_current_route(), 'name' );
  124. }
  125. /**
  126. * Gets URL path.
  127. *
  128. * @param string $name Route name.
  129. * @return string
  130. */
  131. protected function get_url_path( $name ) {
  132. $path = '';
  133. // Get route.
  134. $route = $this->get_route( $name );
  135. // Merge paths.
  136. while ( $route ) {
  137. if ( isset( $route['path'] ) ) {
  138. $path = $route['path'] . $path;
  139. }
  140. if ( isset( $route['base'] ) ) {
  141. $route = $this->get_route( $route['base'] );
  142. } else {
  143. break;
  144. }
  145. }
  146. return $path;
  147. }
  148. /**
  149. * Gets URL parameters.
  150. *
  151. * @param string $name Route name.
  152. * @return array
  153. */
  154. protected function get_url_params( $name ) {
  155. preg_match_all( '/<([a-z_]+)>/', $this->get_url_path( $name ), $params );
  156. array_shift( $params );
  157. return hp\get_first_array_value( $params );
  158. }
  159. /**
  160. * Gets route URL.
  161. *
  162. * @param string $name Route name.
  163. * @param array $query URL query.
  164. * @param bool $filter Remove custom query parameters?
  165. * @return string
  166. */
  167. public function get_url( $name, $query = [], $filter = false ) {
  168. $url = '';
  169. // Get route.
  170. $route = $this->get_route( $name );
  171. if ( $route ) {
  172. if ( isset( $route['url'] ) ) {
  173. // Set URL.
  174. $url = call_user_func( $route['url'], $query );
  175. } else {
  176. // Get URL path.
  177. $path = $this->get_url_path( $name );
  178. if ( $path ) {
  179. // Get URL params.
  180. $params = $this->get_url_params( $name );
  181. // Get query variables.
  182. $vars = array_diff_key( $query, array_flip( $params ) );
  183. // Set URL query.
  184. $query = array_merge(
  185. array_fill_keys( $params, null ),
  186. array_diff_key( $query, $vars ),
  187. [
  188. 'route' => $name,
  189. ]
  190. );
  191. // Set URL path.
  192. if ( get_option( 'permalink_structure' ) || hp\get_array_value( $route, 'rest' ) ) {
  193. foreach ( $params as $param ) {
  194. $path = preg_replace( '/\(\?P<' . preg_quote( $param, '/' ) . '>[^\)]+\)\??/', hp\get_array_value( $query, $param, '' ), $path );
  195. }
  196. $path = rtrim( str_replace( '/?', '/', $path ), '/' ) . '/';
  197. } else {
  198. $path = '/?' . http_build_query( array_combine( hp\prefix( array_keys( $query ) ), $query ) );
  199. }
  200. // Set URL.
  201. if ( hp\get_array_value( $route, 'rest' ) ) {
  202. $url = get_rest_url( null, 'hivepress/v1' . $path );
  203. } else {
  204. $url = home_url( $path );
  205. }
  206. // Add query variables.
  207. if ( $vars && ! $filter ) {
  208. $url = add_query_arg( array_map( 'rawurlencode', array_map( 'strval', $vars ) ), $url );
  209. }
  210. }
  211. }
  212. }
  213. return $url;
  214. }
  215. /**
  216. * Gets the current URL.
  217. *
  218. * @return string
  219. */
  220. public function get_current_url() {
  221. global $wp;
  222. $path = rtrim( $wp->request, '/' ) . '/';
  223. if ( $_GET ) {
  224. $path .= '?' . http_build_query( $_GET );
  225. }
  226. return home_url( $path );
  227. }
  228. /**
  229. * Gets return URL.
  230. *
  231. * @param string $name Route name.
  232. * @param array $query URL query.
  233. * @return string
  234. */
  235. public function get_return_url( $name, $query = [] ) {
  236. return $this->get_url(
  237. $name,
  238. array_merge(
  239. $query,
  240. [
  241. 'redirect' => $this->get_current_url(),
  242. ]
  243. )
  244. );
  245. }
  246. /**
  247. * Gets admin URL.
  248. *
  249. * @param string $type Object type.
  250. * @param int $id Object ID.
  251. * @return string
  252. */
  253. public function get_admin_url( $type, $id ) {
  254. $path = '';
  255. $args = [];
  256. switch ( $type ) {
  257. case 'user':
  258. $path = $type . '-edit.php';
  259. $args = [
  260. 'user_id' => $id,
  261. ];
  262. break;
  263. case 'post':
  264. $path = $type . '.php';
  265. $args = [
  266. 'action' => 'edit',
  267. 'post' => $id,
  268. ];
  269. break;
  270. case 'comment':
  271. $path = $type . '.php';
  272. $args = [
  273. 'action' => 'editcomment',
  274. 'c' => $id,
  275. ];
  276. break;
  277. }
  278. return admin_url( $path . '?' . http_build_query( $args ) );
  279. }
  280. /**
  281. * Gets redirect URL.
  282. *
  283. * @return string
  284. */
  285. public function get_redirect_url() {
  286. return wp_validate_redirect( (string) hp\get_array_value( $_GET, 'redirect' ) );
  287. }
  288. /**
  289. * Gets redirect callbacks.
  290. *
  291. * @param array $callbacks Callback arguments.
  292. * @return array
  293. */
  294. protected function get_redirect_callbacks( $callbacks ) {
  295. // Normalize callbacks.
  296. if ( count( $callbacks ) === 2 && is_object( hp\get_first_array_value( $callbacks ) ) ) {
  297. $callbacks = [
  298. [
  299. 'callback' => $callbacks,
  300. '_order' => 5,
  301. ],
  302. ];
  303. }
  304. // Sort callbacks.
  305. $callbacks = array_filter(
  306. array_map(
  307. function ( $args ) {
  308. return hp\get_array_value( $args, 'callback' );
  309. },
  310. hp\sort_array( $callbacks )
  311. )
  312. );
  313. return $callbacks;
  314. }
  315. /**
  316. * Registers REST routes.
  317. */
  318. public function register_rest_routes() {
  319. foreach ( $this->get_routes() as $name => $route ) {
  320. if ( hp\get_array_value( $route, 'rest' ) && isset( $route['action'] ) ) {
  321. register_rest_route(
  322. 'hivepress/v1',
  323. $this->get_url_path( $name ),
  324. [
  325. 'methods' => hp\get_array_value( $route, 'method', 'GET' ),
  326. 'callback' => $route['action'],
  327. 'permission_callback' => '__return_true',
  328. ]
  329. );
  330. }
  331. }
  332. }
  333. /**
  334. * Adds rewrite rules.
  335. */
  336. public function add_rewrite_rules() {
  337. // Set rewrite tags.
  338. $tags = [ 'route' ];
  339. foreach ( $this->get_routes() as $name => $route ) {
  340. if ( ! hp\get_array_value( $route, 'rest' ) && isset( $route['path'] ) && ( isset( $route['redirect'] ) || isset( $route['action'] ) ) ) {
  341. // Get URL path.
  342. $path = ltrim( $this->get_url_path( $name ), '/' );
  343. // Get URL params.
  344. $params = $this->get_url_params( $name );
  345. // Get query string.
  346. $query = ltrim(
  347. implode(
  348. '&',
  349. array_map(
  350. function ( $index, $param ) {
  351. return hp\prefix( $param ) . '=$matches[' . ( $index + 1 ) . ']';
  352. },
  353. array_keys( $params ),
  354. $params
  355. )
  356. ) . '&hp_route=' . rawurlencode( $name ),
  357. '&'
  358. );
  359. // Add rewrite rules.
  360. add_rewrite_rule( '^' . $path . '/?$', 'index.php?' . $query, 'top' );
  361. if ( hp\get_array_value( $route, 'paginated' ) ) {
  362. add_rewrite_rule( '^' . $path . '/page/(\d+)/?$', 'index.php?paged=$matches[' . ( count( $params ) + 1 ) . ']&' . $query, 'top' );
  363. }
  364. // Add rewrite tags.
  365. $tags = array_merge( $tags, $params );
  366. }
  367. }
  368. // Add rewrite tags.
  369. foreach ( array_unique( $tags ) as $tag ) {
  370. add_rewrite_tag( '%' . hp\prefix( $tag ) . '%', '([^&]+)' );
  371. }
  372. }
  373. /**
  374. * Sets rewrite slug.
  375. *
  376. * @param array $args Default arguments.
  377. * @param string $type Post type or taxonomy.
  378. * @return array
  379. */
  380. public function set_rewrite_slug( $args, $type ) {
  381. // Check arguments.
  382. if ( strpos( $type, 'hp_' ) !== 0 || ! hp\get_array_value( $args, 'public', true ) ) {
  383. return $args;
  384. }
  385. // Get permalinks.
  386. $permalinks = (array) get_option( 'hp_permalinks', [] );
  387. if ( ! $permalinks ) {
  388. return $args;
  389. }
  390. // Set rewrite slug.
  391. $slug = hp\get_array_value( $permalinks, hp\unprefix( $type . '_slug' ) );
  392. if ( $slug ) {
  393. $args['rewrite']['slug'] = $slug;
  394. }
  395. return $args;
  396. }
  397. /**
  398. * Flushes rewrite rules.
  399. */
  400. public function flush_rewrite_rules() {
  401. delete_option( 'rewrite_rules' );
  402. }
  403. /**
  404. * Sets page title.
  405. *
  406. * @param array $parts Title parts.
  407. * @return array
  408. */
  409. public function set_page_title( $parts ) {
  410. // Get the current route.
  411. $route = $this->get_current_route();
  412. if ( $route && isset( $route['title'] ) ) {
  413. // Remove query title.
  414. if ( count( $parts ) > 1 ) {
  415. array_shift( $parts );
  416. }
  417. // Add route title.
  418. array_unshift( $parts, $route['title'] );
  419. }
  420. return $parts;
  421. }
  422. /**
  423. * Disables page title.
  424. *
  425. * @param string $title Page title.
  426. * @return string
  427. */
  428. public function disable_page_title( $title ) {
  429. if ( hp\get_array_value( hivepress()->router->get_current_route(), 'title' ) ) {
  430. return false;
  431. }
  432. return $title;
  433. }
  434. /**
  435. * Sets page context.
  436. *
  437. * @param array $args Template arguments.
  438. * @return array
  439. */
  440. public function set_page_context( $args ) {
  441. $context = [];
  442. // Get title.
  443. $context['page_title'] = hp\get_array_value( $this->get_current_route(), 'title' );
  444. // @todo Remove theme-specific condition once fixed.
  445. if ( is_tax() && ( ! function_exists( 'hivetheme' ) || ! is_tax( 'hp_listing_category' ) ) ) {
  446. $term = get_queried_object();
  447. // Set title.
  448. $context['page_title'] = $term->name;
  449. // Set description.
  450. if ( $term->description ) {
  451. $context['page_description'] = apply_filters( 'the_content', $term->description );
  452. }
  453. }
  454. return hp\merge_arrays(
  455. $args,
  456. [
  457. 'context' => $context,
  458. ]
  459. );
  460. }
  461. /**
  462. * Sets page template.
  463. *
  464. * @param array $template Template filepath.
  465. * @return string
  466. */
  467. public function set_page_template( $template ) {
  468. global $wp_query;
  469. // Get the current route.
  470. $route = $this->get_current_route();
  471. if ( $route ) {
  472. // Set query variables.
  473. if ( isset( $route['path'] ) ) {
  474. $wp_query->is_home = false;
  475. $wp_query->is_404 = false;
  476. }
  477. // Get menu redirect.
  478. $menu_redirect = home_url();
  479. foreach ( hivepress()->get_classes( 'menus' ) as $menu_class ) {
  480. if ( $menu_class::get_meta( 'chained' ) ) {
  481. // Create menu.
  482. $menu = hp\create_class_instance( $menu_class );
  483. if ( in_array( $route['name'], array_column( $menu->get_items(), 'route' ), true ) ) {
  484. // Get menu items.
  485. $menu_items = $menu->get_items();
  486. $menu_item_names = array_keys( $menu_items );
  487. foreach ( $menu_items as $menu_item_name => $menu_item ) {
  488. if ( isset( $menu_item['route'] ) ) {
  489. // Get redirect URL.
  490. if ( $menu_item['route'] === $route['name'] ) {
  491. $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 ) );
  492. if ( $next_menu_item ) {
  493. $menu_redirect = $next_menu_item['url'];
  494. }
  495. break;
  496. }
  497. // Get menu route.
  498. $menu_route = $this->get_route( $menu_item['route'] );
  499. if ( $menu_route && isset( $menu_route['redirect'] ) ) {
  500. foreach ( $this->get_redirect_callbacks( $menu_route['redirect'] ) as $menu_route_redirect ) {
  501. if ( ! in_array( call_user_func( $menu_route_redirect ), [ null, true ], true ) ) {
  502. wp_safe_redirect( $menu_item['url'] );
  503. exit;
  504. }
  505. }
  506. }
  507. }
  508. }
  509. break;
  510. }
  511. }
  512. }
  513. // Set title.
  514. $title = hp\get_array_value( $route, 'title' );
  515. if ( is_callable( $title ) ) {
  516. $this->route['title'] = call_user_func( $title );
  517. }
  518. // Redirect page.
  519. if ( isset( $route['redirect'] ) ) {
  520. foreach ( $this->get_redirect_callbacks( $route['redirect'] ) as $route_redirect ) {
  521. $redirect = call_user_func( $route_redirect );
  522. if ( $redirect ) {
  523. if ( is_bool( $redirect ) ) {
  524. $redirect = $menu_redirect;
  525. }
  526. wp_safe_redirect( $redirect );
  527. exit;
  528. }
  529. }
  530. }
  531. // Render page.
  532. if ( isset( $route['action'] ) ) {
  533. echo call_user_func( $route['action'] );
  534. exit;
  535. }
  536. }
  537. return $template;
  538. }
  539. /**
  540. * Disables page redirect.
  541. *
  542. * @param string $url Redirect URL.
  543. * @return string
  544. */
  545. public function disable_page_redirect( $url ) {
  546. foreach ( hivepress()->get_config( 'post_types' ) as $type => $args ) {
  547. if ( ! hp\get_array_value( $args, 'redirect_canonical', true ) && is_singular( hp\prefix( $type ) ) ) {
  548. $url = false;
  549. break;
  550. }
  551. }
  552. return $url;
  553. }
  554. }