wizard.php 13.2 KB
<?php
// namespace administrator\components\com_jmap\models;
/**
 * @package JMAP::WIZARD::administrator::components::com_jmap
 * @subpackage models
 * @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');
jimport('joomla.filesystem.file');

/**
 * Wizard model responsibilities
 *
 * @package JMAP::WIZARD::administrator::components::com_jmap
 * @subpackage models
 * @since 2.0
 */
interface IJMapModelWizard {
	/**
	 * Main get data method
	 *
	 * @access public
	 * @return Object[]
	 */
	public function getData($path = null);

	/**
	 * Creational method for extensions data source
	 *
	 * @access public
	 * @return Void It doesn't require a return value because called by ajax and manage exceptions app queue
	 */
	public function createEntityProcess();
	
	/**
	 * Try to evaluate if any substitutions to perform is available for extension and data source
	 * chosen, if any perform it calling doSubstitutions, otherwise original string untouched is returned
	 *
	 * @access public
	 * @param string $sqlString
	 * @return array
	 */
	public function getSubstitutionsOnDemand($sqlString);
}

/**
 * Wizard business logic to auto-create extensions data source <<testable_behavior>>
 *
 * @package JMAP::WIZARD::administrator::components::com_jmap
 * @subpackage models
 * @since 2.0
 */
class JMapModelWizard extends JMapModel implements IJMapModelWizard {
	/**
	 * JSON manifest file path for target extension data source to create
	 *
	 * @access private
	 * @var string
	 */
	private $manifestFilePath;
	
	/**
	 * Language tag code used for replacement of placeholders
	 *
	 * @access private
	 * @var string
	 */
	private $language;
	
	/**
	 * Langtag code used for replacement of placeholders
	 *
	 * @access private
	 * @var string
	 */
	private $langtag;
	
	/**
	 * Deserialized manifest object for specific extension wizard informations
	 *
	 * @access protected
	 * @var Object
	 */
	protected $manifestObject;
	
	/**
	 * Name of detected extension to create data source for
	 *
	 * @access protected
	 * @var string
	 */
	protected $extension;
	
	/**
	 * User array sorting service function
	 *
	 * @access private
	 * @param array $a
	 * @param array $b
	 * @return boolean
	 */
	private function cmp($a, $b) {
		if ($a['dataSourceName'] == $b['dataSourceName']) {
			return 0;
		}
		return ($a['dataSourceName'] < $b['dataSourceName']) ? -1 : 1;
	}

	/**
	 * Do replacement for placeholders on JSON manifest chunks for fields to inject into REQUEST
	 * 
	 * @access protected
	 * @param Object $manifest
	 * @return void
	 */
	protected function doReplacement($manifest) {
		// Check if substitutions are set on manifest object
		if(property_exists($manifest, 'placeholders')) {
			// Get manifest object source and detect if any placeholders to replace exists
			if(!empty($manifest->placeholders)) {
				// Iterate on object properties
				foreach ($manifest->placeholders as $postfield=>$toReplace) {
					// Do replacement in $manifest referenced object
					if(!property_exists($manifest->postfields, $postfield)) {
						throw new JMapException(JText::_('COM_JMAP_ERROR_NOPROPERTY_EXISTS_POSTFIELDS'), 'error');
					}
					// Do evaluate
					if(!property_exists($this, $toReplace[1])) {
						throw new JMapException(JText::_('COM_JMAP_ERROR_NOPROPERTY_EXISTS_MODELOBJECT'), 'error');
					}
					$evaluatedValue = $this->{$toReplace[1]};
					// Do replace
					$manifest->postfields->{$postfield} = str_ireplace($toReplace[0], $evaluatedValue, $manifest->postfields->{$postfield});
				}
			}
		}
	}

	/**
	 * Do substitution for auto generated raw SQL query string placed in DB table field after creation process
	 * After substitute chunks in SQL query string for the table field perform an ORM table update
	 * 
	 * @access protected
	 * @param string $sqlString
	 * @param Object $manifest
	 * @return string
	 */
	protected function doSubstitution($sqlString, $manifest) {
		// Check if substitutions are set on manifest object
		if(property_exists($manifest, 'substitutions')) {
			// Check if is array and not empty
			if(!empty($manifest->substitutions)) {
				foreach ($manifest->substitutions as $substitution) {
					$sqlString = str_ireplace($substitution[0], $substitution[1], $sqlString);
				}
			}
		}
		
		return $sqlString;
	}

	/**
	 * Load the JSON format manifest file from file system based on extension wizard configuration folder
	 * Once deserialized contents of manifest file it assigns object to $manifestObject local member property
	 * 
	 * @access protected
	 * @param string $fileName
	 * @return Object
	 */
	protected function loadManifestFile($fileName) {
		// Check if file exists and is valid manifest
		if(!$fileName || !file_exists($fileName)) {
			throw new JMapException(JText::_('COM_JMAP_ERROR_NOMANIFEST_FOUND'), 'error');
		}
		
		// Load the manifest serialized file and assign to local variable
		$manifestContents = file_get_contents($fileName);
		
		// Unserialize data and assign object data to local manifestObject property
		$this->manifestObject = json_decode($manifestContents);
		if(!$this->manifestObject) {
			throw new JMapException(JText::_('COM_JMAP_ERROR_MANIFEST_FORMAT'), 'error');
		}
		
		// Return unserialized manifest object
		return $this->manifestObject;
	}

	/**
	 * Inject from manifest file object array containing sqlquery_managed/postfields and params/querystringlinkparams properties
	 * into the global REQUEST array to imitate a standard POST to sources model from a user compiled form
	 * 
	 * @access protected
	 * @param Object $manifest
	 * @return void
	 */
	protected function injectRequestField($manifest) {
		// Inject fields into POST HTTP request
		
		/**
		 * Mapping is:
		 * postfields = $this->requestArray[$this->requestName]['sqlquery_managed'][]
		 * querystringlinkparams = $this->requestArray[$this->requestName]['params'][]
		 */
		//Inject data source name first
		$dataSourceUserfriendlyName = ucfirst(str_replace('_', ' ', $this->extension));
		$this->requestArray[$this->requestName]['name'] = $dataSourceUserfriendlyName;
		$this->requestArray[$this->requestName]['params']['datasource_extension'] = $this->extension;
		
		// Missing required data to construct data source creation
		if(!property_exists($manifest, 'postfields')) {
			throw new JMapException(JText::_('COM_JMAP_ERROR_INVALID_MANIFEST_MISSING_POSTFIELDS'), 'error');
		}
		
		// Error in object syntax or not valid object syntax
		if(!is_object($manifest->postfields)) {
			throw new JMapException(JText::_('COM_JMAP_ERROR_INVALID_MANIFEST_OBJECT_POSTFIELDS'), 'error');
		}
		
		//Cycle and store postfields REQUIRED
		foreach ($manifest->postfields as $fieldName=>$fieldValue) {
			// Store in superglobal POST array
			$this->requestArray[$this->requestName]['sqlquery_managed'][$fieldName] = $fieldValue;
		}
		
		//Cycle and store querystringlinkparams OPTIONAL
		if(property_exists($manifest, 'querystringlinkparams')) {
			// Error in object syntax or not valid object syntax
			if(!is_object($manifest->querystringlinkparams)) {
				throw new JMapException(JText::_('COM_JMAP_ERROR_INVALID_MANIFEST_OBJECT_QSLINKPARAMS'), 'error');
			}
			foreach ($manifest->querystringlinkparams as $fieldName=>$fieldValue) {
				// Store in superglobal POST array
				$this->requestArray[$this->requestName]['params'][$fieldName] = $fieldValue;
			}
		}
		
	}

	/**
	 * Try to evaluate if any substitutions to perform is available for extension and data source
	 * chosen, if any perform it calling doSubstitutions, otherwise original string untouched is returned
	 *
	 * @access public
	 * @param string $sqlString
	 * @return array
	 */
	public function getSubstitutionsOnDemand($sqlString) {
		// Load manifest file
		$this->manifestFilePath = JPATH_COMPONENT . '/images/wizard/' . $this->extension . '/manifest.json';
		
		// Manifest object could exists or not for specified data source extension
		if(file_exists($this->manifestFilePath)) {
			// Load the manifest serialized file and assign to local variable
			$manifestContents = file_get_contents($this->manifestFilePath);
			// Unserialize data and assign object data to local manifestObject property
			$this->manifestObject = json_decode($manifestContents);
			$sqlString = $this->doSubstitution($sqlString, $this->manifestObject);
		}
		
		return $sqlString;
	}
	
	/**
	 * Main get data method that retrieve and array containing informations to build up a graphic interface
	 * for supported extensions data source creational types both with extension name and icon
	 * It makes a directory listings for supported extensions returning array for data source entity name
	 * and data source entity icon found
	 *
	 * @access public
	 * @param string $path Inject the path where are placed subfolders for supported data source entities
	 * @return array
	 */
	public function getData($path = null) {
		// Init extensions discovery array
		$discoveredExtensions = array();
		
		try {
			if(!$path || !is_dir($path)) {
				throw new JMapException(JText::_('COM_JMAP_EMPTY_ERROR_PATH'), 'error');
			}
			if(!class_exists('DirectoryIterator')) {
				throw new JMapException(JText::_('COM_JMAP_SPL_LIBRARY_NOTFOUND_OLDPHPVERSION'), 'error');
			}
			// Discover supported extensions data sources, get available subfolders and assigned names primary key
			$iterator = new DirectoryIterator($path);
			foreach ($iterator as $subfolder) {
				// get only directory and not dotted
				if($subfolder->isDir() && !$subfolder->isDot()) {
					$folderName = $subfolder->getFilename();
					$innerPath = $path . '/' . $folderName . '/';
					// Check if valid folder name, not to contains spaces or extra characters
					if(!preg_match('/[^0-9A-Za-z_]/', $folderName)) {
						$innerPath = $path . '/' . $folderName . '/';
						
						// Try first method that relies on glob if not disabled by safe mode
						if(function_exists('glob')) {
							$extensionFileName = @array_pop(glob($innerPath . "*.txt"));
							$extensionEffectiveName = str_replace($innerPath, '', $extensionFileName);
							$extensionEffectiveName = str_replace('.txt', '', $extensionEffectiveName);
							$discoveredExtensions[] = array('dataSourceName'=>$folderName, 'extensionName'=>$extensionEffectiveName);
						} else {
							// Do all scan with SPL libraries
							$subIterator = new DirectoryIterator($innerPath);
							foreach ($subIterator as $file) {
								$subFileName = $file->getFilename();
								if(preg_match('/.*\.txt/i', $subFileName)) {
									$extensionFileName = $subFileName;
									$extensionEffectiveName = str_replace($innerPath, '', $extensionFileName);
									$extensionEffectiveName = str_replace('.txt', '', $extensionEffectiveName);
									$discoveredExtensions[] = array('dataSourceName'=>$folderName, 'extensionName'=>$extensionEffectiveName);
								}
							}
						}
					}
				}
			}
			// Natural sorting array
			if(!empty($discoveredExtensions)) {
				uasort($discoveredExtensions, array($this, 'cmp'));	
			}
		} catch (JMapException $e) {
			$this->app->enqueueMessage($e->getMessage(), $e->getErrorLevel());
		}
		
		// Return discovered extensions array
		return $discoveredExtensions;
	}

	/**
	 * Main get data method that retrieve and array containing informations to build up a graphic interface
	 * for supported extensions data source creational types both with extension name and icon
	 *
	 * @access public
	 * @return boolean
	 */
	public function createEntityProcess() {
		// Start creating process for data source
		try {
			// Load manifest file
			$this->manifestFilePath = JPATH_COMPONENT . '/images/wizard/' . $this->extension . '/manifest.json';
			$this->loadManifestFile($this->manifestFilePath);
			
			// Do replacement if any
			$this->doReplacement($this->manifestObject);
			
			// Inject request field into REQUEST
			$this->injectRequestField($this->manifestObject);
			
			// Call data source creation model
			$createdDataSourceTable = $this->sourcesModel->storeEntity(true, true);
			
			// Do substitutions on generated raw query if any....
			$sqlString = $createdDataSourceTable->sqlquery;
			$createdDataSourceTable->sqlquery = $this->doSubstitution($sqlString, $this->manifestObject);
			// ....finally update $createdDataSourceTable with new sqlquery if changed by substitutions
			if($sqlString != $createdDataSourceTable->sqlquery) {
				if (! $createdDataSourceTable->store (false)) {
					throw new JMapException($createdDataSourceTable->getError (), 'error');
				}
			}
		} catch (JMapException $e) {
			$this->setError($e);
			return false;
		}  catch(Exception $e) {
			$jmapException = new JMapException($e->getMessage(), 'error');
			$this->setError($jmapException);
			return false;
		}
		
		return true;
	}
	
	/**
	/* Class constructor
	 * 
	 * @access public
	 * @param $config array
	 * @return Object&
	 */
	public function __construct(array $config = array()) {
		parent::__construct($config);
		
		$langParams = JComponentHelper::getParams('com_languages');
		$this->langtag = $langParams->get('site');
		// Setup predefined site language for placeholders replacement according to RFC 3066 and Virtuemart tables
		$this->language = str_replace('-', '_', strtolower($this->langtag));
		if(!empty($config)) {
			$this->extension = $config['extension'];
			$this->sourcesModel = $config['sourcesModel'];
		}
	}

}