Newer
Older
<?php
/*
+--------------------------------------------------------------------+
| This work is published under the GNU AGPLv3 license with some |
| permitted exceptions and without any warranty. For full license |
| and copyright information, see https://civicrm.org/licensing |
+--------------------------------------------------------------------+
// This file must not accessed directly.
if (!defined('ABSPATH')) {
exit;
}
* Define CiviCRM_For_WordPress_Basepage Class.
*
* @since 4.6
* Plugin object reference.
* @since 4.6
* @access public
// Always listen for activation action.
add_action('civicrm_activation', [$this, 'activate']);
// Always listen for deactivation action.
add_action('civicrm_deactivation', [$this, 'deactivate']);
// Always check if the basepage needs to be created.
add_action('civicrm_instance_loaded', [$this, 'maybe_create_basepage']);
* Register hooks to handle CiviCRM in a WordPress wpBasePage context.
// Cache CiviCRM base page markup.
add_action('wp', [$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
// 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) {
// Bail if not installing.
if (get_option('civicrm_activation_create_basepage') !== 'true') {
// Bail if not WordPress admin.
if (!is_admin()) {
// Create basepage.
add_action('wp_loaded', [$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);
// Create the basepage.
$result = $this->create_basepage($slug);
// Get the post object.
$post = get_post($result);
'version' => 3,
'wpBasePage' => $post->post_name,
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.
*/
// If multisite, switch to main site.
if (is_multisite() && !is_main_site()) {
'post_status' => 'publish',
'post_type' => 'page',
'post_parent' => 0,
'comment_status' => 'closed',
'ping_status' => 'closed',
// Quick fix for Windows.
'to_ping' => '',
// Quick fix for Windows.
'pinged' => '',
// Quick fix for Windows.
'post_content_filtered' => '',
// Quick fix for Windows.
'post_excerpt' => '',
* 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)) {
return $page_id;
}
/**
* Build CiviCRM base page content.
*
* Callback method for 'wp' hook, always called from WordPress front-end.
* @param object $wp The WordPress object, present but not used.
* At this point, all conditional tags are available.
* @see https://codex.wordpress.org/Conditional_Tags
// Bail if this is a 404.
if (is_404()) {
return;
}
// Bail if this is a Favicon request.
if (is_favicon()) {
return;
}
// Do not proceed without the presence of the CiviCRM query var.
$civicrm_in_wordpress = $this->civi->civicrm_in_wordpress();
if (!$civicrm_in_wordpress) {
return;
}
// Get the current Base Page and set a "found" flag.
$basepage = $this->basepage_get();
$basepage_found = FALSE;
// Get the Shortcode Mode setting.
$shortcode_mode = $this->civi->admin->get_shortcode_mode();
* This has the effect of bypassing the logic in:
* @see https://github.com/civicrm/civicrm-wordpress/pull/36
if (have_posts()) {
while (have_posts()) {
the_post();
/**
* Allow "Base Page mode" to be forced.
*
* Return TRUE to force CiviCRM to render a Post/Page as if on the Base Page.
* @param bool By default "Base Page mode" should not be triggered.
* @param WP_Post $post The current WordPress Post object.
* @return bool Whether or not to force "Base Page mode" - FALSE by default.
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
$basepage_mode = (bool) apply_filters('civicrm_force_basepage_mode', FALSE, $post);
// Skip when this is not the Base Page or when "Base Page mode" is not forced or not in "legacy mode".
if ($basepage->ID === $post->ID || $basepage_mode || $shortcode_mode === 'legacy') {
// Set context.
$this->civi->civicrm_context_set('basepage');
// Start buffering.
ob_start();
// Now, instead of echoing, base page output ends up in buffer.
$this->civi->invoke();
// Save the output and flush the buffer.
$this->basepage_markup = ob_get_clean();
/*
* 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';
// Put CiviCRM into "Base Page mode".
$basepage_found = TRUE;
}
// Bail if the Base Page has not been processed.
if (!$basepage_found) {
return;
}
// Hide the edit link.
add_action('edit_post_link', [$this, 'clear_edit_post_link']);
// Tweak admin bar.
add_action('wp_before_admin_bar_render', [$this, 'clear_edit_post_menu_item']);
// Add body classes for easier styling.
add_filter('body_class', [$this, 'add_body_classes']);
// In WordPress 4.6.0+, tell it URL params are part of canonical URL.
add_filter('get_canonical_url', [$this, 'basepage_canonical_url'], 999);
// Yoast SEO has separate way of establishing canonical URL.
add_filter('wpseo_canonical', [$this, 'basepage_canonical_url'], 999);
// And also for All in One SEO to handle canonical URL.
add_filter('aioseop_canonical_url', [$this, 'basepage_canonical_url'], 999);
// Override page title with high priority.
add_filter('wp_title', [$this, 'wp_page_title'], 100, 3);
add_filter('document_title_parts', [$this, 'wp_page_title_parts'], 100, 1);
// Add compatibility with Yoast SEO plugin's Open Graph title.
add_filter('wpseo_opengraph_title', [$this, 'wpseo_page_title'], 100, 1);
// Don't let the Yoast SEO plugin parse the basepage title.
if (class_exists('WPSEO_Frontend')) {
remove_filter('pre_get_document_title', [$frontend, 'title'], 15);
// Regardless of URL, load page template.
add_filter('template_include', [$this, 'basepage_template'], 999);
// Check permission.
$argdata = $this->civi->get_request_args();
if (!$this->civi->users->check_permission($argdata['args'])) {
// Do not show content.
add_filter('the_content', [$this->civi->users, 'get_permission_denied']);
}
else {
// Add core resources for front end.
add_action('wp', [$this->civi, 'front_end_page_load'], 100);
// Include this content when Base Page is rendered.
add_filter('the_content', [$this, 'basepage_render']);
}
/**
* Broadcast that the base page is parsed.
*
* @since 4.4
*/
* Get CiviCRM basepage title for <title> element.
*
* Callback method for 'wp_title' hook, called at the end of function wp_title.
* @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)) {
// 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;
/**
* Get CiviCRM basepage title for <title> element.
*
* Callback method for 'document_title_parts' hook. This filter was introduced
* in WordPress 3.8 but it depends on whether the theme has implemented that
* method for generating the title or not.
*
* @since 5.14
*
* @param array $parts The existing title parts.
* @return array $parts The modified title parts.
*/
// Override with CiviCRM's title.
if (isset($parts['title'])) {
$parts['title'] = $this->basepage_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.
* Callback method for 'the_content' hook, always called from WordPress
* front-end.
* @since 4.6
*
* @return str $basepage_markup The base page 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:
* - `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.
*
* @param string $canonical The canonical URL.
* @return string The complete URL to the page as it should be accessed.
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['civiwp'])
|| empty($_GET['q'])
|| 'CiviCRM' !== $_GET['civiwp']) {
return $canonical;
}
$path = $_GET['q'];
unset($_GET['q']);
unset($_GET['civiwp']);
$query = http_build_query($_GET);
}
else {
$argdata = $this->civi->get_request_args();
$path = $argdata['argString'];
/*
* 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);
Kevin Cristiano
committed
* Get CiviCRM base page template.
*
* Callback method for 'template_include' hook, always called from WordPress
* front-end.
* @since 4.6
*
* @param string $template The path to the existing template.
* @return string $template The modified path to the desired template.
// Get template path relative to the theme's root directory.
$template_name = str_replace(trailingslashit(get_stylesheet_directory()), '', $template);
if ($template_name == $template) {
$template_name = str_replace(trailingslashit(get_template_directory()), '', $template);
}
// Bail in the unlikely event that the template name has not been found.
Kevin Cristiano
committed
/**
* 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.
*/
$basepage_template = apply_filters('civicrm_basepage_template', $template_name);
Kevin Cristiano
committed
// Find the base page template.
$page_template = locate_template([$basepage_template]);
/**
* 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.
*/
$home_template_name = apply_filters('civicrm_basepage_home_template', 'page.php');
Kevin Cristiano
committed
// Find the homepage template.
$home_template = locate_template([$home_template_name]);
Kevin Cristiano
committed
Kevin Cristiano
committed
return $home_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.
// 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]) && !empty($args['args'][0])) {
$classes[] = $args['args'][0];
}
// Check for second level - the component.
if (isset($args['args'][1]) && !empty($args['args'][1])) {
$classes[] = $args['args'][0] . '-' . $args['args'][1];
}
// Check for third level - the component's configuration.
if (isset($args['args'][2]) && !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]) && !empty($args['args'][3])) {
$classes[] = $args['args'][0] . '-' . $args['args'][1] . '-' . $args['args'][2] . '-' . $args['args'][3];
}
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
/**
* Remove edit link from page content.
*
* Callback from 'edit_post_link' hook.
*
* @since 4.6
* @since 5.33 Moved to this class.
*
* @return string Always empty.
*/
public function clear_edit_post_link() {
return '';
}
/**
* Remove edit link in WordPress Admin Bar.
*
* Callback from 'wp_before_admin_bar_render' hook.
*
* @since 4.6
*/
public function clear_edit_post_menu_item() {
// Access object.
global $wp_admin_bar;
// Bail if in admin.
if (is_admin()) {
return;
}
// Remove the menu item from front end.
$wp_admin_bar->remove_menu('edit');
}
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
/**
* Gets the current Base Page object.
*
* @since 5.44
*
* @return WP_Post|bool The Base Page object or FALSE on failure.
*/
public function basepage_get() {
// Kick out if not CiviCRM.
if (!$this->civi->initialize()) {
return FALSE;
}
// Get the setting.
$basepage_slug = civicrm_api3('Setting', 'getvalue', [
'name' => 'wpBasePage',
'group' => 'CiviCRM Preferences',
]);
// Did we get a value?
if (!empty($basepage_slug)) {
// Define the query for our Base Page.
$args = [
'post_type' => 'page',
'name' => strtolower($basepage_slug),
'post_status' => 'publish',
'posts_per_page' => 1,
];
// Do the query.
$pages = get_posts($args);
}
// Find the Base Page object.
$basepage = FALSE;
if (!empty($pages) && is_array($pages)) {
$basepage = array_pop($pages);
}
return $basepage;
}