<?php
namespace XF\Admin\Controller;
use XF\Diff;
use XF\Diff3;
use XF\Mvc\ParameterBag;
use function count, strlen;
class Template extends AbstractController
{
protected function preDispatchController($action, ParameterBag $params)
{
$this->assertAdminPermission('style');
}
public function actionIndex()
{
$style = $this->plugin('XF:Style')->getActiveEditStyle();
$type = $this->filter('type', 'string');
if (!$type)
{
$type = 'public';
}
return $this->redirect($this->buildLink('styles/templates', $style, ['type' => $type]));
}
protected function templateAddEdit(\XF\Entity\Template $template)
{
if ($template->exists() && !$this->request->exists('style_id'))
{
$styleId = $template->style_id;
}
else
{
$styleId = $this->filter('style_id', 'uint');
if (empty($template->title) && $prefix = $this->filter('prefix', 'str'))
{
$template->set('title', $prefix);
}
}
$style = $this->assertStyleExists($styleId);
if (!$style->canEdit())
{
return $this->error(\XF::phrase('templates_in_this_style_can_not_be_modified'));
}
$types = $this->getTemplateRepo()->getTemplateTypes($style);
if (!isset($types[$template->type]))
{
return $this->error(\XF::phrase('templates_in_this_style_can_not_be_modified'));
}
if (!$template->exists() && $style->style_id)
{
$template->addon_id = '';
}
$viewParams = [
'template' => $template,
'hasHistory' => $template->exists() ? $template->History->count() : false,
'style' => $style,
'styleTree' => $this->getStyleRepo()->getStyleTree(),
'types' => $types,
'redirect' => $this->getDynamicRedirect()
];
return $this->view('XF:Template\Edit', 'template_edit', $viewParams);
}
public function actionEdit(ParameterBag $params)
{
$template = $this->assertTemplateExists($params->template_id);
return $this->templateAddEdit($template);
}
public function actionAdd()
{
$template = $this->em()->create('XF:Template');
$template->type = $this->filter('type', 'str', 'public');
return $this->templateAddEdit($template);
}
public function actionCodeEditorModeLoader()
{
$language = $this->filter('language', 'str');
/** @var \XF\ControllerPlugin\CodeEditor $plugin */
$plugin = $this->plugin('XF:CodeEditor');
return $plugin->actionModeLoader($language);
}
protected function templateSaveProcess(\XF\Entity\Template $template)
{
$form = $this->formAction();
$input = $this->filter([
'style_id' => 'uint',
'type' => 'str',
'title' => 'str',
'template' => 'str',
'addon_id' => 'str'
]);
$form->setup(function() use ($template)
{
if ($template->style_id > 0)
{
// force an update to resolve any out of date issues
$template->updateVersionId();
$template->last_edit_date = \XF::$time;
}
});
$form->basicEntitySave($template, $input);
return $form;
}
public function actionSave(ParameterBag $params)
{
$this->assertPostOnly();
if ($params->template_id)
{
$template = $this->assertTemplateExists($params->template_id);
$styleId = $this->filter('style_id', 'uint');
if ($template->style_id != $styleId)
{
$template = $this->finder('XF:Template')->where([
'style_id' => $styleId,
'title' => $template->title,
'type' => $template->type
])->fetchOne();
if (!$template)
{
$template = $this->em()->create('XF:Template');
}
}
}
else
{
$template = $this->em()->create('XF:Template');
}
$this->templateSaveProcess($template)->run();
$dynamicRedirect = $this->getDynamicRedirect('invalid', false);
if ($dynamicRedirect == 'invalid' || !preg_match('#(styles|templates)/#', $dynamicRedirect))
{
$dynamicRedirect = null;
}
if ($this->request->exists('exit'))
{
if ($dynamicRedirect)
{
$redirect = $dynamicRedirect;
}
else
{
$redirect = $this->buildLink('styles/templates', $template->Style, ['type' => $template->type]);
}
$redirect .= $this->buildLinkHash($template->template_id);
}
else
{
$redirect = $this->buildLink('templates/edit', $template, ['_xfRedirect' => $dynamicRedirect]);
}
return $this->redirect($redirect);
}
public function actionHistory(ParameterBag $params)
{
$template = $this->assertTemplateExists($params->template_id);
$oldId = $this->filter('old', 'uint');
$newId = $this->filter('new', 'uint');
$list = [];
if ($template->History)
{
$list = $template->History;
$list = $list->toArray();
arsort($list);
}
$newestHistory = reset($list);
if ($oldId)
{
// doing a comparison
$oldText = isset($list[$oldId]) ? $list[$oldId]['template'] : '';
if ($newId)
{
$newText = isset($list[$newId]) ? $list[$newId]['template'] : '';
}
else
{
$newText = $template['template'];
}
$diff = new Diff();
$diffs = $diff->findDifferences($oldText, $newText);
}
else
{
$diffs = [];
}
$viewId = $this->filter('view', 'uint');
if ($viewId)
{
$history = $list[$viewId] ?? false;
}
else
{
$history = false;
}
$viewParams = [
'template' => $template,
'list' => $list,
'oldId' => ($oldId ? $oldId : ($newestHistory ? $newestHistory['template_history_id'] : 0)),
'newId' => $newId,
'diffs' => $diffs,
'history' => $history
];
if ($history)
{
return $this->view('XF:Template\History\View', 'template_history_view', $viewParams);
}
else if ($oldId)
{
return $this->view('XF:Template\History\Compare', 'template_history_compare', $viewParams);
}
else
{
return $this->view('XF:Template\History', 'template_history', $viewParams);
}
}
public function actionDelete(ParameterBag $params)
{
$template = $this->assertTemplateExists($params->template_id);
if (!$template->Style || !$template->Style->canEdit())
{
return $this->error(\XF::phrase('templates_in_this_style_can_not_be_modified'));
}
if ($this->isPost())
{
$template->delete();
$redirect = $this->getDynamicRedirect('invalid', false);
if ($redirect == 'invalid' || !preg_match('#(styles|templates)/#', $redirect))
{
$redirect = $this->buildLink('styles/templates', $template->Style, ['type' => $template->type]);
}
return $this->redirect($redirect);
}
else
{
$viewParams = [
'template' => $template
];
return $this->view('XF:Template\Delete', 'template_delete', $viewParams);
}
}
protected function filterSearchConditions()
{
return $this->filter([
'addon_id' => 'str',
'type' => 'str',
'title' => 'str',
'template' => 'str',
'template_cs' => 'bool',
'state' => 'array-str'
]);
}
/**
* @deprecated Functionality is now provided by \XF\Filterer\Template
*/
protected function getTemplateSearchFinder(\XF\Entity\Style $style, array &$linkParams = [])
{
// NOTE: XF does not actually use this at all any more, the functionality is provided by the template Filterer
$conditions = $this->filterSearchConditions();
$templateRepo = $this->getTemplateRepo();
$availableTypes = $templateRepo->getTemplateTypes($style);
if (!isset($availableTypes[$conditions['type']]))
{
$conditions['type'] = 'public';
}
$finder = $templateRepo->findEffectiveTemplatesInStyle($style, $conditions['type']);
$finder->Template
->searchTitle($conditions['title'])
->searchTemplate($conditions['template'], $conditions['template_cs'])
->fromAddOn($conditions['addon_id']);
$finder->isTemplateState($conditions['state']);
if ($conditions['type'])
{
$linkParams['type'] = $conditions['type'];
}
if (strlen($conditions['title']))
{
$linkParams['title'] = $conditions['title'];
}
if (strlen($conditions['template']))
{
$linkParams['template'] = $conditions['template'];
$linkParams['template_cs'] = $conditions['template_cs'];
}
if ($conditions['addon_id'])
{
$linkParams['addon_id'] = $conditions['addon_id'];
}
if ($conditions['state'])
{
$linkParams['state'] = $conditions['state'];
}
return $finder;
}
public function actionSearch()
{
$this->setSectionContext('searchTemplates');
$styleRepo = $this->getStyleRepo();
if ($this->filter('search', 'uint'))
{
$style = $this->assertStyleExists($this->filter('style_id', 'uint'));
if (!$style->canEdit())
{
return $this->error(\XF::phrase('templates_in_this_style_can_not_be_modified'));
}
$filterer = $this->setupTemplateFilterer($style);
$finder = $filterer->apply();
$filterLinkParams = $filterer->getLinkParams();
if (count($filterLinkParams) <= 1)
{
$type = $filterer->getRawFilters()['type'] ?? 'public';
// we always get a type filter, so we want to make sure there's something more than that
return $this->redirect($this->buildLink('styles/templates', $style, ['type' => $type]));
}
$linkParams = [
'search' => 1,
'style_id' => $style->style_id
];
$linkParams += $filterLinkParams;
$total = $finder->total();
if ($this->isPost() && $total > 0)
{
return $this->redirect($this->buildLink('templates/search', null, $linkParams));
}
$finder->with('Template.AddOn');
$templates = $finder->fetch();
$viewParams = [
'style' => $style,
'templates' => $templates,
'total' => $total,
'linkParams' => $linkParams,
'filterDisplay' => $filterer->getDisplayValues(),
];
return $this->view('XF:Template\SearchResults', 'template_search_results', $viewParams);
}
else
{
$viewParams = [
'styleTree' => $styleRepo->getStyleTree(),
'styleId' => $this->plugin('XF:Style')->getActiveStyleId(),
'types' => $this->getTemplateRepo()->getTemplateTypes()
];
return $this->view('XF:Template\Search', 'template_search', $viewParams);
}
}
protected function setupTemplateFilterer(\XF\Entity\Style $style): \XF\Filterer\Template
{
$templateRepo = $this->getTemplateRepo();
/** @var \XF\Filterer\Template $filterer */
$filterer = $this->app->filterer('XF:Template', [
'style_id' => $style->style_id,
'template_types' => $templateRepo->getTemplateTypes($style)
]);
$filterer->addFilters($this->request, $this->filter('_skipFilter', 'str'));
return $filterer;
}
public function actionRefineSearch()
{
$style = $this->assertStyleExists($this->filter('style_id', 'uint'));
if (!$style->canEdit())
{
$style = $this->plugin('XF:Style')->getActiveEditStyle();
}
$styleRepo = $this->getStyleRepo();
$filterer = $this->setupTemplateFilterer($style);
$viewParams = [
'style' => $style,
'styleTree' => $styleRepo->getStyleTree(),
'conditions' => $filterer->getFiltersForForm(),
'types' => $this->getTemplateRepo()->getTemplateTypes()
];
return $this->view('XF:Template\RefineSearch', 'template_refine_search', $viewParams);
}
public function actionOutdated()
{
$this->setSectionContext('outdatedTemplates');
$outdatedTemplates = $this->getTemplateRepo()->getOutdatedTemplates();
$outdatedGrouped = \XF\Util\Arr::arrayGroup(
$outdatedTemplates,
function($v) { return $v['template']->style_id; }
);
$viewParams = [
'outdatedTemplates' => $outdatedTemplates,
'outdatedGrouped' => $outdatedGrouped,
'styleTree' => $this->repository('XF:Style')->getStyleTree(),
'total' => count($outdatedTemplates),
'autoMerged' => $this->filter('automerged', 'bool')
];
return $this->view('XF:Template\Outdated', 'template_outdated', $viewParams);
}
public function actionCompare(ParameterBag $params)
{
$template = $this->assertTemplateForComparison($params->template_id);
$parentTemplate = $template->ParentTemplate;
$diff = new Diff();
$diffs = $diff->findDifferences($parentTemplate->template, $template->template);
$viewParams = [
'template' => $template,
'parentTemplate' => $parentTemplate,
'diffs' => $diffs
];
return $this->view('XF:Template\Compare', 'template_compare', $viewParams);
}
public function actionMergeOutdated(ParameterBag $params)
{
$this->setSectionContext('outdatedTemplates');
$template = $this->assertTemplateForComparison($params->template_id);
$parentTemplate = $template->ParentTemplate;
if (!$parentTemplate->last_edit_date || $parentTemplate->last_edit_date < $template->last_edit_date)
{
return $this->error(\XF::phrase('custom_template_out_of_date_edited_recently_no_merge'));
}
/** @var \XF\Repository\TemplateHistory $historyRepo */
$historyRepo = $this->repository('XF:TemplateHistory');
$previousVersion = $historyRepo->getHistoryForMerge($template, $parentTemplate);
if (!$previousVersion)
{
return $this->error(\XF::phrase('no_previous_version_of_parent_could_be_found'));
}
if ($this->isPost())
{
$merged = $this->filter('merged', 'array-str,no-trim');
$final = implode("\n", $merged);
$template->template = $final;
$template->last_edit_date = time();
// even if the template isn't changed we should
// make the custom template the same version so
// it is no longer outdated.
if (!$template->isChanged('template'))
{
$template->updateVersionId();
}
$template->save();
return $this->redirect($this->buildLink('templates/outdated'));
}
else
{
$diff = new Diff3();
$diffs = $diff->findDifferences($template->template, $previousVersion->template, $parentTemplate->template);
$viewParams = [
'template' => $template,
'parentTemplate' => $parentTemplate,
'previousVersion' => $previousVersion,
'diffs' => $diffs
];
return $this->view('XF:Template\MergeOutdated', 'template_merge_outdated', $viewParams);
}
}
public function actionAutoMerge()
{
if ($this->isPost())
{
$templateRepo = $this->getTemplateRepo();
$templateIds = array_keys($templateRepo->getOutdatedTemplates());
$this->app->jobManager()->enqueueUnique('autoMerge', 'XF:TemplateMerge', [
'templateIds' => $templateIds
]);
return $this->redirect($this->buildLink('templates/outdated', null, ['automerged' => 1]));
}
else
{
return $this->view('XF:Template\AutoMerge', 'template_auto_merge');
}
}
protected function assertTemplateForComparison($templateId)
{
$template = $this->assertTemplateExists($templateId, 'Style');
if (!$template->style_id)
{
throw $this->exception($this->error(\XF::phrase('you_cannot_compare_custom_changes_for_master_template')));
}
$parentTemplate = $template->ParentTemplate;
if (!$parentTemplate)
{
throw $this->exception($this->error(\XF::phrase('this_template_does_not_have_parent_version')));
}
return $template;
}
/**
* @param string $id
* @param array|string|null $with
* @param null|string $phraseKey
*
* @return \XF\Entity\Style
*/
protected function assertStyleExists($id, $with = null, $phraseKey = null)
{
return $this->plugin('XF:Style')->assertStyleExists($id, $with, $phraseKey);
}
/**
* @param string $id
* @param array|string|null $with
* @param null|string $phraseKey
*
* @return \XF\Entity\Template
*/
protected function assertTemplateExists($id, $with = null, $phraseKey = null)
{
return $this->assertRecordExists('XF:Template', $id, $with, $phraseKey);
}
/**
* @return \XF\Repository\Style
*/
protected function getStyleRepo()
{
return $this->repository('XF:Style');
}
/**
* @return \XF\Repository\Template
*/
protected function getTemplateRepo()
{
return $this->repository('XF:Template');
}
}