precacher.php 8.39 KB
<?php
// namespace components\com_jmap\libraries\xml;
/**
 *
 * @package JMAP::FRAMEWORK::components::com_jmap
 * @subpackage framework
 * @subpackage xml
 * @author Joomla! Extensions Store
 * @copyright (C) 2015 - Joomla! Extensions Store
 * @license GNU/GPLv2 http://www.gnu.org/licenses/gpl-2.0.html
 */
defined ( '_JEXEC' ) or die ( 'Restricted access' );

/**
 * Sitemaps XML precacher public responsibilities
 *
 * @package JMAP::FRAMEWORK::components::com_jmap
 * @subpackage framework
 * @subpackage xml
 * @since 2.3
 */
interface IJMapXmlPrecacher {
	/**
	 * Merge the sitemap generated in the current iteration in the
	 * precaching file, temp file during iterations process, renamed at the end
	 *
	 * @access public
	 * @param string $sitemapIterationData
	 *        	The formatted XML string for the current sitemap iteration generation
	 * @return Object
	 */
	public function mergeSitemap($sitemapIterationData = null);
}

/**
 * Precacher for XML sitemaps
 * This class is responsible to manage correct merge for every generation iteration,
 * it has to strip out start, both, end tags urlset based on process status and
 * finally write precaching file to disk renaming it only if process is finished
 * and last ajax request is detected
 *
 * <<testable_behavior>>
 *
 * @package JMAP::FRAMEWORK::components::com_jmap
 * @subpackage libraries
 * @subpackage xml
 * @since 2.3
 */
class JMapXmlPrecacher implements IJMapXmlPrecacher {
	/**
	 * Path to store precaching files
	 *
	 * @access private
	 * @var string
	 */
	private $preCachingPath;
	
	/**
	 * File name based on the hash of posted params and sietmap type/format
	 *
	 * @access private
	 * @var string
	 */
	private $fileName;
	
	/**
	 * Once finished processing and after renaming and finalizing file try
	 * to get and send to JS client the filemtime to show in the green label
	 *
	 * @access private
	 * @var string
	 */
	private $finalFileMTime;
	
	/**
	 * Object to perform file writing tasks in write append mode
	 * Support stream context
	 *
	 * @access protected
	 * @var string
	 */
	protected $fileStreamWriter;
	
	/**
	 * True if the ajax request is the first in the whole process
	 * If = 'start' the start tag is not stripped out and the close tag is stripped out
	 * If = 'run' both tags are stripped out we are in the middle of the processing
	 * If = 'end' the close tag is not stripped out and the start tag is stripped out
	 *
	 * @access protected
	 * @var string
	 */
	protected $processStatus;
	
	/**
	 * Application reference
	 *
	 * @access protected
	 * @var Object
	 */
	protected $app;
	
	/**
	 * Strip the start/end tags based on process status
	 *
	 * @access protected
	 * @param string $xmlData        	
	 * @return string
	 */
	protected function stripSitemapTags($xmlData) {
		// Data are missing
		if (! $xmlData && $this->processStatus != 'end') {
			return null;
		}
		
		// Evaluate process status and strip iteration tags accordingly
		switch ($this->processStatus) {
			case 'start' :
				$xmlData = preg_replace ( "/<\/urlset(.|\s)*?>/i", '', $xmlData );
				$xmlData = rtrim ( $xmlData, PHP_EOL );
				break;
			
			case 'end' :
				$xmlData = '</urlset>';
				break;
			
			case 'run' :
			default :
				$xmlData = preg_replace ( "/<\/?\?xml(.|\s)*?>/i", null, $xmlData );
				$xmlData = preg_replace ( "/<urlset(.|\s)*?>/i", null, $xmlData );
				$xmlData = preg_replace ( "/<\/urlset(.|\s)*?>/i", null, $xmlData );
				$xmlData = trim ( $xmlData, PHP_EOL );
				break;
		}
		
		return $xmlData;
	}
	
	/**
	 * Write precaching file on disk accordingly to request params hash
	 * If processStatus is detected as end, the temp named file is renamed to
	 * final name that will be used by main display controller as precached sitemap
	 *
	 * @access protected
	 * @return boolean
	 */
	protected function writeFile($data) {
		// Never write if no data
		if(!$data) {
			return false;
		}
		
		// Manage file name as temp, to avoid that not complete operations have resulting broken sitemap files
		$tempFileName = $this->preCachingPath . 'temp_' . $this->fileName;
		
		// Delete any pre-existant incomplete temp processing files - otherwise the result would be append again
		if($this->processStatus == 'start' && file_exists($tempFileName)) {
			if(!JFile::delete($tempFileName)) {
				throw new JMapExceptionPrecaching ( JText::_ ( 'COM_JMAP_PRECACHING_ERROR_DELETE_TEMPFILE'), 'error', 'delete_temp_file' );
			}
		}
		
		// Open file in write append mode
		if (! $this->fileStreamWriter->open ( $tempFileName, 'a' )) {
			throw new JMapExceptionPrecaching ( JText::sprintf ( 'COM_JMAP_PRECACHING_ERROR_OPENING_FILE', $this->fileStreamWriter->getError () ), 'error', 'open_file' );
		}
		
		// Write append to file
		if ($data) {
			// Add always new line if process status is not start
			if ($this->processStatus != 'start') {
				$data = PHP_EOL . $data;
			}
			
			// Try to append data to precaching file
			$result = $this->fileStreamWriter->write ( $data );
			
			// Something went wrong
			if (! $result) {
				throw new JMapExceptionPrecaching ( JText::sprintf ( 'COM_JMAP_PRECACHING_ERROR_WRITING_FILE', $this->fileStreamWriter->getError () ), 'error', 'write_file' );
			}
		}
		
		// Finished operation, close file handle
		if (! $this->fileStreamWriter->close ()) {
			throw new JMapExceptionPrecaching ( JText::sprintf ( 'COM_JMAP_PRECACHING_ERROR_CLOSING_FILE', $this->fileStreamWriter->getError () ), 'error', 'close_file' );
		}
		
		// Check if process status has ended successfully, and if so try to rename temp file to be ready to use
		if ($this->processStatus == 'end') {
			if (! rename ( $tempFileName, $this->preCachingPath . $this->fileName )) {
				throw new JMapExceptionPrecaching ( JText::_ ( 'COM_JMAP_PRECACHING_ERROR_RENAMING_FILE' ), 'error', 'rename_file' );
			}
			// Set filemtime
			$joomlaConfig = JFactory::getConfig();
			$localTimeZone = new DateTimeZone($joomlaConfig->get('offset'));
			$lastGenerationTimestamp = filemtime ( $this->preCachingPath . $this->fileName );
			$dateObject = new JDate($lastGenerationTimestamp);
			$dateObject->setTimezone($localTimeZone);
			$this->finalFileMTime = $dateObject->format('Y-m-d', true);
		}
		
		return true;
	}
	
	/**
	 * Merge the sitemap generated in the current iteration in the
	 * precaching file, temp file during iterations process, renamed at the end
	 *
	 * @access public
	 * @param string $sitemapIterationData
	 *        	The formatted XML string for the current sitemap iteration generation, if the injected data is null it means
	 *        	that we are on the last end iteration so no need to grab data from model but only append the </urlset>
	 * @return Object
	 */
	public function mergeSitemap($sitemapIterationData = null) {
		// Response JSON object
		$response = new stdClass ();
		
		try {
			// Strip tags according to process status
			$strippedData = $this->stripSitemapTags ( $sitemapIterationData );
			
			// Write sitemap iteration data to file
			$this->writeFile ( $strippedData, $this->fileName );
			
			// If process ended and filemtime is set, return to js app to show inside label
			if($this->finalFileMTime) {
				$response->lastgeneration = $this->finalFileMTime;
			}
		} catch ( JMapExceptionPrecaching $e ) {
			$response->result = false;
			$response->exception_message = $e->getMessage ();
			$response->context = $e->getContext ();
			return $response;
		} catch ( Exception $e ) {
			$jmapException = new JMapExceptionPrecaching ( $e->getMessage (), 'error', 'joomla_framework' );
			$response->result = false;
			$response->exception_message = $jmapException->getMessage ();
			$response->context = $jmapException->getContext ();
			return $response;
		}
		
		// Manage exceptions from DB Model and return to JS domain
		$response->result = true;
		
		return $response;
	}
	
	/**
	 * Class constructor
	 *
	 * @access public
	 * @param string $fileName        	
	 * @param Object $fileWriter        	
	 * @return Object&
	 */
	public function __construct($fileName, JStream $fileWriter) {
		$this->app = JFactory::getApplication ();
		
		// Set precaching path
		$this->preCachingPath = JPATH_COMPONENT_SITE . '/precache/';
		
		// Set precaching filename based on sitemap params
		$this->fileName = $fileName;
		
		// Object to perform file storing tasks
		$this->fileStreamWriter = $fileWriter;
		
		// Set process status based on JS App Client
		$this->processStatus = $this->app->input->get ( 'process_status' );
		
		// Set always to null, value is available only after end of process
		$this->finalFileMTime = null;
	}
}