Skip to content

Instantly share code, notes, and snippets.

@brianoz
Created February 20, 2014 01:10
Show Gist options
  • Save brianoz/9105004 to your computer and use it in GitHub Desktop.
Save brianoz/9105004 to your computer and use it in GitHub Desktop.
WordPress Virtual page with theme
<?php
/*
* Virtual Themed Page class
*
* This class implements virtual pages for a plugin.
*
* It is designed to be included then called for each part of the plugin
* that wants virtual pages.
*
* It supports multiple virtual pages and content generation functions.
* The content functions are only called if a page matches.
*
* The class uses the theme templates and as far as I know is unique in that.
* It also uses child theme templates ahead of main theme templates.
*
* Example code follows class.
*
* August 2013 Brian Coogan
*
*/
// There are several virtual page classes, we want to avoid a clash!
//
//
class Virtual_Themed_Pages_BC
{
public $title = '';
public $body = '';
private $vpages = array(); // the main array of virtual pages
private $mypath = '';
public $blankcomments = "blank-comments.php";
function __construct($plugin_path = null, $blankcomments = null)
{
if (empty($plugin_path))
$plugin_path = dirname(__FILE__);
$this->mypath = $plugin_path;
if (! empty($blankcomments))
$this->blankcomments = $blankcomments;
// Virtual pages are checked in the 'parse_request' filter.
// This action starts everything off if we are a virtual page
add_action('parse_request', array(&$this, 'vtp_parse_request'));
}
function add($virtual_regexp, $contentfunction)
{
$this->vpages[$virtual_regexp] = $contentfunction;
}
// Check page requests for Virtual pages
// If we have one, call the appropriate content generation function
//
function vtp_parse_request(&$wp)
{
//global $wp;
if (empty($wp->query_vars['pagename']))
return; // page isn't permalink
//$p = $wp->query_vars['pagename'];
$p = $_SERVER['REQUEST_URI'];
$matched = 0;
foreach ($this->vpages as $regexp => $func)
{
if (preg_match($regexp, $p))
{
$matched = 1;
break;
}
}
// Do nothing if not matched
if (! $matched)
return;
// setup hooks and filters to generate virtual movie page
add_action('template_redirect', array(&$this, 'template_redir'));
add_filter('the_posts', array(&$this, 'vtp_createdummypost'));
// we also force comments removal; a comments box at the footer of
// a page is rather meaningless.
// This requires the blank_comments.php file be provided
add_filter('comments_template', array(&$this, 'disable_comments'), 11);
// Call user content generation function
// Called last so it can remove any filters it doesn't like
// It should set:
// $this->body -- body of the virtual page
// $this->title -- title of the virtual page
// $this->template -- optional theme-provided template
// eg: page
// $this->subtemplate -- optional subtemplate (eg movie)
// Doco is unclear whether call by reference works for call_user_func()
// so using call_user_func_array() instead, where it's mentioned.
// See end of file for example code.
$this->template = $this->subtemplate = null;
$this->title = null;
unset($this->body);
call_user_func_array($func, array(&$this, $p));
if (! isset($this->body)) //assert
wp_die("Virtual Themed Pages: must save ->body [VTP07]");
return($wp);
}
// Setup a dummy post/page
// From the WP view, a post == a page
//
function vtp_createdummypost($posts)
{
// have to create a dummy post as otherwise many templates
// don't call the_content filter
global $wp, $wp_query;
//create a fake post intance
$p = new stdClass;
// fill $p with everything a page in the database would have
$p->ID = -1;
$p->post_author = 1;
$p->post_date = current_time('mysql');
$p->post_date_gmt = current_time('mysql', $gmt = 1);
$p->post_content = $this->body;
$p->post_title = $this->title;
$p->post_excerpt = '';
$p->post_status = 'publish';
$p->ping_status = 'closed';
$p->post_password = '';
$p->post_name = 'movie_details'; // slug
$p->to_ping = '';
$p->pinged = '';
$p->modified = $p->post_date;
$p->modified_gmt = $p->post_date_gmt;
$p->post_content_filtered = '';
$p->post_parent = 0;
$p->guid = get_home_url('/' . $p->post_name); // use url instead?
$p->menu_order = 0;
$p->post_type = 'page';
$p->post_mime_type = '';
$p->comment_status = 'closed';
$p->comment_count = 0;
$p->filter = 'raw';
$p->ancestors = array(); // 3.6
// reset wp_query properties to simulate a found page
$wp_query->is_page = TRUE;
$wp_query->is_singular = TRUE;
$wp_query->is_home = FALSE;
$wp_query->is_archive = FALSE;
$wp_query->is_category = FALSE;
unset($wp_query->query['error']);
$wp->query = array();
$wp_query->query_vars['error'] = '';
$wp_query->is_404 = FALSE;
$wp_query->current_post = $p->ID;
$wp_query->found_posts = 1;
$wp_query->post_count = 1;
$wp_query->comment_count = 0;
// -1 for current_comment displays comment if not logged in!
$wp_query->current_comment = null;
$wp_query->is_singular = 1;
$wp_query->post = $p;
$wp_query->posts = array($p);
$wp_query->queried_object = $p;
$wp_query->queried_object_id = $p->ID;
$wp_query->current_post = $p->ID;
$wp_query->post_count = 1;
return array($p);
}
// Virtual Movie page - tell wordpress we are using the given
// template if it exists; otherwise we fall back to page.php.
//
// This func gets called before any output to browser
// and exits at completion.
//
function template_redir()
{
// $this->body -- body of the virtual page
// $this->title -- title of the virtual page
// $this->template -- optional theme-provided template eg: 'page'
// $this->subtemplate -- optional subtemplate (eg movie)
//
if (! empty($this->template) && ! empty($this->subtemplate))
{
// looks for in child first, then master:
// template-subtemplate.php, template.php
get_template_part($this->template, $this->subtemplate);
}
elseif (! empty($this->template))
{
// looks for in child, then master:
// template.php
get_template_part($this->template);
}
elseif (! empty($this->subtemplate))
{
// looks for in child, then master:
// template.php
get_template_part($this->subtemplate);
}
else
{
get_template_part('page');
}
// It would be possible to add a filter for the 'the_content' filter
// to detect that the body had been correctly output, and then to
// die if not -- this would help a lot with error diagnosis.
exit;
}
// Some templates always include comments regardless, sigh.
// This replaces the path of the original comments template with a
// empty template file which returns nothing, thus eliminating
// comments reliably.
function disable_comments($file)
{
if (file_exists($this->blankcomments))
return($this->mypath.'/'.$blankcomments);
return($file);
}
} // class
// Example code - you'd use something very like this in a plugin
//
if (0)
{
// require 'BC_Virtual_Themed_pages.php';
// this code segment requires the WordPress environment
$vp = new Virtual_Themed_Pages_BC();
$vp->add('#/mypattern/unique#i', 'mytest_contentfunc');
// Example of content generating function
// Must set $this->body even if empty string
function mytest_contentfunc($v, $url)
{
// extract an id from the URL
$id = 'none';
if (preg_match('#unique/(\d+)#', $url, $m))
$id = $m[1];
// could wp_die() if id not extracted successfully...
$v->title = "My Virtual Page Title";
$v->body = "Some body content for my virtual page test - id $id\n";
$v->template = 'page'; // optional
$v->subtemplate = 'billing'; // optional
}
}
// end
@aaronpeterson
Copy link

Thanks for this. I don't see pagename in $wp->query_vars in the process_request callback. Just this when I request /foobar:

array (size=2)
  'page' => string '' (length=0)
  'name' => string 'foobar' (length=4)

/foobar/test:

array (size=1)
  'attachment' => string 'test' (length=4)

However, $wp->request has the relative path no matter what. /foobar/test/this/thing/all/the/way/out/to/here :

string 'foobar/test/this/thing/all/the/way/out/to/here' (length=46)

This is a clean 3.8.1 install with twentyfourteen.

I couldn't seem to get pass matching test in vtp_parse_request() but maybe WP has changed since you wrote this.

@gilem
Copy link

gilem commented Apr 24, 2014

Excuse my wordpress ignorance here, but I could not get this working unless the following lines in vtp_parse_request were removed. Am I missing something?

if (empty($wp->query_vars['pagename']))
 return; // page isn't permalink

@stephen-cni
Copy link

I think this is exactly what I need!

I am developing a WP plugin to get data from a web service, Photos and names to start with. So far it displays the returned data as a post using a shortcode. However the content is not recognized by WP themes. My knowledge of WP and PHP is minimal so I am struggling with implementing your Virtual Themed Page class.

I am hoping you can help. If so I will explain what I am trying to accomplish in more detail.

Thanks!

@RePeeK
Copy link

RePeeK commented Oct 16, 2014

I have a big problem with this is the Google Crowler can see my Virtual pages. It return always Not Found, please help.

@ZacharyDraper
Copy link

Like gilem, I found that removing the two lines he described were required for this class to function in my case. Thanks for the help gilem!

@andrewsauder
Copy link

This works almost entirely as intended right out of the box! If you need to make virtual/fake pages, I recommend this class (I'm actually moving to it from another similar but less robust option you may run across). Note the following "issues" that you may have to change when implementing:

  1. Remove the lines that gilem notes above. My page wouldn't load with those lines enabled.
    if (empty($wp->query_vars['pagename']))`
    return; // page isn't permalink
  2. Add a line to force wordpress to believe this is a page and not an attachment. I've only had issues with one theme doing weird things as a result of this but if this saves you my headaches, cheers. Add the following line to the vtp_createdummypost function (I added mine on line 171).
    $wp_query->is_attachment = false;

@nirajkvinit
Copy link

Hi! Nice example!

Considering your code $vp->add('#/mypattern/unique#i', 'mytest_contentfunc'); what if I wanted to access the root /mypattern/ ?

@WindowsAndLinux
Copy link

Hi, I was wondering if anyone could help me set this up, how do I actually use this ? I've set up a page template the has a require line to the class file, placed in wp-includes., but how do you actually call it ? I can't figure it out... how do I actually instantiate a virtual page with this ?

@andrewsauder
Copy link

nirajkvinit -
The first parameter of $vp->add('#/mypattern/unique#i', 'mytest_contentfunc'); function is a regular expression. Simply adjust that to be a regular expression with a wildcard on the end so that it will pick up all urls with paths starting at /mypattern/. Something like this should do the trick (I didn't test the regex - the wildcard group may need tweaked):
$vp->add('#/mypattern/?(.+)?#i', 'mytest_contentfunc');

@andrewsauder
Copy link

WindowsAndLinux -
You need to add an init action that is included on every page load. Ex: I built a plugin that includes the virtual page class here and then does the following (this is an abbreviated version, it may not work perfectly if you just copy and paste it). The important piece is that the add_action() is on every page load so that the virtual page class can take over the urls you put in the $v->add() method

add_action('init', 'geo_seo_pageNew');

function geo_seo_pageNew() {
    $vp = new geoseo_Virtual_Themed_Pages();
    $vp->add('#/local/#i', 'geo_seoMagic');
}

function geo_seoMagic($v, $url) {
               $v->body = 'My special page content here';
        $v->template = 'page';
        $v->slug = $url;
}

@MikeiLL
Copy link

MikeiLL commented Mar 8, 2016

Loving this! Thank you. I'm getting a (non-specified) Wordpress error when trying to share virtual page on facebook (via Add to Any plugin). Will report back.

@MikeiLL
Copy link

MikeiLL commented Mar 10, 2016

A couple of small changes so that entire URL can be shared:

Around line 32 add :
public $itemID = '';

Then around line 140 update:
$p->post_name = 'my_path/'.$this->itemID;

...but it's adding a second instance of the itemID to the URL string as well.

@faebulicious
Copy link

I had the same problem with sharing, because WP is not returning a correct permalink.

To solve this problem, I added this line of Code:
add_filter('page_link', array(&$this, 'vtp_createpagelink'), 1, 2);
just after the addition of the filter 'the_post'

The new method vtp_createpagelink is just as follows:
function vtp_createpagelink($link, $postID) { if ($postID == -1) { $link = home_url($_SERVER['REQUEST_URI']); } }

@slayer49
Copy link

Thanks everyone, I have been trying to figure out how to get a virtual post to get all of the meta data added by SEO plugins like Yoast WP-SEO and others. Anyone have any suggestions? Virtual posts working flawlessy except for this for me.

@gaufde
Copy link

gaufde commented Dec 2, 2022

Has anyone been able to get this to work on Wordpress 6.1.1 (or newer once that happens)? I get a couple of errors that look like "Trying to get property 'post_type' of non-object in .../wp-includes/link-template.php on line 4066." If I downgrade to Wordpress 6.0.3 then I can my code working.

@gaufde
Copy link

gaufde commented Dec 11, 2022

Update: Looks like this person figured it out: https://stackoverflow.com/questions/74613027/wordpress-virtual-page-trying-to-get-property-post-type-of-non-object

Also, here is another plugin that could be helpful to the next person!
https://gist.github.com/gmazzap/1efe17a8cb573e19c086

@tobestool
Copy link

tobestool commented Oct 20, 2023

This is such a handy bit of code, I've been really pleased with how it works, but naturally at the eleventh hour I've realised I have a problem.

If the path has multiple slashes in it, the page works fine, but it's returned with a 404 status. So /catalogue/?product_id=123 is fine as is /catalogue/?search=foo however where I have pages like /catalogue/widgets/my_super_widget/ while displaying correctly, they are returned as a 404 error page.

Has anyone else got any pointers on how to sort this out, please?

@gilem
Copy link

gilem commented Mar 30, 2024

This is such a handy bit of code, I've been really pleased with how it works, but naturally at the eleventh hour I've realised I have a problem.

If the path has multiple slashes in it, the page works fine, but it's returned with a 404 status. So /catalogue/?product_id=123 is fine as is /catalogue/?search=foo however where I have pages like /catalogue/widgets/my_super_widget/ while displaying correctly, they are returned as a 404 error page.

Has anyone else got any pointers on how to sort this out, please?

@tobestool Sorry for the late reply. I just discovered this issue myself. I believe it was caused by a wordpress update. Anyway, if you add the following line to dummypost, it should rid the 404

$wp->query_vars['error']='';

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment