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

File size: 13.08Kb
<?php

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

	/**
	 * Option that controls whether language-related caches will be rebuild.
	 * Defaults to true.
	 *
	 * @var string
	 */
	const OPTION_REBUILD_LANGUAGE_CACHE = 'rebuildLanguageCache';

	/**
	 * Option that controls whether templates that use this phrase should be recompiled.
	 * This can be a slow process if updating a lot of phrases. Defaults to true.
	 *
	 * @var string
	 */
	const OPTION_FULL_RECOMPILE = 'fullRecompile';

	/**
	 * Controls whether templates including this phrase are recompiled on change.
	 * Defaults to true.
	 *
	 * @var string
	 */
	const OPTION_RECOMPILE_TEMPLATE = 'recompileTemplate';

	/**
	 * Controls whether effective phrase values are compiled for this phrase on change.
	 * Defaults to true.
	 *
	 * @var string
	 */
	const OPTION_RECOMPILE_PHRASE = 'recompilePhrase';

	/**
	 * Option that controls if phrase map is rebuild when phrase is changed. Defaults to true.
	 *
	 * @var string
	 */
	const OPTION_REBUILD_PHRASE_MAP = 'rebuildPhraseMap';

	/**
	 * 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_phrase_not_found';

	/**
	* Gets the fields that are defined for the table. See parent for explanation.
	*
	* @return array
	*/
	protected function _getFields()
	{
		return array(
			'xf_phrase' => array(
				'phrase_id'    => array('type' => self::TYPE_UINT,   'autoIncrement' => true),
				'language_id'  => array('type' => self::TYPE_UINT,   'required' => true),
				'title'        => array('type' => self::TYPE_BINARY, 'required' => true, 'maxLength' => 100,
					'verification' => array('$this', '_verifyTitle'),
					'requiredError' => 'please_enter_valid_title'
				),
				'phrase_text'  => array('type' => self::TYPE_STRING, 'default' => '', 'noTrim' => true),
				'global_cache' => array('type' => self::TYPE_BOOLEAN, 'default' => 0),
				'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' => '')
			)
		);
	}

	/**
	* 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 (!$phrase_id = $this->_getExistingPrimaryKey($data))
		{
			return false;
		}

		return array('xf_phrase' => $this->_getPhraseModel()->getPhraseById($phrase_id));
	}

	/**
	* Gets SQL condition to update the existing record.
	*
	* @return string
	*/
	protected function _getUpdateCondition($tableName)
	{
		return 'phrase_id = ' . $this->_db->quote($this->getExisting('phrase_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
	* dev output directory automatically.
	*
	* @return array
	*/
	protected function _getDefaultOptions()
	{
		$options = array(
			self::OPTION_DEV_OUTPUT_DIR => '',
			self::OPTION_REBUILD_LANGUAGE_CACHE => true,
			self::OPTION_RECOMPILE_PHRASE => true,
			self::OPTION_RECOMPILE_TEMPLATE => true,
			self::OPTION_REBUILD_PHRASE_MAP => true,
			self::OPTION_CHECK_DUPLICATE => true
		);

		if (XenForo_Application::debugMode())
		{
			$options[self::OPTION_DEV_OUTPUT_DIR] = $this->_getPhraseModel()->getPhraseDevelopmentDirectory();
		}

		return $options;
	}

	/**
	 * Sets an option. If the OPTION_FULL_RECOMPILE option is specified, other options are
	 * set instead.
	 *
	 * @param string $name
	 * @param mixed $value
	 */
	public function setOption($name, $value)
	{
		if ($name === self::OPTION_FULL_RECOMPILE)
		{
			parent::setOption(self::OPTION_RECOMPILE_PHRASE, $value);
			parent::setOption(self::OPTION_RECOMPILE_TEMPLATE, $value);
		}
		else
		{
			parent::setOption($name, $value);
		}
	}

	/**
	 * Verifies that the phrase title ID is valid.
	 *
	 * @param string $title
	 *
	 * @return boolean
	 */
	protected function _verifyTitle(&$title)
	{
		if (preg_match('/[^a-zA-Z0-9_]/', $title))
		{
			$this->error(new XenForo_Phrase('please_enter_title_using_only_alphanumeric'), 'title');
			return false;
		}

		return true;
	}

	/**
	 * Pre-save handling.
	 */
	protected function _preSave()
	{
		if ($this->getOption(self::OPTION_CHECK_DUPLICATE))
		{
			if ($this->isChanged('title') || $this->isChanged('language_id'))
			{
				$existing = $this->_getPhraseModel()->getPhraseInLanguageByTitle($this->get('title'), $this->get('language_id'));
				if ($existing)
				{
					$this->error(new XenForo_Phrase('phrase_titles_must_be_unique_in_language'), 'title');
				}
				else if ($this->get('title'))
				{
					// check for an existing phrase by this title and make sure the case is unchanged
					$effective = $this->_getPhraseModel()->getEffectivePhraseValuesInAllLanguages(array($this->get('title')));
					if ($effective)
					{
						$effective = reset($effective);
						$title = key($effective);
						$this->set('title', $title);
					}
				}
			}
		}

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

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

		if ($this->getOption(self::OPTION_REBUILD_PHRASE_MAP))
		{
			if ($this->isChanged('title'))
			{
				$phraseModel->buildPhraseMap($this->get('title'));
				if ($existingTitle = $this->getExisting('title'))
				{
					$existingUpdates = $phraseModel->buildPhraseMap($existingTitle);
					if ($this->getOption(self::OPTION_RECOMPILE_PHRASE))
					{
						$toDeleteInLanguageIds = array();
						foreach ($existingUpdates AS $languageId => $newPhraseId)
						{
							if ($newPhraseId == 0)
							{
								$toDeleteInLanguageIds[] = $languageId;
							}
						}

						if ($toDeleteInLanguageIds)
						{
							$this->_db->delete('xf_phrase_compiled',
								'title = ' . $this->_db->quote($existingTitle)
								. ' AND language_id IN (' . $this->_db->quote($toDeleteInLanguageIds) . ')'
							);
						}
					}
				}
			}
			else if ($this->isChanged('language_id'))
			{
				$phraseModel->buildPhraseMap($this->get('title'));
			}
		}

		if ($this->getOption(self::OPTION_RECOMPILE_PHRASE))
		{
			$this->_recompilePhrase();
		}
		if ($this->getOption(self::OPTION_RECOMPILE_TEMPLATE))
		{
			$this->_recompileTemplatesIncludingPhrase();
		}

		$this->_rebuildLanguageCaches();

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

	/**
	 * Rebuilds the language caches, if the option is enabled.
	 */
	protected function _rebuildLanguageCaches()
	{
		if ($this->getOption(self::OPTION_REBUILD_LANGUAGE_CACHE))
		{
			$this->_getLanguageModel()->rebuildLanguageCaches();
		}
	}

	/**
	 * 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('language_id') == 0 && $this->get('addon_id') == 'XenForo')
		{
			return $this->getOption(self::OPTION_DEV_OUTPUT_DIR);
		}
		else
		{
			return '';
		}
	}

	/**
	* 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 . '.txt';

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

		$fp = fopen($newFile, 'w');
		fwrite($fp, $this->get('phrase_text'));
		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('global_cache', 'version_id', 'version_string')
		);
	}

	protected function _recompilePhrase()
	{
		$this->_getPhraseModel()->compileNamedPhraseInLanguageTree($this->get('title'), $this->get('language_id'));
	}

	/**
	 * Recompiles all templates (admin and public) that include this phrase.
	 */
	protected function _recompileTemplatesIncludingPhrase()
	{
		XenForo_Template_Compiler::resetPhraseCache();

		$templateModel = $this->_getTemplateModel();
		$adminTemplateModel = $this->_getAdminTemplateModel();
		$emailTemplateModel = $this->_getEmailTemplateModel();

		$title = $this->get('title');

		$templateModel->compileTemplatesThatIncludePhrase($title);
		$adminTemplateModel->compileAdminTemplatesThatIncludePhrase($title);
		$emailTemplateModel->compileEmailTemplatesThatIncludePhrase($title);

		if ($this->isChanged('title') && $this->isUpdate())
		{
			$existingTitle = $this->getExisting('title');

			$templateModel->compileTemplatesThatIncludePhrase($existingTitle);
			$adminTemplateModel->compileAdminTemplatesThatIncludePhrase($existingTitle);
			$emailTemplateModel->compileEmailTemplatesThatIncludePhrase($existingTitle);
		}
	}

	protected function _buildPhraseMap()
	{
		$phraseModel = $this->_getPhraseModel();
		$phraseModel->buildPhraseMap($this->get('phrase_id'), $this->get('language_id'), $this->get('title'));
	}

	/**
	 * Post-delete handler.
	 */
	protected function _postDelete()
	{
		$dataChanged = $this->_deleteMappedData();
		if ($dataChanged && $this->getOption(self::OPTION_RECOMPILE_PHRASE))
		{
			$this->_recompilePhrase();
		}

		if ($this->getOption(self::OPTION_RECOMPILE_TEMPLATE))
		{
			$this->_recompileTemplatesIncludingPhrase();
		}

		$this->_rebuildLanguageCaches();

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

	protected function _deleteMappedData()
	{
		$phraseModel = $this->_getPhraseModel();

		$mappedPhrases = $phraseModel->getMappedPhrasesByPhraseId($this->get('phrase_id'));
		if ($mappedPhrases)
		{
			$myPhraseMapId = 0;
			$phraseMapIds = array();
			$languageIds = array();
			foreach ($mappedPhrases AS $mappedPhrase)
			{

				if ($mappedPhrase['language_id'] == $this->get('language_id'))
				{
					$myPhraseMapId = $mappedPhrase['phrase_map_id'];
				}

				$phraseMapIds[] = $mappedPhrase['phrase_map_id'];
				$languageIds[] = $mappedPhrase['language_id'];
			}

			$phraseMapIdsQuoted = $this->_db->quote($phraseMapIds);

			$parentMappedPhrase = $phraseModel->getParentMappedPhraseByTitle($this->get('title'), $this->get('language_id'));

			if ($parentMappedPhrase)
			{
				// point everything pointing at this phrase to the parent
				$this->_db->update('xf_phrase_map',
					array('phrase_id' => $parentMappedPhrase['phrase_id']),
					'phrase_map_id IN (' . $phraseMapIdsQuoted . ')'
				);
				return true;
			}
			else
			{
				// no parent, remove phrase - this should primarily happen when deleting a master or custom phrase
				$this->_db->delete('xf_phrase_map', 'phrase_map_id IN (' . $phraseMapIdsQuoted . ')');
				$this->_db->delete('xf_phrase_compiled',
					'language_id IN (' . $this->_db->quote($languageIds) . ') AND title = ' . $this->_db->quote($this->get('title'))
				);
			}
		}

		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');
		$fileName = $dir . '/' . $existingTitle . '.txt';

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

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

	/**
	 * Gets the language model object.
	 *
	 * @return XenForo_Model_Language
	 */
	protected function _getLanguageModel()
	{
		return $this->getModelFromCache('XenForo_Model_Language');
	}

	/**
	 * Gets the template model.
	 *
	 * @return XenForo_Model_Template
	 */
	protected function _getTemplateModel()
	{
		return $this->getModelFromCache('XenForo_Model_Template');
	}

	/**
	 * Gets the admin template model.
	 *
	 * @return XenForo_Model_AdminTemplate
	 */
	protected function _getAdminTemplateModel()
	{
		return $this->getModelFromCache('XenForo_Model_AdminTemplate');
	}

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