endpoints = array(); add_action( 'wp', array( $this, 'disable_endpoint_cache' ) ); add_filter( 'pre_handle_404', array( $this, 'block_404_on_endpoints' ) ); add_action( 'init', array( $this, 'setup_rewrite_rules' ) ); add_filter( 'query_vars', array( $this, 'add_query_vars' ) ); add_action( 'template_redirect', array( $this, 'maybe_redirect' ) ); add_filter( 'template_include', array( $this, 'template_loader' ), 12 ); add_filter( 'home_template_hierarchy', array( $this, 'home_template_hierarchy' ) ); add_filter( 'the_content', array( $this, 'get_content' ) ); add_filter( 'body_class', array( $this, 'add_body_classes' ) ); add_filter( 'comments_open', array( $this, 'maybe_disable_comments' ) ); add_filter( 'comments_template', array( $this, 'maybe_remove_comments_template' ) ); add_filter( 'nav_menu_meta_box_object', array( $this, 'add_endpoints_menu_meta_box' ) ); add_filter( 'customize_nav_menu_available_item_types', array( $this, 'add_endpoints_menu_meta_box_to_customizer' ) ); add_filter( 'customize_nav_menu_available_items', array( $this, 'add_endpoints_menu_meta_box_items_to_customizer' ), 10, 4 ); /* Avoid Polylang rewriting the rewrite rules. */ add_filter( 'pll_modify_rewrite_rule', array( $this, 'prevent_polylang_rewrite_modification' ), 10, 2 ); } /** * Register an endpoint. * * @since 1.5.0 * * @param Charitable_Endpoint $endpoint The endpoint object. * @return boolean True if the endpoint was registered. False if it was already registered. */ public function register( Charitable_Endpoint $endpoint ) { $endpoint_id = $endpoint->get_endpoint_id(); if ( $this->endpoint_exists( $endpoint_id ) ) { charitable_get_deprecated()->doing_it_wrong( __METHOD__, /* translators: %s: endpoint id */ sprintf( __( 'Endpoint %s has already been registered.', 'charitable' ), $endpoint_id ), '1.5.0' ); return false; } $this->endpoints[ $endpoint_id ] = $endpoint; /* Record the endpoint by priority. */ if ( ! array_key_exists( $endpoint::PRIORITY, $this->endpoints_prioritized ) ) { $this->endpoints_prioritized[ $endpoint::PRIORITY ] = array(); } $this->endpoints_prioritized[ $endpoint::PRIORITY ][] = $endpoint_id; return true; } /** * Get the permalink/URL of a particular endpoint. * * @since 1.5.0 * * @param string $endpoint The endpoint id. * @param array $args Optional array of arguments. * @return string|false */ public function get_page_url( $endpoint, $args = array() ) { $endpoint = $this->sanitize_endpoint( $endpoint ); $default = ''; if ( $this->endpoint_exists( $endpoint ) ) { $default = $this->endpoints[ $endpoint ]->get_page_url( $args ); } /** * Filter the URL of a particular endpoint. * * The hook takes the format of charitable_permalink_{endpoint}_page. For example, * for the campaign_donation endpoint, the hook is: * * charitable_permalink_campaign_donation_page * * @since 1.0.0 * * @param string $default The endpoint's URL. * @param array $args Mixed set of arguments. */ return apply_filters( 'charitable_permalink_' . $endpoint . '_page', $default, $args ); } /** * Checks if we're currently viewing a particular endpoint/page. * * @since 1.5.0 * * @param string $endpoint The endpoint id. * @param array $args Optional array of arguments. * @return boolean */ public function is_page( $endpoint, $args = array() ) { $endpoint = $this->sanitize_endpoint( $endpoint ); $default = ''; if ( $this->endpoint_exists( $endpoint ) ) { $default = $this->endpoints[ $endpoint ]->is_page( $args ); } /** * Return whether we are currently viewing a particular endpoint. * * The hook takes the format of charitable_is_page_{endpoint}_page. For example, * for the campaign_donation endpoint, the hook is: * * charitable_is_page_campaign_donation_page * * @since 1.0.0 * * @param boolean $default Whether we are currently on the endpoint. * @param array $args Mixed set of arguments. */ return apply_filters( 'charitable_is_page_' . $endpoint . '_page', $default, $args ); } /** * Set up the template for an endpoint. * * @since 1.5.0 * * @param string $endpoint The endpoint id. * @param string $default_template The default template to be used if the endpoint doesn't return its own. * @return string $template */ public function get_endpoint_template( $endpoint, $default_template ) { $endpoint = $this->sanitize_endpoint( $endpoint ); if ( ! $this->endpoint_exists( $endpoint ) ) { charitable_get_deprecated()->doing_it_wrong( __METHOD__, sprintf( /* translators: %s: endpoint id */ __( 'Endpoint %s has not been registered.', 'charitable' ), $endpoint ), '1.5.0' ); return $default_template; } return $this->endpoints[ $endpoint ]->get_template( $default_template ); } /** * Disable page cache on non-cacheable endpoints using the DONOTCACHEPAGE constant. * * @since 1.6.14 * * @return void */ public function disable_endpoint_cache() { if ( defined( 'DONOTCACHEPAGE' ) ) { return; } $endpoint_id = $this->get_current_endpoint(); if ( ! $endpoint_id ) { return; } if ( $this->get_endpoint( $endpoint_id )->is_cacheable() ) { return; } // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound -- DONOTCACHEPAGE is a third-party constant used by caching plugins (WP Super Cache, etc.) for compatibility. define( 'DONOTCACHEPAGE', true ); /** * Fire action to note that the current page should not be cached. * * @since 1.6.14 */ do_action( 'charitable_do_not_cache' ); } /** * If we're viewing a Charitable endpoint, we're not on a 404, even * though WordPress interprets some pages (like the Forgot Password) * as a 404 in certain cases. * * @since 1.6.41 * * @param boolean $not_a_404 Whether to preempt WordPress and instruct * it that this is not a 404 request. * @return boolean */ public function block_404_on_endpoints( $not_a_404 ) { if ( false !== $this->get_current_endpoint() ) { $not_a_404 = true; } else { /* Some endpoints will return false at this point since * it's so early, so we unset the class property to make * sure they are tested again later on. */ unset( $this->current_endpoint ); } return $not_a_404; } /** * Prevent Polylang from changing some rewrite rules. * * @since 1.6.21 * * @param boolean $modify Whether to modify or not the rule, defaults to true. * @param array $rule Original rewrite rule. * @return boolean */ public function prevent_polylang_rewrite_modification( $modify, $rule ) { /** * Filter the list of endpoint URLs that Polylang won't touch. * * @since 1.6.21 * * @param array $protected_rules The protected rules. */ $protected_rules = apply_filters( 'charitable_polylang_protected_rewrite_rules', array( 'charitable-listener(/(.*))?/?$', ) ); return $modify && ! in_array( key( $rule ), $protected_rules ); } /** * Set up the rewrite rules for the site. * * @since 1.5.0 * * @return void */ public function setup_rewrite_rules() { foreach ( $this->endpoints as $endpoint ) { $endpoint->setup_rewrite_rules(); } /* Set up any common rewrite tags */ add_rewrite_tag( '%donation_id%', '([0-9]+)' ); } /** * Add custom query vars. * * @since 1.5.0 * * @param string[] $vars The query vars. * @return string[] */ public function add_query_vars( $vars ) { foreach ( $this->endpoints as $endpoint ) { $vars = $endpoint->add_query_vars( $vars ); } return array_merge( $vars, array( 'donation_id', 'cancel' ) ); } /** * Check the current endpoint to see if we should redirect the user to a different page. * * @since 1.6.26 * * @return void */ public function maybe_redirect() { $current_endpoint = $this->get_current_endpoint(); if ( ! $current_endpoint ) { return; } $url = $this->endpoints[ $current_endpoint ]->get_redirect(); if ( ! $url ) { return; } wp_safe_redirect( $url ); exit; } /** * Load templates for our endpoints. * * @since 1.5.0 * * @param string $template The default template. * @return string */ public function template_loader( $template ) { $current_endpoint = $this->get_current_endpoint(); if ( ! $current_endpoint ) { return $template; } $endpoint = $this->endpoints[ $current_endpoint ]; if ( ! $endpoint->uses_custom_template() ) { return $template; } $endpoint->setup_template(); $template_options = $endpoint->get_template( $template ); if ( $template_options === $template ) { return $template_options; } /** * Filter the list of template options for the given endpoint. * * @since 1.0.0 * * @param array|string $template_options */ $template_options = apply_filters( 'charitable_' . $current_endpoint . '_page_template', $template_options ); return charitable_get_template_path( $template_options, $template ); } /** * When using a block theme, dynamically generated pages like the donation receipt * page are interpreted as using the home template. The following workaround inserts * page.php as the primary template for these page. * * @param mixed $templates The list of templates. * * @since 1.6.55 * * @return mixed */ public function home_template_hierarchy( $templates ) { $current_endpoint = $this->get_current_endpoint(); if ( false !== $current_endpoint ) { $templates = $this->endpoints[ $current_endpoint ]->get_template( $templates ); } return $templates; } /** * Get the content to display for the endpoint we're viewing. * * @since 1.5.0 * * @param string $content The default content. * @param false|string $endpoint Fetch the content for a specific endpoint. * @return string */ public function get_content( $content, $endpoint = false ) { if ( ! $endpoint ) { $endpoint = $this->get_current_endpoint(); } if ( ! $endpoint ) { return $content; } return $this->endpoints[ $endpoint ]->get_content( $content ); } /** * Add any custom body classes defined for the endpoint we're viewing. * * @since 1.5.0 * * @param string[] $classes The list of body classes. * @return string[] */ public function add_body_classes( $classes ) { $endpoint = $this->get_current_endpoint(); if ( ! $endpoint ) { return $classes; } $classes[] = $this->endpoints[ $endpoint ]->get_body_class(); return $classes; } /** * If we're on an endpoint where comments should be disabled, do so. * * @since 1.6.36 * * @param boolean $open Whether comments are open. * @return boolean */ public function maybe_disable_comments( $open ) { if ( ! $open ) { return $open; } $endpoint = $this->get_current_endpoint(); if ( ! $endpoint ) { return $open; } return ! $this->endpoints[ $endpoint ]->comments_disabled(); } /** * If we are on an endpoint where comments are disabled, return an * empty string for the template, so WordPress will not display * anything. * * @since 1.6.36 * * @param string $template The path to the theme template file. * @return string */ public function maybe_remove_comments_template( $template ) { $endpoint = $this->get_current_endpoint(); if ( $endpoint && $this->endpoints[ $endpoint ]->comments_disabled() ) { $template = charitable_get_template_path( 'comments/disabled-comments.php' ); } return $template; } /** * Add a "Charitable" menus meta box. * * @since 1.6.29 * * @param object $object The meta box object. * @return object */ public function add_endpoints_menu_meta_box( $object ) { // phpcs:ignore add_meta_box( 'add-charitable-endpoints', __( 'Charitable', 'charitable' ), [ $this, 'endpoints_menu_meta_box' ], 'nav-menus', 'side', 'low' ); return $object; } /** * The content of the endpoints menu meta box. * * @since 1.6.29 * * @global int|string $nav_menu_selected_id (id, name or slug) of the currently-selected menu. * @return void */ public function endpoints_menu_meta_box() { global $nav_menu_selected_id; $walker = new Charitable_Walker_Nav_Menu_Checklist(); $current_tab = 'all'; $endpoints = $this->get_endpoints_for_nav_menu(); $removed_args = array( 'action', 'customlink-tab', 'edit-menu-item', 'menu-item', 'page-tab', '_wpnonce' ); ?>