Created
November 19, 2011 22:59
-
-
Save chrismeller/1379495 to your computer and use it in GitHub Desktop.
Habari Theme Updates for Page Caching
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* @package Habari | |
* | |
*/ | |
/** | |
* Habari Theme Class | |
* | |
* The Theme class is the behind-the-scenes representation of | |
* of a set of UI files that compose the visual theme of the blog | |
* | |
*/ | |
class Theme extends Pluggable | |
{ | |
public $name = null; | |
public $version = null; | |
public $template_engine = null; | |
public $theme_dir = null; | |
public $config_vars = array(); | |
private $var_stack = array( array() ); | |
private $current_var_stack = 0; | |
public $context = array(); | |
/** | |
* We build the Post filters by analyzing the handler_var | |
* data which is assigned to the handler ( by the Controller and | |
* also, optionally, by the Theme ) | |
*/ | |
public $valid_filters = array( | |
'content_type', | |
'not:content_type', | |
'slug', | |
'not:slug', | |
'user_id', | |
'vocabulary', | |
'status', | |
'page', | |
'tag', | |
'not:tag', | |
'month', | |
'year', | |
'day', | |
'criteria', | |
'limit', | |
'nolimit', | |
'offset', | |
'fetch_fn', | |
'id', | |
'info', | |
'has:info', | |
'all:info', | |
'any:info', | |
'not:info', | |
'not:all:info', | |
'not:any:info', | |
); | |
/** | |
* Constructor for theme | |
* | |
* If no parameter is supplied, then the constructor | |
* Loads the active theme from the database. | |
* | |
* If no theme option is set, a fatal error is thrown | |
* | |
* @param name ( optional ) override the default theme lookup | |
* @param template_engine ( optional ) specify a template engine | |
* @param theme_dir ( optional ) specify a theme directory | |
*/ | |
public function __construct( $themedata ) | |
{ | |
$this->name = $themedata->name; | |
$this->version = $themedata->version; | |
$theme_dir = Utils::single_array($themedata->theme_dir); | |
// Set up the corresponding engine to handle the templating | |
$this->template_engine = new $themedata->template_engine(); | |
$this->theme_dir = $theme_dir; | |
$this->template_engine->set_template_dir( $theme_dir ); | |
$this->plugin_id = $this->plugin_id(); | |
$this->load(); | |
} | |
/** | |
* Loads a theme's metadata from an XML file in theme's | |
* directory. | |
* | |
*/ | |
public function info() | |
{ | |
$xml_file = $this->theme_dir . '/theme.xml'; | |
if(!file_exists($xml_file)) { | |
return new SimpleXMLElement('<?xml version="1.0" encoding="utf-8" ?> | |
<pluggable type="theme"> | |
<name>Unknown Theme</name> | |
<version>1.0</version> | |
</pluggable> | |
'); | |
} | |
if ( $xml_content = file_get_contents( $xml_file ) ) { | |
$theme_data = new SimpleXMLElement( $xml_content ); | |
return $theme_data; | |
} | |
} | |
/** | |
* Assign the default variables that would be used in every template | |
*/ | |
public function add_template_vars() | |
{ | |
// set the locale and character set that habari is configured to use presently | |
if ( !isset( $this->locale ) ) { | |
$this->locale = Options::get('locale', 'en'); // default to 'en' just in case we somehow don't have one? | |
} | |
if ( !isset( $this->charset ) ) { | |
$this->charset = MultiByte::hab_encoding(); | |
} | |
if ( !$this->template_engine->assigned( 'user' ) ) { | |
$this->assign( 'user', User::identify() ); | |
} | |
if ( !$this->template_engine->assigned( 'loggedin' ) ) { | |
$this->assign( 'loggedin', User::identify()->loggedin ); | |
} | |
if ( !$this->template_engine->assigned( 'page' ) ) { | |
$this->assign( 'page', isset( $this->page ) ? $this->page : 1 ); | |
} | |
$handler = Controller::get_handler(); | |
if ( isset( $handler ) ) { | |
Plugins::act( 'add_template_vars', $this, $handler->handler_vars ); | |
} | |
} | |
/** | |
* Find the first template that matches from the list provided and display it | |
* @param array $template_list The list of templates to search for | |
*/ | |
public function display_fallback( $template_list, $display_function = 'display' ) | |
{ | |
foreach ( (array)$template_list as $template ) { | |
if ( $this->template_exists( $template ) ) { | |
$this->assign( '_template_list', $template_list ); | |
$this->assign( '_template', $template ); | |
return $this->$display_function( $template ); | |
} | |
} | |
return false; | |
} | |
/** | |
* Determine if a template exists in the current theme | |
* | |
* @param string $template_name The name of the template to detect | |
* @return boolean True if template exists | |
*/ | |
public function template_exists( $template_name ) | |
{ | |
return $this->template_engine->template_exists( $template_name ); | |
} | |
/** | |
* Grabs post data and inserts that data into the internal | |
* handler_vars array, which eventually gets extracted into | |
* the theme's ( and thereby the template_engine's ) local | |
* symbol table for use in the theme's templates | |
* | |
* This is the default, generic function to grab posts. To | |
* "filter" the posts retrieved, simply pass any filters to | |
* the handler_vars variables associated with the post retrieval. | |
* For instance, to filter by tag, ensure that handler_vars['tag'] | |
* contains the tag to filter by. Simple as that. | |
*/ | |
public function act_display( $paramarray = array( 'user_filters'=> array() ) ) | |
{ | |
Utils::check_request_method( array( 'GET', 'HEAD', 'POST' ) ); | |
// Get any full-query parameters | |
$possible = array( 'user_filters', 'fallback', 'posts', 'post', 'content_type' ); | |
foreach ( $possible as $varname ) { | |
if ( isset( $paramarray[$varname] ) ) { | |
$$varname = $paramarray[$varname]; | |
} | |
} | |
$where_filters = array(); | |
$where_filters = Controller::get_handler_vars()->filter_keys( $this->valid_filters ); | |
$where_filters['vocabulary'] = array(); | |
if ( array_key_exists( 'tag', $where_filters ) ) { | |
$tags = Tags::parse_url_tags( $where_filters['tag'] ); | |
$not_tag = $tags['exclude_tag']; | |
$all_tag = $tags['include_tag']; | |
if ( count( $not_tag ) > 0 ) { | |
$where_filters['vocabulary'] = array_merge( $where_filters['vocabulary'], array( Tags::vocabulary()->name . ':not:term' => $not_tag ) ); | |
} | |
if ( count( $all_tag ) > 0 ) { | |
$where_filters['vocabulary'] = array_merge( $where_filters['vocabulary'], array( Tags::vocabulary()->name . ':all:term' => $all_tag ) ); | |
} | |
$where_filters['tag_slug'] = Utils::slugify( $where_filters['tag'] ); | |
unset( $where_filters['tag'] ); | |
} | |
if ( !isset( $_GET['preview'] ) ) { | |
$where_filters['status'] = Post::status( 'published' ); | |
} | |
if ( !isset( $posts ) ) { | |
$user_filters = Plugins::filter( 'template_user_filters', $user_filters ); | |
$user_filters = array_intersect_key( $user_filters, array_flip( $this->valid_filters ) ); | |
// Work around the tags parameters to Posts::get() being subsumed by the vocabulary parameter | |
if( isset( $user_filters['not:tag'] ) ) { | |
$user_filters['vocabulary'] = array( Tags::vocabulary()->name . ':not:term' => $user_filters['not:tag'] ); | |
unset( $user_filters['not:tag'] ); | |
} | |
if( isset( $user_filters['tag'] ) ) { | |
$user_filters['vocabulary'] = array( Tags::vocabulary()->name . ':term_display' => $user_filters['tag'] ); | |
unset( $user_filters['tag'] ); | |
} | |
$where_filters = $where_filters->merge( $user_filters ); | |
$where_filters = Plugins::filter( 'template_where_filters', $where_filters ); | |
$posts = Posts::get( $where_filters ); | |
} | |
$this->assign( 'posts', $posts ); | |
if ( $posts !== false && count( $posts ) > 0 ) { | |
if ( count( $posts ) == 1 ) { | |
$post = $posts instanceof Post ? $posts : reset( $posts ); | |
Stack::add( 'body_class', Post::type_name( $post->content_type ) . '-' . $post->id ); | |
} | |
else { | |
$post = reset( $posts ); | |
Stack::add( 'body_class', 'multiple' ); | |
} | |
$this->assign( 'post', $post ); | |
$type = Post::type_name( $post->content_type ); | |
} | |
elseif ( ( $posts === false ) || | |
( isset( $where_filters['page'] ) && $where_filters['page'] > 1 && count( $posts ) == 0 ) ) { | |
if ( $this->template_exists( '404' ) ) { | |
$fallback = array( '404' ); | |
// Replace template variables with the 404 rewrite rule | |
$this->request->{URL::get_matched_rule()->name} = false; | |
$this->request->{URL::set_404()->name} = true; | |
$this->matched_rule = URL::get_matched_rule(); | |
// 404 status header sent in act_display_404, but we're past | |
// that, so send it now. | |
header( 'HTTP/1.1 404 Not Found', true, 404 ); | |
} | |
else { | |
$this->display( 'header' ); | |
echo '<h2>'; | |
_e( "Whoops! 404. The page you were trying to access is not really there. Please try again." ); | |
echo '</h2>'; | |
header( 'HTTP/1.1 404 Not Found', true, 404 ); | |
$this->display( 'footer' ); | |
die; | |
} | |
} | |
$extract = $where_filters->filter_keys( 'page', 'type', 'id', 'slug', 'posttag', 'year', 'month', 'day', 'tag', 'tag_slug' ); | |
foreach ( $extract as $key => $value ) { | |
$$key = $value; | |
} | |
$this->assign( 'page', isset( $page )? $page:1 ); | |
if ( !isset( $fallback ) ) { | |
// Default fallbacks based on the number of posts | |
$fallback = array( '{$type}.{$id}', '{$type}.{$slug}', '{$type}.tag.{$posttag}' ); | |
if ( count( $posts ) > 1 ) { | |
$fallback[] = '{$type}.multiple'; | |
$fallback[] = 'multiple'; | |
} | |
else { | |
$fallback[] = '{$type}.single'; | |
$fallback[] = 'single'; | |
} | |
} | |
$searches = array( '{$id}','{$slug}','{$year}','{$month}','{$day}','{$type}','{$tag}', ); | |
$replacements = array( | |
( isset( $post ) && $post instanceof Post ) ? $post->id : '-', | |
( isset( $post ) && $post instanceof Post ) ? $post->slug : '-', | |
isset( $year ) ? $year : '-', | |
isset( $month ) ? $month : '-', | |
isset( $day ) ? $day : '-', | |
isset( $type ) ? $type : '-', | |
isset( $tag_slug ) ? $tag_slug : '-', | |
); | |
$fallback[] = 'home'; | |
$fallback = Plugins::filter( 'template_fallback', $fallback, $posts, isset( $post ) ? $post : null ); | |
$fallback = array_values( array_unique( MultiByte::str_replace( $searches, $replacements, $fallback ) ) ); | |
for ( $z = 0; $z < count( $fallback ); $z++ ) { | |
if ( ( MultiByte::strpos( $fallback[$z], '{$posttag}' ) !== false ) && ( isset( $post ) ) && ( $post instanceof Post ) ) { | |
$replacements = array(); | |
if ( $alltags = $post->tags ) { | |
foreach ( $alltags as $current_tag ) { | |
$replacements[] = MultiByte::str_replace( '{$posttag}', $current_tag->term, $fallback[$z] ); | |
} | |
array_splice( $fallback, $z, 1, $replacements ); | |
} | |
else { | |
break; | |
} | |
} | |
} | |
// @todo probably need to make this private if the user is logged in so proxy's don't cache it? | |
header('Pragma: public', true); | |
header('Cache-Control: public, max-age=' . HabariDateTime::DAY * 30, true); | |
$etag = var_export( $this, true ); | |
$etag = sha1( $etag ); | |
header('ETag: ' . $etag, true); | |
if ( isset( $_SERVER['HTTP_IF_NONE_MATCH'] ) ) { | |
if ( $etag == $_SERVER['HTTP_IF_NONE_MATCH'] ) { | |
header( 'HTTP/1.1 304 Not Modified', true, 304); | |
header( 'X-Habari-Cache-Match: ETag'); | |
die(); | |
} | |
} | |
if ( isset( $post ) ) { | |
$last_modified = $post->modified->set_timezone( 'UTC' )->format( 'D, d M Y H:i:s e' ); | |
$expires = HabariDateTime::date_create( '30 days' )->set_timezone( 'UTC' )->format( 'D, d M Y H:i:s e' ); | |
header('Last-Modified: ' . $last_modified, true); | |
if ( isset( $_SERVER['HTTP_IF_MODIFIED_SINCE'] ) ) { | |
$if_modified_since = HabariDateTime::date_create( $_SERVER['HTTP_IF_MODIFIED_SINCE'] ); | |
if ( $post->modified <= $if_modified_since ) { | |
header( 'HTTP/1.1 304 Not Modified', true, 304 ); | |
header( 'X-Habari-Cache-Match: Modified' ); | |
die(); | |
} | |
} | |
header('Expires: ' . $expires, true); | |
} | |
return $this->display_fallback( $fallback ); | |
} | |
/** | |
* Helper function: Displays the home page | |
* @param array $user_filters Additional arguments used to get the page content | |
*/ | |
public function act_display_home( $user_filters = array() ) | |
{ | |
$paramarray['fallback'] = array( | |
'home', | |
'multiple', | |
); | |
// Makes sure home displays only entries | |
$default_filters = array( | |
'content_type' => Post::type( 'entry' ), | |
); | |
$paramarray['user_filters'] = array_merge( $default_filters, $user_filters ); | |
return $this->act_display( $paramarray ); | |
} | |
/** | |
* Helper function: Displays multiple entries | |
* @param array $user_filters Additional arguments used to get the page content | |
*/ | |
public function act_display_entries( $user_filters = array() ) | |
{ | |
$paramarray['fallback'] = array( | |
'{$type}.multiple', | |
'multiple', | |
); | |
// Makes sure home displays only entries | |
$default_filters = array( | |
'content_type' => Post::type( 'entry' ), | |
); | |
$paramarray['user_filters'] = array_merge( $default_filters, $user_filters ); | |
return $this->act_display( $paramarray ); | |
} | |
/** | |
* Helper function: Display a post | |
* @param array $user_filters Additional arguments used to get the page content | |
*/ | |
public function act_display_post( $user_filters = array() ) | |
{ | |
$paramarray['fallback'] = array( | |
'{$type}.{$id}', | |
'{$type}.{$slug}', | |
'{$type}.tag.{$posttag}', | |
'{$type}.single', | |
'{$type}.multiple', | |
'single', | |
'multiple', | |
); | |
// Does the same as a Post::get() | |
$default_filters = array( | |
'fetch_fn' => 'get_row', | |
'limit' => 1, | |
); | |
// Remove the page from filters. | |
$page_key = array_search( 'page', $this->valid_filters ); | |
unset( $this->valid_filters[$page_key] ); | |
$paramarray['user_filters'] = array_merge( $default_filters, $user_filters ); | |
return $this->act_display( $paramarray ); | |
} | |
/** | |
* Helper function: Display the posts for a tag | |
* @param array $user_filters Additional arguments used to get the page content | |
*/ | |
public function act_display_tag( $user_filters = array() ) | |
{ | |
$paramarray['fallback'] = array( | |
'tag.{$tag}', | |
'tag', | |
'multiple', | |
); | |
// Makes sure home displays only entries | |
$default_filters = array( | |
'content_type' => Post::type( 'entry' ), | |
); | |
$this->assign( 'tag', Controller::get_var( 'tag' ) ); | |
// Assign tag objects to the theme | |
$tags = Tags::parse_url_tags( Controller::get_var( 'tag' ), true ); | |
$this->assign( 'include_tag', $tags['include_tag'] ); | |
$this->assign( 'exclude_tag', $tags['exclude_tag'] ); | |
$paramarray['user_filters'] = array_merge( $default_filters, $user_filters ); | |
return $this->act_display( $paramarray ); | |
} | |
/** | |
* Helper function: Display the posts for a specific date | |
* @param array $user_filters Additional arguments used to get the page content | |
*/ | |
public function act_display_date( $user_filters = array() ) | |
{ | |
$handler_vars = Controller::get_handler()->handler_vars; | |
$y = isset( $handler_vars['year'] ); | |
$m = isset( $handler_vars['month'] ); | |
$d = isset( $handler_vars['day'] ); | |
if ( $y && $m && $d ) { | |
$paramarray['fallback'][] = 'year.{$year}.month.{$month}.day.{$day}'; | |
} | |
if ( $y && $m && $d ) { | |
$paramarray['fallback'][] = 'year.month.day'; | |
} | |
if ( $m && $d ) { | |
$paramarray['fallback'][] = 'month.{$month}.day.{$day}'; | |
} | |
if ( $y && $m ) { | |
$paramarray['fallback'][] = 'year.{$year}.month.{$month}'; | |
} | |
if ( $y && $d ) { | |
$paramarray['fallback'][] = 'year.{$year}.day.{$day}'; | |
} | |
if ( $m && $d ) { | |
$paramarray['fallback'][] = 'month.day'; | |
} | |
if ( $y && $d ) { | |
$paramarray['fallback'][] = 'year.day'; | |
} | |
if ( $y && $m ) { | |
$paramarray['fallback'][] = 'year.month'; | |
} | |
if ( $m ) { | |
$paramarray['fallback'][] = 'month.{$month}'; | |
} | |
if ( $d ) { | |
$paramarray['fallback'][] = 'day.{$day}'; | |
} | |
if ( $y ) { | |
$paramarray['fallback'][] = 'year.{$year}'; | |
} | |
if ( $y ) { | |
$paramarray['fallback'][] = 'year'; | |
} | |
if ( $m ) { | |
$paramarray['fallback'][] = 'month'; | |
} | |
if ( $d ) { | |
$paramarray['fallback'][] = 'day'; | |
} | |
$paramarray['fallback'][] = 'date'; | |
$paramarray['fallback'][] = 'multiple'; | |
$paramarray['fallback'][] = 'home'; | |
$paramarray['user_filters'] = $user_filters; | |
if ( !isset( $paramarray['user_filters']['content_type'] ) ) { | |
$paramarray['user_filters']['content_type'] = Post::type( 'entry' ); | |
} | |
$this->assign( 'year', $y ? (int)Controller::get_var( 'year' ) : null ); | |
$this->assign( 'month', $m ? (int)Controller::get_var( 'month' ) : null ); | |
$this->assign( 'day', $d ? (int)Controller::get_var( 'day' ) : null ); | |
return $this->act_display( $paramarray ); | |
} | |
/** | |
* Helper function: Display the posts for a specific criteria | |
* @param array $user_filters Additional arguments used to get the page content | |
*/ | |
public function act_search( $user_filters = array() ) | |
{ | |
$paramarray['fallback'] = array( | |
'search', | |
'multiple', | |
); | |
$paramarray['user_filters'] = $user_filters; | |
$this->assign( 'criteria', Controller::get_var( 'criteria' ) ); | |
return $this->act_display( $paramarray ); | |
} | |
/** | |
* Helper function: Display a 404 template | |
* | |
* @param array $user_filters Additional arguments user to get the page content | |
*/ | |
public function act_display_404( $user_filters = array() ) | |
{ | |
$paramarray['fallback'] = array( | |
'404', | |
); | |
header( 'HTTP/1.1 404 Not Found' ); | |
$paramarray['user_filters'] = $user_filters; | |
return $this->act_display( $paramarray ); | |
} | |
/** | |
* Helper function: Avoids having to call $theme->template_engine->display( 'template_name' ); | |
* @param string $template_name The name of the template to display | |
*/ | |
public function display( $template_name ) | |
{ | |
$this->add_template_vars(); | |
$this->play_var_stack(); | |
$this->template_engine->assign( 'theme', $this ); | |
$this->template_engine->display( $template_name ); | |
} | |
/** | |
* Helper function: Avoids having to call $theme->template_engine->fetch( 'template_name' ); | |
* | |
* @param string $template_name The name of the template to display | |
* @param boolean $unstack If true, end the current template variable buffer upon returning | |
* @return string The content of the template | |
*/ | |
public function fetch( $template_name, $unstack = false ) | |
{ | |
$this->play_var_stack(); | |
$this->add_template_vars(); | |
$this->template_engine->assign( 'theme', $this ); | |
$return = $this->fetch_unassigned( $template_name ); | |
if ( $unstack ) { | |
$this->end_buffer(); | |
} | |
return $return; | |
} | |
/** | |
* Play back the full stack of template variables to assign them into the template | |
*/ | |
protected function play_var_stack() | |
{ | |
$this->template_engine->clear(); | |
for ( $z = 0; $z <= $this->current_var_stack; $z++ ) { | |
foreach ( $this->var_stack[$z] as $key => $value ) { | |
$this->template_engine->assign( $key, $value ); | |
} | |
} | |
} | |
/** | |
* Calls the template engine's fetch() method without pre-assigning template variables. | |
* Assumes that the template variables have already been set. | |
* | |
* @param string $template_name The name of the template to display | |
* @return string The content of the template | |
*/ | |
public function fetch_unassigned( $template_name ) | |
{ | |
return $this->template_engine->fetch( $template_name ); | |
} | |
/** | |
* Helper function: Avoids having to call $theme->template_engine->key= 'value'; | |
*/ | |
public function assign( $key, $value ) | |
{ | |
$this->var_stack[$this->current_var_stack][$key] = $value; | |
} | |
/** | |
* Aggregates and echos the additional header code by combining Plugins and Stack calls. | |
*/ | |
public function theme_header( $theme ) | |
{ | |
// create a stack of the atom tags before the first action so they can be unset if desired | |
Stack::add( 'template_atom', array( 'alternate', 'application/atom+xml', 'Atom 1.0', implode( '', $this->feed_alternate_return() ) ), 'atom' ); | |
Stack::add( 'template_atom', array( 'edit', 'application/atom+xml', 'Atom Publishing Protocol', URL::get( 'atompub_servicedocument' ) ), 'app' ); | |
Stack::add( 'template_atom', array( 'EditURI', 'application/rsd+xml', 'RSD', URL::get( 'rsd' ) ), 'rsd' ); | |
Plugins::act( 'template_header', $theme ); | |
$atom = Stack::get( 'template_atom', '<link rel="%1$s" type="%2$s" title="%3$s" href="%4$s">' ); | |
$styles = Stack::get( 'template_stylesheet', array( 'Stack', 'styles' ) ); | |
$scripts = Stack::get( 'template_header_javascript', array( 'Stack', 'scripts' ) ); | |
$output = implode( "\n", array( $atom, $styles, $scripts ) ); | |
Plugins::act( 'template_header_after', $theme ); | |
return $output; | |
} | |
/** | |
* Aggregates and echos the additional footer code by combining Plugins and Stack calls. | |
*/ | |
public function theme_footer( $theme ) | |
{ | |
Plugins::act( 'template_footer', $theme ); | |
$output = Stack::get( 'template_footer_stylesheet', array( 'Stack', 'styles' ) ); | |
$output .= Stack::get( 'template_footer_javascript', array( 'Stack', 'scripts' ) ); | |
return $output; | |
} | |
/** | |
* Display an object using a template designed for the type of object it is | |
* The $object is assigned into the theme using the $content template variable | |
* | |
* @param Theme $theme The theme used to display the object | |
* @param object $object An object to display | |
* @param string $context The context in which the object will be displayed | |
* @return | |
*/ | |
public function theme_content( $theme, $object, $context = null ) | |
{ | |
$fallback = array(); | |
$content_types = array(); | |
if ( $object instanceof IsContent ) { | |
$content_types = Utils::single_array( $object->content_type() ); | |
} | |
if ( is_object( $object ) ) { | |
$content_types[] = strtolower( get_class( $object ) ); | |
} | |
$content_types[] = 'content'; | |
$content_types = array_flip( $content_types ); | |
if ( isset( $context ) ) { | |
foreach ( $content_types as $type => $type_id ) { | |
$content_type = $context . $object->content_type(); | |
$fallback[] = strtolower( $context . '.' . $type ); | |
} | |
} | |
foreach ( $content_types as $type => $type_id ) { | |
$fallback[] = strtolower( $type ); | |
} | |
if ( isset( $context ) ) { | |
$fallback[] = strtolower( $context ); | |
} | |
$fallback = array_unique( $fallback ); | |
$this->content = $object; | |
if(isset($context)) { | |
$this->context[] = $context; | |
} | |
$result = $this->display_fallback( $fallback, 'fetch' ); | |
if(isset($context)) { | |
array_pop($this->context); | |
} | |
if( $result === false && DEBUG ) { | |
$fallback_list = implode( ', ', $fallback ); | |
$result = '<p>' . _t( 'Content could not be displayed. One of the following templates - %s - has to be present in the active theme.', array( $fallback_list ) ) . '</p>'; | |
} | |
return $result; | |
} | |
/** | |
* Check to see if the theme is currently rendering a specific context | |
* @param string $context The context to check for. | |
* @return bool True if the context is active. | |
*/ | |
public function theme_has_context($theme, $context) | |
{ | |
if(in_array($context, $theme->context)) { | |
return true; | |
} | |
return false; | |
} | |
/** | |
* Returns the appropriate alternate feed based on the currently matched rewrite rule. | |
* | |
* @param mixed $return Incoming return value from other plugins | |
* @param Theme $theme The current theme object | |
* @return string Link to the appropriate alternate Atom feed | |
*/ | |
public function theme_feed_alternate( $theme ) | |
{ | |
$matched_rule = URL::get_matched_rule(); | |
if ( is_object( $matched_rule ) ) { | |
// This is not a 404 | |
$rulename = $matched_rule->name; | |
} | |
else { | |
// If this is a 404 and no rewrite rule matched the request | |
$rulename = ''; | |
} | |
switch ( $rulename ) { | |
case 'display_entry': | |
case 'display_page': | |
return URL::get( 'atom_entry', array( 'slug' => Controller::get_var( 'slug' ) ) ); | |
break; | |
case 'display_entries_by_tag': | |
return URL::get( 'atom_feed_tag', array( 'tag' => Controller::get_var( 'tag' ) ) ); | |
break; | |
case 'display_home': | |
default: | |
return URL::get( 'atom_feed', array( 'index' => '1' ) ); | |
} | |
return ''; | |
} | |
/** | |
* Returns the feedback URL to which comments should be submitted for the indicated Post | |
* | |
* @param Theme $theme The current theme | |
* @param Post $post The post object to get the feedback URL for | |
* @return string The URL to the feedback entrypoint for this comment | |
*/ | |
public function theme_comment_form_action( $theme, $post ) | |
{ | |
return URL::get( 'submit_feedback', array( 'id' => $post->id ) ); | |
} | |
/** | |
* Build a collection of paginated URLs to be used for pagination. | |
* | |
* @param string The RewriteRule name used to build the links. | |
* @param array Various settings used by the method and the RewriteRule. | |
* @return string Collection of paginated URLs built by the RewriteRule. | |
*/ | |
public static function theme_page_selector( $theme, $rr_name = null, $settings = array() ) | |
{ | |
// We can't detect proper pagination if $theme->posts isn't a Posts object, | |
// so if it's not, bail. | |
if(!$theme->posts instanceof Posts) { | |
return ''; | |
} | |
$current = $theme->page; | |
$items_per_page = isset( $theme->posts->get_param_cache['limit'] ) ? | |
$theme->posts->get_param_cache['limit'] : | |
Options::get( 'pagination' ); | |
$total = Utils::archive_pages( $theme->posts->count_all(), $items_per_page ); | |
// Make sure the current page is valid | |
if ( $current > $total ) { | |
$current = $total; | |
} | |
else if ( $current < 1 ) { | |
$current = 1; | |
} | |
// Number of pages to display on each side of the current page. | |
$leftSide = isset( $settings['leftSide'] ) ? $settings['leftSide'] : 1; | |
$rightSide = isset( $settings['rightSide'] ) ? $settings['rightSide'] : 1; | |
// Add the page '1'. | |
$pages[] = 1; | |
// Add the pages to display on each side of the current page, based on $leftSide and $rightSide. | |
for ( $i = max( $current - $leftSide, 2 ); $i < $total && $i <= $current + $rightSide; $i++ ) { | |
$pages[] = $i; | |
} | |
// Add the last page if there is more than one page. | |
if ( $total > 1 ) { | |
$pages[] = (int) $total; | |
} | |
// Sort the array by natural order. | |
natsort( $pages ); | |
// This variable is used to know the last page processed by the foreach(). | |
$prevpage = 0; | |
// Create the output variable. | |
$out = ''; | |
if ( 1 === count( $pages ) && isset( $settings['hideIfSinglePage'] ) && $settings['hideIfSinglePage'] === true ) { | |
return ''; | |
} | |
foreach ( $pages as $page ) { | |
$settings['page'] = $page; | |
// Add ... if the gap between the previous page is higher than 1. | |
if ( ( $page - $prevpage ) > 1 ) { | |
$out .= ' <span class="sep">…</span>'; | |
} | |
// Wrap the current page number with square brackets. | |
$caption = ( $page == $current ) ? $current : $page; | |
// Build the URL using the supplied $settings and the found RewriteRules arguments. | |
$url = URL::get( $rr_name, $settings, false ); | |
// Build the HTML link. | |
$out .= ' <a href="' . $url . '" ' . ( ( $page == $current ) ? 'class="current-page"' : '' ) . '>' . $caption . '</a>'; | |
$prevpage = $page; | |
} | |
return $out; | |
} | |
/** | |
*Provides a link to the previous page | |
* | |
* @param string $text text to display for link | |
*/ | |
public function theme_prev_page_link( $theme, $text = null ) | |
{ | |
$settings = array(); | |
// If there's no previous page, skip and return null | |
$settings['page'] = (int) ( $theme->page - 1 ); | |
if ( $settings['page'] < 1 ) { | |
return null; | |
} | |
// If no text was supplied, use default text | |
if ( $text == '' ) { | |
$text = '← ' . _t( 'Previous' ); | |
} | |
return '<a class="prev-page" href="' . URL::get( null, $settings, false ) . '" title="' . $text . '">' . $text . '</a>'; | |
} | |
/** | |
*Provides a link to the next page | |
* | |
* @param string $text text to display for link | |
*/ | |
public function theme_next_page_link( $theme, $text = null ) | |
{ | |
$settings = array(); | |
// If there's no next page, skip and return null | |
$settings['page'] = (int) ( $theme->page + 1 ); | |
$items_per_page = isset( $theme->posts->get_param_cache['limit'] ) ? | |
$theme->posts->get_param_cache['limit'] : | |
Options::get( 'pagination' ); | |
$total = Utils::archive_pages( $theme->posts->count_all(), $items_per_page ); | |
if ( $settings['page'] > $total ) { | |
return null; | |
} | |
// If no text was supplied, use default text | |
if ( $text == '' ) { | |
$text = _t( 'Next' ) . ' →'; | |
} | |
return '<a class="next-page" href="' . URL::get( null, $settings, false ) . '" title="' . $text . '">' . $text . '</a>'; | |
} | |
/** | |
* Returns a full qualified URL of the specified post based on the comments count, and links to the post. | |
* | |
* Passed strings are localized prior to parsing therefore to localize "%d Comments" in french, it would be "%d Commentaires". | |
* | |
* Since we use sprintf() in the final concatenation, you must format passed strings accordingly. | |
* | |
* @param Theme $theme The current theme object | |
* @param Post $post Post object used to build the comments link | |
* @param string $zero String to return when there are no comments | |
* @param string $one String to return when there is one comment | |
* @param string $many String to return when there are more than one comment | |
* @param string $fragment Fragment (bookmark) portion of the URL to append to the link | |
* @param string $title Fragment (bookmark) portion of the URL to append to the link | |
* @return string Linked string to display for comment count | |
* @see Theme::theme_comments_count() | |
*/ | |
public function theme_comments_link( $theme, $post, $zero = '', $one = '', $many = '', $fragment = 'comments' ) | |
{ | |
$count = $theme->comments_count_return( $post, $zero, $one, $many ); | |
return '<a href="' . $post->permalink . '#' . $fragment . '" title="' . _t( 'Read Comments' ) . '">' . end( $count ) . '</a>'; | |
} | |
/** | |
* Returns a full qualified URL of the specified post based on the comments count. | |
* | |
* Passed strings are localized prior to parsing therefore to localize "%d Comments" in french, it would be "%d Commentaires". | |
* | |
* Since we use sprintf() in the final concatenation, you must format passed strings accordingly. | |
* | |
* @param Theme $theme The current theme object | |
* @param Post $post Post object used to build the comments link | |
* @param string $zero String to return when there are no comments | |
* @param string $one String to return when there is one comment | |
* @param string $many String to return when there are more than one comment | |
* @return string String to display for comment count | |
*/ | |
public function theme_comments_count( $theme, $post, $zero = '', $one = '', $many = '' ) | |
{ | |
$count = $post->comments->approved->count; | |
if ( $count == 0 ) { | |
$text = empty( $zero ) ? _t( 'No Comments' ) : $zero; | |
return sprintf( $text, $count ); | |
} | |
else { | |
if ( empty( $one ) && empty( $many ) ) { | |
$text = _n( '%s Comment', '%s Comments', $count ); | |
} | |
else { | |
if ( empty( $one ) ) { | |
$one = $many; | |
} | |
if ( empty( $many ) ) { | |
$many = $one; | |
} | |
$text = $count == 1 ? $one : $many; | |
} | |
return sprintf( $text, $count ); | |
} | |
} | |
/** | |
* Returns the count of queries executed | |
* | |
* @return integer The query count | |
*/ | |
public function theme_query_count() | |
{ | |
return count( DB::get_profiles() ); | |
} | |
/** | |
* Returns total query execution time in seconds | |
* | |
* @return float Query execution time in seconds, with fractions. | |
*/ | |
public function theme_query_time() | |
{ | |
return array_sum( array_map( create_function( '$a', 'return $a->total_time;' ), DB::get_profiles() ) ); | |
} | |
/** | |
* Returns a humane commenter's link for a comment if a URL is supplied, or just display the comment author's name | |
* | |
* @param Theme $theme The current theme | |
* @param Comment $comment The comment object | |
* @return string A link to the comment author or the comment author's name with no link | |
*/ | |
public function theme_comment_author_link( $theme, $comment ) | |
{ | |
$url = $comment->url; | |
if ( $url != '' ) { | |
$parsed_url = InputFilter::parse_url( $url ); | |
if ( $parsed_url['host'] == '' ) { | |
$url = ''; | |
} | |
else { | |
$url = InputFilter::glue_url( $parsed_url ); | |
} | |
} | |
if ( $url != '' ) { | |
return '<a href="'.$url.'">' . $comment->name . '</a>'; | |
} | |
else { | |
return $comment->name; | |
} | |
} | |
/** | |
* Detects if a variable is assigned to the template engine for use in | |
* constructing the template's output. | |
* | |
* @param key name of variable | |
* @returns boolean true if name is set, false if not set | |
*/ | |
public function __isset( $key ) | |
{ | |
return isset( $this->var_stack[$this->current_var_stack][$key] ); | |
} | |
/** | |
* Set a template variable, a property alias for assign() | |
* | |
* @param string $key The template variable to set | |
* @param mixed $value The value of the variable | |
*/ | |
public function __set( $key, $value ) | |
{ | |
$this->assign( $key, $value ); | |
} | |
/** | |
* Get a template variable value | |
* | |
* @param string $key The template variable name to get | |
* @return mixed The value of the variable | |
*/ | |
public function __get( $key ) | |
{ | |
if ( isset( $this->var_stack[$this->current_var_stack][$key] ) ) { | |
return $this->var_stack[$this->current_var_stack][$key]; | |
} | |
return ''; | |
} | |
/** | |
* Remove a template variable value | |
* | |
* @param string $key The template variable name to unset | |
*/ | |
public function __unset( $key ) | |
{ | |
unset( $this->var_stack[$this->current_var_stack][$key] ); | |
} | |
/** | |
* Start a new template variable buffer | |
*/ | |
public function start_buffer() | |
{ | |
$this->current_var_stack++; | |
$this->var_stack[$this->current_var_stack] = $this->var_stack[$this->current_var_stack - 1]; | |
} | |
/** | |
* End the current template variable buffer | |
*/ | |
public function end_buffer() | |
{ | |
unset( $this->var_stack[$this->current_var_stack] ); | |
$this->current_var_stack--; | |
} | |
/** | |
* Handle methods called on this class or its descendants that are not defined by this class. | |
* Allow plugins to provide additional theme actions, like a custom act_display_*() | |
* | |
* @param string $function The method that was called. | |
* @param array $params An array of parameters passed to the method | |
**/ | |
public function __call( $function, $params ) | |
{ | |
if ( strpos( $function, 'act_' ) === 0 ) { | |
// The first parameter is an array, get it | |
if ( count( $params ) > 0 ) { | |
list( $user_filters )= $params; | |
} | |
else { | |
$user_filters = array(); | |
} | |
$action = substr( $function, 4 ); | |
Plugins::act( 'theme_action', $action, $this, $user_filters ); | |
} | |
else { | |
$purposed = 'output'; | |
if ( preg_match( '/^(.*)_(return|end|out)$/', $function, $matches ) ) { | |
$purposed = $matches[2]; | |
$function = $matches[1]; | |
} | |
array_unshift( $params, $function, $this ); | |
$result = call_user_func_array( array( 'Plugins', 'theme' ), $params ); | |
switch ( $purposed ) { | |
case 'return': | |
return $result; | |
case 'end': | |
return end( $result ); | |
case 'out': | |
$output = implode( '', (array) $result ); | |
echo $output; | |
return $output; | |
default: | |
$output = implode( '', (array) $result ); | |
return $output; | |
} | |
} | |
} | |
/** | |
* Retrieve the block objects for the current scope and specified area | |
* Incomplete! | |
* | |
* @param string $area The area to which blocks will be output | |
* @param string $scope The scope to which blocks will be output | |
* @param Theme $theme The theme that is outputting these blocks | |
* @return array An array of Block instances to render | |
* @todo Finish this function to pull data from a block_instances table | |
*/ | |
public function get_blocks( $area, $scope, $theme ) | |
{ | |
$blocks = DB::get_results( 'SELECT b.* FROM {blocks} b INNER JOIN {blocks_areas} ba ON ba.block_id = b.id WHERE ba.area = ? AND ba.scope_id = ? ORDER BY ba.display_order ASC', array( $area, $scope ), 'Block' ); | |
$blocks = Plugins::filter( 'get_blocks', $blocks, $area, $scope, $theme ); | |
return $blocks; | |
} | |
/** | |
* Matches the scope criteria against the current request | |
* | |
* @param array $criteria An array of scope criteria data in RPN, where values are arrays and operators are strings | |
* @return boolean True if the criteria matches the current request | |
*/ | |
function check_scope_criteria( $criteria ) | |
{ | |
$stack = array(); | |
foreach ( $criteria as $crit ) { | |
if ( is_array( $crit ) ) { | |
$value = false; | |
switch ( $crit[0] ) { | |
case 'request': | |
$value = URL::get_matched_rule()->name == $crit[1]; | |
break; | |
case 'token': | |
if ( isset( $crit[2] ) ) { | |
$value = User::identify()->can( $crit[1], $crit[2] ); | |
} | |
else { | |
$value = User::identify()->can( $crit[1] ); | |
} | |
break; | |
default: | |
$value = Plugins::filter( 'scope_criteria_value', $value, $crit[1], $crit[2] ); | |
break; | |
} | |
$stack[] = $value; | |
} | |
else { | |
switch ( $crit ) { | |
case 'not': | |
$stack[] = ! array_pop( $stack ); | |
break; | |
case 'or': | |
$value1 = array_pop( $stack ); | |
$value2 = array_pop( $stack ); | |
$stack[] = $value1 || $value2; | |
break; | |
case 'and': | |
$value1 = array_pop( $stack ); | |
$value2 = array_pop( $stack ); | |
$stack[] = $value1 && $value2; | |
break; | |
default: | |
Plugins::act( 'scope_criteria_operator', $stack, $crit ); | |
break; | |
} | |
} | |
} | |
return array_pop( $stack ); | |
} | |
/** | |
* Retrieve current scope data from the database based on the requested area | |
* | |
* @param string $area The area for which a scope may be applied | |
* @return array An array of scope data | |
*/ | |
public function get_scopes( $area ) | |
{ | |
$scopes = DB::get_results( 'SELECT * FROM {scopes} s INNER JOIN {blocks_areas} ba ON ba.scope_id = s.id WHERE ba.area = ? ORDER BY s.priority DESC', array( $area ) ); | |
foreach ( $scopes as $key => $value ) { | |
$scopes[$key]->criteria = unserialize( $value->criteria ); | |
} | |
$scopes = Plugins::filter( 'get_scopes', $scopes ); | |
usort( $scopes, array( $this, 'sort_scopes' ) ); | |
return $scopes; | |
} | |
/** | |
* Sort function for ordering scope object rows by priority | |
* @param StdObject $scope1 A scope to compare | |
* @param StdObject $scope2 A scope to compare | |
* @return integer A sort return value, -1 to 1 | |
**/ | |
public function sort_scopes( $scope1, $scope2 ) | |
{ | |
if ( $scope1->priority == $scope2->priority ) { | |
return 0; | |
} | |
return $scope1->priority < $scope2->priority ? 1 : -1; | |
} | |
/** | |
* Displays blocks associated to the specified area and current scope. | |
* | |
* @param Theme $theme The theme with which this area will be output | |
* @param string $area The area to which blocks will be output | |
* @param string $context The area of context within the theme that could adjust the template used | |
* @param string $scope Used to force a specific scope | |
* @return string the output of all the blocks | |
*/ | |
public function theme_area( $theme, $area, $context = null, $scope = null ) | |
{ | |
// This array would normally come from the database via: | |
$scopes = $this->get_scopes( $area ); | |
$active_scope = 0; | |
foreach ( $scopes as $scope_id => $scope_object ) { | |
if ( $this->check_scope_criteria( $scope_object->criteria ) ) { | |
$scope_block_count = DB::get_value( 'SELECT count( *) FROM {blocks_areas} ba WHERE ba.scope_id = ?', array( $scope_object->id ) ); | |
if ( $scope_block_count > 0 ) { | |
$active_scope = $scope_object->id; | |
} | |
break; | |
} | |
} | |
$area_blocks = $this->get_blocks( $area, $active_scope, $theme ); | |
$this->area = $area; | |
if(isset($context)) { | |
$this->context[] = $context; | |
} | |
// This is the block wrapper fallback template list | |
$fallback = array( | |
$context . '.' . $area . '.blockwrapper', | |
$context . '.blockwrapper', | |
$area . '.blockwrapper', | |
'blockwrapper', | |
'content', | |
); | |
$output = ''; | |
$i = 0; | |
foreach ( $area_blocks as $block_instance_id => $block ) { | |
// Temporarily set some values into the block | |
$block->_area = $area; | |
$block->_instance_id = $block_instance_id; | |
$block->_area_index = $i++; | |
$hook = 'block_content_' . $block->type; | |
Plugins::act( $hook, $block, $this ); | |
Plugins::act( 'block_content', $block, $this ); | |
$block->_content = implode( '', $this->content_return( $block, $context ) ); | |
if ( trim( $block->_content ) == '' ) { | |
unset( $area_blocks[$block_instance_id] ); | |
} | |
} | |
// Potentially render each block inside of a wrapper. | |
reset( $area_blocks ); | |
$firstkey = key( $area_blocks ); | |
end( $area_blocks ); | |
$lastkey = key( $area_blocks ); | |
foreach ( $area_blocks as $block_instance_id => $block ) { | |
$block->_first = $block_instance_id == $firstkey; | |
$block->_last = $block_instance_id == $lastkey; | |
// Set up the theme for the wrapper | |
$this->block = $block; | |
$this->content = $block->_content; | |
// This pattern renders the block inside the wrapper template only if a matching template exists | |
$newoutput = $this->display_fallback( $fallback, 'fetch' ); | |
if ( $newoutput === false ) { | |
$output .= $block->_content; | |
} | |
else { | |
$output .= $newoutput; | |
} | |
// Remove temporary values from the block so they're not saved to the database | |
unset( $block->_area ); | |
unset( $block->_instance_id ); | |
unset( $block->_area_index ); | |
unset( $block->_first ); | |
unset( $block->_last ); | |
} | |
// This is the area fallback template list | |
$fallback = array( | |
$context . '.area.' . $area, | |
$context . '.area', | |
'area.' . $area, | |
'area', | |
); | |
$this->content = $output; | |
$newoutput = $this->display_fallback( $fallback, 'fetch' ); | |
if ( $newoutput !== false ) { | |
$output = $newoutput; | |
} | |
$this->area = ''; | |
if(isset($context)) { | |
array_pop($this->context); | |
} | |
return $output; | |
} | |
/** | |
* A theme function for outputting CSS classes based on the requested content | |
* @param Theme $theme A Theme object instance | |
* @param mixed $args Additional classes that should be added to the ones generated | |
* @return string The resultant classes | |
*/ | |
function theme_body_class( $theme, $args = array() ) | |
{ | |
$body_class = array(); | |
foreach ( get_object_vars( $this->request ) as $key => $value ) { | |
if ( $value ) { | |
$body_class[$key] = $key; | |
} | |
} | |
$body_class = array_unique( array_merge( $body_class, Stack::get_named_stack( 'body_class' ), Utils::single_array( $args ) ) ); | |
$body_class = Plugins::filter( 'body_class', $body_class, $theme ); | |
return implode( ' ', $body_class ); | |
} | |
/** | |
* Add javascript to the stack to be output in the theme. | |
* | |
* @param string $where Where should it be output? Options are header and footer. | |
* @param string $value Either a URL or raw JS to be output inline. | |
* @param string $name A name to reference this script by. Used for removing or using in $requires by other scripts. | |
* @param string|array $requires Either a string or an array of strings of $name's for scripts this script requires. | |
* @return boolean True if added successfully, false otherwise. | |
*/ | |
public function add_script ( $where = 'header', $value, $name = null, $requires = null ) | |
{ | |
$result = false; | |
switch ( $where ) { | |
case 'header': | |
$result = Stack::add( 'template_header_javascript', $value, $name, $requires ); | |
break; | |
case 'footer': | |
$result = Stack::add( 'template_footer_javascript', $value, $name, $requires ); | |
break; | |
} | |
return $result; | |
} | |
/** | |
* Add a stylesheet to the stack to be output in the theme. | |
* | |
* @param string $where Where should it be output? Options are header and footer. | |
* @param string $value Either a URL or raw CSS to be output inline. | |
* @param string $name A name to reference this script by. Used for removing or using in $after by other scripts. | |
* @param string|array $requires Either a string or an array of strings of $name's for scripts this script requires. | |
* @return boolean True if added successfully, false otherwise. | |
*/ | |
public function add_style ( $where = 'header', $value, $name = null, $requires = null ) | |
{ | |
$result = false; | |
switch ( $where ) { | |
case 'header': | |
$result = Stack::add( 'template_stylesheet', $value, $name, $requires ); | |
break; | |
case 'footer': | |
$result = Stack::add( 'template_footer_stylesheet', $value, $name, $requires ); | |
break; | |
} | |
return $result; | |
} | |
/** | |
* Provide a method to return the version number from the theme xml | |
* @return string The theme version from XML | |
**/ | |
public function get_version() | |
{ | |
return (string)$this->info()->version; | |
} | |
/** | |
* Get the URL for a resource in this theme's directory | |
* @param string $resource The resource name | |
* @return string The URL of the requested resource | |
* @todo This method needs to be aware of the class that called it so that it can find the right directory to use | |
*/ | |
public function get_url($resource = false) | |
{ | |
$backtraces = debug_backtrace(false); | |
$stop = false; | |
foreach($backtraces as $b) { | |
if($stop) { | |
$backtrace = $b; | |
break; | |
} | |
if($b['function'] = 'get_url') { | |
$stop = true; | |
} | |
} | |
$r_class = new ReflectionClass($backtrace['class']); | |
$classfile = $r_class->getFileName(); | |
$themedir = basename(dirname($classfile)); | |
$theme = $themedir; //basename(end($this->theme_dir)); | |
if ( file_exists( Site::get_dir( 'config' ) . '/themes/' . $theme ) ) { | |
$url = Site::get_url( 'user' ) . '/themes/' . $theme; | |
} | |
elseif ( file_exists( HABARI_PATH . '/user/themes/' . $theme ) ) { | |
$url = Site::get_url( 'habari' ) . '/user/themes/' . $theme; | |
} | |
elseif ( file_exists( HABARI_PATH . '/3rdparty/themes/' . $theme ) ) { | |
$url = Site::get_url( 'habari' ) . '/3rdparty/themes/' . $theme; | |
} | |
else { | |
$url = Site::get_url( 'habari' ) . '/system/themes/' . $theme; | |
} | |
$url .= Utils::trail( $resource ); | |
$url = Plugins::filter( 'site_url_theme', $url ); | |
return $url; | |
} | |
} | |
?> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment