<?php
namespace XF\Service\Style;
use XF\Entity\Style;
use function count;
class Import extends \XF\Service\AbstractService
{
/**
* @var Style|null
*/
protected $overwriteStyle;
/**
* @var Style|null
*/
protected $parentStyle;
/**
* @var ArchiveImport
*/
protected $archiveImporter;
public function setArchiveImporter(ArchiveImport $archiveImporter)
{
$this->archiveImporter = $archiveImporter;
}
public function setOverwriteStyle(Style $style)
{
$this->overwriteStyle = $style;
$this->parentStyle = null;
}
public function getOverwriteStyle()
{
return $this->overwriteStyle;
}
public function setParentStyle(Style $style = null)
{
$this->parentStyle = $style;
$this->overwriteStyle = null;
}
public function getParentStyle()
{
return $this->parentStyle;
}
public function isValidXml($rootElement, &$error = null)
{
if (!($rootElement instanceof \SimpleXMLElement))
{
$error = \XF::phrase('please_upload_valid_style_xml_file');
return false;
}
if ($rootElement->getName() != 'style' || (string)$rootElement['title'] === '')
{
$error = \XF::phrase('please_upload_valid_style_xml_file');
return false;
}
if ((string)$rootElement['export_version'] != (string)Export::EXPORT_VERSION_ID)
{
$error = \XF::phrase('this_style_xml_file_was_not_built_for_this_version_of_xenforo');
return false;
}
return true;
}
public function isValidConfiguration(\SimpleXMLElement $document, &$errors = null)
{
$errors = [];
$addOnId = (string)$document['addon_id'];
if ($addOnId && $addOnId != 'XF')
{
$addOn = $this->app->addOnManager()->getById($addOnId);
if (!$addOn)
{
$errors['addon_id'] = \XF::phrase('xml_file_relates_add_on_not_installed_install_first');
$expectedVersionId = null;
}
else
{
$expectedVersionId = $addOn->version_id;
}
}
else
{
$expectedVersionId = \XF::$versionId;
}
$baseVersionId = (int)$document['base_version_id'];
if ($expectedVersionId && $baseVersionId)
{
if ($baseVersionId > $expectedVersionId)
{
$errors['version_id'] = \XF::phrase('xml_file_based_on_newer_version_than_installed');
}
}
if ($this->overwriteStyle)
{
$title = (string)$document['title'];
if ($title != $this->overwriteStyle->title)
{
$errors['title'] = \XF::phrase('title_of_style_importing_differs_overwriting_is_correct');
}
}
return (count($errors) == 0);
}
/**
* Returns whether the asset paths associated with this style XML are writable. Only apples to paths
* that refer to areas within the XF root (and not the data:// paths). This is primarily called when
* importing a style archive into its original path.
*
* @param \SimpleXMLElement $document Style XML root
* @param array $failed List of paths that were unwritable
*
* @return bool
*/
public function validateAssetPathsWritable(\SimpleXMLElement $document, &$failed = []): bool
{
$failed = [];
$assets = $this->getAssetValues($document->assets);
foreach ($assets AS $path)
{
if (!$path || preg_match('#^(https?://|data://|/|\\\\)#i', $path))
{
continue;
}
$fullPath = \XF::getRootDirectory() . '/' . $path;
if (!\XF\Util\File::isWritable($fullPath))
{
$failed[] = $path;
}
}
return count($failed) == 0;
}
public function importFromXml(\SimpleXMLElement $document)
{
$db = $this->db();
$db->beginTransaction();
$addOnId = (string)$document['addon_id'];
$style = $this->getTargetStyle($document);
$this->importAssets($style, $document->assets, $addOnId);
$this->importPropertyGroups($style, $document->properties, $addOnId);
$this->importProperties($style, $document->properties, $addOnId);
$this->importTemplates($style, $document->templates, $addOnId);
/** @var \XF\Repository\Style $styleRepo */
$styleRepo = $this->repository('XF:Style');
$styleRepo->triggerStyleDataRebuild();
$db->commit();
return $style;
}
public function importAssets(Style $style, \SimpleXMLElement $container, $addOnId)
{
$assets = $this->getAssetValues($container);
$archiveImporter = $this->archiveImporter;
if ($archiveImporter)
{
$assets = $archiveImporter->copyAssetFiles($style, $assets);
}
$style->assets = $assets;
$style->save(true, false);
}
protected function getAssetValues(\SimpleXMLElement $container): array
{
$assets = [];
if (!$container->asset)
{
return $assets;
}
foreach ($container->asset AS $xmlAsset)
{
$key = (string)$xmlAsset['key'];
$path = (string)$xmlAsset['path'];
$path = str_replace('\\', '/', $path);
if (strpos($path, '../') !== false)
{
continue;
}
$assets[$key] = $path;
}
return $assets;
}
public function importTemplates(Style $style, \SimpleXMLElement $container, $addOnId)
{
$styleId = $style->style_id;
$existingTemplates = $this->getExistingTemplates($style);
foreach ($container->template AS $xmlTemplate)
{
$title = (string)$xmlTemplate['title'];
$type = (string)$xmlTemplate['type'];
$key = "$type-$title";
$template = $existingTemplates[$key] ?? $this->em()->create('XF:Template');
$template->title = $title;
$template->style_id = $styleId;
$template->type = $type;
$this->setupTemplateImport($template, $xmlTemplate);
$template->save(true, false);
unset($existingTemplates[$key]);
}
// removed templates
foreach ($existingTemplates AS $existingTemplate)
{
if ($addOnId && $existingTemplate->addon_id !== $addOnId)
{
// wouldn't be covered so leave it
continue;
}
$this->setTemplateOptions($existingTemplate);
$existingTemplate->delete(true, false);
}
}
/**
* @param Style $style
*
* @return \XF\Entity\Template[]
*/
protected function getExistingTemplates(Style $style)
{
/** @var \XF\Finder\Template $templateFinder */
$templateFinder = $this->finder('XF:Template');
$templateFinder->where('style_id', $style->style_id)
->orderTitle();
$output = [];
foreach ($templateFinder->fetch() AS $template)
{
$output["{$template->type}-{$template->title}"] = $template;
}
return $output;
}
protected function setupTemplateImport(\XF\Entity\Template $template, \SimpleXMLElement $xmlTemplate)
{
$this->setTemplateOptions($template);
$template->template = \XF\Util\Xml::processSimpleXmlCdata($xmlTemplate);
$template->addon_id = (string)$xmlTemplate['addon_id'];
$template->version_id = (int)$xmlTemplate['version_id'];
$template->version_string = (string)$xmlTemplate['version_string'];
}
protected function setTemplateOptions(\XF\Entity\Template $template)
{
$template->setOption('recompile', false);
$template->setOption('test_compile', false);
$template->setOption('rebuild_map', false);
$template->setOption('check_duplicate', false);
$template->getBehavior('XF:DevOutputWritable')->setOption('write_dev_output', false);
}
public function importPropertyGroups(Style $style, \SimpleXMLElement $container, $addOnId)
{
$styleId = $style->style_id;
$existingGroups = $this->getExistingGroups($style);
foreach ($container->group AS $xmlGroup)
{
$groupName = (string)$xmlGroup['group_name'];
$group = $existingGroups[$groupName] ?? $this->em()->create('XF:StylePropertyGroup');
$group->group_name = $groupName;
$group->style_id = $styleId;
$this->setupPropertyGroupImport($group, $xmlGroup);
$group->save(true, false);
unset($existingGroups[$groupName]);
}
// removed groups
foreach ($existingGroups AS $existingGroup)
{
if ($addOnId && $existingGroup->addon_id !== $addOnId)
{
// wouldn't be covered so leave it
continue;
}
$existingGroup->delete(true, false);
}
}
protected function setupPropertyGroupImport(\XF\Entity\StylePropertyGroup $group, \SimpleXMLElement $xmlGroup)
{
$group->getBehavior('XF:DevOutputWritable')->setOption('write_dev_output', false);
$group->title = (string)$xmlGroup['title'];
$group->description = (string)$xmlGroup['description'];
$group->display_order = (int)$xmlGroup['display_order'];
$group->addon_id = (string)$xmlGroup['addon_id'];
}
/**
* @param Style $style
*
* @return \XF\Entity\StylePropertyGroup[]
*/
protected function getExistingGroups(Style $style)
{
$finder = $this->finder('XF:StylePropertyGroup')
->where('style_id', $style->style_id)
->keyedBy('group_name');
return $finder->fetch();
}
public function importProperties(Style $style, \SimpleXMLElement $container, $addOnId)
{
$styleId = $style->style_id;
$existingProperties = $this->getExistingProperties($style);
foreach ($container->property AS $xmlProperty)
{
$propertyName = (string)$xmlProperty['property_name'];
$property = $existingProperties[$propertyName] ?? $this->em()->create('XF:StyleProperty');
$property->property_name = $propertyName;
$property->style_id = $styleId;
$this->setupPropertyImport($property, $xmlProperty);
$property->save(true, false);
unset($existingProperties[$propertyName]);
}
// removed properties
foreach ($existingProperties AS $existingProperty)
{
if ($addOnId && $existingProperty->addon_id !== $addOnId)
{
// wouldn't be covered so leave it
continue;
}
$this->setPropertyOptions($existingProperty);
$existingProperty->delete(false, false);
}
}
protected function setupPropertyImport(\XF\Entity\StyleProperty $property, \SimpleXMLElement $xmlProperty)
{
$this->setPropertyOptions($property);
$property->group_name = (string)$xmlProperty['group_name'];
$property->title = (string)$xmlProperty['title'];
$property->description = (string)$xmlProperty['description'];
$property->property_type = (string)$xmlProperty['property_type'];
$property->value_type = (string)$xmlProperty['value_type'];
$property->depends_on = (string)$xmlProperty['depends_on'];
$property->value_group = (string)$xmlProperty['value_group'];
$property->display_order = (int)$xmlProperty['display_order'];
$property->addon_id = (string)$xmlProperty['addon_id'];
$cssComponents = (string)$xmlProperty['css_components'];
$property->css_components = $cssComponents ? explode(',', $cssComponents) : [];
$property->value_parameters = (string)$xmlProperty->value_parameters;
$value = (string)$xmlProperty->value;
$property->property_value = json_decode($value, true);
}
protected function setPropertyOptions(\XF\Entity\StyleProperty $property)
{
$property->getBehavior('XF:DevOutputWritable')->setOption('write_dev_output', false);
$property->setOption('rebuild_map', false);
$property->setOption('rebuild_style', false);
}
/**
* @param Style $style
*
* @return \XF\Entity\StyleProperty[]
*/
protected function getExistingProperties(Style $style)
{
$finder = $this->finder('XF:StyleProperty')
->where('style_id', $style->style_id)
->keyedBy('property_name');
return $finder->fetch();
}
protected function getTargetStyle(\SimpleXMLElement $document)
{
if ($this->overwriteStyle)
{
return $this->overwriteStyle;
}
else
{
$style = $this->em()->create('XF:Style');
$style->title = (string)$document['title'];
$style->description = (string)$document['description'];
$style->parent_id = $this->parentStyle ? $this->parentStyle->style_id : 0;
$style->user_selectable = (string)$document['user_selectable'];
$style->save(true, false);
return $style;
}
}
}