View file upload/library/XenForo/DataWriter/Template.php

File size: 16.14Kb
<?php

/**
* Data writer for templates.
*
* @package XenForo_Template
*/
class XenForo_DataWriter_Template extends XenForo_DataWriter
{
	/**
	 * Option that takes the path to the development template output directory.
	 * If not specified, output will not be written. Defaults determined based
	 * on config settings.
	 *
	 * @var string
	 */
	const OPTION_DEV_OUTPUT_DIR = 'devOutputDir';

	/**
	 * Option that controls whether a full compile is performed when the template
	 * is modified. If false, the template is only parsed into segments. Defaults
	 * to true, but should be set to false for bulk imports; compilation should
	 * happen in the second pass.
	 *
	 * @var string
	 */
	const OPTION_FULL_COMPILE = 'fullCompile';

	/**
	 * Option that controls whether a test compile will be performed when setting
	 * the value of a template. If false, the error will only be detected when a
	 * full compile is done.
	 *
	 * Note that this does not prevent the template from being parsed. That will
	 * always happen.
	 *
	 * @var string
	 */
	const OPTION_TEST_COMPILE = 'testCompile';

	/**
	 * Option that controls if template map is rebuild when template is changed. Defaults to true.
	 *
	 * @var string
	 */
	const OPTION_REBUILD_TEMPLATE_MAP = 'rebuildTemplateMap';

	/**
	 * If false, duplicate checking is disabled. An error will occur on dupes. Defaults to true.
	 *
	 * @var string
	 */
	const OPTION_CHECK_DUPLICATE = 'checkDuplicate';

	/**
	 * Title of the phrase that will be created when a call to set the
	 * existing data fails (when the data doesn't exist).
	 *
	 * @var string
	 */
	protected $_existingDataErrorPhrase = 'requested_template_not_found';

	/**
	 * If an array, updates the template modification status list
	 *
	 * @var array|null
	 */
	protected $_modificationStatuses = null;

	/**
	* Gets the fields that are defined for the table. See parent for explanation.
	*
	* @return array
	*/
	protected function _getFields()
	{
		return array(
			'xf_template' => array(
				'template_id'       => array('type' => self::TYPE_UINT,   'autoIncrement' => true),
				'title'             => array('type' => self::TYPE_STRING, 'required' => true, 'maxLength' => 50,
						'verification' => array('$this', '_verifyPrepareTitle'), 'requiredError' => 'please_enter_valid_title'),
				'style_id'          => array('type' => self::TYPE_UINT,   'required' => true),
				'template'          => array('type' => self::TYPE_STRING, 'verification' => array('$this', '_verifyPrepareTemplate'), 'noTrim' => true),
				'template_parsed'   => array('type' => self::TYPE_BINARY),
				'addon_id'          => array('type' => self::TYPE_STRING, 'maxLength' => 25, 'default' => ''),
				'version_id'        => array('type' => self::TYPE_UINT,   'default' => 0),
				'version_string'    => array('type' => self::TYPE_STRING, 'maxLength' => 30, 'default' => ''),
				'disable_modifications' => array('type' => self::TYPE_BOOLEAN, 'default' => 0),
				'last_edit_date'    => array('type' => self::TYPE_UINT, 'default' => 0),
			)
		);
	}

	/**
	* Gets the actual existing data out of data that was passed in. See parent for explanation.
	*
	* @param mixed
	*
	* @return array|false
	*/
	protected function _getExistingData($data)
	{
		if (!$template_id = $this->_getExistingPrimaryKey($data))
		{
			return false;
		}

		return array('xf_template' => $this->_getTemplateModel()->getTemplateById($template_id));
	}

	/**
	* Gets SQL condition to update the existing record.
	*
	* @return string
	*/
	protected function _getUpdateCondition($tableName)
	{
		return 'template_id = ' . $this->_db->quote($this->getExisting('template_id'));
	}

	/**
	* Gets the default set of options for this data writer.
	* If in debug mode and we have a development directory config, we set the template
	* dev output directory automatically.
	*
	* @return array
	*/
	protected function _getDefaultOptions()
	{
		$options = array(
			self::OPTION_DEV_OUTPUT_DIR => '',
			self::OPTION_FULL_COMPILE => true,
			self::OPTION_TEST_COMPILE => true,
			self::OPTION_REBUILD_TEMPLATE_MAP => true,
			self::OPTION_CHECK_DUPLICATE => true
		);

		$config = XenForo_Application::get('config');
		if ($config->debug)
		{
			$options[self::OPTION_DEV_OUTPUT_DIR] = $this->_getTemplateModel()->getTemplateDevelopmentDirectory();
		}

		return $options;
	}

	/**
	 * Verifies that the provided template title contains only valid characters
	 *
	 * @param string Title
	 *
	 * @return boolean
	 */
	protected function _verifyPrepareTitle(&$title)
	{
		if (preg_match('/[^a-zA-Z0-9_\.]/', $title))
		{
			$this->error(new XenForo_Phrase('please_enter_title_using_only_alphanumeric_dot'), 'title');
			return false;
		}

		return true;
	}

	/**
	* Verification callback to prepare a template. This isn't actually a verifier;
	* it just automatically compiles the template.
	*
	* @param string $string Uncompiled template
	*
	* @return boolean
	*/
	protected function _verifyPrepareTemplate($template)
	{
		$standardParse = true;
		$parsed = null;

		if (!$this->get('disable_modifications'))
		{
			$templateWithModifications = $this->_getModificationModel()->applyModificationsToTemplate(
				$this->get('title'), $template, $modificationStatuses
			);
		}
		else
		{
			$modificationStatuses = null;
			$templateWithModifications = $template;
		}

		if ($modificationStatuses)
		{
			try
			{
				$compiler = new XenForo_Template_Compiler($templateWithModifications);
				$parsed = $compiler->lexAndParse();

				if ($this->getOption(self::OPTION_TEST_COMPILE))
				{
					$compiler->setFollowExternal(false);
					$compiler->compileParsed($parsed, $this->get('title'), 0, 0);
				}
				$standardParse = false;
			}
			catch (XenForo_Template_Compiler_Exception $e)
			{
				foreach ($modificationStatuses AS &$status)
				{
					if (is_int($status))
					{
						$status = 'error_compile';
					}
				}
			}
		}

		if ($standardParse)
		{
			try
			{
				$compiler = new XenForo_Template_Compiler($template);
				$parsed = $compiler->lexAndParse();

				if ($this->getOption(self::OPTION_TEST_COMPILE))
				{
					$compiler->setFollowExternal(false);
					$compiler->compileParsed($parsed, $this->get('title'), 0, 0);
				}
			}
			catch (XenForo_Template_Compiler_Exception $e)
			{
				$this->error($e->getMessage(), 'template');
				return false;
			}
		}

		$this->set('template_parsed', serialize($parsed));
		$this->_modificationStatuses = $modificationStatuses;
		return true;
	}

	public function reparseTemplate()
	{
		$template = $this->get('template');
		$this->_verifyPrepareTemplate($template);
	}

	/**
	 * Helper to get the developer data output directory only if it is enabled
	 * and applicable to this situation.
	 *
	 * @return string
	 */
	protected function _getDevOutputDir()
	{
		if ($this->get('style_id') == 0 && $this->get('addon_id') == 'XenForo')
		{
			return $this->getOption(self::OPTION_DEV_OUTPUT_DIR);
		}
		else
		{
			return '';
		}
	}

	/**
	 * Verifies that the specified title is not a duplicate
	 */
	protected function _preSave()
	{
		if ($this->isInsert() && !$this->isChanged('template') && !$this->getError('template'))
		{
			$this->error(new XenForo_Phrase('template_value_has_not_been_set_properly'), 'template');
		}

		if ($this->getOption(self::OPTION_CHECK_DUPLICATE))
		{
			if ($this->isInsert() || $this->get('title') != $this->getExisting('title'))
			{
				$titleConflict = $this->_getTemplateModel()->getTemplateInStyleByTitle($this->getNew('title'), $this->get('style_id'));
				if ($titleConflict)
				{
					$this->error(new XenForo_Phrase('template_titles_must_be_unique'), 'title');
				}
			}
		}

		if (
			($this->isChanged('addon_id') || $this->isChanged('title') || $this->isChanged('template'))
			&& !$this->isChanged('version_id')
		)
		{
			$this->updateVersionId();
		}

		if (($this->isChanged('template') || $this->isChanged('version_id')) && !$this->isChanged('last_edit_date'))
		{
			$this->set('last_edit_date', XenForo_Application::$time);
		}
	}

	/**
	* Post-save handler.
	*/
	protected function _postSave()
	{
		$templateModel = $this->_getTemplateModel();

		if ($this->isUpdate() && $this->isChanged('template'))
		{
			$this->_db->insert('xf_template_history', array(
				'title'     => $this->get('title'),
				'style_id'  => $this->get('style_id'),
				'template'  => $this->getExisting('template'),
				'edit_date' => $this->getExisting('last_edit_date'),
				'log_date'  => XenForo_Application::$time
			));
		}

		if (is_array($this->_modificationStatuses))
		{
			$this->_getModificationModel()->updateTemplateModificationLog($this->get('template_id'), $this->_modificationStatuses);
		}

		if ($this->getOption(self::OPTION_REBUILD_TEMPLATE_MAP))
		{
			if ($this->isChanged('title'))
			{
				$templateModel->buildTemplateMap($this->get('title'));
				if ($existingTitle = $this->getExisting('title'))
				{
					if ($this->getOption(self::OPTION_FULL_COMPILE))
					{
						// need to recompile anything including this template
						$mappedTemplates = $templateModel->getMappedTemplatesByTemplateId($this->get('template_id'));
						$mappedTemplateIds = array();
						foreach ($mappedTemplates AS $mappedTemplate)
						{
							$mappedTemplateIds[] = $mappedTemplate['template_map_id'];
						}

						$templateModel->buildTemplateMap($existingTitle);

						$templateModel->compileMappedTemplatesInStyleTree(
							$templateModel->getIncludingTemplateMapIds($mappedTemplateIds)
						);
					}
					else
					{
						$templateModel->buildTemplateMap($existingTitle);
					}
				}
			}
			else if ($this->isChanged('style_id'))
			{
				$templateModel->buildTemplateMap($this->get('title'));
			}
		}

		if ($this->getOption(self::OPTION_FULL_COMPILE))
		{
			XenForo_Template_Compiler::removeTemplateFromCache($this->get('title'));
			XenForo_Template_Compiler::removeTemplateFromCache($this->getExisting('title'));

			$this->_recompileTemplate();

			$this->getModelFromCache('XenForo_Model_Style')->updateAllStylesLastModifiedDate();
			$this->getModelFromCache('XenForo_Model_AdminTemplate')->updateAdminStyleLastModifiedDate();
		}

		if ($devDir = $this->_getDevOutputDir())
		{
			$this->_writeDevFileOutput($devDir);
		}
	}

	/**
	 * Recompiles the changed template and any templates that include it.
	 */
	protected function _recompileTemplate()
	{
		$templateModel = $this->_getTemplateModel();

		$compiledMapIds = $templateModel->compileNamedTemplateInStyleTree($this->get('title'), $this->get('style_id'));
		$templateModel->compileMappedTemplatesInStyleTree($templateModel->getIncludingTemplateMapIds($compiledMapIds));
	}

	/**
	* Writes the development file output to the specified directory. This will write
	* each template into an individual file for easier tracking in source control.
	*
	* @param string Path to directory to write to
	*/
	protected function _writeDevFileOutput($dir)
	{
		$title = $this->get('title');
		$newFile = $dir . '/' . $title . '.html';

		if (!is_dir($dir) || !is_writable($dir))
		{
			throw new XenForo_Exception("Template development directory $dir is not writable");
		}

		$fp = fopen($newFile, 'w');
		fwrite($fp, $this->get('template'));
		fclose($fp);

		$this->_writeMetaDataDevFileOutput($dir, $title, $this->getMergedData());

		if ($this->isUpdate() && $this->isChanged('title'))
		{
			$this->_deleteExistingDevFile($dir);
		}
	}

	protected function _writeMetaDataDevFileOutput($dir, $title, $data)
	{
		$metaDataFile = $dir . '/_metadata.xml';
		XenForo_Helper_DevelopmentXml::writeMetaDataOutput(
			$metaDataFile, $title, $data, array('version_id', 'version_string')
		);
	}

	/**
	 * Post-delete handler.
	 */
	protected function _postDelete()
	{
		$this->_db->delete('xf_template_modification_log',
			'template_id = ' . $this->_db->quote($this->get('template_id'))
		);

		$recompileTemplates = $this->_deleteMappedData();

		if ($recompileTemplates && $this->getOption(self::OPTION_FULL_COMPILE))
		{
			if ($recompileTemplates === true)
			{
				// new template still exists in this position -- recompile it and follow includes
				$this->_recompileTemplate();
			}
			else
			{
				// template no longer exists -- recompile includes
				$this->_getTemplateModel()->compileMappedTemplatesInStyleTree($recompileTemplates);
			}
		}

		if ($this->getOption(self::OPTION_FULL_COMPILE))
		{
			$this->getModelFromCache('XenForo_Model_Style')->updateAllStylesLastModifiedDate();
			$this->getModelFromCache('XenForo_Model_AdminTemplate')->updateAdminStyleLastModifiedDate();
		}

		if ($devDir = $this->_getDevOutputDir())
		{
			$this->_deleteExistingDevFile($devDir);
		}
	}

	/**
	 * Deletes mapped data (template map entries, includes, compiled info) and determines
	 * what templates (if any need to be recompiled). A deletion can be a "revert" or it
	 * can actually remove a template from the hierarchy.
	 *
	 * @return boolean|array If true, recompile this template and includes; if array, recompile specified map IDs; else, no recompile
	 */
	protected function _deleteMappedData()
	{
		$templateModel = $this->_getTemplateModel();

		$mappedTemplates = $templateModel->getMappedTemplatesByTemplateId($this->get('template_id'));
		if ($mappedTemplates)
		{
			$myTemplateMapId = 0;
			$templateMapIds = array();
			$styleIds = array();
			foreach ($mappedTemplates AS $mappedTemplate)
			{
				if ($mappedTemplate['style_id'] == $this->get('style_id'))
				{
					$myTemplateMapId = $mappedTemplate['template_map_id'];
				}

				$templateMapIds[] = $mappedTemplate['template_map_id'];
				$styleIds[] = $mappedTemplate['style_id'];
			}

			$templateMapIdsQuoted = $this->_db->quote($templateMapIds);

			$parentMappedTemplate = $templateModel->getParentMappedTemplateByTitle($this->get('title'), $this->get('style_id'));
			if ($parentMappedTemplate)
			{
				// point everything pointing at this template to the parent
				$this->_db->update('xf_template_map',
					array('template_id' => $parentMappedTemplate['template_id']),
					'template_map_id IN (' . $templateMapIdsQuoted . ')'
				);

				// template_include and template_compiled will be updated on a recompile
				return true;
			}
			else
			{
				// no parent, remove template - this should primarily happen when deleting a master or custom template
				$this->_db->delete('xf_template_map', 'template_map_id IN (' . $templateMapIdsQuoted . ')');
				$this->_db->delete('xf_template_phrase', 'template_map_id IN (' . $templateMapIdsQuoted . ')');
				$this->_db->delete('xf_template_include', 'source_map_id IN (' . $templateMapIdsQuoted . ')');
				$this->_db->delete('xf_template_compiled',
					'style_id IN (' . $this->_db->quote($styleIds) . ') AND title = ' . $this->_db->quote($this->get('title'))
				);
				if (XenForo_Application::get('options')->templateFiles)
				{
					XenForo_Template_FileHandler::delete($this->get('title'), $styleIds, null);
				}

				if ($myTemplateMapId)
				{
					// need to recompile includes
					return $templateModel->getIncludingTemplateMapIds($myTemplateMapId);
				}
			}
		}

		return false;
	}

	/**
	 * Deletes the corresponding file when a template is deleted from the database
	 *
	 * @param string Path to admin templates directory
	 */
	protected function _deleteExistingDevFile($dir)
	{
		$existingTitle = $this->getExisting('title');
		$templateFile = $dir . '/' . $existingTitle . '.html';

		if (file_exists($templateFile))
		{
			if (!is_writable($templateFile))
			{
				throw new XenForo_Exception("Template development file $dir is not writable");
			}
			unlink($templateFile);

			$this->_writeMetaDataDevFileOutput($dir, $existingTitle, false);
		}
	}

	/**
	 * @return XenForo_Model_Template
	 */
	protected function _getTemplateModel()
	{
		return $this->getModelFromCache('XenForo_Model_Template');
	}

	/**
	 * @return XenForo_Model_TemplateModification
	 */
	protected function _getModificationModel()
	{
		return $this->getModelFromCache('XenForo_Model_TemplateModification');
	}
}