<?php
/**
 * This file is part of the mobileOK Pythia plug-in for Joomla! and defines the
 * {@link plgSystemMobileOKPythia} 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 Joomla
 * @version $Revision: 1.20 $
 * @link http://www.w3.org/2009/11/mobileOKPythia/plugin-joomla.html mobileOK Pythia for Joomla!
 * @license http://www.gnu.org/licenses/gpl.html GNU General Public License v3 or later
 * @copyright Copyright (c) 2009, W3C (MIT, ERCIM, Keio)
 */

// No direct access allowed to this file
defined( '_JEXEC' ) or die( 'Restricted access' );


/**
 * Import Joomla! libraries.
 */
jimport('joomla.plugin.plugin');
jimport('joomla.html.toolbar.button');


/**
 * Include required files.
 */
require_once(dirname(__FILE__) . '/mobileOKPythia/common/transcoding/transcoder.php');
require_once(dirname(__FILE__) . '/mobileOKPythia/common/transcoding/transcodingaction.php');
require_once(dirname(__FILE__) . '/mobileOKPythia/common/ddrsimpleapi/interface/serviceFactory.php');
require_once(dirname(__FILE__) . '/mobileOKPythia/includes/WURFLServiceConfiguration.php');
require_once(dirname(__FILE__) . '/mobileOKPythia/includes/W3CmobileOKCheckerButton.php');


/**
 * The plgSystemMobileOKPythia class contains the logic of the
 * mobileOK Pythia plugin for Joomla!
 * 
 * The class wraps the common AskPythia and TransPythia libraries for
 * use in Joomla!, and registers the events handled by the plugin to
 * generate mobileOK content.
 * 
 * The mobileOK Pythia plugin is designed to help generate a mobileOK
 * version of a Web site.
 *
 * @author Sylvain Lequeux
 * @author Francois Daoust <fd@w3.org>
 * @package mobileOKPythia
 * @subpackage Joomla
 * @version $Revision: 1.20 $
 * @link http://www.w3.org/2009/11/mobileOKPythia/plugin-joomla.html mobileOK Pythia for Joomla!
 * @license http://www.gnu.org/licenses/gpl.html GNU General Public License v3 or later
 * @copyright Copyright (c) 2009, W3C (MIT, ERCIM, Keio)
 */
class plgSystemMobileOKPythia extends JPlugin
{
	/**
	 * @var string Flag that identifies a "page break" in Joomla!
	 */
	static private $PAGEBREAK = '<hr class="system-pagebreak" />';
	/**
	 * @var JApplication Local pointer to the underlying JApplication
	 */
	private $app;
	/**
	 * @var bool Flag set when browsing administrative pages
	 */
	private $adminMode;
	/**
	 * @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;
	
	/**
	 * Constructor for the class.
	 *
	 * @param	object	$subject The object to observe
	 * @param 	array   $config  An array that holds the plugin configuration
	 * @since	1.0
	 */
	function plgSystemMobileOKPythia( &$subject, $config ) {
		parent::__construct( $subject, $config );
		global $mainframe;
		
		$this->app = $mainframe;
		
		// Browsing an admin page?
		if ($this->app->isAdmin()) {
			$this->adminMode = true;
		}
		else {
			$this->adminMode = false;
		}
		
		if ($this->adminMode) {
			$this->initAdmin();
		}
		else {
			$this->init();
		}
	}
	
	
	/**
	 * Initializes the plug-in when browsing a regular non-admin page.
	 */
	private function init() {
		// Initialize AskPythia
		$ddrsimpleapi_implementation = $this->params->get("mobileOKPythia_ddrsimpleapi_implementation");
		$config = DDRServiceConfigurationFactory::getServiceConfiguration(
			$ddrsimpleapi_implementation, $this->params);
		$this->service = ServiceFactory::newService(
			$ddrsimpleapi_implementation, 
			'http://www.w3.org/2008/01/ddr-core-vocabulary',
			$config->getConfig());
			
		// Initialize HTTP headers and 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);
		
		// Add POWDER link and make sure POWDER file was created
		if ($this->params->get('mobileOKPythia_link_powder') == 'yes') {
			$this->addPowderLink();
		}
	}

	
	/**
	 * Initializes the plug-in when browsing an admin page.
	 */
	private function initAdmin() {
		// Add a link to the W3C mobileOK Checker when
		// an article is being edited.

		$option = JRequest::getString('option');
		$task = JRequest::getString('task');
		
		if (($option == 'com_content')
		&& (($task == 'edit'))) {
			$bar =& JToolBar::getInstance('toolbar');
			$bar->appendButton('W3CmobileOKChecker', 'W3CmobileOKChecker', false);
		}
	}

	
	/**
	 * Switches template based on the requesting device and forces number of
	 * search results to 5 on mobile devices.
	 */
	function onAfterInitialise() {
		if ($this->adminMode) {
			return;
		}
		
		// For search purpose, restrict number of results returned
		// to 5 when browsing on a device identified as mobile
		// TODO: this should rather be implemented as a transcoding action
		$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()) {
			$uri =& JURI::getInstance();
			if ($uri->getVar("limit") == NULL) {
				$uri->setVar("limit", 5);
			}
		}
		
		if ($this->params->get('mobileOKPythia_template_switching') == 'yes') {
			$this->switchTemplate();
		}
	}
	
	/**
	 * Applies content transcoding at the article level.
	 * 
	 * @param $article A reference to the article that is being rendered by the view.
	 * @param $params A reference to an associative array of relevant parameters.
	 *   The view determines what it considers to be relevant and passes that
	 *   information along.
	 * @param $limitstart An integer that determines the "page" of the content that
	 *   is to be generated. Note that in the context of views that might not
	 *   generate HTML output, a page is a reasonably abstract concept that depends
	 *   on the context.
	 */
	function onPrepareContent(&$article, &$params, $limitstart) {
		if ($this->adminMode) {
			return;
		}
		
		try {
			$transcoder = $this->getTranscoder(true);
			$article->text = $transcoder->apply($article->text, $this->evidence);
			
			if (JRequest::getVar('view') == 'frontpage') {
				// The "Read more" link must be explicitly set at this step
				// on the front page 
				if (strpos($article->text, self::$PAGEBREAK)) {
					$article->text = str_replace(self::$PAGEBREAK, '', $article->text);
                    $article->readmore["readmore"] = true;
				}
			}
			else {
				// If there are multiple pages, the article now ends up with a
				// PAGEBREAK flag that needs to be removed not to create an
				// empty page
				$lastPos = strrpos($article->text, self::$PAGEBREAK);
				if ($lastPos) {
					$article->text = substr($article->text, 0, $lastPos)
						. substr($article->text, $lastPos + strlen(self::$PAGEBREAK));
				}
			}
			
			unset($transcoder);
		}
		catch(Exception $e) {
			echo 'Error while transcoding content. Error ' . $e->getCode() .
				' occured in ' . $e->getFile() . ' line ' . $e->getLine() .
				'. ' . $e->getMessage();
		}		
	}
	
	
	/**
	 * Applies content transcoding at the page level after the framework has
	 * rendered the application and sets a few HTTP headers as required.
	 * 
	 * Pagination is not performed at the page level, because this would result
	 * in a very poor user experience where navigation menus and sidebards may
	 * either be displayed on first page while content is rendered on second page,
	 * or be moved to subsequent pages. Sidebards and menus should rather be
	 * handled in the design through the administrative page or in the mobile
	 * template being used.
	 */
	function onAfterRender() {
		if ($this->adminMode) {
			return;
		}
		
		try {
			// Get prepared content
			$content = JResponse::getBody();
			
			// Set Vary HTTP header
			if ($this->params->get("mobileOKPythia_template_switching") == 'yes') {
				JResponse::setHeader('Vary', 'User-Agent');
			}
			
			// Set content-type according to the Accept HTTP header of the request.
			if ($this->params->get("mobileOKPythia_serve_xhtml") == 'yes') {
				// Content-type depends on the Accept HTTP header,
				// let's tell network proxies!
				JResponse::setHeader('Vary', 'Accept');
				
	      		$contentType = $this->getContentType();
	      		if ($contentType == "application/xhtml+xml") {
		      		JResponse::setHeader('Content-Type', 'application/xhtml+xml; charset=utf-8', true);
	      			$content = preg_replace('|<meta http-equiv="content-type" content="(.*)" />|Usi',
		      			'<meta http-equiv="content-type" content="application/xhtml+xml; charset=utf-8" />',
	      				$content);
	      		}
			}

			// Pagination applies at the article level, the user experience
			// would be rather poor if we applied it at the page level, since
			// sidebars would thus get splitted across pages.
			$transcoder = $this->getTranscoder(false);
			
			// Apply transcoding actions at the page level
			$content = $transcoder->apply($content, $this->evidence);
			
			// Update the response to send
			JResponse::setBody($content);
			
			unset($transcoder);
		}
		catch(Exception $e) {
			echo 'Error while transcoding content. Error ' . $e->getCode() .
				' occured in ' . $e->getFile() . ' line ' . $e->getLine() .
				'. ' . $e->getMessage();
		}		
	}
	
	
	/**
	 * 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 = $this->app->getTemplate();
		$mobile_template = $this->params->get('mobileOKPythia_mobile_template');
		
		$trans->setOption('default', $default_template);
		$trans->setOption('mobile', $mobile_template);
		
		$template = $transcoder->apply('', $this->evidence);
		unset($transcoder);
		
		$this->app->setTemplate(strtolower($template));
		
		return;
	}
	
	/**
	 * Retrieves a transcoder that applies to HTML content.
	 * 
	 * The transcoding actions are selected based on the options of the plugin.
	 * 
	 * @param bool $includePagination True when pagination should be included
	 *   in the returned transcoder, False when not.
	 * @return Transcoder a transcoder initialized with transcoding actions.
	 */
	function getTranscoder($includePagination) {
		// In Joomla!, all pages go through index.php in the root directory 
		$rootUri = JURI::base();
		
		if (defined('JOOMLA_ROOT_FOLDER')) {
			$rootFolder = JOOMLA_ROOT_FOLDER;
		}
		else {
			$fileName = dirname(__FILE__);
			$rootFolder = realpath(substr($fileName, 0, strpos($fileName, 'plugins/'))) . '/';
		}
		
		$transcoder = new Transcoder($this->service);	
		
		if($this->params->get("mobileOKPythia_delete_script") == 'yes'){
			$trans = $transcoder->newTranscodingAction('DeleteScript');
			$transcoder->addTranscodingAction($trans);
		}
		
		if($this->params->get("mobileOKPythia_delete_popup") == 'yes'){
			$trans = $transcoder->newTranscodingAction('DeletePopup');
			$transcoder->addTranscodingAction($trans);
		}
		
		if($this->params->get("mobileOKPythia_delete_embeds") == 'yes'){
			$trans = $transcoder->newTranscodingAction('DeleteEmbeds');
			$transcoder->addTranscodingAction($trans);
		}
		
		if($this->params->get("mobileOKPythia_serve_xhtml") == 'yes'){
			$trans = $transcoder->newTranscodingAction('ReplaceEntities');
			$transcoder->addTranscodingAction($trans);
		}
		
		if($this->params->get("mobileOKPythia_resize_img")=="yes"){
			$trans = $transcoder->newTranscodingAction('ResizeIMG');
			
			// Adjust maximum image size if user is browsing the home page
			$maxSize = intVal($this->params->get('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. 
			if (JRequest::getVar('view') == 'frontpage') {
				// TODO: blocks that appear on the front page should be taken into account
			}
			else {
				// TODO: blocks that appear on pages other than the front page should be taken into account 
			}
			$trans->setOption('max_image_size', $maxSize);		
			$trans->setOption('img_cache', $rootFolder . 'cache/');
			$trans->setOption('img_cache_uri', $rootUri . 'cache/');
			$trans->setOption('base_uri', $rootUri);
			$trans->setOption('uri_mappings', $rootUri . '|' . $rootFolder);

			$transcoder->addTranscodingAction($trans);
			
		}
		
		if($this->params->get('mobileOKPythia_linear_tables') == 'yes'){
			$trans = $transcoder->newTranscodingAction('LinearTables');
			$transcoder->addTranscodingAction($trans);		
		}
		
		if($includePagination && ($this->params->get('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($this->params->get('mobileOKPythia_max_weight')) * 1024;
			
			// CSS Stylesheet and other sections of the page usually account
			// for about 10Kb.
			$maxSize -= 10 * 1024;
			if ($maxSize < 5 * 1024) {
				// Consider 5Kb as a bare minimum for content
				$maxSize = 5 * 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 space to the first
			// (most recent) post, and less space to other (older) posts.
			if (JRequest::getVar('view') == 'frontpage') {
				$maxSize = intval($maxSize / 5);
				// TODO: blocks that appear on the front page should be taken into account
				
				// On the home page, we need to handle the read more link
				// a bit by ourselves as the article should already have
				// been prepared.
				$trans->setOption('page_index', 1);
			}
			else {
				// On an article's page, we just need to flag page breaks
				// and let Joomla! do the actual pagination.
				$trans->setOption('page_index', -1);
			}
			
			$trans->setOption('max_size', $maxSize);
			$trans->setOption('max_markup_size', intVal($maxSize / 2));
			$trans->setOption('nav_block', self::$PAGEBREAK);
			$trans->setOption('base_uri', $rootUri);
			$trans->setOption('uri_mappings', $rootUri . '|' . $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__) . '/mobileOKPythia/common/powder/powder.php' );
		
		// $fileName = dirname(__FILE__);
		// TODO: replace following line by above line!
		$fileName = '/var/www/joomla/plugins/system/mobileOKPythia/includes';
		$rootFolder = realpath(substr($fileName, 0, strpos($fileName, 'plugins/'))) . '/';
		$rootUri = JURI::base();
		
		// Create the mobileOK POWDER file for the blog if not already done
		$powder = MobileOKPowderFactory::generatePowderFile(
			JURI::base(),
			$rootFolder . $this->params->get('mobileOKPythia_powder_file'));
		if($powder == NULL){
			throw new SystemException(
				'The POWDER file could not be generated.',
				SystemException::$CANNOT_PROCEED
			);
		}
		
		// Add the Link HTTP header to the mobileOK POWDER file
		JResponse::setHeader('Link',
			'<'. $rootUri . $this->params->get('mobileOKPythia_powder_file')
			. '>; rel="describedby"; type="application/powder+xml";');
		return;
	}
	
	
	/**
	 * 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 ($_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';
	}
}
?>