Файл: library/XenForo/DataWriter/Template.php
Строк: 408
<?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('template') && !$this->isChanged('last_edit_date'))
{
$this->set('last_edit_date', XenForo_Application::$time);
}
if (
($this->isChanged('addon_id') || $this->isChanged('title') || $this->isChanged('template'))
&& !$this->isChanged('version_id')
)
{
$this->updateVersionId();
}
}
/**
* 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');
}
}