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 Base Page needs to be created.
add_action('civicrm_instance_loaded', [$this, 'maybe_create_basepage']);
* Register hooks to handle CiviCRM in a WordPress Base Page context.
* Triggers the process whereby the WordPress Base Page is created.
* Sets a one-time-only option to flag that we need to create a Base Page -
* 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 Base Page needs to be created,
* but the moment to do so is once CiviCRM has been successfully installed.
// 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');
* Auto-creates the WordPress Base Page if necessary.
* Changes the one-time-only option so that the Base Page can only be created
* once. Thereafter, we're on our own until there's a 'delete_post' callback
*
* @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') {
add_action('wp_loaded', [$this, 'create_wp_basepage']);
// Change option so the callback above never runs again.
update_option('civicrm_activation_create_basepage', 'done');
* Creates the WordPress Base Page and saves the CiviCRM "wpBasePage" setting.
*
* @since 4.6
* @since 5.6 Relocated from CiviCRM_For_WordPress to here.
* @since 5.44 Returns success or failure.
*
* @return bool TRUE if successful, FALSE otherwise.
*/
public function create_wp_basepage() {
if (!$this->civi->initialize()) {
if (version_compare(CRM_Core_BAO_Domain::getDomain()->version, '4.7.0', '<')) {
return FALSE;
}
// Bail if we already have a Base Page setting.
$config = CRM_Core_Config::singleton();
$slug = apply_filters('civicrm_basepage_slug', 'civicrm');
// Get the ID if the Base Page already exists.
$result = 0;
if ($page instanceof WP_Post) {
// Create the Base Page if it's missing.
if ($result === 0) {
// Save the Page slug as the setting if we have one.
if ($result !== 0 && !is_wp_error($result)) {
$post = get_post($result);
* 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()) {
/**
* Allow plugins to override the switch to the main site.
*
* This filter changes the default behaviour on WordPress Multisite so
* that the Base Page *is* created on every site on which CiviCRM is
* activated. This is a more sensible and inclusive default, since the
* absence of the Base Page on a sub-site often leads to confusion.
*
* To restore the previous functionality, return boolean TRUE.
*
* The previous functionality may be the desired behaviour when the
* WordPress Multisite instance in question is one where sub-sites aren't
* truly "separate" e.g. sites built on frameworks such as "Commons in
* a Box" or "MultilingualPress".
*
* @since 5.44
*
* @param bool False by default prevents the switch to the main site.
*/
$switch = apply_filters('civicrm/basepage/main_site_only', FALSE);
if ($switch !== FALSE) {
// Store this site.
$original_site = get_current_blog_id();
// Switch to main site.
switch_to_blog(get_main_site_id());
}
'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' => '',
$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');
* @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)) {
* 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;
}
if (function_exists('is_favicon') && 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.
$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.
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
$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 Base Page title.
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'], 21);
* Get CiviCRM Base Page 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 Base Page 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.
* @return str $basepage_markup The Base Page markup.
* Provide the canonical URL for a page accessed through a Base Page.
*
* 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).
Kevin Cristiano
committed
*
* 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
*
* 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.
*/
$basepage_template = apply_filters('civicrm_basepage_template', $template_name);
Kevin Cristiano
committed
$page_template = locate_template([$basepage_template]);
/**
* Override the template, but allow plugins to amend.
*
* This filter handles the scenario where no Base Page 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 Base Page *must*
* be set if this is the case.
*
* @since 4.6
*
* @param string The template name (set to the default page template).
*/
$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 Base Page.
*
* This allows selectors to be written for particular CiviCRM "pages" despite
* them all being rendered on the one WordPress Base Page.
* @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];
}
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
/**
* 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');
}
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
846
847
848
849
850
851
852
853
854
855
856
857
/**
* 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;
}