marc-leopold/cms/plugins/rainlab/builder/classes/LocalizationModel.php

436 lines
13 KiB
PHP

<?php namespace RainLab\Builder\Classes;
use ApplicationException;
use Symfony\Component\Yaml\Dumper as YamlDumper;
use SystemException;
use DirectoryIterator;
use ValidationException;
use Yaml;
use Exception;
use Config;
use Lang;
use File;
/**
* Represents and manages plugin localization files.
*
* @package rainlab\builder
* @author Alexey Bobkov, Samuel Georges
*/
class LocalizationModel extends BaseModel
{
public $strings;
public $language;
public $originalLanguage;
protected static $fillable = [
'strings',
'language'
];
protected $validationRules = [
'language' => ['required', 'regex:/^[a-z0-9\.\-]+$/i']
];
protected $originalStringArray = [];
public function load($language)
{
$this->language = $language;
$this->originalLanguage = $language;
$filePath = $this->getFilePath();
if (!File::isFile($filePath)) {
throw new ApplicationException(Lang::get('rainlab.builder::lang.localization.error_cant_load_file'));
}
if (!$this->validateFileContents($filePath)) {
throw new ApplicationException(Lang::get('rainlab.builder::lang.localization.error_bad_localization_file_contents'));
}
$strings = include($filePath);
if (!is_array($strings)) {
throw new ApplicationException(Lang::get('rainlab.builder::lang.localization.error_file_not_array'));
}
$this->originalStringArray = $strings;
if (count($strings) > 0) {
$dumper = new YamlDumper();
$this->strings = $dumper->dump($strings, 20, 0, false, true);
}
else {
$this->strings = '';
}
$this->exists = true;
}
public static function initModel($pluginCode, $language)
{
$model = new self();
$model->setPluginCode($pluginCode);
$model->language = $language;
return $model;
}
public function save()
{
$data = $this->modelToLanguageFile();
$this->validate();
$filePath = File::symbolizePath($this->getFilePath());
$isNew = $this->isNewModel();
if (File::isFile($filePath)) {
if ($isNew || $this->originalLanguage != $this->language) {
throw new ValidationException(['fileName' => Lang::get('rainlab.builder::lang.common.error_file_exists', ['path'=>$this->language.'/'.basename($filePath)])]);
}
}
$fileDirectory = dirname($filePath);
if (!File::isDirectory($fileDirectory)) {
if (!File::makeDirectory($fileDirectory, 0777, true, true)) {
throw new ApplicationException(Lang::get('rainlab.builder::lang.common.error_make_dir', ['name'=>$fileDirectory]));
}
}
if (@File::put($filePath, $data) === false) {
throw new ApplicationException(Lang::get('rainlab.builder::lang.localization.save_error', ['name'=>$filePath]));
}
@File::chmod($filePath);
if (!$this->isNewModel() && strlen($this->originalLanguage) > 0 && $this->originalLanguage != $this->language) {
$this->originalFilePath = $this->getFilePath($this->originalLanguage);
@File::delete($this->originalFilePath);
}
$this->originalLanguage = $this->language;
$this->exists = true;
}
public function deleteModel()
{
if ($this->isNewModel()) {
throw new ApplicationException('Cannot delete language file which is not saved yet.');
}
$filePath = File::symbolizePath($this->getFilePath());
if (File::isFile($filePath)) {
if (!@unlink($filePath)) {
throw new ApplicationException(Lang::get('rainlab.builder::lang.localization.error_delete_file'));
}
}
}
public function initContent()
{
$templatePath = '$/rainlab/builder/classes/localizationmodel/templates/lang.php';
$templatePath = File::symbolizePath($templatePath);
$strings = include($templatePath);
$dumper = new YamlDumper();
$this->strings = $dumper->dump($strings, 20, 0, false, true);
}
public static function listPluginLanguages($pluginCodeObj)
{
$languagesDirectoryPath = $pluginCodeObj->toPluginDirectoryPath().'/lang';
$languagesDirectoryPath = File::symbolizePath($languagesDirectoryPath);
if (!File::isDirectory($languagesDirectoryPath)) {
return [];
}
$result = [];
foreach (new DirectoryIterator($languagesDirectoryPath) as $fileInfo) {
if (!$fileInfo->isDir() || $fileInfo->isDot()) {
continue;
}
$langFilePath = $fileInfo->getPathname().'/lang.php';
if (File::isFile($langFilePath)) {
$result[] = $fileInfo->getFilename();
}
}
return $result;
}
public function copyStringsFrom($destinationText, $sourceLanguageCode)
{
$sourceLanguageModel = new self();
$sourceLanguageModel->setPluginCodeObj($this->getPluginCodeObj());
$sourceLanguageModel->load($sourceLanguageCode);
$srcArray = $sourceLanguageModel->getOriginalStringsArray();
$languageMixer = new LanguageMixer();
return $languageMixer->addStringsFromAnotherLanguage($destinationText, $srcArray);
}
public function getOriginalStringsArray()
{
return $this->originalStringArray;
}
public function createStringAndSave($stringKey, $stringValue)
{
$stringKey = trim($stringKey, '.');
if (!strlen($stringKey)) {
throw new ValidationException(['key' => Lang::get('rainlab.builder::lang.localization.string_key_is_empty')]);
}
if (!strlen($stringValue)) {
throw new ValidationException(['value' => Lang::get('rainlab.builder::lang.localization.string_value_is_empty')]);
}
$originalStringArray = $this->getOriginalStringsArray();
$languagePrefix = strtolower($this->getPluginCodeObj()->toCode()).'::lang.';
$existingStrings = self::convertToStringsArray($originalStringArray, $languagePrefix);
if (array_key_exists($languagePrefix.$stringKey, $existingStrings)) {
throw new ValidationException(['key' => Lang::get('rainlab.builder::lang.localization.string_key_exists')]);
}
$existingSections = self::convertToSectionsArray($originalStringArray);
if (array_key_exists($stringKey.'.', $existingSections)) {
throw new ValidationException(['key' => Lang::get('rainlab.builder::lang.localization.string_key_exists')]);
}
$sectionArray = [];
self::createStringSections($sectionArray, $stringKey, $stringValue) ;
$this->checkKeyWritable($stringKey, $existingStrings, $languagePrefix);
$newStrings = LanguageMixer::arrayMergeRecursive($originalStringArray, $sectionArray);
$dumper = new YamlDumper();
$this->strings = $dumper->dump($newStrings, 20, 0, false, true);
$this->save();
return $languagePrefix.$stringKey;
}
public static function getDefaultLanguage()
{
$language = Config::get('app.locale');
if (!$language) {
throw new ApplicationException('The default language is not defined in the application configuration (app.locale).');
}
return $language;
}
public static function getPluginRegistryData($pluginCode, $subtype)
{
$defaultLanguage = self::getDefaultLanguage();
$model = new self();
$model->setPluginCode($pluginCode);
$model->language = $defaultLanguage;
$filePath = $model->getFilePath();
if (!File::isFile($filePath)) {
return [];
}
$model->load($defaultLanguage);
$array = $model->getOriginalStringsArray();
$languagePrefix = strtolower($model->getPluginCodeObj()->toCode()).'::lang.';
if ($subtype !== 'sections') {
return self::convertToStringsArray($array, $languagePrefix);
}
return self::convertToSectionsArray($array);
}
public static function languageFileExists($pluginCode, $language)
{
$model = new self();
$model->setPluginCode($pluginCode);
$model->language = $language;
$filePath = $model->getFilePath();
return File::isFile($filePath);
}
protected static function createStringSections(&$arr, $path, $value) {
$keys = explode('.', $path);
while ($key = array_shift($keys)) {
$arr = &$arr[$key];
}
$arr = $value;
}
protected static function convertToStringsArray($stringsArray, $prefix, $currentKey = '')
{
$result = [];
foreach ($stringsArray as $key=>$value) {
$newKey = strlen($currentKey) ? $currentKey.'.'.$key : $key;
if (is_scalar($value)) {
$result[$prefix.$newKey] = $value;
}
else {
$result = array_merge($result, self::convertToStringsArray($value, $prefix, $newKey));
}
}
return $result;
}
protected static function convertToSectionsArray($stringsArray, $currentKey = '')
{
$result = [];
foreach ($stringsArray as $key=>$value) {
$newKey = strlen($currentKey) ? $currentKey.'.'.$key : $key;
if (is_scalar($value)) {
$result[$currentKey.'.'] = $currentKey.'.';
}
else {
$result = array_merge($result, self::convertToSectionsArray($value, $newKey));
}
}
return $result;
}
protected function validateLanguage($language)
{
return preg_match('/^[a-z0-9\.\-]+$/i', $language);
}
protected function getFilePath($language = null)
{
if ($language === null) {
$language = $this->language;
}
$language = trim($language);
if (!strlen($language)) {
throw new SystemException('The form model language is not set.');
}
if (!$this->validateLanguage($language)) {
throw new SystemException('Invalid language file name: '.$language);
}
$path = $this->getPluginCodeObj()->toPluginDirectoryPath().'/lang/'.$language.'/lang.php';
return File::symbolizePath($path);
}
protected function modelToLanguageFile()
{
$this->strings = trim($this->strings);
if (!strlen($this->strings)) {
return "<?php return [\n];";
}
try {
$data = $this->getSanitizedPHPStrings(Yaml::parse($this->strings));
$phpData = var_export($data, true);
$phpData = preg_replace('/^(\s+)\),/m', '$1],', $phpData);
$phpData = preg_replace('/^(\s+)array\s+\(/m', '$1[', $phpData);
$phpData = preg_replace_callback('/^(\s+)/m', function($matches) {
return str_repeat($matches[1], 2); // Increase indentation
}, $phpData);
$phpData = preg_replace('/\n\s+\[/m', '[', $phpData);
$phpData = preg_replace('/^array\s\(/', '[', $phpData);
$phpData = preg_replace('/^\)\Z/m', ']', $phpData);
return "<?php return ".$phpData.";";
}
catch (Exception $ex) {
throw new ApplicationException(sprintf('Cannot parse the YAML content: %s', $ex->getMessage()));
}
}
protected function validateFileContents($path)
{
$fileContents = File::get($path);
$stream = new PhpSourceStream($fileContents);
$invalidTokens = [
T_CLASS,
T_FUNCTION,
T_INCLUDE,
T_INCLUDE_ONCE,
T_REQUIRE,
T_REQUIRE_ONCE,
T_EVAL,
T_ECHO,
T_GOTO,
T_HALT_COMPILER,
T_STRING // Unescaped strings - function names, etc.
];
while ($stream->forward()) {
$tokenCode = $stream->getCurrentCode();
if (in_array($tokenCode, $invalidTokens)) {
return false;
}
}
return true;
}
protected function getSanitizedPHPStrings($strings)
{
array_walk_recursive($strings, function(&$item, $key){
if (!is_scalar($item)) {
return;
}
// In YAML single quotes are escaped with two single quotes
// http://yaml.org/spec/current.html#id2534365
$item = str_replace("''", "'", $item);
});
return $strings;
}
protected function checkKeyWritable($stringKey, $existingStrings, $languagePrefix)
{
$sectionList = explode('.', $stringKey);
$lastElement = array_pop($sectionList);
while (strlen($lastElement)) {
if (count($sectionList) > 0) {
$fullKey = implode('.', $sectionList).'.'.$lastElement;
}
else {
$fullKey = $lastElement;
}
if (array_key_exists($languagePrefix.$fullKey, $existingStrings)) {
throw new ValidationException(['key' => Lang::get('rainlab.builder::lang.localization.string_key_is_a_string', ['key'=>$fullKey])]);
}
$lastElement = array_pop($sectionList);
}
}
}