<?php
/**
 * This file is part of the mobileOK Pythia plug-in for Wordpress and defines the
 * {@link mobileOKPythiaForWordpress} class that contains most of the logic of the
 * mobileOK Pythia plug-in.
 * 
 * @author Sylvain Lequeux
 * @author Francois Daoust <fd@w3.org>
 * @package mobileOKPythia
 * @subpackage Wordpress
 * @version $Revision: 1.16 $
 * @link http://www.w3.org/2009/11/mobileOKPythia/plugin-wordpress.html mobileOK Pythia for WordPress
 * @license http://www.w3.org/Consortium/Legal/2002/copyright-software-20021231.html W3C Software Notice and License
 * @copyright Copyright (c) 2009, W3C (MIT, ERCIM, Keio)
 */

/**
 * Include required files.
 */
// AskPythia's DDR Service configuration
require_once(dirname(__FILE__) . '/../common/plugins/DDRServiceConfigurationFactory.php');
require_once(dirname(__FILE__) . '/WURFLServiceConfiguration.php'); 
// TransPythia 
require_once(dirname(__FILE__) . '/../common/transcoding/transcoder.php');
require_once(dirname(__FILE__) . '/../common/transcoding/transcodingaction.php');
// AskPythia
require_once(dirname(__FILE__) . '/../common/ddrsimpleapi/interface/serviceFactory.php');
// Possible errors will be reported using AskPythia's SystemException class
require_once(dirname(__FILE__) . '/../common/ddrsimpleapi/interface/systemException.php');


/**
 * The mobileOKPythiaForWordpress class contains the logic of the
 * mobileOK Pythia plugin for Wordpress for regular non-admin pages.
 * 
 * The class wraps the common AskPythia and TransPythia libraries for
 * use in Wordpress, and registers the hooks handled by the plugin to
 * generate mobileOK content.
 * 
 * The mobileOK Pythia plugin is designed to help generate a mobileOK
 * version of a blog.
 *
 * @author Sylvain Lequeux
 * @author Francois Daoust <fd@w3.org>
 * @package mobileOKPythia
 * @subpackage Wordpress
 * @version $Revision: 1.16 $
 * @link http://www.w3.org/2009/11/mobileOKPythia/plugin-wordpress.html mobileOK Pythia for WordPress
 * @license http://www.w3.org/Consortium/Legal/2002/copyright-software-20021231.html W3C Software Notice and License
 * @copyright Copyright (c) 2009, W3C (MIT, ERCIM, Keio)
 */
class mobileOKPythiaForWordpress {
	/**
	 * @var Service The interface to use to retrieve device properties from an
	 *              underlying Device Description Repository (DDR). 
	 */
	private $service;
	/**
	 * @var Evidence Evidence that uniquely identifies the requesting device.
	 */
	private $evidence;
	
	/**
	 * @var Link HTTP header to return with the page that targets the POWDER file.
	 */
	private $powderLink;
	
	/** 
	 * @var bool True when the requesting device is identified as mobile
	 */
	private $isMobile;

	/**
	 * Creates an empty instance of the mobileOKPythia class
	 * 
	 * @return A new mobileOKPythia instance.
	 */
	public function __construct() {
	}
	
	/**
	 * Initializes this instance, i.e. registers the hooks and
	 * prepares the identification of the requesting device.
	 */
	public function init() {
		// Load internationalization properties
		$plugin_dir = basename(dirname(__FILE__));
		load_plugin_textdomain('mobileOKPythia', 'wp-content/plugins/' . $plugin_dir, $plugin_dir);
		
		// Initialize AskPythia
		$ddrsimpleapi_implementation = get_option("mobileOKPythia_ddrsimpleapi_implementation");
		$config = DDRServiceConfigurationFactory::getServiceConfiguration(
			$ddrsimpleapi_implementation, NULL);
		$this->service = ServiceFactory::newService(
			$ddrsimpleapi_implementation, 
			'http://www.w3.org/2008/01/ddr-core-vocabulary',
			$config->getConfig());
		
		// Initialize the evidence that identifies the requesting device.
		$map = array();
		foreach ($_SERVER as $name=>$value) {
			if (strpos($name, 'HTTP_') == 0) {
				$name = str_replace('_', '-', substr($name, strlen('HTTP_')));
				$map[$name] = $value;
			}
		}
		$this->evidence = $this->service->newHTTPEvidenceM($map);
		
		// Sets global mobile flag 
		$propName = $this->service->newPropertyNameSS(
			TranscodingAction::$WURFL_MOBILE_DEVICE,
			TranscodingAction::$WURFL_VOCABULARY);
		$propRef = $this->service->newPropertyRefPn($propName);
		$propValue = $this->service->getPropertyValueEPr($this->evidence, $propRef);
		if ($propValue && $propValue->getBoolean()) {
			$this->isMobile = true;
		}
		else {
			$this->isMobile = false;
		}
		
		// Register actions and filters that update HTTP headers
		add_action('wp_headers', array($this, 'getHttpHeaders'));
		add_filter('pre_option_html_type', array($this, 'getContentType'));
		
		// Hi-jack the number of posts and comments displayed per page
		// when browsing the site on a mobile device.
		add_filter('pre_option_posts_per_page', array($this, 'getNumberOfPostsPerPage'));
		add_filter('pre_option_comments_per_page', array($this, 'getNumberOfCommentsPerPage'));
		add_filter('get_comments_pagenum_link', array($this, 'addPaginationParameter'));
		
		// Template switching
		if (get_option('mobileOKPythia_template_switching') == 'yes') {
			add_filter('template', array($this, 'switchTemplate'));
			add_filter('stylesheet', array($this, 'switchTemplate'));
		}
		
		// Delete registered scripts when the requesting device does not support them
		if(get_option('mobileOKPythia_delete_script') == "yes"){
			add_action('wp_print_scripts', array($this, 'deleteRegisteredScripts'));
		}
		
		// Transcode the content of the articles
		add_action('the_content', array($this, 'transcodeContent'));
				
		// Add POWDER link and make sure POWDER file was created
		if (get_option('mobileOKPythia_link_powder') == 'yes') {
			$this->addPowderLink();
		}
	}
	
	
	/**
	 * Make sure that content can be cached a bit.
	 * 
	 * TODO: adds the possibility to define the max-age setting.
	 * 
	 * @param $headers List of HTTP headers that are about to be sent.
	 * @return array(string=>string) List of HTTP headers to send.
	 */
	public function getHttpHeaders($headers) {
		$adaptedHeaders = Array();
		$vary = '';
		
		if (get_option('mobileOKPythia_template_switching') == "yes") {
			$vary .= 'User-Agent';
		}
		if (get_option('mobileOKPythia_serve_xhtml') == "yes") {
			if ($vary != '') {
				$vary .= ',';
			}
			$vary .= 'Accept';
		}
		
		foreach ($headers as $name=>$value) {
			$lname = strtolower($name);
			
			// TODO: Default HTTP headers in Wordpress prevent caching
			// but this may have been overwritten by some other plug-in.
			if ($lname == 'cache-control') {
			}
			else if ($lname == 'expires') {
			}
			else if ($lname == 'pragma') {
			}
			else if ($lname == 'vary') {
				// TODO: The following is slightly wrong, as the value
				// could already be a list of values!
				$lvalue = strtolower($value);
				if (($lvalue != 'user-agent')
				&& ($lvalue != 'accept')) {
					$vary .= ',' . $value;
				}
			}
			else {
				$adaptedHeaders[$name] = $value;
			}
		}
		$adaptedHeaders['Cache-Control'] = 'max-age=60, must-revalidate';
		$adaptedHeaders['Vary'] = $vary;
		
		// Append POWDER Link HTTP header if needed
		if ($this->powderLink != '') {
			$adaptedHeaders['Link'] = $this->powderLink;
		}
		
		return $adaptedHeaders;
	}
	
	
	/**
	 * Determines the Content-Type that should be used based on the
	 * Accept HTTP header sent by the requesting device.
	 * 
	 * @return string 'application/xhtml+xml' or 'text/html', depending
	 *   on whether the requesting device supports the XHTML media type
	 */
	public function getContentType() {
		if (get_option('mobileOKPythia_serve_xhtml') == "yes") {
			if ($_SERVER['HTTP_ACCEPT']) {
				if (strpos($_SERVER['HTTP_ACCEPT'], 'application/xhtml+xml') !== false) {
					// TODO: handle the case where the XHTML media type is
					// explicitly set with a qvalue of 0  
					return 'application/xhtml+xml';
				}
			}
		}
		
		return 'text/html';
	}

	
	/**
	 * Switches template based on the requesting device.
	 * 
	 * A mobile template is used when the requesting device is identified as
	 * mobile, the regular desktop template is used otherwise.
	 * 
	 * This method is a wrapper around the {@link TranscodingActionSwitchTemplate}
	 * transcoding action.
	 */
	public function switchTemplate() {
		$transcoder = new Transcoder($this->service);
		$trans = $transcoder->newTranscodingAction('SwitchTemplate');
		$transcoder->addTranscodingAction($trans);
		
		$default_template = get_option('template');
		$mobile_template = get_option('mobileOKPythia_mobile_template');
		
		$trans->setOption('default', $default_template);
		$trans->setOption('mobile', $mobile_template);
		
		$template = $transcoder->apply('', $this->evidence);
		
		return $template;
	}
	
	
	/**
	 * Deletes scripts that registered in Wordpress when the requesting
	 * device does not support scripting.
	 * 
	 * This method is a wrapper around the
	 * {@link TranscodingActionDeleteWordpressScripts}
	 * transcoding action.
	 */
	function deleteRegisteredScripts(){
		global $wp_scripts;
		
		if(!isset($wp_scripts)){
			//If there is no script in the page
			return true;
		}
		
		require_once(dirname(__FILE__) . '/transcodingactiondeletewordpressscripts.php');
		
		$transcoder = new Transcoder($this->service);
		$trans = $transcoder->newTranscodingAction('DeleteWordpressScripts');
		$transcoder->addTranscodingAction($trans);
		$transcoder->apply($wp_scripts, $this->evidence);
		unset($transcoder);
		
		return;
	}
	
	
	/**
 	* Transcodes content before delivery.
 	* 
 	* @param $content string The content to transcode.
 	* @return string transcoded content.
 	*/
	function transcodeContent($content){
		// TODO: RSS feeds should perhaps be truncated as well
		if (is_feed()) {
			return $content;			
		}
		
		try {
			$transcoder = $this->getTranscoder();
			$content = $transcoder->apply($content, $this->evidence);
			unset($transcoder);
			return $content;
		}
		catch(SystemException $e) {
			$message = 'An error has occured in the content analysis. Error ' . $e->getCode()
				. ' (' . $e->getFile() . ':'. $e->getLine() . ')' . ' : '.$e->getMessage();
			return $message;
		}
	}

	
	/**
	 * Retrieves a transcoder that applies to HTML content.
	 * 
	 * The transcoding actions are selected based on the options of the plugin.
	 * 
	 * @return Transcoder a transcoder initialized with transcoding actions.
	 */
	function getTranscoder() {
		$transcoder = new Transcoder($this->service);	
		
		if(get_option("mobileOKPythia_delete_script") == 'yes'){
			$trans = $transcoder->newTranscodingAction('DeleteScript');
			$transcoder->addTranscodingAction($trans);
		}
		
		if(get_option("mobileOKPythia_delete_popup") == 'yes'){
			$trans = $transcoder->newTranscodingAction('DeletePopup');
			$transcoder->addTranscodingAction($trans);
		}
		
		if(get_option("mobileOKPythia_delete_embeds") == 'yes'){
			$trans = $transcoder->newTranscodingAction('DeleteEmbeds');
			$transcoder->addTranscodingAction($trans);
		}
		
		if (get_option('mobileOKPythia_serve_xhtml') == "yes") {
			$trans = $transcoder->newTranscodingAction('ReplaceEntities');
			$transcoder->addTranscodingAction($trans);
		}
		
		if(get_option("mobileOKPythia_resize_img")=="yes"){
			$trans = $transcoder->newTranscodingAction('ResizeIMG');
			
			// Adjust maximum image size if user is browsing the home page
			$maxSize = intVal(get_option('mobileOKPythia_max_image_size')) * 1024;
			 
			// If we're on the home page, the thing is we have to split the
			// maximum size between the different posts that appear on the
			// home page. We can but operate at the post level, so the only
			// thing we can do is to divide the available size between the
			// different posts. 
			// TODO: a smarter algorithm would give more image space to the first
			// (most recent) post, and less space to other (older) posts.
			$requestedUri = 'http://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
			if ($requestedUri == (get_option('home') . '/')) {
				$maxSize /= intVal(get_option('posts_per_page'));
				$maxSize = intVal($maxSize);
			}
			$trans->setOption('max_image_size', $maxSize);
			
			if (defined('WORDPRESS_ROOT_FOLDER')) {
				$rootFolder = WORDPRESS_ROOT_FOLDER;
			}
			else {
				$fileName = dirname(__FILE__);
				$rootFolder = substr($fileName, 0, strpos($fileName, 'wp-content'));
			}
			
			$trans->setOption('img_cache', $rootFolder . get_option('mobileOKPythia_img_cache'));
			$trans->setOption('img_cache_uri', get_option('home') . '/' . get_option('mobileOKPythia_img_cache'));
			$trans->setOption('base_uri', $requestedUri);
			$trans->setOption('uri_mappings', get_option('home') . '/|' . $rootFolder);
			
			$transcoder->addTranscodingAction($trans);
			
		}
		
		if(get_option("mobileOKPythia_linear_tables")=="yes"){
			$trans = $transcoder->newTranscodingAction('LinearTables');
			
			$transcoder->addTranscodingAction($trans);		
		}
		
		if(get_option('mobileOKPythia_pagination') == "yes"){
			$trans = $transcoder->newTranscodingAction('Pagination');
			
			// Adjust maximum weight based on the average weight of the remaining content,
			// and on whether we are browsing the home page or not.
			$maxSize = intVal(get_option('mobileOKPythia_max_weight')) * 1024;
			
			// CSS Stylesheet and other divs in the page usually account for
			// about 8Kb.
			$maxSize -= 8 * 1024;
			
			// Allowed markup size should be half of the total size
			$maxMarkupSize = intVal($maxSize / 2);
			 
			// If we're on the home page, the thing is we have to split the
			// maximum size between the different posts that appear on the
			// home page. We can but operate at the post level, so the only
			// thing we can do is to divide the available size between the
			// different posts. 
			// TODO: a smarter algorithm would give more space to the first
			// (most recent) post, and less space to other (older) posts.
			$requestedUri = 'http://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
			if ($requestedUri == (get_option('home') . '/')) {
				$maxSize /= intVal(get_option('posts_per_page'));
				$maxSize = intVal($maxSize);
				
				// Replace prev/next links on the home page by a "Read more" link
				$trans->setOption('nav_block',
					'<div><p>[...] <a href="' . get_page_link(get_the_id()) . '">Read more</a></p></div>');
			}
			
			$trans->setOption('max_size', $maxSize);
			$trans->setOption('max_markup_size', $maxMarkupSize);
			$trans->setOption('page_index', intVal($_GET['pagination'] ? $_GET['pagination'] : 1));
			$trans->setOption('base_uri', $requestedUri);
			
			if (defined('WORDPRESS_ROOT_FOLDER')) {
				$rootFolder = WORDPRESS_ROOT_FOLDER;
			}
			else {
				$fileName = dirname(__FILE__);
				$rootFolder = substr($fileName, 0, strpos($fileName, 'wp-content'));				
			}			
			$trans->setOption('uri_mappings', get_option('home') . '/|' . $rootFolder);
		
			$transcoder->addTranscodingAction($trans);
		}
		return $transcoder;
	}
	
	/**
 	* Serves the page with an HTTP Link header to a POWDER file
 	* that asserts that the blog is mobileOK.
 	*/
	function addPowderLink(){
		// Import the POWDER generator factory
		require_once(dirname(__FILE__) . '/../common/powder/powder.php' );
		
		// Create the mobileOK POWDER file for the blog if not already done
		$powder = MobileOKPowderFactory::generatePowderFile(
			get_option('home'),
			realpath(dirname(__FILE__) . '/../../../../') . get_option('mobileOKPythia_powder_file'));
		if($powder == NULL){
			throw new SystemException(
				'The POWDER file could not be generated.',
				SystemException::$CANNOT_PROCEED
			);
		}
		
		// Prepare the Link HTTP header to the mobileOK POWDER file
		$this->powderLink = "<".get_bloginfo('wpurl').'/wp-content/plugins/mobileOKPythia/common/powder/powder.xml>; rel="describedby"; type="application/powder+xml";';
		
		return;
	}
	
	
	/**
	 * Forces a low number of posts per page when browsing the site on a mobile
	 * device.
	 * 
	 * @return 2 when requesting device is a mobile, false otherwise
	 *   (will use default value) 
	 */
	function getNumberOfPostsPerPage() {
		if ($this->isMobile) {
			return 3;
		}
		else {
			return false;
		}
	}
	
	/**
	 * Forces a low number of comments per page when browsing the site on
	 * a mobile device.
	 * 
	 * @return 2 when requesting device is a mobile, false otherwise
	 *   (will use default value)
	 */
	function getNumberOfCommentsPerPage() {
		if ($this->isMobile) {
			return 2;
		}
		else {
			return false;
		}
	}
	
	/**
	 * Adds the pagination parameter to the URI when browsing through
	 * comments on a mobile device.
	 * 
	 * @param string $uri The URI of the prev/next link to comments
	 * @return string The updated URI
	 */
	function addPaginationParameter($uri) {
		$pageIndex = intVal($_GET['pagination'] ? $_GET['pagination'] : '1');
		if ($pageIndex > 1) {
			$hash = strpos($uri, '#');
			if ($hash !== false) {
				$uri = substr($uri, 0, $hash)
					. '&amp;pagination='
					. $pageIndex
					. substr($uri, $hash);
			}
			else {
				$uri .= '&amp;pagination='
					. $pageIndex;		
			}
		}
		return $uri;
	}
}
?>