ORIENT/plugins/rainlab/pages/controllers/Index.php

938 lines
30 KiB
PHP
Raw Normal View History

<?php namespace RainLab\Pages\Controllers;
use Url;
use Lang;
use Flash;
use Event;
use Config;
use Request;
use Response;
use BackendMenu;
use Cms\Classes\Layout;
use Cms\Classes\Theme;
use Cms\Classes\CmsObject;
use Cms\Classes\CmsCompoundObject;
use Cms\Widgets\TemplateList;
use System\Helpers\DateTime;
use Backend\Classes\Controller;
use RainLab\Pages\Widgets\PageList;
use RainLab\Pages\Widgets\MenuList;
use RainLab\Pages\Widgets\SnippetList;
use RainLab\Pages\Classes\Snippet;
use RainLab\Pages\Classes\Page as StaticPage;
use RainLab\Pages\Classes\Router;
use RainLab\Pages\Classes\Content;
use RainLab\Pages\Classes\MenuItem;
use RainLab\Pages\Plugin as PagesPlugin;
use RainLab\Pages\Classes\SnippetManager;
use ApplicationException;
use Exception;
/**
* Pages and Menus index
*
* @package rainlab\pages
* @author Alexey Bobkov, Samuel Georges
*/
class Index extends Controller
{
use \Backend\Traits\InspectableContainer;
protected $theme;
public $requiredPermissions = ['rainlab.pages.*'];
/**
* Constructor.
*/
public function __construct()
{
parent::__construct();
try {
if (!($this->theme = Theme::getEditTheme())) {
throw new ApplicationException(Lang::get('cms::lang.theme.edit.not_found'));
}
if ($this->user) {
if ($this->user->hasAccess('rainlab.pages.manage_pages')) {
new PageList($this, 'pageList');
$this->vars['activeWidgets'][] = 'pageList';
}
if ($this->user->hasAccess('rainlab.pages.manage_menus')) {
new MenuList($this, 'menuList');
$this->vars['activeWidgets'][] = 'menuList';
}
if ($this->user->hasAccess('rainlab.pages.manage_content')) {
new TemplateList($this, 'contentList', function() {
return $this->getContentTemplateList();
});
$this->vars['activeWidgets'][] = 'contentList';
}
if ($this->user->hasAccess('rainlab.pages.access_snippets')) {
new SnippetList($this, 'snippetList');
$this->vars['activeWidgets'][] = 'snippetList';
}
}
}
catch (Exception $ex) {
$this->handleError($ex);
}
$context = [
'pageList' => 'pages',
'menuList' => 'menus',
'contentList' => 'content',
'snippetList' => 'snippets',
];
BackendMenu::setContext('RainLab.Pages', 'pages', @$context[$this->vars['activeWidgets'][0]]);
}
//
// Pages, menus and text blocks
//
public function index()
{
$this->addJs('/modules/backend/assets/js/october.treeview.js', 'core');
$this->addJs('/plugins/rainlab/pages/assets/js/pages-page.js', 'RainLab.Pages');
$this->addJs('/plugins/rainlab/pages/assets/js/pages-snippets.js', 'RainLab.Pages');
$this->addCss('/plugins/rainlab/pages/assets/css/pages.css', 'RainLab.Pages');
// Preload the code editor class as it could be needed
// before it loads dynamically.
$this->addJs('/modules/backend/formwidgets/codeeditor/assets/js/build-min.js', 'core');
$this->bodyClass = 'compact-container';
$this->pageTitle = 'rainlab.pages::lang.plugin.name';
$this->pageTitleTemplate = Lang::get('rainlab.pages::lang.page.template_title');
if (Request::ajax() && Request::input('formWidgetAlias')) {
$this->bindFormWidgetToController();
}
}
public function index_onOpen()
{
$this->validateRequestTheme();
$type = Request::input('type');
$object = $this->loadObject($type, Request::input('path'));
return $this->pushObjectForm($type, $object);
}
public function onSave()
{
$this->validateRequestTheme();
$type = Request::input('objectType');
$object = $this->fillObjectFromPost($type);
$object->save();
/*
* Extensibility
*/
Event::fire('pages.object.save', [$this, $object, $type]);
$this->fireEvent('object.save', [$object, $type]);
$result = $this->getUpdateResponse($object, $type);
$successMessages = [
'page' => 'rainlab.pages::lang.page.saved',
'menu' => 'rainlab.pages::lang.menu.saved',
'content' => 'rainlab.pages::lang.content.saved',
];
$successMessage = isset($successMessages[$type])
? $successMessages[$type]
: $successMessages['page'];
Flash::success(Lang::get($successMessage));
return $result;
}
public function onCreateObject()
{
$this->validateRequestTheme();
$type = Request::input('type');
$object = $this->createObject($type);
$parent = Request::input('parent');
$parentPage = null;
if ($type == 'page') {
if (strlen($parent)) {
$parentPage = StaticPage::load($this->theme, $parent);
}
$object->setDefaultLayout($parentPage);
}
$widget = $this->makeObjectFormWidget($type, $object);
$this->vars['objectPath'] = '';
$this->vars['canCommit'] = $this->canCommitObject($object);
$this->vars['canReset'] = $this->canResetObject($object);
$result = [
'tabTitle' => $this->getTabTitle($type, $object),
'tab' => $this->makePartial('form_page', [
'form' => $widget,
'objectType' => $type,
'objectTheme' => $this->theme->getDirName(),
'objectMtime' => null,
'objectParent' => $parent,
'parentPage' => $parentPage
])
];
return $result;
}
public function onDelete()
{
$this->validateRequestTheme();
$type = Request::input('objectType');
$deletedObjects = $this->loadObject($type, trim(Request::input('objectPath')))->delete();
$result = [
'deletedObjects' => $deletedObjects,
'theme' => $this->theme->getDirName()
];
return $result;
}
public function onDeleteObjects()
{
$this->validateRequestTheme();
$type = Request::input('type');
$objects = Request::input('object');
if (!$objects) {
$objects = Request::input('template');
}
$error = null;
$deleted = [];
try {
foreach ($objects as $path => $selected) {
if (!$selected) {
continue;
}
$object = $this->loadObject($type, $path, true);
if (!$object) {
continue;
}
$deletedObjects = $object->delete();
if (is_array($deletedObjects)) {
$deleted = array_merge($deleted, $deletedObjects);
}
else {
$deleted[] = $path;
}
}
}
catch (Exception $ex) {
$error = $ex->getMessage();
}
return [
'deleted' => $deleted,
'error' => $error,
'theme' => Request::input('theme')
];
}
public function onOpenConcurrencyResolveForm()
{
return $this->makePartial('concurrency_resolve_form');
}
public function onGetMenuItemTypeInfo()
{
$type = Request::input('type');
return [
'menuItemTypeInfo' => MenuItem::getTypeInfo($type)
];
}
public function onUpdatePageLayout()
{
$this->validateRequestTheme();
$type = Request::input('objectType');
$object = $this->fillObjectFromPost($type);
return $this->pushObjectForm($type, $object, Request::input('formWidgetAlias'));
}
public function onGetInspectorConfiguration()
{
$configuration = [];
$snippetCode = Request::input('snippet');
$componentClass = Request::input('component');
if (strlen($snippetCode)) {
$snippet = SnippetManager::instance()->findByCodeOrComponent($this->theme, $snippetCode, $componentClass);
if (!$snippet) {
throw new ApplicationException(trans('rainlab.pages::lang.snippet.not_found', ['code' => $snippetCode]));
}
$configuration = $snippet->getProperties();
}
return [
'configuration' => [
'properties' => $configuration,
'title' => $snippet->getName(),
'description' => $snippet->getDescription()
]
];
}
public function onGetSnippetNames()
{
$codes = array_unique(Request::input('codes'));
$result = [];
foreach ($codes as $snippetCode) {
$parts = explode('|', $snippetCode);
$componentClass = null;
if (count($parts) > 1) {
$snippetCode = $parts[0];
$componentClass = $parts[1];
}
$snippet = SnippetManager::instance()->findByCodeOrComponent($this->theme, $snippetCode, $componentClass);
if (!$snippet) {
$result[$snippetCode] = trans('rainlab.pages::lang.snippet.not_found', ['code' => $snippetCode]);
}
else {
$result[$snippetCode] =$snippet->getName();
}
}
return [
'names' => $result
];
}
public function onMenuItemReferenceSearch()
{
$alias = Request::input('alias');
$widget = $this->makeFormWidget(
'Rainlab\Pages\FormWidgets\MenuItemSearch',
[],
['alias' => $alias]
);
return $widget->onSearch();
}
/**
* Commits the DB changes of a object to the filesystem
*
* @return array $response
*/
public function onCommit()
{
$this->validateRequestTheme();
$type = Request::input('objectType');
$object = $this->loadObject($type, trim(Request::input('objectPath')));
if ($this->canCommitObject($object)) {
2021-05-10 12:01:19 +00:00
if (class_exists('System')) {
// v1.2
$datasource = $this->getThemeDatasource();
$datasource->updateModelAtIndex(1, $object);
$datasource->forceDeleteModelAtIndex(0, $object);
}
else {
// v1.1
$datasource = $this->getThemeDatasource();
$datasource->pushToSource($object, 'filesystem');
$datasource->removeFromSource($object, 'database');
}
Flash::success(Lang::get('cms::lang.editor.commit_success', ['type' => $type]));
}
return array_merge($this->getUpdateResponse($object, $type), ['forceReload' => true]);
}
/**
* Resets a object to the version on the filesystem
*
* @return array $response
*/
public function onReset()
{
$this->validateRequestTheme();
$type = Request::input('objectType');
$object = $this->loadObject($type, trim(Request::input('objectPath')));
if ($this->canResetObject($object)) {
2021-05-10 12:01:19 +00:00
if (class_exists('System')) {
// v1.2
$datasource = $this->getThemeDatasource();
$datasource->forceDeleteModelAtIndex(0, $object);
}
else {
// v1.1
$datasource = $this->getThemeDatasource();
$datasource->removeFromSource($object, 'database');
}
Flash::success(Lang::get('cms::lang.editor.reset_success', ['type' => $type]));
}
return array_merge($this->getUpdateResponse($object, $type), ['forceReload' => true]);
}
//
// Methods for internal use
//
/**
* Get the response to return in an AJAX request that updates an object
*
* @param CmsObject $object The object that has been affected
* @param string $type The type of object being affected
* @return array $result;
*/
protected function getUpdateResponse(CmsObject $object, string $type)
{
$result = [
'objectPath' => $type != 'content' ? $object->getBaseFileName() : $object->fileName,
'objectMtime' => $object->mtime,
'tabTitle' => $this->getTabTitle($type, $object)
];
if ($type == 'page') {
$result['pageUrl'] = Url::to($object->getViewBag()->property('url'));
PagesPlugin::clearCache();
}
$result['canCommit'] = $this->canCommitObject($object);
$result['canReset'] = $this->canResetObject($object);
return $result;
}
/**
* Get the active theme's datasource
*/
protected function getThemeDatasource()
{
return $this->theme->getDatasource();
}
/**
* Check to see if the provided object can be committed
* Only available in debug mode, the DB layer must be enabled, and the object must exist in the database
*
* @param CmsObject $object
* @return boolean
*/
protected function canCommitObject(CmsObject $object)
{
$result = false;
2021-05-10 12:01:19 +00:00
if (class_exists('System')) {
// v1.2
if (
Config::get('app.debug', false) &&
$this->theme->secondLayerEnabled() &&
$this->getThemeDatasource()->hasModelAtIndex(1, $object)
) {
$result = true;
}
}
else {
// v1.1
if (Config::get('app.debug', false) &&
Theme::databaseLayerEnabled() &&
$this->getThemeDatasource()->sourceHasModel('database', $object)
) {
$result = true;
}
}
return $result;
}
/**
* Check to see if the provided object can be reset
* Only available when the DB layer is enabled and the object exists in both the DB & Filesystem
*
* @param CmsObject $object
* @return boolean
*/
protected function canResetObject(CmsObject $object)
{
$result = false;
2021-05-10 12:01:19 +00:00
if (class_exists('System')) {
// v1.2
if ($this->theme->secondLayerEnabled()) {
$datasource = $this->getThemeDatasource();
$result = $datasource->hasModelAtIndex(0, $object) &&
$datasource->hasModelAtIndex(1, $object);
}
}
else {
// v1.1
if (Theme::databaseLayerEnabled()) {
$datasource = $this->getThemeDatasource();
$result = $datasource->sourceHasModel('database', $object) && $datasource->sourceHasModel('filesystem', $object);
}
}
return $result;
}
protected function validateRequestTheme()
{
if ($this->theme->getDirName() != Request::input('theme')) {
throw new ApplicationException(trans('cms::lang.theme.edit.not_match'));
}
}
protected function loadObject($type, $path, $ignoreNotFound = false)
{
$class = $this->resolveTypeClassName($type);
if (!($object = call_user_func(array($class, 'load'), $this->theme, $path))) {
if (!$ignoreNotFound) {
throw new ApplicationException(trans('rainlab.pages::lang.object.not_found'));
}
return null;
}
return $object;
}
protected function createObject($type)
{
$class = $this->resolveTypeClassName($type);
if (!($object = $class::inTheme($this->theme))) {
throw new ApplicationException(trans('rainlab.pages::lang.object.not_found'));
}
return $object;
}
protected function resolveTypeClassName($type)
{
$types = [
'page' => 'RainLab\Pages\Classes\Page',
'menu' => 'RainLab\Pages\Classes\Menu',
'content' => 'RainLab\Pages\Classes\Content'
];
if (!array_key_exists($type, $types)) {
throw new ApplicationException(Lang::get('rainlab.pages::lang.object.invalid_type') . ' - type - ' . $type);
}
$allowed = false;
if ($type === 'content') {
$allowed = $this->user->hasAccess('rainlab.pages.manage_content');
} else {
$allowed = $this->user->hasAccess("rainlab.pages.manage_{$type}s");
}
if (!$allowed) {
throw new ApplicationException(Lang::get('rainlab.pages::lang.object.unauthorized_type', ['type' => $type]));
}
return $types[$type];
}
protected function makeObjectFormWidget($type, $object, $alias = null)
{
$formConfigs = [
'page' => '~/plugins/rainlab/pages/classes/page/fields.yaml',
'menu' => '~/plugins/rainlab/pages/classes/menu/fields.yaml',
'content' => '~/plugins/rainlab/pages/classes/content/fields.yaml'
];
if (!array_key_exists($type, $formConfigs)) {
throw new ApplicationException(Lang::get('rainlab.pages::lang.object.not_found'));
}
$widgetConfig = $this->makeConfig($formConfigs[$type]);
$widgetConfig->model = $object;
$widgetConfig->alias = $alias ?: 'form' . studly_case($type) . md5($object->exists ? $object->getFileName() : uniqid());
$widgetConfig->context = !$object->exists ? 'create' : 'update';
$widget = $this->makeWidget('Backend\Widgets\Form', $widgetConfig);
if ($type == 'page') {
$widget->bindEvent('form.extendFieldsBefore', function() use ($widget, $object) {
$this->checkContentField($widget, $object);
$this->addPagePlaceholders($widget, $object);
$this->addPageSyntaxFields($widget, $object);
});
}
return $widget;
}
protected function checkContentField($formWidget, $page)
{
if (!($layout = $page->getLayoutObject())) {
return;
}
$component = $layout->getComponent('staticPage');
if (!$component) {
return;
}
if (!$component->property('useContent', true)) {
unset($formWidget->secondaryTabs['fields']['markup']);
}
}
protected function addPageSyntaxFields($formWidget, $page)
{
$fields = $page->listLayoutSyntaxFields();
foreach ($fields as $fieldCode => $fieldConfig) {
if ($fieldConfig['type'] == 'fileupload') continue;
if ($fieldConfig['type'] == 'repeater') {
if (empty($fieldConfig['form']) || !is_string($fieldConfig['form'])) {
$fieldConfig['form']['fields'] = array_get($fieldConfig, 'fields', []);
unset($fieldConfig['fields']);
}
}
/*
* Custom fields placement
*/
$placement = (!empty($fieldConfig['placement']) ? $fieldConfig['placement'] : NULL);
switch ($placement) {
case 'primary':
$formWidget->tabs['fields']['viewBag[' . $fieldCode . ']'] = $fieldConfig;
break;
default:
$fieldConfig['cssClass'] = 'secondary-tab ' . array_get($fieldConfig, 'cssClass', '');
$formWidget->secondaryTabs['fields']['viewBag[' . $fieldCode . ']'] = $fieldConfig;
break;
}
/*
* Translation support
*/
2021-05-10 12:01:19 +00:00
$translatableTypes = ['text', 'textarea', 'richeditor', 'repeater', 'markdown', 'mediafinder'];
if (in_array($fieldConfig['type'], $translatableTypes) && array_get($fieldConfig, 'translatable', true)) {
$page->translatable[] = 'viewBag['.$fieldCode.']';
}
}
}
protected function addPagePlaceholders($formWidget, $page)
{
$placeholders = $page->listLayoutPlaceholders();
foreach ($placeholders as $placeholderCode => $info) {
if ($info['ignore']) {
continue;
}
$placeholderTitle = $info['title'];
$fieldConfig = [
'tab' => $placeholderTitle,
'stretch' => '1',
'size' => 'huge'
];
if ($info['type'] != 'text') {
$fieldConfig['type'] = 'richeditor';
}
else {
$fieldConfig['type'] = 'codeeditor';
$fieldConfig['language'] = 'text';
$fieldConfig['theme'] = 'chrome';
$fieldConfig['showGutter'] = false;
$fieldConfig['highlightActiveLine'] = false;
$fieldConfig['cssClass'] = 'pagesTextEditor';
$fieldConfig['showInvisibles'] = false;
$fieldConfig['fontSize'] = 13;
$fieldConfig['margin'] = '20';
}
$formWidget->secondaryTabs['fields']['placeholders['.$placeholderCode.']'] = $fieldConfig;
/*
* Translation support
*/
$page->translatable[] = 'placeholders['.$placeholderCode.']';
}
}
protected function getTabTitle($type, $object)
{
if ($type == 'page') {
$viewBag = $object->getViewBag();
$result = $viewBag ? $viewBag->property('title') : false;
if (!$result) {
$result = trans('rainlab.pages::lang.page.new');
}
return $result;
}
elseif ($type == 'menu') {
$result = $object->name;
if (!strlen($result)) {
$result = trans('rainlab.pages::lang.menu.new');
}
return $result;
}
elseif ($type == 'content') {
$result = in_array($type, ['asset', 'content'])
? $object->getFileName()
: $object->getBaseFileName();
if (!$result) {
$result = trans('cms::lang.'.$type.'.new');
}
return $result;
}
return $object->getFileName();
}
protected function fillObjectFromPost($type)
{
$objectPath = trim(Request::input('objectPath'));
$object = $objectPath ? $this->loadObject($type, $objectPath) : $this->createObject($type);
$formWidget = $this->makeObjectFormWidget($type, $object, Request::input('formWidgetAlias'));
$saveData = $formWidget->getSaveData();
$postData = post();
$objectData = [];
if ($viewBag = array_get($saveData, 'viewBag')) {
$objectData['settings'] = ['viewBag' => $viewBag];
}
$fields = ['markup', 'code', 'fileName', 'content', 'itemData', 'name'];
if ($type != 'menu' && $type != 'content') {
$object->parentFileName = Request::input('parentFileName');
}
foreach ($fields as $field) {
if (array_key_exists($field, $saveData)) {
$objectData[$field] = $saveData[$field];
}
elseif (array_key_exists($field, $postData)) {
$objectData[$field] = $postData[$field];
}
}
if ($type == 'page') {
$placeholders = array_get($saveData, 'placeholders');
2021-05-10 12:01:19 +00:00
$comboConfig = Config::get('cms.convertLineEndings', Config::get('system.convert_line_endings', false));
if (is_array($placeholders) && $comboConfig === true) {
$placeholders = array_map([$this, 'convertLineEndings'], $placeholders);
}
$objectData['placeholders'] = $placeholders;
}
if ($type == 'content') {
$fileName = $objectData['fileName'];
if (dirname($fileName) == 'static-pages') {
throw new ApplicationException(trans('rainlab.pages::lang.content.cant_save_to_dir'));
}
$extension = pathinfo($fileName, PATHINFO_EXTENSION);
if ($extension === 'htm' || $extension === 'html' || !strlen($extension)) {
$objectData['markup'] = array_get($saveData, 'markup_html');
}
}
if ($type == 'menu') {
// If no item data is sent through POST, this means the menu is empty
if (!isset($objectData['itemData'])) {
$objectData['itemData'] = [];
} else {
$objectData['itemData'] = json_decode($objectData['itemData'], true);
if (json_last_error() !== JSON_ERROR_NONE || !is_array($objectData['itemData'])) {
$objectData['itemData'] = [];
}
}
}
2021-05-10 12:01:19 +00:00
$comboConfig = Config::get('cms.convertLineEndings', Config::get('system.convert_line_endings', false));
if (!empty($objectData['markup']) && $comboConfig === true) {
$objectData['markup'] = $this->convertLineEndings($objectData['markup']);
}
if (!Request::input('objectForceSave') && $object->mtime) {
if (Request::input('objectMtime') != $object->mtime) {
throw new ApplicationException('mtime-mismatch');
}
}
$object->fill($objectData);
/*
* Rehydrate the object viewBag array property where values are sourced.
*/
if ($object instanceof CmsCompoundObject && is_array($viewBag)) {
$object->viewBag = $viewBag + $object->viewBag;
}
return $object;
}
protected function pushObjectForm($type, $object, $alias = null)
{
$widget = $this->makeObjectFormWidget($type, $object, $alias);
$this->vars['canCommit'] = $this->canCommitObject($object);
$this->vars['canReset'] = $this->canResetObject($object);
$this->vars['objectPath'] = Request::input('path');
$this->vars['lastModified'] = DateTime::makeCarbon($object->mtime);
if ($type == 'page') {
$this->vars['pageUrl'] = Url::to($object->getViewBag()->property('url'));
}
return [
'tabTitle' => $this->getTabTitle($type, $object),
'tab' => $this->makePartial('form_page', [
'form' => $widget,
'objectType' => $type,
'objectTheme' => $this->theme->getDirName(),
'objectMtime' => $object->mtime,
'objectParent' => Request::input('parentFileName')
])
];
}
protected function bindFormWidgetToController()
{
$alias = Request::input('formWidgetAlias');
$type = Request::input('objectType');
$objectPath = trim(Request::input('objectPath'));
2021-05-10 12:01:19 +00:00
if (!$objectPath) {
$object = $this->createObject($type);
if ($type === 'page') {
/**
* If layout is in POST, populate that into the object's viewBag to allow placeholders and syntax
* fields to still work when editing a new page.
*
* Fixes https://github.com/octobercms/october/issues/4628
*/
$layout = Request::input('viewBag.layout');
if ($layout) {
$object->getViewBag()->setProperty('layout', $layout);
}
}
} else {
$object = $this->loadObject($type, $objectPath);
}
$widget = $this->makeObjectFormWidget($type, $object, $alias);
$widget->bindToController();
}
/**
* Replaces Windows style (/r/n) line endings with unix style (/n)
* line endings.
* @param string $markup The markup to convert to unix style endings
* @return string
*/
protected function convertLineEndings($markup)
{
$markup = str_replace("\r\n", "\n", $markup);
$markup = str_replace("\r", "\n", $markup);
return $markup;
}
/**
* Returns a list of content files
* @return \October\Rain\Database\Collection
*/
protected function getContentTemplateList()
{
$templates = Content::listInTheme($this->theme, true);
/**
* @event pages.content.templateList
* Provides opportunity to filter the items returned to the ContentList widget used by the RainLab.Pages plugin in the backend.
*
* >**NOTE**: Recommended to just use cms.object.listInTheme instead
*
* Parameter provided is `$templates` (a collection of the Content CmsObjects being returned).
* > Note: The `$templates` parameter provided is an object reference to a CmsObjectCollection, to make changes you must use object modifying methods.
*
* Example usage (only shows allowed content files):
*
* \Event::listen('pages.content.templateList', function ($templates) {
* foreach ($templates as $index = $content) {
* if (!in_array($content->fileName, $allowedContent)) {
* $templates->forget($index);
* }
* }
* });
*
* Or:
*
* \RainLab\Pages\Controller\Index::extend(function ($controller) {
* $controller->bindEvent('content.templateList', function ($templates) {
* foreach ($templates as $index = $content) {
* if (!in_array($content->fileName, $allowedContent)) {
* $templates->forget($index);
* }
* }
* });
* });
* }
*/
if (
($event = $this->fireEvent('content.templateList', [$templates], true)) ||
($event = Event::fire('pages.content.templateList', [$this, $templates], true))
) {
return $event;
}
return $templates;
}
}