Файл: library/XenForo/Model/Template.php
Строк: 1466
<?php
/**
 * Model for templates
 *
 * @package XenForo_Templates
 */
class XenForo_Model_Template extends XenForo_Model
{
    /**
     * Returns all templates customized in a style in alphabetical title order
     *
     * @param integer $styleId Style ID
     * @param boolean $basicData If true, gets basic data only
     *
     * @return array Format: [title] => (array) template
     */
    public function getAllTemplatesInStyle($styleId, $basicData = false)
    {
        return $this->fetchAllKeyed('
            SELECT ' . ($basicData ? 'template_id, title, style_id, addon_id' : '*') . '
            FROM xf_template
            WHERE style_id = ?
            ORDER BY CONVERT(title USING utf8)
        ', 'title', $styleId);
    }
    /**
     * Get the effective template list for a style. "Effective" means a merged/flattened
     * system where every valid template has a record.
     *
     * This only returns data appropriate for a list view (map id, template id, title).
     * Template_state is also calculated based on whether this template has been customized.
     * State options: default, custom, inherited.
     *
     * @param integer $styleId
     *
     * @return array Format: [] => (array) template list info
     */
    public function getEffectiveTemplateListForStyle($styleId, array $conditions = array(), $fetchOptions = array())
    {
        $whereClause = $this->prepareTemplateConditions($conditions, $fetchOptions);
        $limitOptions = $this->prepareLimitFetchOptions($fetchOptions);
        return $this->_getDb()->fetchAll($this->limitQueryResults('
            SELECT template_map.template_map_id,
                template_map.style_id AS map_style_id,
                template.template_id,
                template.title,
                addon.addon_id, addon.title AS addonTitle,
                IF(template.style_id = 0, 'default', IF(template.style_id = template_map.style_id, 'custom', 'inherited')) AS template_state,
                IF(template.style_id = template_map.style_id, 1, 0) AS canDelete
            FROM xf_template_map AS template_map
            INNER JOIN xf_template AS template ON
                (template_map.template_id = template.template_id)
            LEFT JOIN xf_addon AS addon ON
                (addon.addon_id = template.addon_id)
            WHERE template_map.style_id = ?
                AND ' . $whereClause . '
            ORDER BY CONVERT(template_map.title USING utf8)
        ', $limitOptions['limit'], $limitOptions['offset']), $styleId);
    }
    /**
     * Prepares conditions for searching templates. Often, this search will
     * be done on an effective template set (using the map). Some conditions
     * may require this.
     *
     * @param array $conditions
     * @param array $fetchOptions
     *
     * @return string SQL conditions
     */
    public function prepareTemplateConditions(array $conditions, array &$fetchOptions)
    {
        $db = $this->_getDb();
        $sqlConditions = array();
        if (!empty($conditions['title']))
        {
            if (is_array($conditions['title']))
            {
                $sqlConditions[] = 'CONVERT(template.title USING utf8) LIKE ' . XenForo_Db::quoteLike($conditions['title'][0], $conditions['title'][1], $db);
            }
            else
            {
                $sqlConditions[] = 'CONVERT(template.title USING utf8) LIKE ' . XenForo_Db::quoteLike($conditions['title'], 'lr', $db);
            }
        }
        if (!empty($conditions['template']))
        {
            $caseSensitive = (empty($conditions['template_case_sensitive']) ? '' : 'BINARY ');
            if (is_array($conditions['template']))
            {
                $sqlConditions[] = 'template.template LIKE ' . $caseSensitive . XenForo_Db::quoteLike($conditions['template'][0], $conditions['phrase_text'][1], $db);
            }
            else
            {
                $sqlConditions[] = 'template.template LIKE ' . $caseSensitive . XenForo_Db::quoteLike($conditions['template'], 'lr', $db);
            }
        }
        if (!empty($conditions['template_state']))
        {
            $stateIf = 'IF(template.style_id = 0, 'default', IF(template.style_id = template_map.style_id, 'custom', 'inherited'))';
            if (is_array($conditions['template_state']))
            {
                $sqlConditions[] = $stateIf . ' IN (' . $db->quote($conditions['template_state']) . ')';
            }
            else
            {
                $sqlConditions[] = $stateIf . ' = ' . $db->quote($conditions['template_state']);
            }
        }
        return $this->getConditionsForClause($sqlConditions);
    }
    /**
     * Gets all effective templates in a style. "Effective" means a merged/flattened system
     * where every valid template has a record.
     *
     * @param integer $styleId
     *
     * @return array Format: [] => (array) effective template info
     */
    public function getAllEffectiveTemplatesInStyle($styleId)
    {
        return $this->_getDb()->fetchAll('
            SELECT template_map.template_map_id,
                template_map.style_id AS map_style_id,
                template.*
            FROM xf_template_map AS template_map
            INNER JOIN xf_template AS template ON
                (template_map.template_id = template.template_id)
            WHERE template_map.style_id = ?
            ORDER BY CONVERT(template_map.title USING utf8)
        ', $styleId);
    }
    /**
     * Gets style ID/template ID pairs for all styles where the named template
     * is modified.
     *
     * @param string $templateTitle
     *
     * @return array Format: [style_id] => template_id
     */
    public function getTemplateIdInStylesByTitle($templateTitle)
    {
        return $this->_getDb()->fetchPairs('
            SELECT style_id, template_id
            FROM xf_template
            WHERE title = ?
        ', $templateTitle);
    }
    /**
     * Gets the effective template in a style by its title. This includes all
     * template information and the map ID.
     *
     * @param string $title
     * @param integer $styleId
     *
     * @return array|false Effective template info.
     */
    public function getEffectiveTemplateByTitle($title, $styleId)
    {
        return $this->_getDb()->fetchRow('
            SELECT template_map.template_map_id,
                template_map.style_id AS map_style_id,
                template.*
            FROM xf_template_map AS template_map
            INNER JOIN xf_template AS template ON
                (template.template_id = template_map.template_id)
            WHERE template_map.title = ? AND template_map.style_id = ?
        ', array($title, $styleId));
    }
    /**
     * Gets effective templates in a style by their titles
     *
     * @param array $titles
     * @param integer $styleId
     *
     * @return array|false Effective template info
     */
    public function getEffectiveTemplatesByTitles(array $titles, $styleId)
    {
        if (empty($titles))
        {
            return array();
        }
        return $this->fetchAllKeyed('
            SELECT template.*
            FROM xf_template_map AS template_map
            INNER JOIN xf_template AS template ON
                (template.template_id = template_map.template_id)
            WHERE template_map.title IN(' . $this->_getDb()->quote($titles) . ') AND template_map.style_id = ?
        ', 'title', $styleId);
    }
    /**
     * Gets the effective template based on a known map idea. Returns all template
     * information and the map ID.
     *
     * @param integer $templateMapId
     *
     * @return array|false Effective template info.
     */
    public function getEffectiveTemplateByMapId($templateMapId)
    {
        return $this->_getDb()->fetchRow('
            SELECT template_map.template_map_id,
                template_map.style_id AS map_style_id,
                template.*
            FROM xf_template_map AS template_map
            INNER JOIN xf_template AS template ON
                (template.template_id = template_map.template_id)
            WHERE template_map.template_map_id = ?
        ', $templateMapId);
    }
    /**
     * Gets multiple effective templates based on 1 or more map IDs. Returns all template
     * information and the map ID.
     *
     * @param integery|array $templateMapIds Either one map ID as a scalar or any array of map IDs
     *
     * @return array Format: [] => (array) effective template info
     */
    public function getEffectiveTemplatesByMapIds($templateMapIds)
    {
        if (!is_array($templateMapIds))
        {
            $templateMapIds = array($templateMapIds);
        }
        if (!$templateMapIds)
        {
            return array();
        }
        $db = $this->_getDb();
        return $db->fetchAll('
            SELECT template_map.template_map_id,
                template_map.style_id AS map_style_id,
                template.*
            FROM xf_template_map AS template_map
            INNER JOIN xf_template AS template ON
                (template.template_id = template_map.template_id)
            WHERE template_map.template_map_id IN (' . $db->quote($templateMapIds) . ')
        ');
    }
    /**
     * Gets all the unique templates pointed to by a set of template map IDs.
     *
     * @param array $templateMapIds
     *
     * @return array
     */
    public function getUniqueTemplatesByMapIds($templateMapIds)
    {
        if (!is_array($templateMapIds))
        {
            $templateMapIds = array($templateMapIds);
        }
        if (!$templateMapIds)
        {
            return array();
        }
        $db = $this->_getDb();
        return $this->fetchAllKeyed('
            SELECT DISTINCT template.*
            FROM xf_template_map AS template_map
            INNER JOIN xf_template AS template ON
                (template.template_id = template_map.template_id)
            WHERE template_map.template_map_id IN (' . $db->quote($templateMapIds) . ')
        ', 'template_id');
    }
    public function getMapIdsByTemplateTitles(array $titles)
    {
        if (!$titles)
        {
            return array();
        }
        $db = $this->_getDb();
        return $db->fetchCol("
            SELECT template_map_id
            FROM xf_template_map
            WHERE title IN (" . $db->quote($titles) . ")
        ");
    }
    /**
     * Returns the template specified by template_id
     *
     * @param integer $templateId Template ID
     *
     * @return array|false Template
     */
    public function getTemplateById($templateId)
    {
        return $this->_getDb()->fetchRow('
            SELECT *
            FROM xf_template
            WHERE template_id = ?
        ', $templateId);
    }
    /**
     * Returns the templates specified by template IDs
     *
     * @param array $templateIds
     *
     * @return array
     */
    public function getTemplatesByIds(array $templateIds)
    {
        if (!$templateIds)
        {
            return array();
        }
        return $this->fetchAllKeyed('
            SELECT *
            FROM xf_template
                WHERE template_id IN(' . $this->_getDb()->quote($templateIds) . ')
        ', 'template_id');
    }
    /**
     * Fetches a template from a particular style based on its title.
     * Note that if a version of the requested template does not exist
     * in the specified style, nothing will be returned.
     *
     * @param string Title
     * @param integer Style ID (defaults to master style)
     *
     * @return array
     */
    public function getTemplateInStyleByTitle($title, $styleId = 0)
    {
        return $this->_getDb()->fetchRow('
            SELECT *
            FROM xf_template
            WHERE title = ?
                AND style_id = ?
        ', array($title, $styleId));
    }
    /**
     * Fetches templates from a particular style based on their titles.
     * Note that if a version of the requested template does not exist
     * in the specified style, nothing will be returned for it.
     *
     * @param array $titles List of titles
     * @param integer $styleId Style ID (defaults to master style)
     *
     * @return array Format: [title] => info
     */
    public function getTemplatesInStyleByTitles(array $titles, $styleId = 0)
    {
        if (!$titles)
        {
            return array();
        }
        return $this->fetchAllKeyed('
            SELECT *
            FROM xf_template
            WHERE title IN (' . $this->_getDb()->quote($titles) . ')
                AND style_id = ?
        ', 'title', $styleId);
    }
    /**
     * Gets all templates with a specific title
     *
     * @param array $titles
     *
     * @return array Format: [template_id] => info
     */
    public function getTemplatesByTitles(array $titles)
    {
        if (!$titles)
        {
            return array();
        }
        return $this->fetchAllKeyed('
            SELECT *
            FROM xf_template
            WHERE title IN (' . $this->_getDb()->quote($titles) . ')
        ', 'template_id');
    }
    /**
     * Gets all templates that are outdated (parent version edited more recently).
     * Does not include contents of template.
     *
     * @return array [template id] => template info, including master_version_string
     */
    public function getOutdatedTemplates()
    {
        return $this->fetchAllKeyed('
            SELECT template.template_id, template.title, template.style_id,
                template.addon_id, template.version_string, template.last_edit_date,
                parent.style_id AS parent_style_id,
                parent.version_string AS parent_version_string,
                parent.last_edit_date AS parent_last_edit_date,
                IF(parent.last_edit_date > 0 AND parent.last_edit_date >= template.last_edit_date, 1, 0) AS outdated_by_date
            FROM xf_template AS template
            INNER JOIN xf_style AS style ON (style.style_id = template.style_id)
            INNER JOIN xf_template_map AS map ON (map.style_id = style.parent_id AND map.title = template.title)
            INNER JOIN xf_template AS parent ON (map.template_id = parent.template_id
                AND (
                    (parent.last_edit_date > 0 AND parent.last_edit_date >= template.last_edit_date)
                    OR parent.version_id > template.version_id
                )
            )
            WHERE template.style_id > 0
        ', 'template_id');
    }
    public function getLatestTemplateHistoryForTemplate($title, $styleId, $priorTo = null)
    {
        return $this->_getDb()->fetchRow("
            SELECT *
            FROM xf_template_history
            WHERE title = ? AND style_id = ?
                " . ($priorTo ? " AND edit_date <= " . intval($priorTo) : '') . "
            ORDER BY edit_date DESC
            LIMIT 1
        ", array($title, $styleId));
    }
    public function getHistoryForTemplate($title, $styleId)
    {
        return $this->fetchAllKeyed("
            SELECT *
            FROM xf_template_history
            WHERE title = ? AND style_id = ?
            ORDER BY edit_date DESC
        ", 'template_history_id', array($title, $styleId));
    }
    public function pruneEditHistory($cutOff = null)
    {
        if ($cutOff === null)
        {
            $logLength = XenForo_Application::get('options')->templateHistoryLength;
            if (!$logLength)
            {
                return 0;
            }
            $cutOff = XenForo_Application::$time - 86400 * $logLength;
        }
        $db = $this->_getDb();
        return $db->delete('xf_template_history', 'log_date < ' . $db->quote($cutOff));
    }
    public function autoMergeTemplate(array $template, XenForo_Diff3 $diff)
    {
        /** @var $styleModel XenForo_Model_Style */
        $styleModel = $this->getModelFromCache('XenForo_Model_Style');
        $style = $styleModel->getStyleById($template['style_id']);
        if (!$style)
        {
            return false;
        }
        $parentStyle = $styleModel->getStyleById($style['parent_id'], true);
        if (!$parentStyle)
        {
            return false;
        }
        if ($parentStyle['style_id'])
        {
            $parentTemplate = $this->getEffectiveTemplateByTitle($template['title'], $parentStyle['style_id']);
        }
        else
        {
            $parentTemplate = $this->getTemplateInStyleByTitle($template['title'], 0);
        }
        if (!$parentTemplate)
        {
            return false;
        }
        if (!$parentTemplate['last_edit_date'] || $parentTemplate['last_edit_date'] < $template['last_edit_date'])
        {
            return false;
        }
        $previousVersion = $this->getLatestTemplateHistoryForTemplate(
            $template['title'], $parentTemplate['style_id'], $template['last_edit_date']
        );
        if (!$previousVersion)
        {
            return false;
        }
        if (!isset($template['template']))
        {
            $template = $this->getTemplateById($template['template_id']);
        }
        if (!$template)
        {
            return false;
        }
        $final = $diff->mergeToFinal(
            $template['template'], $previousVersion['template'], $parentTemplate['template']
        );
        if ($final === false)
        {
            return false;
        }
        $dw = XenForo_DataWriter::create('XenForo_DataWriter_Template');
        $dw->setExistingData($template);
        $dw->set('template', $final);
        $dw->set('last_edit_date', XenForo_Application::$time);
        return $dw->save();
    }
    /**
     * Returns all the templates that belong to the specified add-on.
     *
     * @param string $addOnId
     *
     * @return array Format: [title] => info
     */
    public function getMasterTemplatesInAddOn($addOnId)
    {
        return $this->fetchAllKeyed('
            SELECT *
            FROM xf_template
            WHERE addon_id = ?
                AND style_id = 0
            ORDER BY CONVERT(title USING utf8)
        ', 'title', $addOnId);
    }
    /**
     * Gets the template map IDs of any templates that include the source
     * map IDs. For example, this would pass in the map ID of _header
     * and get the map ID of the PAGE_CONTAINER.
     *
     * @param integer|array $mapIds One map ID as a scalar or an array of many.
     *
     * @return array Array of map IDs
     */
    public function getIncludingTemplateMapIds($mapIds)
    {
        if (!is_array($mapIds))
        {
            $mapIds = array($mapIds);
        }
        if (!$mapIds)
        {
            return array();
        }
        $db = $this->_getDb();
        return $db->fetchCol('
            SELECT source_map_id
            FROM xf_template_include
            WHERE target_map_id IN (' . $db->quote($mapIds) . ')
        ');
    }
    /**
     * Gets the template map information for all templates that are mapped
     * to the specified template ID.
     *
     * @param integer $templateId
     *
     * @return array Format: [] => (array) template map info
     */
    public function getMappedTemplatesByTemplateId($templateId)
    {
        return $this->_getDb()->fetchAll('
            SELECT *
            FROM xf_template_map
            WHERE template_id = ?
        ', $templateId);
    }
    /**
     * Gets mapped template information from the parent style of the named
     * template. If the named style is 0 (or invalid), returns false.
     *
     * @param string $title
     * @param integer $styleId
     *
     * @return array|false
     */
    public function getParentMappedTemplateByTitle($title, $styleId)
    {
        if ($styleId == 0)
        {
            return false;
        }
        return $this->_getDb()->fetchRow('
            SELECT parent_template_map.*
            FROM xf_template_map AS template_map
            INNER JOIN xf_style AS style ON
                (template_map.style_id = style.style_id)
            INNER JOIN xf_template_map AS parent_template_map ON
                (parent_template_map.style_id = style.parent_id AND parent_template_map.title = template_map.title)
            WHERE template_map.title = ? AND template_map.style_id = ?
        ', array($title, $styleId));
    }
    /**
     * Gets the list of all template map IDs that include the named phrase.
     *
     * @param string $phraseTitle
     *
     * @return array List of template map IDs
     */
    public function getTemplateMapIdsThatIncludePhrase($phraseTitle)
    {
        return $this->_getDb()->fetchCol('
            SELECT template_map_id
            FROM xf_template_phrase
            WHERE phrase_title = ?
        ', $phraseTitle);
    }
    /**
     * Returns the path to the template development directory, if it has been configured and exists
     *
     * @return string Path to templates directory
     */
    public function getTemplateDevelopmentDirectory()
    {
        $config = XenForo_Application::get('config');
        if (!$config->debug || !$config->development->directory)
        {
            return '';
        }
        return XenForo_Application::getInstance()->getRootDir()
            . '/' . $config->development->directory . '/file_output/templates';
    }
    /**
     * Checks that the templates directory has been configured and exists
     *
     * @return boolean
     */
    public function canImportTemplatesFromDevelopment()
    {
        $dir = $this->getTemplateDevelopmentDirectory();
        return ($dir && is_dir($dir));
    }
    /**
     * Deletes the templates that belong to the specified add-on.
     *
     * @param string $addOnId
     */
    public function deleteTemplatesForAddOn($addOnId)
    {
        $db = $this->_getDb();
        $titles = $db->fetchPairs('
            SELECT template_id, title
            FROM xf_template
            WHERE style_id = 0
                AND addon_id = ?
        ', $addOnId);
        if (!$titles)
        {
            return;
        }
        $templateIds = array_keys($titles);
        $this->_deleteTemplates($templateIds);
        $db->query('
            DELETE FROM xf_template_compiled
            WHERE style_id = 0
                AND title IN (' . $db->quote($titles) . ')
        ');
        if (XenForo_Application::get('options')->templateFiles)
        {
            XenForo_Template_FileHandler::delete($titles, 0, null);
        }
        XenForo_Template_Compiler::resetTemplateCache();
    }
    public function deleteTemplatesInStyle($styleId)
    {
        $db = $this->_getDb();
        $titles = $db->fetchPairs('
            SELECT template_id, title
            FROM xf_template
            WHERE style_id = ?
        ', $styleId);
        if (!$titles)
        {
            return;
        }
        $templateIds = array_keys($titles);
        $this->_deleteTemplates($templateIds);
        $db->query('
            DELETE FROM xf_template_compiled
            WHERE style_id = ?
                AND title IN (' . $db->quote($titles) . ')
        ', $styleId);
        if (XenForo_Application::get('options')->templateFiles)
        {
            XenForo_Template_FileHandler::delete($titles, $styleId, null);
        }
        XenForo_Template_Compiler::resetTemplateCache();
    }
    protected function _deleteTemplates(array $templateIds)
    {
        $db = $this->_getDb();
        $quotedIds = $db->quote($templateIds);
        $db->query('
            DELETE FROM xf_template_include
            WHERE source_map_id IN (
                SELECT template_map_id
                FROM xf_template AS template
                INNER JOIN xf_template_map AS template_map ON
                    (template.template_id = template_map.template_id)
                WHERE template.template_id IN ('. $quotedIds . ')
            )
        ');
        $db->query('
            DELETE FROM xf_template_phrase
            WHERE template_map_id IN (
                SELECT template_map_id
                FROM xf_template AS template
                INNER JOIN xf_template_map AS template_map ON
                    (template.template_id = template_map.template_id)
                WHERE template.template_id IN ('. $quotedIds . ')
            )
        ');
        $db->delete('xf_template', "template_id IN ($quotedIds)");
        $db->delete('xf_template_map', "template_id IN ($quotedIds)");
        $db->delete('xf_template_modification_log', "template_id IN ($quotedIds)");
    }
    /**
     * Imports all templates from the templates directory into the database
     */
    public function importTemplatesFromDevelopment()
    {
        $db = $this->_getDb();
        $templateDir = $this->getTemplateDevelopmentDirectory();
        if (!$templateDir && !is_dir($templateDir))
        {
            throw new XenForo_Exception("Template development directory not enabled or doesn't exist");
        }
        $files = glob("$templateDir/*.html");
        if (!$files)
        {
            throw new XenForo_Exception("Template development directory does not have any templates");
        }
        $metaData = XenForo_Helper_DevelopmentXml::readMetaDataFile($templateDir . '/_metadata.xml');
        $addOnTemplates = $this->getMasterTemplatesInAddOn('XenForo');
        XenForo_Db::beginTransaction($db);
        $titles = array();
        foreach ($files AS $templateFile)
        {
            $filename = basename($templateFile);
            if (preg_match('/^(.+).html$/', $filename, $match))
            {
                $titles[] = $match[1];
            }
        }
        $existingTemplates = $this->getTemplatesInStyleByTitles($titles, 0);
        foreach ($files AS $templateFile)
        {
            if (!is_readable($templateFile))
            {
                throw new XenForo_Exception("Template file '$templateFile' not readable");
            }
            $filename = basename($templateFile);
            if (preg_match('/^(.+).html$/', $filename, $match))
            {
                $templateName = $match[1];
                $data = file_get_contents($templateFile);
                $dw = XenForo_DataWriter::create('XenForo_DataWriter_Template');
                if (isset($existingTemplates[$templateName]))
                {
                    $dw->setExistingData($existingTemplates[$templateName], true);
                }
                $dw->setOption(XenForo_DataWriter_Template::OPTION_DEV_OUTPUT_DIR, '');
                $dw->setOption(XenForo_DataWriter_Template::OPTION_FULL_COMPILE, false);
                $dw->setOption(XenForo_DataWriter_Template::OPTION_TEST_COMPILE, false);
                $dw->setOption(XenForo_DataWriter_Template::OPTION_CHECK_DUPLICATE, false);
                $dw->setOption(XenForo_DataWriter_Template::OPTION_REBUILD_TEMPLATE_MAP, false);
                $dw->bulkSet(array(
                    'style_id' => 0,
                    'title' => $templateName,
                    'template' => $data,
                    'addon_id' => 'XenForo',
                    'version_id' => 0,
                    'version_string' => ''
                ));
                if (isset($metaData[$templateName]))
                {
                    $dw->bulkSet($metaData[$templateName]);
                }
                $dw->save();
                unset($addOnTemplates[$templateName]);
            }
        }
        // removed templates
        foreach ($addOnTemplates AS $addOnTemplate)
        {
            $dw = XenForo_DataWriter::create('XenForo_DataWriter_Template');
            $dw->setExistingData($addOnTemplate, true);
            $dw->setOption(XenForo_DataWriter_Template::OPTION_DEV_OUTPUT_DIR, '');
            $dw->setOption(XenForo_DataWriter_Template::OPTION_FULL_COMPILE, false);
            $dw->setOption(XenForo_DataWriter_Template::OPTION_TEST_COMPILE, false);
            $dw->setOption(XenForo_DataWriter_Template::OPTION_CHECK_DUPLICATE, false);
            $dw->setOption(XenForo_DataWriter_Template::OPTION_REBUILD_TEMPLATE_MAP, false);
            $dw->delete();
        }
        XenForo_Db::commit($db);
    }
    /**
     * Imports the add-on templates XML.
     *
     * @param SimpleXMLElement $xml XML element pointing to the root of the data
     * @param string $addOnId Add-on to import for
     * @param integer $maxExecution Maximum run time in seconds
     * @param integer $offset Number of elements to skip
     *
     * @return boolean|integer True on completion; false if the XML isn't correct; integer otherwise with new offset value
     */
    public function importTemplatesAddOnXml(SimpleXMLElement $xml, $addOnId, $maxExecution = 0, $offset = 0)
    {
        $db = $this->_getDb();
        XenForo_Db::beginTransaction($db);
        $startTime = microtime(true);
        $templates = XenForo_Helper_DevelopmentXml::fixPhpBug50670($xml->template);
        $titles = array();
        $current = 0;
        foreach ($templates AS $template)
        {
            $current++;
            if ($current <= $offset)
            {
                continue;
            }
            $titles[] = (string)$template['title'];
        }
        $existingTemplates = $this->getTemplatesInStyleByTitles($titles, 0);
        $current = 0;
        $restartOffset = false;
        foreach ($templates AS $template)
        {
            $current++;
            if ($current <= $offset)
            {
                continue;
            }
            $templateName = (string)$template['title'];
            $dw = XenForo_DataWriter::create('XenForo_DataWriter_Template');
            if (isset($existingTemplates[$templateName]))
            {
                $dw->setExistingData($existingTemplates[$templateName], true);
            }
            $dw->setOption(XenForo_DataWriter_Template::OPTION_DEV_OUTPUT_DIR, '');
            $dw->setOption(XenForo_DataWriter_Template::OPTION_FULL_COMPILE, false);
            $dw->setOption(XenForo_DataWriter_Template::OPTION_TEST_COMPILE, false);
            $dw->setOption(XenForo_DataWriter_Template::OPTION_CHECK_DUPLICATE, false);
            $dw->setOption(XenForo_DataWriter_Template::OPTION_REBUILD_TEMPLATE_MAP, false);
            try
            {
                $dw->bulkSet(array(
                    'style_id' => 0,
                    'title' => $templateName,
                    'template' => XenForo_Helper_DevelopmentXml::processSimpleXmlCdata($template),
                    'addon_id' => $addOnId,
                    'version_id' => (int)$template['version_id'],
                    'version_string' => (string)$template['version_string']
                ));
                $dw->save();
            } catch (XenForo_Exception $e)
            {
                throw new XenForo_Exception("$templateName: " . $e->getMessage(), true);
            }
            if ($maxExecution && (microtime(true) - $startTime) > $maxExecution)
            {
                $restartOffset = $current;
                break;
            }
        }
        if (!$restartOffset)
        {
            unset($existingTemplates); // just save memory
            // now look for templates that have been removed
            $addOnTemplates = $this->getMasterTemplatesInAddOn($addOnId);
            foreach ($templates AS $template)
            {
                $title = (string)$template['title'];
                unset($addOnTemplates[$title]);
            }
            foreach ($addOnTemplates AS $addOnTemplate)
            {
                $dw = XenForo_DataWriter::create('XenForo_DataWriter_Template');
                $dw->setExistingData($addOnTemplate, true);
                $dw->setOption(XenForo_DataWriter_Template::OPTION_DEV_OUTPUT_DIR, '');
                $dw->setOption(XenForo_DataWriter_Template::OPTION_FULL_COMPILE, false);
                $dw->setOption(XenForo_DataWriter_Template::OPTION_TEST_COMPILE, false);
                $dw->setOption(XenForo_DataWriter_Template::OPTION_CHECK_DUPLICATE, false);
                $dw->setOption(XenForo_DataWriter_Template::OPTION_REBUILD_TEMPLATE_MAP, false);
                $dw->delete();
            }
        }
        XenForo_Db::commit($db);
        return ($restartOffset ? $restartOffset : true);
    }
    /**
     * Imports templates into a given style. Note that this assumes the style is already empty.
     * It does not check for conflicts.
     *
     * @param SimpleXMLElement $xml
     * @param integer $styleId
     * @param string|null $addOnId If non-null, consider only templates from this add-on to be imported
     */
    public function importTemplatesStyleXml(SimpleXMLElement $xml, $styleId, $addOnId = null)
    {
        $db = $this->_getDb();
        if ($xml->template === null)
        {
            return;
        }
        $existingTemplates = $this->getAllTemplatesInStyle($styleId);
        XenForo_Db::beginTransaction($db);
        foreach ($xml->template AS $template)
        {
            $templateName = (string)$template['title'];
            $dw = XenForo_DataWriter::create('XenForo_DataWriter_Template');
            if (isset($existingTemplates[$templateName]))
            {
                $dw->setExistingData($existingTemplates[$templateName], true);
            }
            $dw->setOption(XenForo_DataWriter_Template::OPTION_DEV_OUTPUT_DIR, '');
            $dw->setOption(XenForo_DataWriter_Template::OPTION_FULL_COMPILE, false);
            $dw->setOption(XenForo_DataWriter_Template::OPTION_TEST_COMPILE, false);
            $dw->setOption(XenForo_DataWriter_Template::OPTION_CHECK_DUPLICATE, false);
            $dw->setOption(XenForo_DataWriter_Template::OPTION_REBUILD_TEMPLATE_MAP, false);
            $dw->bulkSet(array(
                'style_id' => $styleId,
                'title' => (string)$template['title'],
                'template' => XenForo_Helper_DevelopmentXml::processSimpleXmlCdata($template),
                'addon_id' => (string)$template['addon_id'],
                'version_id' => (int)$template['version_id'],
                'version_string' => (string)$template['version_string'],
                'disable_modifications' => (int)$template['disable_modifications']
            ));
            $dw->save();
            unset($existingTemplates[$templateName]);
        }
        // removed templates
        foreach ($existingTemplates AS $existingTemplate)
        {
            if ($addOnId !== null && $existingTemplate['addon_id'] !== $addOnId)
            {
                continue;
            }
            $dw = XenForo_DataWriter::create('XenForo_DataWriter_Template');
            $dw->setExistingData($existingTemplate, true);
            $dw->setOption(XenForo_DataWriter_Template::OPTION_DEV_OUTPUT_DIR, '');
            $dw->setOption(XenForo_DataWriter_Template::OPTION_FULL_COMPILE, false);
            $dw->setOption(XenForo_DataWriter_Template::OPTION_TEST_COMPILE, false);
            $dw->setOption(XenForo_DataWriter_Template::OPTION_CHECK_DUPLICATE, false);
            $dw->setOption(XenForo_DataWriter_Template::OPTION_REBUILD_TEMPLATE_MAP, false);
            $dw->delete();
        }
        XenForo_Db::commit($db);
    }
    /**
     * Appends the add-on template XML to a given DOM element.
     *
     * @param DOMElement $rootNode Node to append all elements to
     * @param string $addOnId Add-on ID to be exported
     */
    public function appendTemplatesAddOnXml(DOMElement $rootNode, $addOnId)
    {
        $document = $rootNode->ownerDocument;
        $templates = $this->getMasterTemplatesInAddOn($addOnId);
        foreach ($templates AS $template)
        {
            $templateNode = $document->createElement('template');
            $templateNode->setAttribute('title', $template['title']);
            $templateNode->setAttribute('version_id', $template['version_id']);
            $templateNode->setAttribute('version_string', $template['version_string']);
            $templateNode->appendChild(XenForo_Helper_DevelopmentXml::createDomCdataSection($document, $template['template']));
            $rootNode->appendChild($templateNode);
        }
    }
    /**
     * Appends the template XML for templates in the specified style.
     *
     * @param DOMElement $rootNode
     * @param integer $styleId
     * @param string|null $limitAddOnId If non-null, limits only to templates in this add-on
     * @param boolean $independent If true, all customizations from parent styles will be included in this
     */
    public function appendTemplatesStyleXml(DOMElement $rootNode, $styleId, $limitAddOnId = null, $independent = false)
    {
        $document = $rootNode->ownerDocument;
        if (!$styleId)
        {
            // getting master data
            $independent = false;
        }
        if ($independent)
        {
            $templates = $this->getAllEffectiveTemplatesInStyle($styleId);
        }
        else
        {
            $templates = $this->getAllTemplatesInStyle($styleId);
        }
        foreach ($templates AS $template)
        {
            if ($limitAddOnId !== null && $template['addon_id'] !== $limitAddOnId)
            {
                // wrong add-on
                continue;
            }
            if ($independent && !$template['style_id'])
            {
                // master version of a template
                continue;
            }
            $templateNode = $document->createElement('template');
            $templateNode->setAttribute('title', $template['title']);
            $templateNode->setAttribute('addon_id', $template['addon_id']);
            $templateNode->setAttribute('version_id', $template['version_id']);
            $templateNode->setAttribute('version_string', $template['version_string']);
            $templateNode->setAttribute('disable_modifications', $template['disable_modifications']);
            $templateNode->appendChild(XenForo_Helper_DevelopmentXml::createDomCdataSection($document, $template['template']));
            $rootNode->appendChild($templateNode);
        }
    }
    /**
     * Gets the templates development XML.
     *
     * @return DOMDocument
     */
    public function getTemplatesDevelopmentXml()
    {
        $document = new DOMDocument('1.0', 'utf-8');
        $document->formatOutput = true;
        $rootNode = $document->createElement('templates');
        $document->appendChild($rootNode);
        $this->appendTemplatesAddOnXml($rootNode, 'XenForo');
        return $document;
    }
    public function reparseTemplate($templateId, $fullCompile = true)
    {
        $dw = XenForo_DataWriter::create('XenForo_DataWriter_Template', XenForo_DataWriter::ERROR_SILENT);
        $dw->setExistingData($templateId);
        $dw->reparseTemplate();
        $dw->setOption(XenForo_DataWriter_Template::OPTION_DEV_OUTPUT_DIR, '');
        $dw->setOption(XenForo_DataWriter_Template::OPTION_FULL_COMPILE, $fullCompile);
        $dw->save();
        return $dw;
    }
    public function getMapIdsToCompileByTitles(array $titles)
    {
        $mapIds = $this->getMapIdsByTemplateTitles($titles);
        $mapIds = array_merge($mapIds, $this->getIncludingTemplateMapIds($mapIds));
        return array_unique($mapIds);
    }
    /**
     * Reparses all templates.
     *
     * @param integer $maxExecution The approx maximum length of time this function will run for
     * @param integer $startStyle The ID of the style to start with
     * @param integer $startTemplate The number of the template to start with in that style (not ID, just counter)
     *
     * @return boolean|array True if completed successful, otherwise array of where to restart (values start style ID, start template counter)
     */
    public function reparseAllTemplates($maxExecution = 0, $startStyle = 0, $startTemplate = 0)
    {
        $db = $this->_getDb();
        $styles = $this->getModelFromCache('XenForo_Model_Style')->getAllStyles();
        $styleIds = array_merge(array(0), array_keys($styles));
        sort($styleIds);
        $lastStyle = 0;
        $startTime = microtime(true);
        $complete = true;
        XenForo_Db::beginTransaction($db);
        foreach ($styleIds AS $styleId)
        {
            if ($styleId < $startStyle)
            {
                continue;
            }
            $lastStyle = $styleId;
            $lastTemplate = 0;
            $templates = $this->getAllTemplatesInStyle($styleId, true);
            foreach ($templates AS $template)
            {
                $lastTemplate++;
                if ($styleId == $startStyle && $lastTemplate < $startTemplate)
                {
                    continue;
                }
                $this->reparseTemplate($template, false);
                if ($maxExecution && (microtime(true) - $startTime) > $maxExecution)
                {
                    $complete = false;
                    break 2;
                }
            }
        }
        XenForo_Db::commit($db);
        if ($complete)
        {
            return true;
        }
        else
        {
            return array($lastStyle, $lastTemplate + 1);
        }
    }
    /**
     * Recompiles all templates.
     *
     * @param integer $maxExecution The approx maximum length of time this function will run for
     * @param integer $startStyle The ID of the style to start with
     * @param integer $startTemplate The number of the template to start with in that style (not ID, just counter)
     *
     * @return boolean|array True if completed successfull, otherwise array of where to restart (values start style ID, start template counter)
     */
    public function compileAllTemplates($maxExecution = 0, $startStyle = 0, $startTemplate = 0)
    {
        $db = $this->_getDb();
        $styles = $this->getModelFromCache('XenForo_Model_Style')->getAllStyles();
        $styleIds = array_merge(array(0), array_keys($styles));
        sort($styleIds);
        $lastStyle = 0;
        $startTime = microtime(true);
        $complete = true;
        XenForo_Db::beginTransaction($db);
        foreach ($styleIds AS $styleId)
        {
            if ($styleId < $startStyle)
            {
                continue;
            }
            $lastStyle = $styleId;
            $lastTemplate = 0;
            $templates = $this->getAllTemplatesInStyle($styleId, true);
            foreach ($templates AS $template)
            {
                $lastTemplate++;
                if ($styleId == $startStyle && $lastTemplate < $startTemplate)
                {
                    continue;
                }
                $this->compileNamedTemplateInStyleTree($template['title'], $template['style_id']);
                if ($maxExecution && (microtime(true) - $startTime) > $maxExecution)
                {
                    $complete = false;
                    break 2;
                }
            }
        }
        if ($complete)
        {
            $compiledRemove = $db->fetchAll("
                SELECT DISTINCT c.title, c.style_id
                FROM xf_template_compiled AS c
                LEFT JOIN xf_template_map AS m ON (c.title = m.title AND c.style_id = m.style_id)
                WHERE m.title IS NULL
            ");
            foreach ($compiledRemove AS $remove)
            {
                $db->delete('xf_template_compiled',
                    "style_id = " . $db->quote($remove['style_id']) . " AND title = " . $db->quote($remove['title'])
                );
                if (XenForo_Application::get('options')->templateFiles)
                {
                    XenForo_Template_FileHandler::delete($remove['title'], $remove['style_id'], null);
                }
            }
            $this->getModelFromCache('XenForo_Model_Style')->updateAllStylesLastModifiedDate();
            $this->getModelFromCache('XenForo_Model_AdminTemplate')->updateAdminStyleLastModifiedDate();
        }
        XenForo_Db::commit($db);
        if ($complete)
        {
            return true;
        }
        else
        {
            return array($lastStyle, $lastTemplate + 1);
        }
    }
    /**
     * Compiles the named template in the style tree. Any child templates that
     * use this template will be recompiled as well.
     *
     * @param string $title
     * @param integer $styleId
     *
     * @return array A list of template map IDs that were compiled
     */
    public function compileNamedTemplateInStyleTree($title, $styleId)
    {
        $parsedRecord = $this->getEffectiveTemplateByTitle($title, $styleId);
        if (!$parsedRecord)
        {
            return array();
        }
        return $this->compileTemplateInStyleTree($parsedRecord);
    }
    /**
     * Compiles the list of template map IDs and any child templates that are using
     * the same core template.
     *
     * @param integer|array $templateMapIds One map ID as a scalar or many as an array
     *
     * @return array A list of template map IDs that were compiled
     */
    public function compileMappedTemplatesInStyleTree($templateMapIds)
    {
        $templates = $this->getUniqueTemplatesByMapIds($templateMapIds);
        $mapIds = array();
        foreach ($templates AS $template)
        {
            $mapIds = array_merge($mapIds, $this->compileTemplateInStyleTree($template));
        }
        return $mapIds;
    }
    /**
     * Compiles the specified template data in the style tree. This compiles this template
     * in any style that is actually using this template.
     *
     * @param array $parsedRecord Full template information
     *
     * @return array List of template map IDs that were compiled
     */
    public function compileTemplateInStyleTree(array $parsedRecord)
    {
        $parsedTemplate = unserialize($parsedRecord['template_parsed']);
        $dependentTemplates = array();
        $templateMaps = $this->getMappedTemplatesByTemplateId($parsedRecord['template_id']);
        $compileResults = false;
        foreach ($templateMaps AS $templateMap)
        {
            if ($templateMap['style_id'] == $parsedRecord['style_id'])
            {
                // only compile the root version and then figure out if we need to compile children
                $compileResults = $this->compileAndInsertParsedTemplate(
                    $templateMap['template_map_id'],
                    $parsedTemplate,
                    $parsedRecord['title'],
                    $templateMap['style_id']
                );
                if ($compileResults)
                {
                    $compileResults['templateMapId'] = $templateMap['template_map_id'];
                }
                break;
            }
        }
        $db = $this->_getDb();
        XenForo_Db::beginTransaction($db);
        $includeDeleteIds = array();
        $includeInserts = array();
        $phraseDeleteIds = array();
        $phraseInserts = array();
        if ($compileResults && !$compileResults['failedTemplateIncludes'])
        {
            if ($compileResults['includedTemplateIds'])
            {
                $includedTemplates = $db->fetchPairs("
                    SELECT title, template_id
                    FROM xf_template_map
                    WHERE template_map_id IN (" . $db->quote($compileResults['includedTemplateIds']) . ")
                ");
            }
            else
            {
                $includedTemplates = array();
            }
            // each template here is using the same parent version.
            // if we have no includes or all of the includes point to the same
            // template ID, then one compilation is all that's necessary and we
            // can simply copy the results between styles
            foreach ($templateMaps AS $templateMap)
            {
                $dependentTemplates[] = $templateMap['template_map_id'];
                if ($templateMap['style_id'] == $parsedRecord['style_id'])
                {
                    // already handled
                    continue;
                }
                $isSame = true;
                if ($includedTemplates)
                {
                    $localIncludedTemplates = $db->fetchAll("
                        SELECT title, template_id, template_map_id
                        FROM xf_template_map
                        WHERE style_id = ?
                            AND title IN (" . $db->quote(array_keys($includedTemplates)) . ")
                    ", array($templateMap['style_id']));
                    foreach ($localIncludedTemplates AS $localInclude)
                    {
                        if (!isset($includedTemplates[$localInclude['title']])
                            || $includedTemplates[$localInclude['title']] != $localInclude['template_id']
                        )
                        {
                            // including a different template, so we need to do a full compile
                            $isSame = false;
                            break;
                        }
                    }
                }
                else
                {
                    $localIncludedTemplates = array();
                }
                if ($isSame)
                {
                    $includeDeleteIds[] = $templateMap['template_map_id'];
                    foreach ($localIncludedTemplates AS $localInclude)
                    {
                        $includeInserts[] = '(' . $db->quote($templateMap['template_map_id']) . ', ' . $db->quote($localInclude['template_map_id']) . ')';
                    }
                    $phraseDeleteIds[] = $templateMap['template_map_id'];
                    foreach ($compileResults['includedPhraseTitles'] AS $phraseTitles)
                    {
                        $phraseInserts[] = '(' . $db->quote($templateMap['template_map_id']) . ', ' . $db->quote($phraseTitles) . ')';
                    }
                    foreach ($compileResults['compiledCache'] AS $languageId => $compiled)
                    {
                        $this->_insertCompiledTemplateRecord(
                            $templateMap['style_id'], $languageId, $parsedRecord['title'], $compiled
                        );
                    }
                }
                else
                {
                    $this->compileAndInsertParsedTemplate(
                        $templateMap['template_map_id'],
                        $parsedTemplate,
                        $parsedRecord['title'],
                        $templateMap['style_id']
                    );
                }
            }
        }
        else
        {
            foreach ($templateMaps AS $templateMap)
            {
                $this->compileAndInsertParsedTemplate(
                    $templateMap['template_map_id'],
                    $parsedTemplate,
                    $parsedRecord['title'],
                    $templateMap['style_id']
                );
                $dependentTemplates[] = $templateMap['template_map_id'];
            }
        }
        if ($includeDeleteIds)
        {
            $db->delete('xf_template_include', 'source_map_id IN (' . $db->quote($includeDeleteIds) . ')');
        }
        if ($includeInserts)
        {
            $db->query("
                INSERT IGNORE INTO xf_template_include
                    (source_map_id, target_map_id)
                VALUES " . implode(',', $includeInserts)
            );
        }
        if ($phraseDeleteIds)
        {
            $db->delete('xf_template_phrase', 'template_map_id IN (' . $db->quote($phraseDeleteIds) . ')');
        }
        if ($phraseInserts)
        {
            $db->query("
                INSERT IGNORE INTO xf_template_phrase
                    (template_map_id, phrase_title)
                VALUES " . implode(',', $phraseInserts)
            );
        }
        XenForo_Db::commit($db);
        return $dependentTemplates;
    }
    /**
     * Compiles and inserts the specified effective templates.
     *
     * @param array $templates Array of effective template info
     */
    public function compileAndInsertEffectiveTemplates(array $templates)
    {
        foreach ($templates AS $template)
        {
            $this->compileAndInsertParsedTemplate(
                $template['template_map_id'],
                unserialize($template['template_parsed']),
                $template['title'],
                isset($template['map_style_id']) ? $template['map_style_id'] : $template['style_id']
            );
        }
    }
    /**
     * Recompiles all templates that include the named phrase.
     *
     * @param string $phraseTitle
     * @param bool $deferred If true, defer this
     *
     * @return array List of template map IDs including the phrase
     */
    public function compileTemplatesThatIncludePhrase($phraseTitle, $deferred = false)
    {
        $mapIds = $this->getTemplateMapIdsThatIncludePhrase($phraseTitle);
        if ($deferred)
        {
            XenForo_Application::defer('TemplatePartialCompile', array(
                'recompileMapIds' => $mapIds
            ), null, true);
        }
        else
        {
            $this->compileMappedTemplatesInStyleTree($mapIds);
        }
        return $mapIds;
    }
    /**
     * Compiles the specified parsed template and updates the compiled table
     * and included templates list.
     *
     * @param integer $templateMapId The map ID of the template being compiled (for includes)
     * @param string|array $parsedTemplate Parsed form of the template
     * @param string $title Title of the template
     * @param integer $compileStyleId Style ID of the template
     * @param boolean $doDbWrite If non null, controls whether the DB write/template cache is written
     *
     * @return array|bool
     */
    public function compileAndInsertParsedTemplate($templateMapId, $parsedTemplate, $title, $compileStyleId, $doDbWrite = null)
    {
        $isCss = (substr($title, -4) == '.css');
        if ($doDbWrite === null)
        {
            $doDbWrite = ($isCss || $compileStyleId);
        }
        $compiler = new XenForo_Template_Compiler('');
        $languages = $this->getModelFromCache('XenForo_Model_Language')->getAllLanguages();
        $db = $this->_getDb();
        $compiledCache = array();
        if ($isCss)
        {
            $compiledTemplate = $compiler->compileParsed($parsedTemplate, $title, $compileStyleId, 0);
            $compiledCache[0] = $compiledTemplate;
            if ($doDbWrite)
            {
                $this->_insertCompiledTemplateRecord($compileStyleId, 0, $title, $compiledTemplate);
            }
        }
        else
        {
            foreach ($languages AS $language)
            {
                $compiledTemplate = $compiler->compileParsed($parsedTemplate, $title, $compileStyleId, $language['language_id']);
                $compiledCache[$language['language_id']] = $compiledTemplate;
                if ($doDbWrite)
                {
                    $this->_insertCompiledTemplateRecord($compileStyleId, $language['language_id'], $title, $compiledTemplate);
                }
            }
        }
        $mapIdQuoted = $db->quote($templateMapId);
        $ins = array();
        $includedTemplateIds = array();
        foreach ($compiler->getIncludedTemplates() AS $includedMapId)
        {
            $ins[] = '(' . $mapIdQuoted . ', ' . $db->quote($includedMapId) . ')';
            $includedTemplateIds[] = $includedMapId;
        }
        if ($doDbWrite)
        {
            $db->delete('xf_template_include', 'source_map_id = ' . $db->quote($templateMapId));
            if ($ins)
            {
                $db->query("
                    INSERT IGNORE INTO xf_template_include
                        (source_map_id, target_map_id)
                    VALUES
                        " . implode(',', $ins)
                );
            }
        }
        $ins = array();
        $includedPhraseTitles = array();
        foreach ($compiler->getIncludedPhrases() AS $includedPhrase)
        {
            if (strlen($includedPhrase) > 75)
            {
                continue; // too long, can't be a valid phrase
            }
            $ins[] = '(' . $mapIdQuoted . ', ' . $db->quote($includedPhrase) . ')';
            $includedPhraseTitles[] = $includedPhrase;
        }
        if ($doDbWrite)
        {
            $db->delete('xf_template_phrase', 'template_map_id = ' . $db->quote($templateMapId));
            if ($ins)
            {
                $db->query("
                    INSERT IGNORE INTO xf_template_phrase
                        (template_map_id, phrase_title)
                    VALUES
                        " . implode(',', $ins)
                );
            }
        }
        return array(
            'includedTemplateIds' => $includedTemplateIds,
            'failedTemplateIncludes' => $compiler->getFailedTemplateIncludes(),
            'includedPhraseTitles' => $includedPhraseTitles,
            'compiledCache' => $compiledCache,
            'doDbWrite' => $doDbWrite
        );
    }
    protected function _insertCompiledTemplateRecord($styleId, $languageId, $title, $compiledTemplate)
    {
        $this->_getDb()->query("
            INSERT INTO xf_template_compiled
                (style_id, language_id, title, template_compiled)
            VALUES
                (?, ?, ?, ?)
            ON DUPLICATE KEY UPDATE template_compiled = VALUES(template_compiled)
        ", array($styleId, $languageId, $title, $compiledTemplate));
        if (XenForo_Application::get('options')->templateFiles)
        {
            XenForo_Template_FileHandler::save($title, $styleId, $languageId, $compiledTemplate);
        }
    }
    /**
     * Determines if the visiting user can modify a template in the specified style.
     * If debug mode is not enabled, users can't modify templates in the master style.
     *
     * @param integer $styleId
     *
     * @return boolean
     */
    public function canModifyTemplateInStyle($styleId)
    {
        return ($styleId != 0 || XenForo_Application::debugMode());
    }
    /**
     * Builds (and inserts) the template map for a specified template, from
     * the root of the style tree.
     *
     * @param string $title Title of the template being build
     * @param array $data Injectable data. Supports styleTree and styleTemplateMap.
     */
    public function buildTemplateMap($title, array $data = array())
    {
        if (!isset($data['styleTree']))
        {
            /* @var $styleModel XenForo_Model_Style */
            $styleModel = $this->getModelFromCache('XenForo_Model_Style');
            $data['styleTree'] = $styleModel->getStyleTreeAssociations($styleModel->getAllStyles());
        }
        if (!isset($data['styleTemplateMap']))
        {
            $data['styleTemplateMap'] = $this->getTemplateIdInStylesByTitle($title);
        }
        $mapUpdates = $this->findTemplateMapUpdates(0, $data['styleTree'], $data['styleTemplateMap']);
        if ($mapUpdates)
        {
            $db = $this->_getDb();
            $toDeleteInStyleIds = array();
            foreach ($mapUpdates AS $styleId => $newTemplateId)
            {
                if ($newTemplateId == 0)
                {
                    $toDeleteInStyleIds[] = $styleId;
                    continue;
                }
                $db->query('
                    INSERT INTO xf_template_map
                        (style_id, title, template_id)
                    VALUES
                        (?, ?, ?)
                    ON DUPLICATE KEY UPDATE
                        template_id = ?
                ', array($styleId, $title, $newTemplateId, $newTemplateId));
            }
            if ($toDeleteInStyleIds)
            {
                $db->delete('xf_template_map',
                    'title = ' . $db->quote($title) . ' AND style_id IN (' . $db->quote($toDeleteInStyleIds) . ')'
                );
                $db->delete('xf_template_compiled',
                    'title = ' . $db->quote($title) . ' AND style_id IN (' . $db->quote($toDeleteInStyleIds) . ')'
                );
                if (XenForo_Application::get('options')->templateFiles)
                {
                    XenForo_Template_FileHandler::delete($title, $toDeleteInStyleIds, null);
                }
            }
        }
    }
    /**
     * Finds the necessary template map updates for the specified template within the
     * sub-tree.
     *
     * If {$defaultTemplateId} is non-0, a return entry will be inserted for {$parentId}.
     *
     * @param integer $parentId Parent of the style sub-tree to search.
     * @param array $styleTree Tree of styles
     * @param array $styleTemplateMap List of styleId => templateId pairs for the places where this template has been customized.
     * @param integer $defaultTemplateId The default template ID that non-customized template in the sub-tree should get.
     *
     * @return array Format: [style id] => [effective template id]
     */
    public function findTemplateMapUpdates($parentId, array $styleTree, array $styleTemplateMap, $defaultTemplateId = 0)
    {
        $output = array();
        if (isset($styleTemplateMap[$parentId]))
        {
            $defaultTemplateId = $styleTemplateMap[$parentId];
        }
        $output[$parentId] = $defaultTemplateId;
        if (!isset($styleTree[$parentId]))
        {
            return $output;
        }
        foreach ($styleTree[$parentId] AS $styleId)
        {
            $output += $this->findTemplateMapUpdates($styleId, $styleTree, $styleTemplateMap, $defaultTemplateId);
        }
        return $output;
    }
    /**
     * Inserts the template map records for all elements of various styles.
     *
     * @param array $styleMapList Format: [style id][title] => template id
     * @param bolean $truncate If true, all map data is truncated (quicker that way)
     */
    public function insertTemplateMapForStyles(array $styleMapList, $truncate = false)
    {
        $db = $this->_getDb();
        XenForo_Db::beginTransaction($db);
        if ($truncate)
        {
            $db->query('TRUNCATE TABLE xf_template_map');
            $db->query('TRUNCATE TABLE xf_template_include');
            $db->query('TRUNCATE TABLE xf_template_phrase');
        }
        foreach ($styleMapList AS $builtStyleId => $map)
        {
            if (!$truncate)
            {
                $db->delete('xf_template_map', 'style_id = ' . $db->quote($builtStyleId));
            }
            foreach ($map AS $title => $templateId)
            {
                $db->insert('xf_template_map', array(
                    'style_id' => $builtStyleId,
                    'title' => $title,
                    'template_id' => $templateId
                ));
            }
        }
        XenForo_Db::commit($db);
    }
    /**
     * Builds the full template map data for an entire style sub-tree.
     *
     * @param integer $styleId Starting style. This style and all children will be built.
     *
     * @return array Format: [style id][title] => template id
     */
    public function buildTemplateMapForStyleTree($styleId)
    {
        /* @var $styleModel XenForo_Model_Style */
        $styleModel = $this->getModelFromCache('XenForo_Model_Style');
        $styles = $styleModel->getAllStyles();
        $styleTree = $styleModel->getStyleTreeAssociations($styles);
        $styles[0] = true;
        if ($styleId && !isset($styles[$styleId]))
        {
            return array();
        }
        $map = array();
        if ($styleId)
        {
            $style = $styles[$styleId];
            $templates = $this->getEffectiveTemplateListForStyle($style['parent_id']);
            foreach ($templates AS $template)
            {
                $map[$template['title']] = $template['template_id'];
            }
        }
        return $this->_buildTemplateMapForStyleTree($styleId, $map, $styles, $styleTree);
    }
    /**
     * Internal handler to build the template map data for a style sub-tree.
     * Calls itself recursively.
     *
     * @param integer $styleId Style to build (builds children automatically)
     * @param array $map Base template map data. Format: [title] => template id
     * @param array $styles List of styles
     * @param array $styleTree Style tree
     *
     * @return array Format: [style id][title] => template id
     */
    protected function _buildTemplateMapForStyleTree($styleId, array $map, array $styles, array $styleTree)
    {
        if (!isset($styles[$styleId]))
        {
            return array();
        }
        $customTemplates = $this->getAllTemplatesInStyle($styleId);
        foreach ($customTemplates AS $template)
        {
            $map[$template['title']] = $template['template_id'];
        }
        $output = array($styleId => $map);
        if (isset($styleTree[$styleId]))
        {
            foreach ($styleTree[$styleId] AS $childStyleId)
            {
                $output += $this->_buildTemplateMapForStyleTree($childStyleId, $map, $styles, $styleTree);
            }
        }
        return $output;
    }
    /**
     * Replaces <xen:require/include/edithint with <link rel="xenforo_x"
     * for the purposes of easy WebDAV editing.
     *
     * @param string $templateText
     *
     * @return string
     */
    public static function replaceIncludesWithLinkRel($templateText)
    {
        $search = array(
            '#<xen:requires+css="([^"]+)"s*/>#siU'
            =>    '<link rel="xenforo_stylesheet" type="text/css" href="1" />',
            '#<xen:edithints+template="([^"]+.css)"s*/>#siU'
            => '<link rel="xenforo_stylesheet_hint" type="text/css" href="1" />',
            '#<xen:edithints+template="([^"]+)"s*/>#siU'
            => '<link rel="xenforo_template_hint" type="text/html" href="1.html" />',
            '#<xen:includes+template="([^"]+)"(s*/)?>#siU'
            => '<link rel="xenforo_template" type="text/html" href="1.html"2>',
            '#</xen:include>#siU'
            => '</link>',
        );
        return preg_replace(array_keys($search), $search, $templateText);
    }
    /**
     * Replaces <link rel="xenforo_x" with <xen:require/include/edithint
     * for the purposes of easy WebDAV editing.
     *
     * @param string $templateText
     *
     * @return string
     */
    public static function replaceLinkRelWithIncludes($templateText)
    {
        $search = array(
            '#</link>#siU'
            => '</xen:include>',
            '#<link rel="xenforo_template" type="text/html" href="([^"]+)(.html)?"(s*/)?>#siU'
            => '<xen:include template="1"3>',
            '#<link rel="xenforo_template_hint" type="text/html" href="([^"]+)(.html)?"s/>#siU'
            => '<xen:edithint template="1" />',
            '#<link rel="xenforo_stylesheet_hint" type="text/css" href="([^"]+)"s*/>#siU'
            => '<xen:edithint template="1" />',
            '#<link rel="xenforo_stylesheet" type="text/css" href="([^"]+)"s*/>#siU'
            => '<xen:require css="1" />'
        );
        return preg_replace(array_keys($search), $search, $templateText);
    }
    /**
     * Writes out the complete set of template files to the file system
     *
     * @param boolean Enable the templateFiles option after completion.
     * @param boolean Manipulate the option values to ensure failsafe operation.
     */
    public function writeTemplateFiles($enable = false, $handleOptions = true)
    {
        if ($handleOptions && XenForo_Application::get('options')->templateFiles)
        {
            $this->getModelFromCache('XenForo_Model_Option')->updateOptions(array('templateFiles' => 0));
        }
        $this->deleteTemplateFiles();
        $templates = $this->_getDb()->query('SELECT * FROM xf_template_compiled');
        while ($template = $templates->fetch())
        {
            XenForo_Template_FileHandler::save($template['title'], $template['style_id'], $template['language_id'], $template['template_compiled']);
        }
        if ($handleOptions && $enable)
        {
            $this->getModelFromCache('XenForo_Model_Option')->updateOptions(array('templateFiles' => 1));
        }
    }
    /**
     * Deletes the file versions of all templates
     */
    public function deleteTemplateFiles()
    {
        XenForo_Template_FileHandler::delete(null, null, null);
    }
}