Location: PHPKode > projects > UseBB > UseBB/sources/template.php
<?php

/*
	Copyright (C) 2003-2012 UseBB Team
	http://www.usebb.net
	
	$Id$
	
	This file is part of UseBB.
	
	UseBB is free software; you can redistribute it and/or modify
	it under the terms of the GNU General Public License as published by
	the Free Software Foundation; either version 2 of the License, or
	(at your option) any later version.
	
	UseBB is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
	GNU General Public License for more details.
	
	You should have received a copy of the GNU General Public License
	along with UseBB; if not, write to the Free Software
	Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
*/

/**
 * Template parser
 *
 * Contains the template class to do template handling.
 *
 * @author	UseBB Team
 * @link	http://www.usebb.net
 * @license	GPL-2
 * @version	$Revision$
 * @copyright	Copyright (C) 2003-2012 UseBB Team
 * @package	UseBB
 * @subpackage	Core
 */

//
// Die when called directly in browser
//
if ( !defined('INCLUDED') )
	exit();

/**
 * Template parser
 *
 * Does all the template handling for UseBB.
 *
 * @author	UseBB Team
 * @link	http://www.usebb.net
 * @license	GPL-2
 * @version	$Revision$
 * @copyright	Copyright (C) 2003-2012 UseBB Team
 * @package	UseBB
 * @subpackage	Core
 */
class template {
	
	/**
	 * @var string Output content type (used in HTTP headers and templates). This may be set during runtime.
	 */
	var $content_type = '';
	/**
	 * @var string Character encoding (used in HTTP headers and templates). This may be set during runtime.
	 */
	var $character_encoding = '';
	/**
	 * @var bool Parse templates marked as special only
	 */
	var $parse_special_templates_only = false;
	/**#@+
	 * @access private
	 */
	var $loaded_sections = array();
	var $templates = array();
	var $requests = array();
	var $global_vars = array();
	var $raw_contents = array();
	var $js_onload = array();
	var $breadcrumbs = array();
	var $config_debug = true;
	/**#@-*/
	
	/**
	 * Constructor for template class
	 *
	 * Activates gzip compression if needed, before doing a session_start(). 
	 * Also activates a second output buffer to trigger unwanted output from mods.
	 */
	function template() {
		
		global $functions;
		
		if ( !defined('NO_GZIP') && (int)$functions->get_config('output_compression') >= 2 && !ini_get('zlib.output_compression') )
			ob_start('ob_gzhandler');
		
		ob_start();
		
	}
	
	/**
	 * Load a given template section
	 *
	 * Called automatically when needed
	 *
	 * @param string $section Template section to load
	 */
	function load_section($section) {
		
		global $functions;
		
		if ( !in_array($section, $this->loaded_sections) ) {
			
			$templates_file = ROOT_PATH.'templates/'.$functions->get_config('template').'/'.$section.'.tpl.php';
			if ( !file_exists($templates_file) || !is_readable($templates_file) )
				trigger_error('Unable to load '.$section.' templates file for set "'.$functions->get_config('template').'"!', E_USER_ERROR);
			else
				require($templates_file);
			
			$this->templates[$section] = $templates;
			$this->loaded_sections[] = $section;
			unset($templates);
			
		}
		
	}
	
	/**
	 * Get template configuration variables
	 *
	 * @param string $setting Configuration variable
	 * @returns mixed Configuration value
	 */
	function get_config($setting) {
		
		global $functions;
		
		$this->load_section('global');

		if ( isset($this->templates['global']['config'][$setting]) )
			return $this->templates['global']['config'][$setting];

		if ( $this->config_debug )
			usebb_debug_output('Template configuration variable '.$setting.' not found.');

		return ( strpos($setting, 'delimiter') !== false ) ? ' - ' : '';
		
	}
	
	/**
	 * Parse a template
	 *
	 * @param string $name Template name
	 * @param string $section Template section
	 * @param array $variables Template variables
	 * @param bool $is_special Mark as a special template
	 * @param bool $enable_token Enable token
	 */
	function parse($name, $section, $variables=array(), $is_special=false, $enable_token=false) {
		
		global $functions;
		
		if ( $this->parse_special_templates_only && !$is_special )
			return;
		
		//
		// Load the template set
		//
		$this->load_section($section);
		
		if ( !array_key_exists($name, $this->templates[$section]) ) {
			
			usebb_debug_output('Template '.$name.' in section '.$section.' not found.');
			$this->templates[$section][$name] = '';
			
		}

		$variables = ( is_array($variables) && count($variables) ) ? $variables : array();

		if ( $enable_token )
			$this->install_token($variables);

		$this->requests[] = array(
			'section' => $section,
			'template_name' => $name,
			'variables' => $variables,
		);
		
	}

	/**
	 * Install token
	 *
	 * Called by parse(). Inserts form token into first variable containing input field.
	 *
	 * @param array $variables Template variables
	 */
	function install_token(&$variables) {

		global $functions;
		
		//
		// Find a variable where an input field was added.
		// This location is good to add a hidden field without breaking HTML validity.
		//
		$alter_key = null;
		foreach ( $variables as $key => $val ) {

			if ( strpos($val, '<input ') !== false ) {

				$alter_key = $key;
				break;

			}

		}
		
		if ( !isset($alter_key) )
			trigger_error('No template variable with input field found to install form token!', E_USER_ERROR);

		$token = $functions->generate_token();
		$variables[$alter_key] .= '<input type="hidden" name="_form_token_" value="'.$token.'" />';

	}
	
	/**
	 * Add global template variables
	 *
	 * @param array $variables Template variables
	 * @param bool $override Override variables when already exist
	 */
	function add_global_vars($variables, $override=false) {
		
		foreach ( $variables as $key => $val ) {
			
			if ( $override || !array_key_exists($key, $this->global_vars) )
				$this->global_vars[$key] = $val;
			
		}
		
	}

	/**
	 * Clear all breadcrumbs
	 */
	function clear_breadcrumbs() {

		$this->breadcrumbs = array();

	}

	/**
	 * Add breadcrumb
	 *
	 * @param string $title Title
	 * @param array $link_args Arguments to make_url()
	 * @param string $hash URL hash
	 */
	function add_breadcrumb($title, $link_args=NULL, $hash=NULL) {

		$this->breadcrumbs[] = array($title, $link_args, $hash);

	}
	
	/**
	 * Set the page title
	 *
	 * @param string $page_title Page title (may be HTML)
	 *
	 * Deprecated - reimplemented for backwards compatibility using breadcrumbs.
	 */
	function set_page_title($page_title) {
		
		global $functions;

		usebb_debug_output('$template->set_page_title is deprecated.');

		$parts = explode($this->get_config('locationbar_item_delimiter'), $page_title);
		foreach ( $parts as $part )
			$this->add_breadcrumb($part);
		
	}
	
	/**
	 * Add raw content outside templates
	 *
	 * @param string $content Raw content to place between the templates
	 * @param bool $strip_slashes Strip slashes from $content (true by default)
	 */
	function add_raw_content($content, $strip_slashes=true) {
		
		$this->requests[] = array(
			'raw' => true,
			'num' => count($this->raw_contents)
		);
		
		$this->raw_contents[] = ( $strip_slashes ) ? stripslashes($content) : $content;
		
	}
	
	/**
	 * Add a JavaScript onload statement
	 *
	 * @param string $statement JavaScript statement
	 */
	function set_js_onload($statement) {
		
		if ( substr($statement, -1) == ';' )
			$statement = substr_replace($statement, '', -1);
		
		if ( empty($statement) || in_array($statement, $this->js_onload) )
			return;
		
		$this->js_onload[] = $statement;
		
	}
	
	/**
	 * Replace all whitespace by a space except in <textarea /> and <pre />
	 *
	 * @param string $string Source code to compress
	 * @returns string Compressed source code
	 */
	function compress_sourcecode($string) {
		
		$matches = array();
		preg_match_all("#<textarea.*?>(.*?)</textarea>#is", $string, $matches[0]);
		preg_match_all("#<pre.*?>(.*?)</pre>#is", $string, $matches[1]);
		preg_match_all("#<script.*?>(.*?)</script>#is", $string, $matches[2]);
		$matches = array_merge($matches[0][0], $matches[1][0], $matches[2][0]);
		foreach ( $matches as $oldpart ) {
			
			$newpart = str_replace("\n", "\0", $oldpart);
			$string = str_replace($oldpart, $newpart, $string);
			
		}
		$string = str_replace("\r", "", $string);
		$string = preg_replace("#\s+#", ' ', $string);
		$string = str_replace("\0", "\n", $string);
		return $string;
		
	}

	/**
	 * Generate breadcrumbs variables
	 */
	function generate_breadcrumbs() {

		global $functions;
		
		$breadcrumbs_last_index = count($this->breadcrumbs) - 1;
		$breadcrumbs_isset = $breadcrumbs_last_index > -1;
		$breadcrumbs_delim = $this->get_config('breadcrumbs_item_delimiter');

		//
		// Backwards compatibility
		//
		if ( empty($breadcrumbs_delim) ) {

			$this->config_debug = false;
			$breadcrumbs_delim = $this->get_config('locationbar_item_delimiter');
			$this->config_debug = true;

		}

		if ( $breadcrumbs_isset ) {

			//
			// Add index link to start when set
			//
			array_unshift($this->breadcrumbs, array(
				unhtml($functions->get_config('board_name')), 
				array('index.php')
			));
			$breadcrumbs_last_index++;
			
			$breadcrumbs_butfirst = array();
			$breadcrumbs_butlast = array();

			for ( $i = 0; $i <= $breadcrumbs_last_index; $i++ ) {

				if ( isset($this->breadcrumbs[$i][1]) ) {

					$link = call_user_func_array(array(&$functions, 'make_url'), $this->breadcrumbs[$i][1]);
					$link .= ( !empty($this->breadcrumbs[$i][2]) ) ? '#'.$this->breadcrumbs[$i][2] : '';
					$item = '<a href="'.$link.'">'.$this->breadcrumbs[$i][0].'</a>';

				} else {
				
					$item = $this->breadcrumbs[$i][0];

				}

				if ( $i > 0 )
					$breadcrumbs_butfirst[] = $item;

				if ( $i < $breadcrumbs_last_index )
					$breadcrumbs_butlast[] = $item;
				else
					$breadcrumbs_last = $this->breadcrumbs[$i][0];
				
			}

			$breadcrumbs_all = array_merge($breadcrumbs_butlast, array($breadcrumbs_last));

			$breadcrumbs_butfirst = implode($breadcrumbs_delim, $breadcrumbs_butfirst);
			$breadcrumbs_butlast = implode($breadcrumbs_delim, $breadcrumbs_butlast);
			$breadcrumbs_all = implode($breadcrumbs_delim, $breadcrumbs_all);

		} else {

			$breadcrumbs_butfirst = $breadcrumbs_butlast = $breadcrumbs_last = $breadcrumbs_all = '';

		}

		$breadcrumbs_butfirst_nolinks = strip_tags($breadcrumbs_butfirst);
		$this->add_global_vars(array(
			'breadcrumbs_butfirst' => $breadcrumbs_butfirst,
			'breadcrumbs_butlast' => $breadcrumbs_butlast,
			'breadcrumbs_last' => $breadcrumbs_last,
			'breadcrumbs_all' => $breadcrumbs_all,

			'breadcrumbs_butfirst_nolinks' => $breadcrumbs_butfirst_nolinks,
			'breadcrumbs_butlast_nolinks' => strip_tags($breadcrumbs_butlast),
			'breadcrumbs_all_nolinks' => strip_tags($breadcrumbs_all),

			//
			// Backwards compatibility
			//
			'page_title' => $breadcrumbs_butfirst_nolinks,
			'location_bar' => $breadcrumbs_all
		));

	}
	
	/**
	 * Output the page body
	 *
	 * This method parses all the template data.
	 */
	function body() {
		
		global $db, $functions, $lang, $session;
		
		$body = '';
		
		//
		// Eventually set the content type and charset
		//
		$content_type = $this->get_config('content_type');
		if ( empty($this->content_type) )
			$this->content_type = ( !empty($content_type) ) ? $content_type : 'text/html';
		if ( empty($this->character_encoding) )
			$this->character_encoding = $lang['character_encoding'];
		
		//
		// application/xhtml+xml check
		// Sends as text/html when browser does not support XHTML or XHTML header is disabled
		//
		$this->content_type = ( preg_match('#^application/(xhtml\+)?xml$#i', $this->content_type) && ( empty($_SERVER['HTTP_ACCEPT']) || strstr($_SERVER['HTTP_ACCEPT'], $this->content_type) === false || $functions->get_config('disable_xhtml_header') ) ) ? 'text/html' : $this->content_type;
		
		//
		// Set content type and charset
		//
		header('Content-Type: '.$this->content_type.'; charset='.$this->character_encoding);
		
		//
		// Set cache control
		// Borrowed from phpBB 2.0.0 branch
		//
		if ( !empty($_SERVER['SERVER_SOFTWARE']) && strstr($_SERVER['SERVER_SOFTWARE'], 'Apache/2') )
			header('Cache-Control: no-cache, pre-check=0, post-check=0');
		else
			header('Cache-Control: private, pre-check=0, post-check=0, max-age=0');
		header('Expires: 0');
		header('Pragma: no-cache');
		
		//
		// Debug features
		//
		if ( $functions->get_config('debug') != DEBUG_DISABLED && empty($session->sess_info['ip_banned']) ) {
			
			//
			// Timer for checking parsetime
			//
			$timer_end = explode(' ', microtime());
			$timer_end = (float)$timer_end[1] + (float)$timer_end[0];
			$parsetime = round($timer_end - TIMER_BEGIN, 4);
			
			$debug_info = array();
			$debug_info[] = $lang['ParseTime'].': '.$parsetime.' s';
			if ( function_exists('memory_get_peak_usage') )
				$debug_info[] = $lang['MemoryUsage'].': '.sprintf('%.2f', (memory_get_peak_usage() / 1024 / 1024)).' '.$lang['MegaByteShort'];
			if ( ( $server_load = $functions->get_server_load() ) == true )
				$debug_info[] = $lang['ServerLoad'].': '.sprintf('%.2f', $server_load);
			$debug_info[] = $lang['TemplateSections'].': '.count($this->loaded_sections);
			$debug_info[] = $lang['SQLQueries'].': '.count($db->get_used_queries());
			
			if ( $functions->get_config('debug') == DEBUG_SIMPLE ) {
				
				//
				// List parsetime and queries in short
				//
				$debug_info_small = sprintf($this->get_config('debug_info_small'), join($this->get_config('item_delimiter'), $debug_info));
				$debug_info_large = '';
				
			} elseif ( $functions->get_config('debug') == DEBUG_EXTENDED ) {
				
				//
				// Lists parsetime and queries fully
				//
				$debug_info_small = '';
				$debug_info_large = sprintf($this->get_config('debug_info_large'), '<div><strong>'.$lang['DebugMode'].'</strong>'.$this->get_config('item_delimiter').join($this->get_config('item_delimiter'), $debug_info).':</div><textarea rows="10" cols="50" readonly="readonly">'.unhtml(join("\n\n", $db->get_used_queries())).'</textarea>');
				
			}
			
		} else {
			
			$debug_info_small = '';
			$debug_info_large = '';
			
		}
		$this->add_global_vars(array(
			'debug_info_small' => $debug_info_small,
			'debug_info_large' => $debug_info_large
		));
		unset($debug_info, $debug_info_small, $debug_info_large);

		//
		// Breadcrumbs
		//
		$this->generate_breadcrumbs();
		
		//
		// Add some global template variables such as content type and charset
		//
		$this->add_global_vars(array(
			'content_type' => $this->content_type,
			'character_encoding' => $this->character_encoding,
			'language_code' => ( !empty($lang['language_code']) ) ? $lang['language_code'] : 'en',
			'text_direction' => ( !empty($lang['text_direction']) ) ? $lang['text_direction'] : 'ltr',
			'img_dir' => ROOT_PATH.'templates/'.$functions->get_config('template').'/gfx/',
			'css_url' => ROOT_PATH.'templates/'.$functions->get_config('template').'/styles.css',
			'acp_css_head_link' => ( !defined('IS_INSTALLER') && $session->sess_info['location'] == 'admin' ) ? '<link rel="stylesheet" type="text/css" href="'.ROOT_PATH.'templates/'.$functions->get_config('template').'/admin.css" />' : '',
			'js_onload' => ( count($this->js_onload) ) ? ' onload="javascript:'.join(';', $this->js_onload).'"' : '',
			'more_css_classes' => '',
		));
		
		//
		// Parse all templates
		//
		foreach ( $this->requests as $request ) {
			
			if ( isset($request['raw']) ) {
				
				$body .= "\n".$this->raw_contents[$request['num']]."\n";
				continue;
				
			}
			
			$finds = $replaces = array();			
			foreach ( $request['variables'] as $key => $val ) {
				
				$finds[] = '{'.$key.'}';
				$replaces[] = str_replace(array('{', '}', '$'), array('&#123;', '&#125;', '&#36;'), $val);
				
			}
			$current_template = $this->templates[$request['section']][$request['template_name']];
			$body .= str_replace($finds, $replaces, $current_template);
			
		}
		unset($current_template);
		
		//
		// Parse global and language variables
		//
		$finds = $replaces = array();
		foreach ( $this->global_vars as $key => $val ) {
			
			$finds[] = '{'.$key.'}';
			$replaces[] = ( substr($key, 0, 3) == 'js_' ) ? $val : str_replace(array('{', '}', '$'), array('&#123;', '&#125;', '&#36;'), $val);
			
		}
		foreach ( $lang as $key => $val ) {
			
			if ( !is_string($val) || strpos($body, '{l_'.$key.'}') === false )
				continue;
			
			$finds[] = '{l_'.$key.'}';
			$replaces[] = $val;
			
		}
		$body = str_replace($finds, $replaces, $body);
		unset($finds, $replaces);
		
		//
		// Compression and output
		//
		if ( (int)$functions->get_config('output_compression') % 2 == 1 )
			$body = $this->compress_sourcecode($body);
		
		//
		// Clean bad ASCII characters
		//
		if ( strtolower($this->character_encoding) != 'utf-8' ) {
			
			$ascii = range(0, 31);
			unset($ascii[9], $ascii[10], $ascii[13]); // tab, newline and carriage return
			
			$finds = $replaces = '';
			foreach ( $ascii as $val ) {
				
				$finds .= chr($val);
				$replaces .= '.';
				
			}
			$body = strtr($body, $finds, $replaces);
			
		}
		
		ob_end_clean();
		echo trim($body);
		
	}
	
}

?>
Return current item: UseBB