<?php /* +--------------------------------------------------------------------+ | CiviCRM version 5 | +--------------------------------------------------------------------+ | Copyright CiviCRM LLC (c) 2004-2019 | +--------------------------------------------------------------------+ | This file is a part of CiviCRM. | | | | CiviCRM is free software; you can copy, modify, and distribute it | | under the terms of the GNU Affero General Public License | | Version 3, 19 November 2007 and the CiviCRM Licensing Exception. | | | | CiviCRM is distributed in the hope that it will be useful, but | | WITHOUT ANY WARRANTY; without even the implied warranty of | | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. | | See the GNU Affero General Public License for more details. | | | | You should have received a copy of the GNU Affero General Public | | License and the CiviCRM Licensing Exception along | | with this program; if not, contact CiviCRM LLC | | at info[AT]civicrm[DOT]org. If you have questions about the | | GNU Affero General Public License or the licensing of CiviCRM, | | see the CiviCRM license FAQ at http://civicrm.org/licensing | +--------------------------------------------------------------------+ */ /** * * @package CRM * @copyright CiviCRM LLC (c) 2004-2019 * */ // This file must not accessed directly if ( ! defined( 'ABSPATH' ) ) exit; /** * Define CiviCRM_For_WordPress_Basepage Class. * * @since 4.6 */ class CiviCRM_For_WordPress_Basepage { /** * Plugin object reference. * * @since 4.6 * @access public * @var object $civi The plugin object reference. */ public $civi; /** * Instance constructor. * * @since 4.6 */ function __construct() { // Store reference to CiviCRM plugin object $this->civi = civi_wp(); // Always listen for activation action add_action( 'civicrm_activation', array( $this, 'activate' ) ); // Always listen for deactivation action add_action( 'civicrm_deactivation', array( $this, 'deactivate' ) ); // Always check if the basepage needs to be created add_action( 'civicrm_instance_loaded', array( $this, 'maybe_create_basepage' ) ); } /** * Register hooks to handle CiviCRM in a WordPress wpBasePage context. * * @since 4.6 */ public function register_hooks() { // Kick out if not CiviCRM if (!$this->civi->initialize()) { return; } // In WP 4.6.0+, tell it URL params are part of canonical URL add_filter( 'get_canonical_url', array( $this, 'basepage_canonical_url' ), 999 ); // Yoast SEO has separate way of establishing canonical URL add_filter( 'wpseo_canonical', array( $this, 'basepage_canonical_url' ), 999 ); // And also for All in One SEO to handle canonical URL add_filter( 'aioseop_canonical_url', array( $this, 'basepage_canonical_url' ), 999 ); // Regardless of URL, load page template add_filter( 'template_include', array( $this, 'basepage_template' ), 999 ); // Check permission $argdata = $this->civi->get_request_args(); if ( ! $this->civi->users->check_permission( $argdata['args'] ) ) { add_filter( 'the_content', array( $this->civi->users, 'get_permission_denied' ) ); return; } // Cache CiviCRM base page markup add_action( 'wp', array( $this, 'basepage_handler' ), 10, 1 ); } /** * Trigger the process whereby the WordPress basepage is created. * * Sets a one-time-only option to flag that we need to create a basepage - * it will not update the option once it has been set to another value nor * create a new option with the same name. * * As a result of doing this, we know that a basepage needs to be created, but * the moment to do so is once CiviCRM has been successfully installed. * * @see do_basepage_creation() * * @since 5.6 */ public function activate() { // Save option add_option( 'civicrm_activation_create_basepage', 'true' ); } /** * Plugin deactivation. * * @since 5.6 */ public function deactivate() { // Delete option delete_option( 'civicrm_activation_create_basepage' ); } /** * Register the hook to create the WordPress basepage, if necessary. * * Changes the one-time-only option so that the basepage can only be created * once. Thereafter, we're on our own until there's a 'delete_post' callback * to prevent the basepage from being deleted. * * @since 5.6 */ public function maybe_create_basepage() { // Bail if CiviCRM not installed if ( ! CIVICRM_INSTALLED ) { return; } // Bail if not installing if ( get_option( 'civicrm_activation_create_basepage' ) !== 'true' ) { return; } // Bail if not WordPress admin if ( ! is_admin() ) { return; } // Create basepage add_action( 'wp_loaded', array( $this, 'create_wp_basepage' ) ); // Change option so the callback above never runs again update_option( 'civicrm_activation_create_basepage', 'done' ); } /** * Create WordPress basepage and save setting. * * @since 4.6 * @since 5.6 Relocated from CiviCRM_For_WordPress to here. */ public function create_wp_basepage() { if (!$this->civi->initialize()) { return; } $config = CRM_Core_Config::singleton(); // Bail if we already have a basepage setting if ( !empty( $config->wpBasePage ) ) { return; } /** * Filter the default page slug. * * @since 4.6 * * @param str The default basepage slug. * @return str The modified basepage slug. */ $slug = apply_filters( 'civicrm_basepage_slug', 'civicrm' ); // Get existing page with that slug $page = get_page_by_path( $slug ); // Does it exist? if ( $page ) { // We already have a basepage $result = $page->ID; } else { // Create the basepage $result = $this->create_basepage( $slug ); } // Were we successful? if ( $result !== 0 AND !is_wp_error($result) ) { // Get the post object $post = get_post( $result ); $params = array( 'version' => 3, 'wpBasePage' => $post->post_name, ); // Save the setting civicrm_api3('setting', 'create', $params); } } /** * Create a WordPress page to act as the CiviCRM base page. * * @since 4.6 * @since 5.6 Relocated from CiviCRM_For_WordPress to here. * * @param string $slug The unique slug for the page - same as wpBasePage setting. * @return int|WP_Error The page ID on success. The value 0 or WP_Error on failure. */ private function create_basepage( $slug ) { // If multisite, switch to main site if ( is_multisite() && !is_main_site() ) { // Store this site $original_site = get_current_blog_id(); // Switch global $current_site; switch_to_blog( $current_site->blog_id ); } // Define basepage $page = array( 'post_status' => 'publish', 'post_type' => 'page', 'post_parent' => 0, 'comment_status' => 'closed', 'ping_status' => 'closed', 'to_ping' => '', // Quick fix for Windows 'pinged' => '', // Quick fix for Windows 'post_content_filtered' => '', // Quick fix for Windows 'post_excerpt' => '', // Quick fix for Windows 'menu_order' => 0, 'post_name' => $slug, ); /** * Filter the default page title. * * @since 4.6 * * @param str The default base page title. * @return str The modified base page title. */ $page['post_title'] = apply_filters( 'civicrm_basepage_title', __( 'CiviCRM', 'civicrm' ) ); // Default content $content = __( 'Do not delete this page. Page content is generated by CiviCRM.', 'civicrm' ); /** * Filter the default page content. * * @since 4.6 * * @param str $content The default base page content. * @return str $content The modified base page content. */ $page['post_content'] = apply_filters( 'civicrm_basepage_content', $content ); // Insert the post into the database $page_id = wp_insert_post( $page ); // Switch back if we've switched if ( isset( $original_site ) ) { restore_current_blog(); } return $page_id; } /** * Build CiviCRM base page content. * * Callback method for 'wp' hook, always called from WP front-end. * * @since 4.6 * * @param object $wp The WP object, present but not used. */ public function basepage_handler( $wp ) { /* * At this point, all conditional tags are available * @see http://codex.wordpress.org/Conditional_Tags */ // Bail if this is a 404 if ( is_404() ) return; // Kick out if not CiviCRM if (!$this->civi->initialize()) { return ''; } // Add core resources for front end add_action( 'wp', array( $this->civi, 'front_end_page_load' ), 100 ); // CMW: why do we need this? Nothing that follows uses it... require_once ABSPATH . WPINC . '/pluggable.php'; /* * Let's do the_loop. * This has the effect of bypassing the logic in * https://github.com/civicrm/civicrm-wordpress/pull/36 */ if ( have_posts() ) { while ( have_posts() ) : the_post(); global $post; ob_start(); // Start buffering $this->civi->invoke(); // Now, instead of echoing, base page output ends up in buffer $this->basepage_markup = ob_get_clean(); // Save the output and flush the buffer /* * The following logic is in response to some of the complexities of how * titles are handled in WordPress, particularly when there are SEO * plugins present that modify the title for Open Graph purposes. There * have also been issues with the default WordPress themes, which modify * the title using the 'wp_title' filter. * * First, we try and set the title of the page object, which will work * if the loop is not run subsequently and if there are no additional * filters on the title. * * Second, we store the CiviCRM title so that we can construct the base * page title if other plugins modify it. */ // Override post title global $civicrm_wp_title; $post->post_title = $civicrm_wp_title; // Because the above seems unreliable, store title for later use $this->basepage_title = $civicrm_wp_title; // Disallow commenting $post->comment_status = 'closed'; endwhile; } // Reset loop rewind_posts(); // Override page title with high priority add_filter( 'wp_title', array( $this, 'wp_page_title' ), 100, 3 ); // Add compatibility with WordPress SEO plugin's Open Graph title add_filter( 'wpseo_opengraph_title', array( $this, 'wpseo_page_title' ), 100, 1 ); // Include this content when base page is rendered add_filter( 'the_content', array( $this, 'basepage_render' ) ); // Hide the edit link add_action( 'edit_post_link', array( $this->civi, 'clear_edit_post_link' ) ); // Tweak admin bar add_action( 'wp_before_admin_bar_render', array( $this->civi, 'clear_edit_post_menu_item' ) ); // Add body classes for easier styling add_filter( 'body_class', array( $this, 'add_body_classes' ) ); // Flag that we have parsed the base page $this->basepage_parsed = TRUE; /** * Broadcast that the base page is parsed. * * @since 4.4 */ do_action( 'civicrm_basepage_parsed' ); } /** * Get CiviCRM basepage title for <title> element. * * Callback method for 'wp_title' hook, called at the end of function wp_title. * * @since 4.6 * * @param string $title Title that might have already been set. * @param string $separator Separator determined in theme (but defaults to WordPress default). * @param string $separator_location Whether the separator should be left or right. */ public function wp_page_title( $title, $separator = '»', $separator_location = '' ) { // If feed, return just the title if ( is_feed() ) return $this->basepage_title; // Set default separator location, if it isn't defined if ( '' === trim( $separator_location ) ) { $separator_location = ( is_rtl() ) ? 'left' : 'right'; } // If we have WP SEO present, use its separator if ( class_exists( 'WPSEO_Options' ) ) { $separator_code = WPSEO_Options::get_default( 'wpseo_titles', 'separator' ); $separator_array = WPSEO_Option_Titles::get_instance()->get_separator_options(); if ( array_key_exists( $separator_code, $separator_array ) ) { $separator = $separator_array[$separator_code]; } } // Construct title depending on separator location if ( $separator_location == 'right' ) { $title = $this->basepage_title . " $separator " . get_bloginfo( 'name', 'display' ); } else { $title = get_bloginfo( 'name', 'display' ) . " $separator " . $this->basepage_title; } // Return modified title return $title; } /** * Get CiviCRM base page title for Open Graph elements. * * Callback method for 'wpseo_opengraph_title' hook, to provide compatibility * with the WordPress SEO plugin. * * @since 4.6.4 * * @param string $post_title The title of the WordPress page or post. * @return string $basepage_title The title of the CiviCRM entity. */ public function wpseo_page_title( $post_title ) { // Hand back our base page title return $this->basepage_title; } /** * Get CiviCRM base page content. * * Callback method for 'the_content' hook, always called from WP front-end. * * @since 4.6 * * @return str $basepage_markup The base page markup. */ public function basepage_render() { // Hand back our base page markup return $this->basepage_markup; } /** * Provide the canonical URL for a page accessed through a basepage. * * WordPress will default to saying the canonical URL is the URL of the base * page itself, but we need to indicate that in this case, the whole thing * matters. * * Note: this function is used for three different but similar hooks: * - `get_canonical_url` (WP 4.6.0+) * - `aioseop_canonical_url` (All in One SEO) * - `wpseo_canonical` (Yoast WordPress SEO) * * The native WordPress one passes the page object, while the other two do * not. We don't actually need the page object, so the argument is omitted * here. * * @since 4.6 * * @param string $canonical The canonical URL. * @return string The complete URL to the page as it should be accessed. */ public function basepage_canonical_url( $canonical ) { // Access Civi config object $config = CRM_Core_Config::singleton(); // Retain old logic when not using clean URLs if (!$config->cleanURL) { /* * It would be better to specify which params are okay to accept as the * canonical URLs, but this will work for the time being. */ if ( empty( $_GET['page'] ) || empty( $_GET['q'] ) || 'CiviCRM' !== $_GET['page'] ) { return $canonical; } $path = $_GET['q']; unset( $_GET['q'] ); unset( $_GET['page'] ); $query = http_build_query( $_GET ); } else { $argdata = $this->civi->get_request_args(); $path = $argdata['argString']; $query = http_build_query( $_GET ); } // We should, however, build the URL the way that CiviCRM expects it to be // (rather than through some other funny base page). return CRM_Utils_System::url( $path, $query ); } /** * Get CiviCRM base page template. * * Callback method for 'template_include' hook, always called from WP front-end. * * @since 4.6 * * @param string $template The path to the existing template. * @return string $template The modified path to the desired template. */ public function basepage_template( $template ) { // Get template filename $template_name = basename( $template ); // Use the provided page template, but allow overrides. $page_template = locate_template( array( /** * Allow base page template to be overridden. * * In most cases, the logic will not progress beyond here. Shortcodes in * posts and pages will have a template set, so we leave them alone unless * specifically overridden by the filter. * * @since 4.6 * * @param string $template_name The provided template name. * @return string The overridden template name. */ apply_filters( 'civicrm_basepage_template', $template_name ) ) ); // If not homepage and template is found if ( '' != $page_template && !is_front_page() ) { return $page_template; } // Find homepage the template $home_template = locate_template( array( /** * Override the template, but allow plugins to amend. * * This filter handles the scenario where no basepage has been set, in * which case CiviCRM will try to load its content in the site's homepage. * Many themes, however, do not have a call to "the_content()" on the * homepage - it is often used as a gateway page to display widgets, * archives and so forth. * * Be aware that if the homepage is set to show latest posts, then this * template override will not have the desired effect. A basepage *must* * be set if this is the case. * * @since 4.6 * * @param string The template name (set to the default page template). * @return string The overridden template name. */ apply_filters( 'civicrm_basepage_home_template', 'page.php' ) ) ); // Use it if found if ( '' != $home_template ) { return $home_template; } // Fall back to provided template return $template; } /** * Add classes to body element when on basepage. * * This allows selectors to be written for particular CiviCRM "pages" despite * them all being rendered on the one WordPress basepage. * * @since 4.7.18 * * @param array $classes The existing body classes. * @return array $classes The modified body classes. */ public function add_body_classes( $classes ) { $args = $this->civi->get_request_args(); // Bail if we don't have any if ( is_null( $args['argString'] ) ) { return $classes; } // Check for top level - it can be assumed this always 'civicrm' if ( isset( $args['args'][0] ) AND ! empty( $args['args'][0] ) ) { $classes[] = $args['args'][0]; } // Check for second level - the component if ( isset( $args['args'][1] ) AND ! empty( $args['args'][1] ) ) { $classes[] = $args['args'][0] . '-' . $args['args'][1]; } // Check for third level - the component's configuration if ( isset( $args['args'][2] ) AND ! empty( $args['args'][2] ) ) { $classes[] = $args['args'][0] . '-' . $args['args'][1] . '-' . $args['args'][2]; } // Check for fourth level - because well, why not? if ( isset( $args['args'][3] ) AND ! empty( $args['args'][3] ) ) { $classes[] = $args['args'][0] . '-' . $args['args'][1] . '-' . $args['args'][2] . '-' . $args['args'][3]; } return $classes; } } // Class CiviCRM_For_WordPress_Basepage ends