praiadeseselle/wire/modules/Process/ProcessPageEdit/ProcessPageEdit.module
2022-03-08 15:55:41 +01:00

3225 lines
99 KiB
Text
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php namespace ProcessWire;
/**
* ProcessWire Page Edit Process
*
* Provides the UI for editing a page
*
* For more details about how Process modules work, please see:
* /wire/core/Process.php
*
* ProcessWire 3.x, Copyright 2021 by Ryan Cramer
* https://processwire.com
*
* @property string $noticeUnknown
* @property string $noticeLocked
* @property string $noticeNoAccess
* @property string $noticeIncomplete
* @property string $viewAction One of 'panel', 'modal', 'new', 'this' (see getViewActions method)
* @property bool $useBookmarks
*
* @method Page loadPage($id)
* @method string execute()
* @method string executeTemplate()
* @method void executeSaveTemplate($template = null)
* @method string executeBookmarks()
* @method array getViewActions($actions = array(), $configMode = false)
* @method array getSubmitActions()
* @method bool processSubmitAction($value)
* @method void processSaveRedirect($redirectUrl)
* @method void deletedPage($page, $redirectUrl, $trashed = false)
* @method InputfieldForm buildForm(InputfieldForm $form)
* @method InputfieldWrapper buildFormContent()
* @method InputfieldWrapper buildFormChildren()
* @method InputfieldWrapper buildFormSettings()
* @method InputfieldWrapper buildFormDelete()
* @method void buildFormView($url)
* @method InputfieldMarkup buildFormRoles()
* @method void processInput(InputfieldWrapper $form, $level = 0, $formRoot = null)
* @method void ajaxSave(Page $page)
* @method bool ajaxEditable(Page $page, $fieldName = '')
* @method array getTabs()
*
*/
class ProcessPageEdit extends Process implements WirePageEditor, ConfigurableModule {
/**
* Module information
*
* @return array
*
*/
public static function getModuleInfo() {
return array(
'title' => 'Page Edit',
'summary' => 'Edit a Page',
'version' => 110,
'permanent' => true,
'permission' => 'page-edit',
'icon' => 'edit',
'useNavJSON' => true
);
}
/**
* Page edit form
*
* @var InputfieldForm
*
*/
protected $form;
/**
* Page being edited
*
* @var Page
*
*/
protected $page;
/**
* Single field to edit (if only 'fields' specified, this contains first field present in 'fields')
*
* @var null|Field
*
*/
protected $field = null;
/**
* Array of fields to edit, indexed by field name
*
* @var array|Field[]
*
*/
protected $fields = array();
/**
* Field name suffix, applicable only when field or fields (above) is also set, in specific situations like repeaters
*
* @var string
*
*/
protected $fnsx = '';
/**
* Substituted master page (deprecated)
*
* @var null|Page
*
*/
protected $masterPage = null;
/**
* Parent of page being edited
*
* @var Page
*
*/
protected $parent;
/**
* User that is editing
*
* @var User
*
*/
protected $user;
/**
* @var int ID of page being edited
*
*/
protected $id;
/**
* URL to redirect to
*
* @var string
*
*/
protected $redirectUrl;
/**
* @var string PHP class name of Page being edited
*
*/
protected $pageClass;
/**
* Is the page in the trash?
*
* @var bool
*
*/
protected $isTrash;
/**
* Cache used by getAllowedTemplates() method
*
* Contains Template objects indexed by template ID.
*
* @var array|Template[]
*
*/
protected $allowedTemplates = null; // cache
/**
* Is this a POST request to save a page?
*
* @var bool
*
*/
protected $isPost = false;
/**
* Show the "settings" tab?
*
* @var bool
*
*/
protected $useSettings = true;
/**
* Show the "children" tab?
*
* @var bool
*
*/
protected $useChildren = true;
/**
* Show the "view" tab/link?
*
* @var bool
*
*/
protected $useView = true;
/**
* Identified tabs in the form indexed by tab ID and values are tab labels
*
* @var array
*
*/
protected $tabs = array();
/**
* Predefined list of parents allowed for edited page (array of Page objects), set by setPredefinedParents() method
*
* @var array|PageArray
*
*/
protected $predefinedParents = array();
/**
* Predefined list of templates allowed for edited page (array of Template objects), set by setPredefinedTemplates() method
*
* @var array|Template[]
*
*/
protected $predefinedTemplates = array();
/**
* Primary editor process, if not $this
*
* @var null|WirePageEditor
*
*/
protected $editor = null;
/**
* Tell the Page what Process is being used to edit it?
*
*/
protected $setEditor = true;
/**
* Names of changed fields
*
* @var array
*
*/
protected $changes = array();
/**
* @var Modules
*
*/
protected $modules;
/**
* @var WireInput
*
*/
protected $input;
/**
* @var Config
*
*/
protected $config;
/**
* @var Sanitizer
*
*/
protected $sanitizer;
/**
* @var Session
*
*/
protected $session;
/**
* Sanitized contents of get[modal]
*
* @var int|string|bool|null
*
*/
protected $requestModal = null;
/**
* Sanitized contents of get[context]
*
* @var string
*
*/
protected $requestContext = '';
/**
* Sanitized contents of get[language]
*
* @var Language|null
*
*/
protected $requestLanguage = null;
/**
* Is the LanguageSupportPageNames module installed?
*
* @var bool
*
*/
protected $hasLanguagePageNames = false;
/**
* Contents of $config->pageEdit
*
* @var array
*
*/
protected $configSettings = array(
'viewNew' => false,
'confirm' => true,
'ajaxChildren' => true,
'ajaxParent' => true,
'editCrumbs' => false,
);
/**
* Other core page classes
*
* @var array
*
*/
protected $otherCorePageClasses = array(
'User', 'UserPage',
'Role', 'RolePage',
'Permission', 'PermissionPage',
'Language', 'LanguagePage',
);
/***********************************************************************************************************************
* METHODS
*
*/
/**
* Construct
*
*/
public function __construct() {
$this->set('useBookmarks', false);
$this->set('viewAction', 'this');
return parent::__construct();
}
/**
* Wired to API
*
*/
public function wired() {
parent::wired();
if($this->wire('process') instanceof WirePageEditor) {
// keep existing process, which may be building on top of this one
} else {
$this->wire('process', $this);
}
}
/**
* Initialize the page editor by loading the requested page and any dependencies
*
* @throws WireException|Wire404Exception|WirePermissionException
*
*/
public function init() {
$this->modules = $this->wire('modules');
$this->input = $this->wire('input');
$this->config = $this->wire('config');
$this->user = $this->wire('user');
$this->sanitizer = $this->wire('sanitizer');
$this->session = $this->wire('session');
// predefined messages that maybe used in multiple places
$this->set('noticeUnknown', $this->_("Unknown page")); // Init error: Unknown page
$this->set('noticeLocked', $this->_("This page is locked for edits")); // Init error: Page is locked
$this->set('noticeNoAccess', $this->_("You don't have access to edit")); // Init error: User doesn't have access
$this->set('noticeIncomplete', $this->_("This page might have one or more incomplete fields (attempt to save or publish for more info)"));
$settings = $this->config->pageEdit;
if(is_array($settings)) $this->configSettings = array_merge($this->configSettings, $settings);
if(in_array($this->input->urlSegment1, array('navJSON', 'bookmarks'))) return;
$getID = $this->input->get('id');
if($getID === 'bookmark') {
$this->session->redirect('./bookmarks/');
return;
}
$getID = (int) $getID;
$postID = (int) $this->input->post('id');
$id = abs($postID ? $postID : $getID);
if(!$id) {
$this->session->redirect('./bookmarks/');
throw new Wire404Exception($this->noticeUnknown, Wire404Exception::codeSecondary); // Init error: no page provided
}
$this->page = $this->loadPage($id);
$this->id = $this->page->id;
$this->pageClass = $this->page->className();
$this->page->setOutputFormatting(false);
$this->parent = $this->pages->get($this->page->parent_id);
$this->isTrash = $this->page->isTrash();
// check if editing specific field or fieldset only
if($this->page) {
$field = $this->input->get('field');
$fields = $this->input->get('fields');
if($this->input->get('fnsx') !== null) $this->fnsx = $this->input->get->fieldName('fnsx');
if($field && !$fields) $fields = $field;
if($fields) {
$fields = explode(',', $fields);
foreach($fields as $fieldName) {
$fieldName = $this->sanitizer->fieldName($fieldName);
if(!$fieldName) throw new WireException("Invalid field name specified");
$field = $this->page->template->fieldgroup->getField($fieldName, true); // get in context
if(!$field) throw new WireException("Field '$fieldName' is not applicable to this page");
$this->fields[$field->name] = $field;
}
$this->field = reset($this->fields);
$this->useChildren = false;
$this->useSettings = false;
$this->useView = false;
}
}
// determine if we're going to be dealing with a save/post request
$this->isPost = ($postID > 0 && ($postID === $this->page->id))
|| ($this->config->ajax && (count($_POST) || isset($_SERVER['HTTP_X_FIELDNAME'])));
if(!$this->isPost) {
$this->setupHeadline();
$this->setupBreadcrumbs();
}
// optional context GET var
$context = $this->input->get('context');
if($context) $this->requestContext = $this->sanitizer->name($context);
// optional language GET var
$languages = $this->wire()->languages;
if($languages) {
$this->hasLanguagePageNames = $this->modules->isInstalled('LanguageSupportPageNames');
if($this->hasLanguagePageNames) {
$languageID = (int) $this->input->get('language');
if($languageID > 0) {
$language = $languages->get($languageID);
if($language->id && $language->id != $this->user->language->id) $this->requestLanguage = $language;
}
}
}
// optional modal setting
if($this->config->modal) {
$this->requestModal = $this->sanitizer->name($this->config->modal);
}
parent::init();
if(!$this->isPost) {
$this->modules->get('JqueryWireTabs');
/** @var JqueryUI $jQueryUI */
$jQueryUI = $this->modules->get('JqueryUI');
$jQueryUI->use('modal');
}
}
/**
* Given a page ID, return the Page object
*
* @param int $id
* @return Page
* @throws WireException|WirePermissionException
*
*/
protected function ___loadPage($id) {
/** @var Page|NullPage $page */
$page = $this->wire()->pages->get((int) $id);
if($page instanceof NullPage) {
throw new WireException($this->noticeUnknown); // page doesn't exist
}
$editable = $page->editable();
if($page instanceof User) {
// special case when page is a User
$userAdmin = $this->user->hasPermission('user-admin');
if($userAdmin && $this->wire()->process != 'ProcessUser') {
// only allow user pages to be edited from the access section (at least for non-superusers)
$this->session->redirect($this->config->urls->admin . 'access/users/edit/?id=' . $page->id);
}
if(!$userAdmin && $page->id === $this->user->id && $this->config->ajax) {
// user that lacks user-admin permission editing themself during ajax request
$fieldName = $this->input->get->fieldName('field');
$field = $fieldName ? $this->wire()->fields->get($fieldName) : null;
if($field instanceof Field) {
// respond to ajax request for field that is editable
/** @var PagePermissions $pagePermissions */
$pagePermissions = $this->modules->get('PagePermissions');
$editable = $pagePermissions->userFieldEditable($field);
// prevent a later potential redirect to user editor
if($editable) $this->setEditor = false;
}
}
}
if(!$editable) {
throw new WirePermissionException($this->noticeNoAccess);
}
return $page;
}
/**
* Execute the Page Edit process by building the form and checking if it was submitted
*
* @return string
* @throws WireException
*
*/
public function ___execute() {
if(!$this->page) throw new WireException("No page found");
if($this->setEditor) {
// note that setting the editor can force a redirect to a ProcessPageType editor
$this->page->setEditor($this->editor ? $this->editor : $this);
}
if($this->config->ajax && (isset($_SERVER['HTTP_X_FIELDNAME']) || count($_POST))) {
$this->ajaxSave($this->page);
return '';
}
if($this->page->hasStatus(Page::statusTemp) && $this->page->parent->template->childNameFormat == 'title') {
// make it set page name from page title
$this->page->name = '';
}
$adminTheme = $this->wire('adminTheme');
if($adminTheme) {
$className = $this->className();
$adminTheme->addBodyClass("$className-id-{$this->page->id}");
$adminTheme->addBodyClass("$className-template-{$this->page->template->name}");
}
$this->form = $this->modules->get('InputfieldForm');
$this->form = $this->buildForm($this->form);
$this->form->setTrackChanges();
if($this->isPost && count($_POST)) $this->processSave();
if($this->page->hasStatus(Page::statusLocked)) {
if($this->user->hasPermission('page-lock', $this->page)) {
$this->warning($this->noticeLocked); // Page locked message
} else {
$this->error($this->noticeLocked); // Page locked error
}
} else if(!$this->isPost && $this->page->hasStatus(Page::statusFlagged) && !$this->input->get('s')) {
$this->warning($this->noticeIncomplete);
}
return $this->renderEdit();
}
/*********************************************************************************************************************
* EDITOR FORM BUILDING
*
*/
/**
* Render the Page Edit form
*
* @return string
*
*/
protected function renderEdit() {
$class = '';
$numFields = count($this->fields);
// $out = "<p id='PageIDIndicator' class='$class'>" . ($this->page->id ? $this->page->id : "New") . "</p>";
$out = "<p id='PageIDIndicator' class='$class'>{$this->page->id}</p>";
$description = $this->form->getSetting('description');
if($description) {
$out .= "<h2>" . $this->form->entityEncode($description, Inputfield::textFormatBasic) . "</h2>";
$this->form->set('description', '');
}
if(!$numFields) {
/** @var JqueryWireTabs $tabs */
$tabs = $this->modules->get('JqueryWireTabs');
$this->form->value = $tabs->renderTabList($this->getTabs(), array('id' => 'PageEditTabs'));
}
$out .= $this->form->render();
// buttons with dropdowns
if(!$numFields) {
$submitActions = $this->getSubmitActions();
if(count($submitActions)) {
$config = $this->config;
$file = $config->debug ? 'dropdown.js' : 'dropdown.min.js';
$config->scripts->add($config->urls('InputfieldSubmit') . $file);
$input = "<input type='hidden' id='after-submit-action' name='_after_submit_action' value='' />";
$out = str_replace('</form>', "$input</form>", $out);
$out .= "<ul class='pw-button-dropdown' data-pw-dropdown-input='#after-submit-action' data-my='right top' data-at='right bottom+1'>";
foreach($submitActions as $action) {
$icon = empty($action['icon']) ? "" : "<i class='fa fa-fw fa-$action[icon]'></i>";
$class = empty($action['class']) ? "after-submit-$action[value]" : $action['class'];
$out .= "<li><a class='$class' data-pw-dropdown-value='$action[value]' href='#'>$icon $action[label]</a></li>";
}
$out .= "</ul>";
}
}
if(!$numFields && !$this->requestModal && $this->page->viewable()) {
// this supports code in the buildFormView() method
$out .= "<ul id='_ProcessPageEditViewDropdown' class='pw-dropdown-menu pw-dropdown-menu-rounded' data-my='left top' data-at='left top-9'>";
foreach($this->getViewActions() as $name => $action) {
$out .= "<li class='page-view-action-$name'>$action</li>";
}
$out .= "</ul>";
}
$func = 'initPageEditForm();'; // to prevent IDE from flagging as unknown function
$out .= "<scr" . "ipt>$func</script>"; // ends up being slightly faster than ready() (or at least appears that way)
return $out;
}
/**
* Get actions for submit button(s)
*
* Should return array where each item in the array is itself an array like this:
* ~~~~~
* [
* 'value' => 'value of action, i.e. view, edit, add, etc.',
* 'icon' => 'icon name excluding the “fa-” part',
* 'label' => 'text label where %s is replaced with submit button label',
* 'class' => 'optional class attribute',
* ]
* ~~~~~~
* Array returned by this method is indexed by the 'value', though this is not required for hooks.
*
* #pw-hooker
*
* @return array
* @throws WireException
* @since 3.0.142
* @see ___processSubmitAction()
*
*/
protected function ___getSubmitActions() {
if($this->requestModal) return array();
$viewable = $this->page->viewable();
$process = $this->wire()->process;
$actions = array();
$actions['exit'] = array(
'value' => 'exit',
'icon' => 'close',
'label' => $this->_('%s + Exit'),
'class' => '',
);
if($viewable) $actions['view'] = array(
'value' => 'view',
'icon' => 'eye',
'label' => $this->_('%s + View'),
'class' => '',
);
if("$process" === "$this" && $this->page->id > 1) {
$parent = $this->page->parent();
if($parent->addable()) $actions['add'] = array(
'value' => 'add',
'icon' => 'plus-circle',
'label' => $this->_('%s + Add New'),
'class' => '',
);
if($parent->numChildren > 1) $actions['next'] = array(
'value' => 'next',
'icon' => 'edit',
'label' => $this->_('%s + Next'),
'class' => '',
);
}
return $actions;
}
/**
* Get URL to view this page
*
* @param Language|int|string|null $language
* @return string
* @throws WireException
* @since 3.0.142 Was protected in previous versions
*
*/
public function getViewUrl($language = null) {
$url = '';
if(!$this->page) throw new WireException('No page yet');
if($this->hasLanguagePageNames) {
/** @var Languages $languages */
$languages = $this->wire('languages');
if($language) {
if(is_string($language) || is_int($language)) $language = $languages->get($language);
$userLanguage = $language;
} else if($this->requestLanguage) {
$userLanguage = $this->requestLanguage;
} else {
$userLanguage = $this->user->language;
}
if($userLanguage && $userLanguage->id) {
$url = $this->page->localHttpUrl($userLanguage);
}
}
if(!$url) $url = $this->page->httpUrl();
return $url;
}
/**
* Get actions for the "View" dropdown
*
* #pw-hooker
*
* @param array $actions Actions in case hook wants to populate them
* @param bool $configMode Specify true if retrieving for configuration purposes rather than runtime purposes.
* @return array of <a> tags or array of labels if $configMode == true
*
*/
protected function ___getViewActions($actions = array(), $configMode = false) {
$labels = array(
'view' => $this->_x('Page View', 'panel-title'),
'panel' => $this->_x('Panel', 'view-label'),
'modal' => $this->_x('Modal Popup', 'view-label'),
'new' => $this->_x('New Window/Tab', 'view-label'),
'this' => $this->_x('Exit + View', 'view-label'),
);
$icons = array(
'panel' => 'columns',
'modal' => 'picture-o',
'new' => 'external-link-square',
'this' => 'eye',
);
if($configMode) {
unset($labels['view']);
return $labels;
}
$url = $this->getViewUrl();
if($this->page->hasStatus(Page::statusDraft) && strpos($url, '?') === false) $url .= '?draft=1';
$languages = $this->hasLanguagePageNames ? $this->page->template->getLanguages() : null;
foreach($icons as $name => $icon) {
$labels[$name] = "<i class='fa fa-fw fa-$icon'></i>&nbsp;" . $labels[$name];
}
$class = '';
$languageUrls = array();
if($languages) {
$class .= ' pw-has-items';
foreach($languages as $language) {
if(!$this->page->viewable($language)) continue;
$localUrl = $this->page->localHttpUrl($language);
if($this->page->hasStatus(Page::statusDraft) && strpos($localUrl, '?') === false) $localUrl .= '?draft=1';
$languageUrls[$language->id] = $localUrl;
}
}
$actions = array_merge(array(
"panel" => "<a class='pw-panel pw-panel-reload$class' href='$url' data-tab-text='$labels[view]' data-tab-icon='eye'>$labels[panel]</a>",
"modal" => "<a class='pw-modal pw-modal-large$class' href='$url'>$labels[modal]</a>",
"new" => "<a class='$class' target='_blank' href='$url'>$labels[new]</a>",
"this" => "<a class='$class' target='_top' href='$url'>$labels[this]</a>",
), $actions);
foreach($actions as $name => $action) {
if(count($languageUrls) > 1) {
$ul = "<ul class=''>";
foreach($languages as $language) {
/** @var Language $language */
if(!isset($languageUrls[$language->id])) continue;
$localUrl = $languageUrls[$language->id];
$label = $language->get('title|name');
$_action = str_replace(' pw-has-items', '', $action);
$_action = str_replace("'$url'", "'$localUrl'", $_action);
$_action = str_replace(">" . $labels[$name] . "<", ">$label<", $_action);
$_action = str_replace("='$labels[view]'", "='$label'", $_action); // panel language
$ul .= "<li>$_action</li>";
}
$ul .= "</ul>";
$actions[$name] = str_replace('</a>', ' &nbsp;</a>', $actions[$name]) . $ul;
} else {
$actions[$name] = str_replace(' pw-has-items', '', $action);
}
}
return $actions;
}
/**
* Get URL (or form action attribute) for editing this page
*
* @param array $options
* - `id` (int): Page ID to edit
* - `modal` (int|string): Modal mode, when applicable
* - `context` (string): Additional request context string, when applicable
* - `language` (int|Language|string): Language for editor, if different from users language
* - `field` (string): Only edit field with this name
* - `fields` (string): CSV string of fields to edit, rather than all fields on apge
* - `fnsx` (string): Field name suffix, applicable only when field or fields (above) is also set, in specific situations like repeaters
* - `uploadOnlyMode (string|int): Upload only mode (internal use)
* @return string
*
*/
public function getEditUrl($options = array()) {
$defaults = array(
'id' => $this->page->id,
'modal' => $this->requestModal,
'context' => $this->requestContext,
'language' => $this->requestLanguage,
'field' => '',
'fields' => '',
'fnsx' => $this->fnsx,
'uploadOnlyMode' => '',
);
if($this->field) {
$numFields = count($this->fields);
if($numFields == 1 && $this->field) {
$defaults['field'] = $this->field->name;
} else if($numFields > 1) {
$defaults['fields'] = implode(',', array_keys($this->fields));
}
}
$uploadOnlyMode = (int) $this->input->get('uploadOnlyMode');
if($uploadOnlyMode && !$this->config->ajax) $defaults['uploadOnlyMode'] = $uploadOnlyMode;
$options = array_merge($defaults, $options);
$qs = array();
foreach($options as $name => $value) {
if(!empty($value)) $qs[] = "$name=$value";
}
return './?' . implode('&', $qs);
}
/**
* Build the form used for Page Edits
*
* @param InputfieldForm $form
* @return InputfieldForm
*
*/
protected function ___buildForm(InputfieldForm $form) {
$form->attr('id+name', 'ProcessPageEdit');
$form->attr('action', $this->getEditUrl(array('id' => $this->id)));
$form->attr('method', 'post');
$form->attr('enctype', 'multipart/form-data');
$form->attr('class', 'ui-helper-clearfix template_' . $this->page->template . ' class_' . $this->page->className);
$form->attr('autocomplete', 'off');
$form->attr('data-uploading', $this->_('Are you sure? An upload is currently in progress and it may be lost if you proceed.'));
if($this->configSettings['confirm']) $form->addClass('InputfieldFormConfirm');
// for ProcessPageEditImageSelect support
if($this->input->get('uploadOnlyMode') && !$this->config->ajax) {
// for modal uploading with InputfieldFile or InputfieldImage
if(count($this->fields) && $this->field->type instanceof FieldtypeImage) {
$this->setRedirectUrl("../image/?id=$this->id");
}
}
$saveName = 'submit_save';
$saveLabel = $this->_("Save"); // Button: save
$submit2 = null; // second submit button, when applicable
if($this->field) {
// focus in on a specific field or fields
$form->addClass('ProcessPageEditSingleField');
foreach($this->fields as $field) {
$options = array(
'contextStr' => $this->fnsx,
'fieldName' => $field->name,
'namespace' => '',
'flat' => true,
);
foreach($this->page->getInputfields($options) as $inputfield) {
if(!$this->page->editable($field->name, false)) continue;
$skipCollapsed = array(
Inputfield::collapsedHidden,
Inputfield::collapsedNoLocked,
Inputfield::collapsedYesLocked,
);
$collapsed = $inputfield->getSetting('collapsed');
if($collapsed > 0 && !in_array($collapsed, $skipCollapsed)) {
$inputfield->collapsed = Inputfield::collapsedNo;
}
$form->add($inputfield);
}
}
} else {
// all fields
// determine what content fields should become tabs
$contentTab = $this->buildFormContent();
$tabs = array();
$tabWrap = null;
$tabOpen = null;
$tabViewable = null;
foreach($contentTab as $inputfield) {
if(!$tabOpen && $inputfield->className == 'InputfieldFieldsetTabOpen') {
// open new tab
$showable = $this->isTrash ? 'editable' : 'viewable';
$tabViewable = $this->page->$showable($inputfield->attr('name'));
if($this->isPost) {
// only remove non-visible tabs when in post/save mode, for proper processInput()
if(!$tabViewable) $contentTab->remove($inputfield);
// during post requests, this goes no further, as theres no need for visual tab manipulation
continue;
}
$tabOpen = $inputfield;
$tabWrap = $this->wire(new InputfieldWrapper());
$tabWrap->attr('title', $tabOpen->getSetting('label'));
$tabWrap->id = $tabOpen->attr('id');
$tabWrap->collapsed = $tabOpen->getSetting('collapsed');
// @todo support description in fieldset tab: works but needs styles for each admin theme, so commented out for now
// $tabWrap->description = $inputfield->description;
$tabWrap->notes = $inputfield->notes;
$contentTab->remove($inputfield);
if(!$tabViewable) continue;
if($inputfield->modal) {
$href = $this->getEditUrl(array('field' => $inputfield->name, 'modal' => 1));
$this->addTab($tabOpen->id, "<a class='pw-modal' " .
"title='" . $this->sanitizer->entities($tabOpen->label) . "' " .
"data-buttons='#ProcessPageEdit button[type=submit]' " .
"data-autoclose='1' " .
"href='$href'>" .
$this->sanitizer->entities1($tabOpen->label) . "</a>");
/** @var JqueryUI $jqueryUI */
$jqueryUI = $this->modules->get('JqueryUI');
$jqueryUI->use('modal');
$tabOpen = null;
} else {
$this->addTab($tabOpen->id, $this->sanitizer->entities1($tabOpen->label));
}
} else if($tabOpen && !$this->isPost) {
/** @var Inputfield $tabOpen */
// already have a tab open
if($inputfield->attr('name') == $tabOpen->attr('name') . '_END') {
// close tab
if($tabViewable) $tabs[] = $tabWrap;
$tabOpen = null;
} else if($tabViewable) {
// add to already open tab
$tabWrap->add($inputfield);
}
$contentTab->remove($inputfield);
}
}
$form->append($contentTab);
if(!$this->isPost) {
foreach($tabs as $tab) $form->append($tab);
}
if($this->page->addable() || $this->page->numChildren) $form->append($this->buildFormChildren());
if(!$this->page->template->noSettings && $this->useSettings) $form->append($this->buildFormSettings());
if($this->isTrash && !$this->isPost) {
$this->message($this->_("This page is in the Trash"));
$tabRestore = $this->buildFormRestore();
if($tabRestore) $form->append($tabRestore);
}
$tabDelete = $this->buildFormDelete();
if($tabDelete->children()->count()) $form->append($tabDelete);
if($this->page->viewable() && !$this->requestModal) $this->buildFormView($this->getViewUrl());
if($this->page->hasStatus(Page::statusUnpublished)) {
$pageClassName = wireClassName($this->page, false);
$publishable = $this->page->publishable();
if($publishable && (in_array($pageClassName, $this->otherCorePageClasses) || $this->page->template->noUnpublish)) {
// Do not show a button allowing page to remain unpublished for User, Permission, Role, Language or
// if the page's template indicates it cannot be unpublished
} else {
/** @var InputfieldSubmit $submit2 */
$submit2 = $this->modules->get('InputfieldSubmit');
$submit2->attr('name', 'submit_save');
$submit2->attr('id', 'submit_save_unpublished');
$submit2->showInHeader();
$submit2->setSecondary();
if($this->session->get('clientWidth') > 900) {
$submit2->attr('value', $this->_('Save + Keep Unpublished')); // Button: save unpublished
} else {
$submit2->attr('value', $saveLabel); // Button: save unpublished
}
}
if($publishable) {
$saveName = 'submit_publish';
$saveLabel = $this->_("Publish"); // Button: publish
} else {
$saveName = '';
}
} else {
// use saveName and saveLabel defined at top of method
}
} // !$fieldName
if($saveName) {
/** @var InputfieldSubmit $submit */
$submit = $this->modules->get('InputfieldSubmit');
$submit->attr('id+name', $saveName);
$submit->attr('value', $saveLabel);
$submit->showInHeader();
$form->append($submit);
}
if($submit2) $form->append($submit2);
/** @var InputfieldHidden $field */
$field = $this->modules->get('InputfieldHidden');
$field->attr('name', 'id');
$field->attr('value', $this->page->id);
$form->append($field);
return $form;
}
/**
* Build the 'content' tab on the Page Edit form
*
* @return InputfieldWrapper
*
*/
protected function ___buildFormContent() {
$fields = $this->page->getInputfields(array('flat' => !$this->isPost));
$id = $this->className() . 'Content';
$title = $this->page->template->getTabLabel('content');
if(!$title) $title = $this->_('Content'); // Tab Label: Content
$fields->attr('id', $id);
$fields->attr('title', $title);
$fields->addClass('WireTab');
$this->addTab($id, $title);
if($this->page->template->nameContentTab) {
$fields->prepend($this->buildFormPageName());
}
return $fields;
}
/**
* Build the 'children' tab on the Page Edit form
*
* @return InputfieldWrapper
*
*/
protected function ___buildFormChildren() {
$page = $this->masterPage ? $this->masterPage : $this->page;
$wrapper = $this->wire(new InputfieldWrapper());
$id = $this->className() . 'Children';
$wrapper->attr('id+name', $id);
if(!empty($this->configSettings['ajaxChildren'])) $wrapper->collapsed = Inputfield::collapsedYesAjax;
$defaultTitle = $this->_('Children'); // Tab Label: Children
$title = $this->page->template->getTabLabel('children');
if(!$title) $title = $defaultTitle;
if($page->numChildren) $wrapper->attr('title', "<em>$title</em>");
else $wrapper->attr('title', $title);
$this->addTab($id, $title);
$templateSortfield = $this->page->template->sortfield;
if(!$this->isPost) {
$pageListParent = $page ? $page : $this->parent;
if($pageListParent->numChildren) {
/** @var ProcessPageList $pageList */
$pageList = $this->modules->get('ProcessPageList');
$pageList->set('id', $pageListParent->id);
$pageList->set('showRootPage', false);
} else $pageList = null;
/** @var InputfieldMarkup $field */
$field = $this->modules->get("InputfieldMarkup");
$field->attr('id+name', 'ChildrenPageList');
$field->label = $title == $defaultTitle ? $this->_("Children / Subpages") : $title; // Children field label
if($pageList) {
$field->value = $pageList->execute();
} else {
$field->description = $this->_("There are currently no children/subpages below this page.");
}
if($templateSortfield && $templateSortfield != 'sort') {
$field->notes = sprintf($this->_('Children are sorted by "%s", per the template setting.'), $templateSortfield);
}
if($page->addable()) {
/** @var InputfieldButton $button */
$button = $this->modules->get("InputfieldButton");
$button->attr('id+name', 'AddPageBtn');
$button->attr('value', $this->_('Add New Page Here')); // Button: add new child page
$button->icon = 'plus-circle';
$button->attr('href', "../add/?parent_id={$page->id}" . ($this->requestModal ? "&modal=$this->requestModal" : ''));
$field->append($button);
}
$wrapper->append($field);
}
if(empty($this->page->template->sortfield) && $this->user->hasPermission('page-sort', $this->page)) {
$sortfield = $this->page->sortfield && $this->page->sortfield != 'sort' ? $this->page->sortfield : '';
$fieldset = self::buildFormSortfield($sortfield, $this);
$fieldset->attr('id+name', 'ChildrenSortSettings');
$fieldset->label = $this->_('Sort Settings'); // Children sort settings field label
$fieldset->icon = 'sort';
$fieldset->description = $this->_("If you want all current and future children to automatically sort by a specific field, select the field below and optionally check the 'reverse' checkbox to make the sort descending. Leave the sort field blank if you want to be able to drag-n-drop to your own order."); // Sort settings description text
$wrapper->append($fieldset);
}
return $wrapper;
}
/**
* Build the sortfield configuration fieldset
*
* NOTE: This is also used by ProcessTemplate, so it is self contained
*
* @param string $sortfield Current sortfield value
* @param Process $caller The calling process
* @return InputfieldFieldset
*
*/
public static function buildFormSortfield($sortfield, Process $caller) {
$fieldset = $caller->wire('modules')->get("InputfieldFieldset");
if(!$sortfield) $fieldset->collapsed = Inputfield::collapsedYes;
$field = $caller->wire('modules')->get('InputfieldSelect');
$field->name = 'sortfield';
$field->value = ltrim($sortfield, '-');
$field->columnWidth = 60;
$field->label = __('Children are sorted by', __FILE__); // Children sort field label
// if in ProcessTemplate, give a 'None' option that indicates the Page has control
if($caller instanceof ProcessTemplate) $field->addOption('', __('None', __FILE__));
$field->addOption('sort', __('Manual drag-n-drop', __FILE__));
$options = array(
'name' => 'name',
'status' => 'status',
'modified' => 'modified',
'created' => 'created',
'published' => 'published',
);
$field->addOption(__('Native Fields', __FILE__), $options); // Optgroup label for sorting by fields native to ProcessWire
$customOptions = array();
foreach($caller->wire('fields') as $f) {
//if(!($f->flags & Field::flagAutojoin)) continue;
if($f->flags & Field::flagSystem && $f->name != 'title' && $f->name != 'email') continue;
if($f->type instanceof FieldtypeFieldsetOpen) continue;
$customOptions[$f->name] = $f->name;
}
ksort($customOptions);
$field->addOption(__('Custom Fields', __FILE__), $customOptions); // Optgroup label for sorting by custom fields
$fieldset->append($field);
$f = $caller->wire('modules')->get('InputfieldCheckbox');
$f->value = 1;
$f->attr('id+name', 'sortfield_reverse');
$f->label = __('Reverse sort direction?', __FILE__); // Checkbox labe to reverse the sort direction
$f->icon = 'rotate-left';
if(substr($sortfield, 0, 1) == '-') $f->attr('checked', 'checked');
$f->showIf = "sortfield!='', sortfield!=sort";
$f->columnWidth = 40;
$fieldset->append($f);
return $fieldset;
}
/**
* Build the 'settings' tab on the Page Edit form
*
* @return InputfieldWrapper
*
*/
protected function ___buildFormSettings() {
$superuser = $this->wire('user')->isSuperuser();
/** @var InputfieldWrapper $wrapper */
$wrapper = $this->wire(new InputfieldWrapper());
$id = $this->className() . 'Settings';
$title = $this->_('Settings'); // Tab Label: Settings
$wrapper->attr('id', $id);
$wrapper->attr('title', $title);
$this->addTab($id, $title);
// name
if(($this->page->id > 1 || $this->hasLanguagePageNames) && !$this->page->template->nameContentTab) {
$wrapper->prepend($this->buildFormPageName());
}
// template
$wrapper->add($this->buildFormTemplate());
// parent
if($this->page->id > 1 && $this->page->editable('parent', false)) {
$wrapper->add($this->buildFormParent());
}
// createdUser
if($this->page->id && $superuser && $this->page->template->allowChangeUser) {
$wrapper->add($this->buildFormCreatedUser());
}
// status
$wrapper->add($this->buildFormStatus());
// roles and references
if(!$this->isPost) {
// what users may access this page
$wrapper->add($this->buildFormRoles());
// what pages link tot his page
$wrapper->add($this->buildFormReferences());
}
// page path history (previous URLs)
if($superuser) {
$f = $this->buildFormPrevPaths();
if($f) $wrapper->add($f);
}
// information about created and modified user and time
if(!$this->isPost) {
$wrapper->add($this->buildFormInfo());
}
return $wrapper;
}
/**
* Build the page name input
*
* @return InputfieldPageName
*
*/
protected function buildFormPageName() {
/** @var InputfieldPageName $field */
$field = $this->modules->get('InputfieldPageName');
$field->attr('name', '_pw_page_name');
$field->attr('value', $this->page->name);
$field->slashUrls = $this->page->template->slashUrls;
$field->required = $this->page->id != 1 && !$this->page->hasStatus(Page::statusTemp);
$label = $this->page->template->getNameLabel();
if($label) $field->label = $label;
if(!$this->page->editable('name', false)) {
$field->attr('disabled', 'disabled');
$field->required = false;
}
if($this->hasLanguagePageNames) {
// Using 'hasLanguages' as opposed to 'useLanguages' for different support from LanguageSupportPageNames
$field->setQuietly('hasLanguages', true);
}
$field->editPage = $this->page;
if($this->page->parent) $field->parentPage = $this->page->parent;
return $field;
}
/**
* Build the template selection field
*
* @return InputfieldMarkup|InputfieldSelect
*
*/
protected function buildFormTemplate() {
if($this->page->editable('template', false)) {
/** @var Languages $languages */
$languages = $this->wire('languages');
/** @var Language $language */
$language = $this->user->language;
/** @var InputfieldSelect $field */
$field = $this->modules->get('InputfieldSelect');
$field->attr('id+name', 'template');
$field->attr('value', $this->page->template->id);
$field->required = true;
foreach($this->getAllowedTemplates() as $template) {
/** @var Template $template */
$label = '';
if($languages && $language) $label = $template->get('label' . $language->id);
if(!$label) $label = $template->label ? $template->label : $template->name;
$field->addOption($template->id, $label);
}
} else {
/** @var InputfieldMarkup $field */
$field = $this->modules->get('InputfieldMarkup');
$field->attr('value', "<p>" . $this->page->template->getLabel() . "</p>");
}
$field->label = $this->_('Template'); // Settings: Template field label
$field->icon = 'cubes';
return $field;
}
/**
* Build the parent selection Inputfield
*
* @return InputfieldPageListSelect|InputfieldSelect
*
*/
protected function buildFormParent() {
if(count($this->predefinedParents)) {
/** @var InputfieldSelect $field */
$field = $this->modules->get('InputfieldSelect');
foreach($this->predefinedParents as $p) {
$field->addOption($p->id, $p->path);
}
} else {
/** @var InputfieldPageListSelect $field */
$field = $this->modules->get('InputfieldPageListSelect');
$field->set('parent_id', 0);
if(!empty($this->configSettings['ajaxParent'])) {
$field->collapsed = Inputfield::collapsedYesAjax;
}
}
$field->required = true;
$field->label = $this->_('Parent'); // Settings: Parent field label
$field->icon = 'folder-open-o';
$field->attr('id+name', 'parent_id');
$field->attr('value', $this->page->parent_id);
return $field;
}
/**
* Build the created user selection
*
* @return InputfieldPageListSelect
*
*/
protected function buildFormCreatedUser() {
/** @var InputfieldPageListSelect $field */
$field = $this->modules->get('InputfieldPageListSelect');
$field->label = $this->_('Created by User');
$field->attr('id+name', 'created_users_id');
$field->attr('value', $this->page->created_users_id);
$field->parent_id = $this->config->usersPageID; // @todo support $config->usersPageIDs (array)
$field->showPath = false;
$field->required = true;
return $field;
}
/**
* Build the Settings > References fieldset on the Page Edit form
*
* @return InputfieldMarkup
*
*/
protected function buildFormReferences() {
/** @var InputfieldMarkup $field */
$field = $this->modules->get('InputfieldMarkup');
$field->attr('id', 'ProcessPageEditReferences');
$field->label = $this->_('What pages link to this page?');
$field->icon = 'link';
$field->collapsed = Inputfield::collapsedYesAjax;
if($this->input->get('renderInputfieldAjax') != 'ProcessPageEditReferences') return $field;
$links = $this->page->links("include=all, limit=100");
$references = $this->page->references("include=all, limit=100");
$numTotal = $references->getTotal() + $links->getTotal();
$numShown = $references->count() + $links->count();
$numNotShown = $numTotal - $numShown;
$labelNotListable = $this->_('Not listable');
if($numTotal) {
$field->description = sprintf(
$this->_('Found %d other page(s) linking to this one in Page fields or href links.'),
$numTotal
);
$out = "<ul>";
$itemsByType = array(
$this->_('(in page field)') => $references,
$this->_('(in href link)') => $links
);
foreach($itemsByType as $label => $items) {
$label = "<span class='detail'>$label</span>";
foreach($items as $item) {
/** @var Page $item */
if($item->listable()) {
$url = $item->editable() ? $item->editUrl() : $item->url();
$out .= "<li><a href='$url' title='$item->url' target='_blank'>" . $item->get('title|path') . "</a> $label</li>";
} else {
$out .= "<li>$item->id $labelNotListable $label</li>";
}
}
}
$out .= "</ul>";
if($numNotShown) {
$out .= "<div class='notes'>" . sprintf($this->_('%d additional pages not shown.'), $numNotShown) . "</div>";
}
} else {
$out = "<p>" . $this->_('Did not find any other pages pointing to this one in page fields or href links.') . "</p>";
}
$field->value = $out;
return $field;
}
/**
* Build the “Settings > What URLs redirect to this page?” fieldset on the Page Edit form
*
* @return InputfieldMarkup|null
*
*/
protected function buildFormPrevPaths() {
$input = $this->input;
$modules = $this->modules;
$sanitizer = $this->sanitizer;
$languages = $this->wire()->languages;
if($this->isPost && $input->post('_prevpath_add') === null) return null;
if(!$modules->isInstalled('PagePathHistory')) return null;
/** @var InputfieldMarkup $field */
$field = $modules->get('InputfieldMarkup');
$field->attr('id', 'ProcessPageEditPrevPaths');
$field->label = $this->_('What other URLs redirect to this page?');
$field->icon = 'map-signs';
if(!$this->isPost) {
$field->collapsed = Inputfield::collapsedYesAjax;
if($input->get('renderInputfieldAjax') != 'ProcessPageEditPrevPaths') return $field;
}
$field->description =
$this->_('Whenever a page is moved or the name changes, we remember the previous location for redirects.') . ' ' .
$this->_('Below is a list of URLs (paths) that automatically redirect to this page (using 301 permanent redirect).') . ' ' .
$this->_('You may delete any paths/URLs or manually add new ones.');
/** @var PagePathHistory $history */
$history = $modules->get('PagePathHistory');
$data = $history->getPathHistory($this->page, array(
'verbose' => true,
'virtual' => true
));
$multilang = $languages && $modules->isInstalled('LanguageSupportPageNames');
$slashUrls = $this->page->template->slashUrls;
$deleteIDs = array();
$rootUrl = $this->wire('config')->urls->root;
/** @var InputfieldCheckbox $delete */
$delete = $modules->get('InputfieldCheckbox');
$delete->label = wireIconMarkup('trash-o');
$delete->attr('name', '_prevpath_delete[]');
$delete->entityEncodeLabel = false;
$delete->attr('title', $this->_x('Delete', 'prev-path-delete'));
$delete->renderReady();
if($this->isPost) {
$deleteIDs = array_flip($input->post->array('_prevpath_delete'));
}
/** @var MarkupAdminDataTable $table */
$table = $modules->get('MarkupAdminDataTable');
$table->setEncodeEntities(false);
$table->setSortable(false);
$header = array(
$this->_x('URL', 'prev-path'),
$this->_x('When', 'prev-path-date'),
);
if(count($data)) {
if($multilang) $header[] = $this->_x('Language', 'prev-path-language');
$header[] = '&nbsp;';
if(!$multilang) {
$row = array(
$sanitizer->entities($this->page->path),
$this->_x('Current', 'prev-path-current'),
'&nbsp;',
);
$table->row($row);
}
} else {
$table->row(array(
$this->_('No redirect paths'),
$this->_('Not yet')
));
}
$table->headerRow($header);
foreach($data as $n => $item) {
$id = md5($item['path'] . $item['date']);
$path = $item['path'];
if($this->isPost && isset($deleteIDs[$id])) {
if($history->deletePathHistory($this->page, $path)) {
$this->message(sprintf($this->_('Deleted redirect for previous URL: %s'), $path));
continue;
}
}
if($slashUrls) $path .= '/';
$url = $sanitizer->entities(rtrim($rootUrl, '/') . $path);
$path = $sanitizer->entities($path);
$row = array(
"<a href='$url' target='_blank'>$path</a>",
wireRelativeTimeStr($item['date']),
);
if($multilang && isset($item['language'])) {
/** @var Language $language */
$language = $item['language'];
if($language && $language->id) {
$langLabel = $language->get('title|name');
if(!$language->isDefault() && !$this->page->get("status$language")) $langLabel = "<s>$langLabel</s>";
$row[] = $langLabel;
} else {
$row[] = '?';
}
}
if(empty($item['virtual'])) {
$delete->attr('name', '_prevpath_delete[]');
$delete->attr('value', $id);
$row[] = "<div class='InputfieldCheckbox'>" . $delete->render() . "</div>";
} else {
$parentLabel = $this->_x('Parent', 'prev-path-parent');
$parent = $this->wire('pages')->get((int) $item['virtual']);
if($parent->id) $parentLabel = "<a target='_blank' title='$parent->path' href='$parent->editUrl'>$parentLabel</a>";
$row[] = $parentLabel;
}
$table->row($row);
}
/** @var InputfieldTextarea $add */
$add = $modules->get('InputfieldTextarea');
$add->attr('name', '_prevpath_add');
$add->label = $this->_('Add new redirect URLs');
$add->description =
$this->_('Enter additional paths/URLs (one per line) that should redirect to this page.') . ' ' .
$this->_('Enter the URL path only (i.e. “/hello/world/”), do NOT include scheme, domain, port, query string or fragments.') . ' ';
if($rootUrl != '/') {
$add->description .= sprintf(
$this->_('Paths are relative to site root so do NOT include the %s subdirectory at the beginning.'),
$rootUrl
);
}
$add->collapsed = Inputfield::collapsedYes;
$add->icon = 'plus';
$add->addClass('InputfieldIsSecondary', 'wrapClass');
if($multilang) {
$add->notes = $this->_('To specify a language for the redirect, enter path/URL on line prefixed with language name:');
foreach($languages->findNonDefault() as $language) {
$add->notes .= "\n`$language->name:" .
sprintf($this->_('/your/%s/url/'), $language->name) . "` " . // /your/[language-name]/url/
sprintf($this->_('(for %s)'), $language->get('title|name')); // (for [language-title])
}
}
if($this->isPost) {
$add->processInput($input->post);
if($add->val()) {
foreach(explode("\n", $add->val()) as $path) {
if(strpos($path, ':')) {
list($langName, $path) = explode(':', $path, 2);
$language = $languages->get($sanitizer->pageName($langName));
if(!$language || !$language->id) $language = null;
} else {
$language = null;
}
$path = $sanitizer->pagePathName($path);
if(!strlen($path)) continue;
if($history->addPathHistory($this->page, $path, $language)) {
$this->message(sprintf(
$this->_('Added redirect: %s'),
$path
));
} else {
$this->warning(sprintf(
$this->_('Unable to add redirect %s because it appears to conflict with another path'),
$path
));
}
}
}
} else {
$field->val($table->render());
$field->add($add);
}
return $field;
}
/**
* Build the Settings > Info fieldset on the Page Edit form
*
* @return InputfieldMarkup
*
*/
protected function buildFormInfo() {
$page = $this->page;
$dateFormat = $this->config->dateFormat;
$unknown = '[?]';
/** @var InputfieldMarkup $field */
$field = $this->modules->get("InputfieldMarkup");
$createdName = $page->createdUser ? $page->createdUser->name : '';
$modifiedName = $page->modifiedUser ? $page->modifiedUser->name : '';
if(empty($createdName)) $createdName = $unknown;
if(empty($modifiedName)) $modifiedName = $unknown;
if($this->user->isSuperuser()) {
$url = $this->config->urls->admin . 'access/users/edit/?id=';
if($createdName != $unknown && $page->createdUser instanceof User) $createdName = "<a href='$url{$page->createdUser->id}'>$createdName</a>";
if($modifiedName != $unknown && $page->modifiedUser instanceof User) $modifiedName = "<a href='$url{$page->modifiedUser->id}'>$modifiedName</a>";
}
$lowestDate = strtotime('1974-10-10');
$createdDate = $page->created > $lowestDate ? date($dateFormat, $page->created) . " " .
"<span class='detail'>(" . wireRelativeTimeStr($page->created) . ")</span>" : $unknown;
$modifiedDate = $page->modified > $lowestDate ? date($dateFormat, $page->modified) . " " .
"<span class='detail'>(" . wireRelativeTimeStr($page->modified) . ")</span>" : $unknown;
$publishedDate = $page->published > $lowestDate ? date($dateFormat, $page->published) . " " .
"<span class='detail'>(" . wireRelativeTimeStr($page->published) . ")</span>" : $unknown;
$info = "\n<p>" .
sprintf($this->_('Created by %1$s on %2$s'), $createdName, $createdDate) . "<br />" . // Settings: created user/date information line
sprintf($this->_('Last modified by %1$s on %2$s'), $modifiedName, $modifiedDate) . "<br />" . // Settings: modified user/date information line
sprintf($this->_('Published on %s'), $publishedDate) . // Settings: published information line
"</p>";
$field->attr('id+name', 'ProcessPageEditInfo');
$field->label = $this->_('Info'); // Settings: Info field label
$field->icon = 'info-circle';
if($this->config->advanced) $field->notes = "Object type: " . $page->className();
$field->value = $info;
return $field;
}
/**
* Build the Settings > Status fieldset on the Page Edit form
*
* @return InputfieldCheckboxes
*
*/
protected function buildFormStatus() {
$status = (int) $this->page->status;
$debug = $this->config->debug;
$statuses = $this->getAllowedStatuses();
/** @var InputfieldCheckboxes $field */
$field = $this->modules->get('InputfieldCheckboxes');
$field->attr('name', 'status');
$field->icon = 'sliders';
$value = array();
foreach($statuses as $s => $label) {
if($s & $status) $value[] = $s;
if(strpos($label, ': ')) $label = str_replace(': ', ': [span.detail]', $label) . '[/span]';
$field->addOption($s, $label);
}
$field->attr('value', $value);
$field->label = $this->_('Status'); // Settings: Status field label
if($debug) $field->notes = $this->page->statusStr;
return $field;
}
/**
* Build the 'delete' tab on the Page Edit form
*
* @return InputfieldWrapper
*
*/
protected function ___buildFormDelete() {
$wrapper = $this->wire(new InputfieldWrapper());
$deleteable = $this->page->deleteable();
$trashable = $deleteable || $this->page->trashable();
if(!$trashable) return $wrapper;
$id = $this->className() . 'Delete';
$deleteLabel = $this->_('Delete'); // Tab Label: Delete
$wrapper->attr('id', $id);
$wrapper->attr('title', $deleteLabel);
$this->addTab($id, $deleteLabel);
if($trashable) {
/** @var InputfieldCheckbox $field */
$field = $this->modules->get('InputfieldCheckbox');
$field->attr('id+name', 'delete_page');
$field->attr('value', $this->page->id);
if($deleteable && ($this->isTrash || $this->page->template->noTrash)) {
$deleteLabel = $this->_('Delete Permanently'); // Delete permanently checkbox label
} else {
$deleteLabel = $this->_('Move to Trash'); // Move to trash checkbox label
}
$field->icon = 'trash-o';
$field->label = $deleteLabel;
$field->description = $this->_('Check the box to confirm that you want to do this.'); // Delete page confirmation instruction
$field->label2 = $this->_('Confirm');
$wrapper->append($field);
}
if(count($wrapper->children())) {
$field = $this->modules->get('InputfieldButton');
$field->attr('id+name', 'submit_delete');
$field->value = $deleteLabel;
$wrapper->append($field);
} else {
$wrapper->description = $this->_('This page may not be deleted at this time'); // Page can't be deleted message
}
return $wrapper;
}
/**
* Build the 'restore' tab shown for pages in the trash
*
* Returns boolean false if restore not possible.
*
* @return InputfieldWrapper|bool
*
*/
protected function buildFormRestore() {
if(!$this->page->isTrash()) return false;
if(!$this->page->restorable()) return false;
$info = $this->wire()->pages->trasher()->getRestoreInfo($this->page);
if(!$info['restorable']) return false;
/** @var InputfieldWrapper $wrapper */
$wrapper = $this->wire(new InputfieldWrapper());
$id = $this->className() . 'Restore';
$restoreLabel = $this->_('Restore'); // Tab Label: Restore
$restoreLabel2 = $this->_('Move out of trash and restore to original location');
$wrapper->attr('id', $id);
$wrapper->attr('title', $restoreLabel);
$this->addTab($id, $restoreLabel);
/** @var Page $parent */
$parent = $info['parent'];
$newPath = $parent->path() . $info['name'] . '/';
/** @var InputfieldCheckbox $field */
$field = $this->modules->get('InputfieldCheckbox');
$field->attr('id+name', 'restore_page');
$field->attr('value', $this->page->id);
$field->icon = 'trash-o';
$field->label = $restoreLabel2;
$field->description = $this->_('Check the box to confirm that you want to restore this page.'); // Restore page confirmation instruction
$field->notes = sprintf($this->_('The page will be restored to: **%s**.'), $newPath);
if($info['namePrevious']) $field->notes .= ' ' .
sprintf($this->_('Original name will be adjusted from **%1$s** to **%2$s** to be unique.'), $info['namePrevious'], $info['name']);
$field->label2 = $restoreLabel;
$wrapper->append($field);
return $wrapper;
}
/**
* Build the 'view' tab on the Page Edit form
*
* @param string $url
*
*/
protected function ___buildFormView($url) {
$label = $this->_('View'); // Tab Label: View
$id = $this->className() . 'View';
if((!empty($this->configSettings['viewNew'])) || $this->viewAction == 'new') {
$target = '_blank';
} else {
$target = '_top';
}
$a =
"<a id='_ProcessPageEditView' target='$target' href='$url' data-action='$this->viewAction'>$label" .
"<span id='_ProcessPageEditViewDropdownToggle' class='pw-dropdown-toggle' data-pw-dropdown='#_ProcessPageEditViewDropdown'>" .
"<i class='fa fa-angle-down'></i></span></a>";
$this->addTab($id, $a);
}
/**
* Build the Settings > Roles fieldset on the Page Edit form
*
* @return InputfieldMarkup
*
*/
protected function ___buildFormRoles() {
/** @var InputfieldMarkup $field */
$field = $this->modules->get("InputfieldMarkup");
$field->label = $this->_('Who can access this page?'); // Roles information field label
$field->icon = 'users';
$field->attr('id+name', 'ProcessPageEditRoles');
$field->collapsed = Inputfield::collapsedYesAjax;
/** @var MarkupAdminDataTable $table */
$table = $this->modules->get("MarkupAdminDataTable");
$pageHasTemplateFile = $this->page->template->filenameExists();
if($this->input->get('renderInputfieldAjax') == 'ProcessPageEditRoles') {
$roles = $this->page->getAccessRoles();
$accessTemplate = $this->page->getAccessTemplate('edit');
if($accessTemplate) {
$editRoles = $accessTemplate->editRoles;
$addRoles = $accessTemplate->addRoles;
$createRoles = $accessTemplate->createRoles;
} else {
$editRoles = array();
$addRoles = array();
$createRoles = array();
}
$table->headerRow(array(
$this->_('Role'), // Roles table column header: Role
$this->_('What they can do') // Roles table colum header: what they can do
));
$table->setEncodeEntities(false);
$addLabel = 'add';
if(count($roles)) {
$hasPublishPermission = $this->wire('permissions')->has('page-publish');
foreach($roles as $role) {
$permissions = array();
$roleName = $role->name;
if($roleName == 'guest') $roleName .= " " . $this->_('(everyone)'); // Identifies who guest is (everyone)
$permissions["page-view"] = 'view' . ($pageHasTemplateFile ? '' : '¹');
$checkEditable = true;
if($hasPublishPermission && !$this->page->hasStatus(Page::statusUnpublished)
&& !$role->hasPermission('page-publish', $this->page)) {
$checkEditable = false;
}
$key = array_search($role->id, $addRoles);
if($key !== false && $role->hasPermission('page-add', $this->page)) {
$permissions["page-add"] = 'add';
unset($addRoles[$key]);
}
$editable = $role->hasPermission('page-edit', $this->page) && in_array($role->id, $editRoles);
if($checkEditable && $editable) {
foreach($role->permissions as $permission) {
if(strpos($permission->name, 'page-') !== 0) continue;
if(in_array($permission->name, array('page-view', 'page-publish', 'page-create', 'page-add'))) continue;
if(!$role->hasPermission($permission, $this->page)) continue;
$permissions[$permission->name] = str_replace('page-', '', $permission->name); // only page-context permissions
}
if($hasPublishPermission && $role->hasPermission('page-publish', $this->page)) {
$permissions["page-publish"] = 'publish';
}
}
if(in_array($role->id, $createRoles) && $editable) {
$permissions["page-create"] = 'create';
}
$table->row(array($roleName, implode(', ', $permissions)));
}
}
if(count($addRoles)) {
foreach($addRoles as $roleID) {
$role = $this->wire('roles')->get($roleID);
if(!$role->id) continue;
if(!$role->hasPermission("page-add", $this->page)) continue;
$table->row(array($role->name, $addLabel));
}
}
$table->row(array('superuser', $this->_x('all', 'all permissions')));
$field->value = $table->render();
}
$accessParent = $this->page->getAccessParent();
if($accessParent === $this->page) {
$field->notes = sprintf($this->_('Access is defined with this pages template: %s'), $accessParent->template); // Where access is defined: with this page's template
} else {
$field->notes = sprintf($this->_('Access is inherited from page "%1$s" and defined with template: %2$s'), $accessParent->path, $accessParent->template); // Where access is defined: inherited from a parent
}
if(!$pageHasTemplateFile) {
$field->notes = trim("¹ " . $this->_('Viewable by its URL if page had a template file (it does not currently).') . "\n$field->notes");
}
return $field;
}
/***********************************************************************************************************************
* FORM PROCESSING
*
*/
/**
* Save a submitted Page Edit form
*
*/
protected function processSave() {
if($this->page->hasStatus(Page::statusLocked)) {
if(!$this->user->hasPermission('page-lock', $this->page) || (!empty($_POST['status']) && in_array(Page::statusLocked, $_POST['status']))) {
$this->error($this->noticeLocked);
$this->processSaveRedirect($this->redirectUrl);
return;
}
}
$formErrors = 0;
// remove temporary status that may have been assigned by ProcessPageAdd quick add mode
if($this->page->hasStatus(Page::statusTemp)) $this->page->removeStatus(Page::statusTemp);
if($this->input->post('submit_delete')) {
if($this->input->post('delete_page')) $this->deletePage();
} else {
$this->processInput($this->form);
$changes = array_unique($this->page->getChanges());
$numChanges = count($changes);
if($numChanges) {
$this->changes = $changes;
$this->message(sprintf($this->_('Change: %s'), implode(', ', $changes)), Notice::debug); // Message shown for each changed field
}
foreach($this->notices as $notice) {
if($notice instanceof NoticeError) $formErrors++;
}
// if any Inputfields threw errors during processing, give the page a 'flagged' status
// so that it can later be identified the page may be missing something
if($formErrors && count($this->form->getErrors())) {
// add flagged status when form had errors
$this->page->addStatus(Page::statusFlagged);
} else if($this->page->hasStatus(Page::statusFlagged)) {
// if no errors, remove incomplete status
$this->page->removeStatus(Page::statusFlagged);
$this->message($this->_('Removed flagged status because no errors reported during save'));
}
$isUnpublished = $this->page->hasStatus(Page::statusUnpublished);
if($this->input->post('submit_publish') || $this->input->post('submit_save')) {
try {
$options = array();
$name = '';
if($this->page->isChanged('name')) {
if(!strlen($this->page->name) && $this->page->namePrevious) {
// blank page name when there was a previous name, set back the previous
// example instance: when template.childNameFormat in use and template.noSettings active
$this->page->name = $this->page->namePrevious;
} else {
$name = $this->page->name;
}
$options['adjustName'] = true;
}
$numChanges = $numChanges > 0 ? ' (' . sprintf($this->_n('%d change', '%d changes', $numChanges) . ')', $numChanges) : '';
if($this->input->post('submit_publish') && $isUnpublished && $this->page->publishable() && !$formErrors) {
$this->page->removeStatus(Page::statusUnpublished);
$message = sprintf($this->_('Published Page: %s'), '{path}') . $numChanges; // Message shown when page is published
} else {
$message = sprintf($this->_('Saved Page: %s'), '{path}') . $numChanges; // Message shown when page is saved
if($isUnpublished && $formErrors && $this->input->post('submit_publish')) {
$message .= ' - ' . $this->_('Cannot be published until errors are corrected');
}
}
$restored = false;
if($this->input->post('restore_page') && $this->page->isTrash() && $this->page->restorable()) {
if($formErrors) {
$this->warning($this->_('Page cannot be restored while errors are present'));
} else if($this->wire('pages')->restore($this->page, false)) {
$message = sprintf($this->_('Restored Page: %s'), '{path}') . $numChanges;
$restored = true;
} else {
$this->warning($this->_('Error restoring page'));
}
}
$this->wire('pages')->save($this->page, $options);
if($restored) $this->wire('pages')->restored($this->page);
$message = str_replace('{path}', $this->page->path, $message);
$this->message($message);
if($name && $name != $this->page->name) {
$this->warning(sprintf($this->_('Changed page URL name to "%s" because requested name was already taken.'), $this->page->name));
}
} catch(\Exception $e) {
$show = true;
$message = $e->getMessage();
foreach($this->errors('all') as $error) {
if(strpos($error, $message) === false) continue;
$show = false;
break;
}
if($show) $this->error($message);
}
}
}
if($this->redirectUrl) {
// non-default redirectUrl overrides after_submit_action
} else if($formErrors) {
// if there were errors to attend to, stay where we are
} else {
// after submit action
$submitAction = $this->input->post('_after_submit_action');
if($submitAction) $this->processSubmitAction($submitAction);
}
$this->processSaveRedirect($this->getRedirectUrl());
}
/**
* Process the given submit action value
*
* #pw-hooker
*
* @param string $value Value of selected action, i.e. 'exit', 'view', 'add', next', etc.
* @return bool Returns true if value was acted upon or false if not
* @since 3.0.142
* @see ___getSubmitActions(), setRedirectUrl()
*
*/
protected function ___processSubmitAction($value) {
if($value == 'exit') {
$this->setRedirectUrl('../');
} else if($value == 'view') {
$this->setRedirectUrl($this->getViewUrl());
} else if($value == 'add') {
$this->setRedirectUrl("../add/?parent_id={$this->page->parent_id}");
} else if($value == 'next') {
$nextPage = $this->page->next("include=unpublished");
if($nextPage->id) {
if(!$nextPage->editable()) {
$nextPage = $this->page->next("include=hidden");
if($nextPage->id && !$nextPage->editable()) {
$nextPage = $this->page->next();
if($nextPage->id && !$nextPage->editable()) $nextPage = new NullPage();
}
}
}
if($nextPage->id) {
$this->setRedirectUrl($this->getEditUrl(array('id' => $nextPage->id)));
} else {
$this->warning($this->_('There is no editable next page to edit.'));
}
} else {
return false;
}
return true;
}
/**
* Perform an after save redirect
*
* @param string $redirectUrl
*
*/
protected function ___processSaveRedirect($redirectUrl = '') {
if($redirectUrl) {
$c = substr($redirectUrl, 0, 1);
$admin = $c === '.' || $c === '?' || strpos($redirectUrl, $this->config->urls->admin) === 0;
if($admin) {
$redirectUrl .= (strpos($redirectUrl, '?') === false ? '?' : '&') . 's=1';
}
} else {
$admin = true;
$redirectUrl = $this->getEditUrl(array('s' => 1));
}
if($admin) {
$redirectUrl .= "&c=" . count($this->changes);
if(count($this->fields) && count($this->changes)) {
$redirectUrl .= "&changes=" . implode(',', $this->changes);
}
}
$this->setRedirectUrl($redirectUrl);
$this->session->redirect($this->getRedirectUrl());
}
/**
* Process the input from a submitted Page Edit form, delegating to other methods where appropriate
*
* @param InputfieldWrapper $form
* @param int $level
* @param Inputfield $formRoot
*
*/
protected function ___processInput(InputfieldWrapper $form, $level = 0, $formRoot = null) {
static $skipFields = array(
'sortfield_reverse',
'submit_publish',
'submit_save',
'delete_page',
);
if(!$level) {
$form->processInput($this->input->post);
$formRoot = $form;
$this->page->setQuietly('_forceAddStatus', 0);
}
$languages = $this->wire()->languages;
$errorAction = (int) $this->page->template->errorAction;
foreach($form as $inputfield) {
/** @var Inputfield|InputfieldWrapper $inputfield */
$name = $inputfield->attr('name');
if($name == '_pw_page_name') $name = 'name';
if(in_array($name, $skipFields)) continue;
if(!$this->page->editable($name, false)) {
$this->page->untrackChange($name); // just in case
continue;
}
if($name == 'sortfield' && $this->useChildren && $form->isProcessable($inputfield->parent->parent)) {
$this->processInputSortfield($inputfield) ;
continue;
}
if($this->useSettings) {
if($name == 'template') {
$this->processInputTemplate($inputfield);
continue;
} else if($name == 'created_users_id') {
$this->processInputUser($inputfield);
continue;
} else if($name == 'parent_id' && count($this->predefinedParents)) {
if(!$this->predefinedParents->has("id=$inputfield->value")) {
$this->error("Parent $inputfield->value is not allowed for $this->page");
continue;
}
}
if($name == 'status' && $this->processInputStatus($inputfield)) continue;
}
if($this->processInputErrorAction($this->page, $inputfield, $name, $errorAction)) continue;
if($name && $inputfield->isChanged()) {
if($languages && $inputfield->getSetting('useLanguages')) {
$v = $this->page->get($name);
if(is_object($v)) {
$v = clone $v;
$v->setFromInputfield($inputfield);
$this->page->set($name, $v);
} else {
$this->page->set($name, $inputfield->value);
}
} else {
$this->page->set($name, $inputfield->value);
}
}
if($inputfield instanceof InputfieldWrapper && count($inputfield->getChildren())) {
$this->processInput($inputfield, $level + 1, $formRoot);
}
}
if(!$level) {
$forceAddStatus = $this->page->get('_forceAddStatus');
if($forceAddStatus && !$this->page->hasStatus($forceAddStatus)) {
$this->page->addStatus($forceAddStatus);
}
}
}
/**
* Process required error actions as configured with pages template
*
* @param Page $page
* @param Inputfield|InputfieldRepeater $inputfield Inputfield that has already had its processInput() method called.
* @param string $name Name of field that we are checking.
* @param null|int $errorAction Error action from $page->template->errorAction, or omit to auto-detect.
* @return bool Returns true if field $name should be skipped over during processing, or false if not
*
*/
public function processInputErrorAction(Page $page, Inputfield $inputfield, $name, $errorAction = null) {
if(empty($name)) return false;
if($errorAction === null) $errorAction = (int) $page->template->get('errorAction');
if(!$errorAction) return false;
if($page->isUnpublished()) return false;
$isRequired = $inputfield->getSetting('required');
$isRepeater = strpos($inputfield->className(), 'Repeater') > 0 && wireInstanceOf($inputfield, 'InputfieldRepeater', false);
if(!$isRepeater && !$isRequired) return false;
if($inputfield->getSetting('requiredSkipped')) return false;
if($isRepeater) {
if($inputfield->numRequiredEmpty() > 0) {
// repeater has required fields that are empty
} else if($isRequired && $inputfield->numPublished() < 1) {
// repeater is required and has no published items
} else {
// repeater is okay for now
return false;
}
} else if(!$inputfield->isEmpty()) {
return false;
}
if($errorAction === 1) {
// restore existing value by skipping processing of empty when required
$value = $inputfield->attr('value');
if($value instanceof Wire) $value->resetTrackChanges();
if($page->getField($name)) $page->remove($name); // force fresh copy to reload
$previousValue = $page->get($name);
$page->untrackChange($name);
if($previousValue) {
// we should have a previous value to restore
if(WireArray::iterable($previousValue) && !count($previousValue)) {
// previous value still empty
} else {
// previous value restored by simply not setting new value to $page
$inputfield->error($this->_('Restored previous value'));
return true;
}
}
} else if($errorAction === 2 && $page->publishable() && $page->id > 1) {
// unpublish page missing required value
$page->setQuietly('_forceAddStatus', Page::statusUnpublished);
$label = $inputfield->getSetting('label');
if(empty($label)) $label = $inputfield->attr('name');
$inputfield->error(sprintf($this->_('Page unpublished because field "%s" is required'), $label));
return false;
}
return false;
}
/**
* Check to see if the page's created user has changed and make sure it's valid
*
* @param Inputfield $inputfield
*
*/
protected function processInputUser(Inputfield $inputfield) {
if(!$this->user->isSuperuser() || !$this->page->id || !$this->page->template->allowChangeUser) return;
$userID = (int) $inputfield->attr('value');
if(!$userID) return;
if($userID == $this->page->created_users_id) return; // no change
$user = $this->pages->get($userID);
if(!in_array($user->template->id, $this->config->userTemplateIDs)) return; // invalid user template
if(!in_array($user->parent_id, $this->config->usersPageIDs)) return; // invalid user parent
$this->page->created_users_id = $userID;
$this->page->trackChange('created_users_id');
}
/**
* Check to see if the page's template has changed and setup a redirect to a confirmation form if it has
*
* @param Inputfield $inputfield
* @return bool
* @throws WireException
*
*/
protected function processInputTemplate(Inputfield $inputfield) {
if($this->page->template->noChangeTemplate) return true;
$templateID = (int) $inputfield->attr('value');
if(!$templateID) return true;
$template = $this->wire('templates')->get((int) $inputfield->attr('value'));
if(!$template) return true; // invalid template
if($template->id == $this->page->template->id) return true; // no change
if(!$this->isAllowedTemplate($template)) {
throw new WireException(sprintf($this->_("Template '%s' is not allowed"), $template)); // Selected template is not allowed
}
// template has changed, set a redirect URL which will confirm the change
$this->setRedirectUrl("template?id={$this->page->id}&template={$template->id}");
return true;
}
/**
* Process the submitted 'status' field and account for the bitwise logic present
*
* @param Inputfield $inputfield
* @return bool
*
*/
protected function processInputStatus(Inputfield $inputfield) {
$inputStatusFlags = $inputfield->val();
if(!is_array($inputStatusFlags)) $inputStatusFlags = array();
foreach($inputStatusFlags as $k => $v) $inputStatusFlags[$k] = (int) $v;
$allowedStatusFlags = array_keys($this->getAllowedStatuses());
$statusLabels = array_flip(Page::getStatuses());
$value = $this->page->status;
foreach($allowedStatusFlags as $flag) {
if(in_array($flag, $inputStatusFlags, true)) {
if($value & $flag) {
// already has flag
} else {
$value = $value | $flag; // add status
$this->message(sprintf($this->_('Added status: %s'), $statusLabels[$flag]), Notice::debug);
}
} else if($value & $flag) {
$value = $value & ~$flag; // remove flag
$this->message(sprintf($this->_('Removed status: %s'), $statusLabels[$flag]), Notice::debug);
}
}
$this->page->status = $value;
return true;
}
/**
* Process the Children > Sortfield input
*
* @param Inputfield $inputfield
* @return bool
*
*
*/
protected function processInputSortfield(Inputfield $inputfield) {
if(!$this->user->hasPermission('page-sort', $this->page)) return true;
$sortfield = $this->sanitizer->name($inputfield->value);
if($sortfield != 'sort' && !empty($_POST['sortfield_reverse'])) $sortfield = '-' . $sortfield;
if(empty($sortfield)) $sortfield = 'sort';
$this->page->sortfield = $sortfield;
return true;
}
/**
* Process a delete page request, moving the page to the trash if applicable
*
* @return bool
*
*/
protected function deletePage() {
$page = $this->page;
if(!$page->trashable(true)) {
$this->error($this->_('This page is not deleteable'));
return false;
}
$redirectUrl = $this->wire()->config->urls->admin . "page/?open={$this->parent->id}";
if($this->wire()->page->process != $this->className()) $redirectUrl = "../";
$pagePath = $page->path();
if(($this->isTrash || $page->template->noTrash) && $page->deleteable()) {
$this->wire()->session->message(sprintf($this->_('Deleted page: %s'), $pagePath)); // Page deleted message
$this->pages->delete($page, true);
$this->deletedPage($page, $redirectUrl, false);
} else if($this->pages->trash($page)) {
$this->wire()->session->message(sprintf($this->_('Moved page to trash: %s'), $pagePath)); // Page moved to trash message
$this->deletedPage($page, $redirectUrl, true);
} else {
$this->error($this->_('Unable to move page to trash')); // Page can't be moved to the trash error
return false;
}
return true;
}
/**
* Called after a page has been deleted or trashed, performs redirect
*
* @param Page $page Page that was deleted or trashed
* @param string $redirectUrl URL that should be redirected to
* @param bool $trashed True if page was trashed rather than deleted
* @since 3.0.173
*
*/
protected function ___deletedPage($page, $redirectUrl, $trashed = false) {
if($page || $trashed) {} // ignore
$this->wire()->session->location($redirectUrl);
}
/**
* Save only the fields posted via ajax
*
* - Field name must be included in server header HTTP_X_FIELDNAME or directly in the POST vars.
* - Note that fields that would be not present in POST vars (like a checkbox) are only supported
* by the HTTP_X_FIELDNAME version.
* - Works for custom fields only at present.
*
* @param Page $page
* @throws WireException
*
*/
protected function ___ajaxSave(Page $page) {
if($this->config->demo) throw new WireException("Ajax save is disabled in demo mode");
if($page->hasStatus(Page::statusLocked)) throw new WireException($this->noticeLocked);
if(!$this->ajaxEditable($page)) throw new WirePermissionException($this->noticeNoAccess);
$this->session->CSRF->validate(); // throws exception when invalid
/** @var InputfieldWrapper $form */
$form = $this->wire(new InputfieldWrapper());
$form->useDependencies = false;
$keys = array();
if(isset($_SERVER['HTTP_X_FIELDNAME'])) {
$keys[] = $this->sanitizer->fieldName($_SERVER['HTTP_X_FIELDNAME']);
} else {
foreach($this->input->post as $key => $unused) {
if($key == 'id') continue;
$keys[] = $this->sanitizer->fieldName($key);
}
}
foreach($keys as $key) {
if(!$field = $page->template->fieldgroup->getFieldContext($key)) continue;
if(!$this->ajaxEditable($page, $key)) continue;
if(!$inputfield = $field->getInputfield($page)) continue;
$inputfield->showIf = ''; // cancel showIf dependencies since other fields may not be present
$inputfield->name = $key;
$inputfield->value = $page->get($key);
$form->add($inputfield);
}
$form->processInput($this->input->post);
$page->setTrackChanges(true);
$numFields = 0;
$lastFieldName = null;
$languages = $this->wire('languages');
foreach($form->children() as $inputfield) {
$name = $inputfield->name;
if($languages && $inputfield->getSetting('useLanguages')) {
$v = $page->get($name);
if(is_object($v)) {
$v = clone $v;
$v->setFromInputfield($inputfield);
$page->set($name, $v);
} else {
$page->set($name, $inputfield->value);
}
} else {
$page->set($name, $inputfield->value);
}
$numFields++;
$lastFieldName = $inputfield->name;
}
if($page->isChanged()) {
if($numFields === 1) {
$page->save((string)$lastFieldName);
$this->message("AJAX Saved page '{$page->id}' field '$lastFieldName'");
} else {
$page->save();
$this->message("AJAX Saved page '{$page->id}' multiple fields");
}
} else {
$this->message("AJAX Page not saved (no changes)");
}
}
/***************************************************************************************************************
* OTHER ACTIONS
*
*/
/**
* Execute a template change for a page, building an info + confirmation form (handler for /template/ action)
*
* @return string
* @throws WireException
*
*/
public function ___executeTemplate() {
if(!$this->useSettings || !$this->user->hasPermission('page-template', $this->page)) {
throw new WireException("You don't have permission to change the template on this page.");
}
$templateID = (int) $this->input->get('template');
if($templateID < 1) throw new WireException("This method requires a 'template' get var");
$template = $this->templates->get($templateID);
if(!$template) throw new WireException("Unknown template");
if(!$this->isAllowedTemplate($template->id)) {
throw new WireException("That template is not allowed");
}
$labelConfirm = $this->_('Confirm template change'); // Change template confirmation subhead
$labelAction = sprintf($this->_('Change template from "%1$s" to "%2$s"'), $this->page->template, $template); // Change template A to B headline
$this->headline($labelConfirm);
if($this->requestModal) $this->error("$labelConfirm $labelAction"); // force modal open
/** @var InputfieldForm $form */
$form = $this->modules->get("InputfieldForm");
$form->attr('action', 'saveTemplate');
$form->attr('method', 'post');
$form->description = $labelAction;
/** @var InputfieldMarkup $f */
$f = $this->modules->get("InputfieldMarkup");
$f->icon = 'cubes';
$f->label = $labelConfirm;
$list = array();
foreach($this->page->template->fieldgroup as $field) {
if(!$template->fieldgroup->has($field)) {
$list[] = $this->sanitizer->entities($field->getLabel()) . " ($field->name)";
}
}
if(!$list) $this->executeSaveTemplate($template);
$f->description = $this->_('Warning, changing the template will delete the following fields:'); // Headline that precedes list of fields that will be deleted as a result of template change
$icon = "<i class='fa fa-times-circle'></i> ";
$f->attr('value', "<p class='ui-state-error-text'>$icon" . implode("<br />$icon", $list) . '</p>');
$form->append($f);
/** @var InputfieldCheckbox $f */
$f = $this->modules->get("InputfieldCheckbox");
$f->attr('name', 'template');
$f->attr('value', $template->id);
$f->label = $this->_('Are you sure?'); // Checkbox label to confirm they want to change template
$f->label2 = $labelAction;
$f->icon = 'warning';
$f->description = $this->_('Please confirm that you understand the above by clicking the checkbox below.'); // Checkbox description to confirm they want to change template
$form->append($f);
/** @var InputfieldHidden $f */
$f = $this->modules->get("InputfieldHidden");
$f->attr('name', 'id');
$f->attr('value', $this->page->id);
$form->append($f);
/** @var InputfieldSubmit $f */
$f = $this->modules->get("InputfieldSubmit");
$form->append($f);
$page = $this->masterPage ? $this->masterPage : $this->page;
$this->wire('breadcrumbs')->add(new Breadcrumb("./?id={$page->id}", $page->get("title|name")));
return $form->render();
}
/**
* Save a template change for a page (handler for /saveTemplate/ action)
*
* @param Template $template
* @throws WireException
*
*/
public function ___executeSaveTemplate($template = null) {
if(!$this->useSettings || !$this->user->hasPermission('page-template', $this->page)) {
throw new WireException($this->_("You don't have permission to change the template on this page.")); // Error: user doesn't have permission to change template
}
if(!$this->page->template->noChangeTemplate) {
if(!is_null($template) || (isset($_POST['template']) && ($template = $this->templates->get((int) $_POST['template'])))) {
try {
if(!$this->isAllowedTemplate($template)) {
throw new WireException($this->_('That template is not allowed')); // Error: selected template is not allowed
}
$this->page->template = $template;
$this->page->save();
$this->message(sprintf($this->_("Changed template to '%s'"), $template)); // Message: template was changed
} catch(\Exception $e) {
$this->error($e->getMessage());
}
}
}
$this->session->redirect("./?id={$this->page->id}");
}
/**
* Returns an array of templates that are allowed to be used here
*
* @return array|Template[] Array of Template objects
*
*/
protected function getAllowedTemplates() {
if(is_array($this->allowedTemplates)) return $this->allowedTemplates;
$templates = array();
$user = $this->user;
$isSuperuser = $user->isSuperuser();
$page = $this->masterPage ? $this->masterPage : $this->page;
$parent = $page->parent;
$parentEditable = ($parent->id && $parent->editable());
/** @var Config $config */
$config = $this->wire('config');
$superAdvanced = $isSuperuser && $config->advanced;
// current page template is assumed, otherwise we wouldn't be here
$templates[$page->template->id] = $page->template;
// check if they even have permission to change it
if(!$user->hasPermission('page-template', $page) || $page->template->noChangeTemplate) {
$this->allowedTemplates = $templates;
return $templates;
}
$allTemplates = count($this->predefinedTemplates) ? $this->predefinedTemplates : $this->wire('templates');
foreach($allTemplates as $template) {
/** @var Template $template */
if(isset($templates[$template->id])) continue;
if($template->flags & Template::flagSystem) {
// if($template->name == 'user' && $parent->id != $this->config->usersPageID) continue;
if(in_array($template->id, $config->userTemplateIDs) && !in_array($parent->id, $config->usersPageIDs)) continue;
if($template->name == 'role' && $parent->id != $config->rolesPageID) continue;
if($template->name == 'permission' && $parent->id != $config->permissionsPageID) continue;
if(strpos($template->name, 'repeater_') === 0 || strpos($template->name, 'fieldset_') === 0) continue;
}
if(count($template->parentTemplates) && $parent->id && !in_array($parent->template->id, $template->parentTemplates)) {
// this template specifies it can only be used with certain parents, and our parent's template isn't one of them
continue;
}
if($parent->id && count($parent->template->childTemplates)) {
// the page's parent only allows certain templates for it's children
// if this isn't one of them, then continue;
if(!in_array($template->id, $parent->template->childTemplates)) continue;
}
if(!$superAdvanced && ((int) $template->noParents) < 0 && $template->getNumPages() > 0) {
// only one of these is allowed to exist (noParents=-1)
continue;
} else if($template->noParents > 0) {
// user can't change to a template that has been specified as no more instances allowed
continue;
} else if($isSuperuser) {
$templates[$template->id] = $template;
} else if((!$template->useRoles && $parentEditable) || $user->hasPermission('page-edit', $template)) {
// determine if the template's assigned roles match up with the users's roles
// and that at least one of those roles has page-edit permission
if($user->hasPermission('page-create', $page)) {
// user is allowed to create more pages of this type, so template may be used
$templates[$template->id] = $template;
}
}
}
$this->allowedTemplates = $templates;
return $templates;
}
/**
* Is the given template or template ID allowed here?
*
* @param int|Template $id
* @return bool
*
*/
protected function isAllowedTemplate($id) {
// if $id is a template, then convert it to it's numeric ID
if(is_object($id) && $id instanceof Template) $id = $id->id;
$id = (int) $id;
// if the template is the same one already in place, of course it's allowed
if($id == $this->page->template->id) return true;
// if we've made it this far, then get a list of templates that are allowed...
$templates = $this->getAllowedTemplates();
// ...and determine if the supplied template is in that list
return isset($templates[$id]);
}
/**
* Returns true if this page may be ajax saved (user has access), or false if not
*
* @param Page $page
* @param string $fieldName Optional field name
* @return bool
*
*/
protected function ___ajaxEditable(Page $page, $fieldName = '') {
return $page->editable($fieldName);
}
/**
* Return instance of the Page being edited (required by WirePageEditor interface)
*
* For Inputfields/Fieldtypes to use if they want to retrieve the editing page rather than the viewing page
*
* @return Page
*
*/
public function getPage() {
return $this->page;
}
/**
* Set the page being edited
*
* @param Page $page
*
*/
public function setPage(Page $page) {
$this->page = $page;
}
/**
* Set the 'master' page
*
* @param Page $page
* @deprecated
*
*/
public function setMasterPage(Page $page) {
$this->masterPage = $page;
}
/**
* Get the 'master' page (if set)
*
* @return null|Page
* @deprecated
*
*/
public function getMasterPage() {
return $this->masterPage;
}
/**
* Set whether or not 'settings' tab should show
*
* @param bool $useSettings
*
*/
public function setUseSettings($useSettings) {
$this->useSettings = (bool) $useSettings;
}
/**
* Set predefined allowed templates
*
* @param array|Template[] $templates
*
*/
public function setPredefinedTemplates($templates) {
if(WireArray::iterable($templates)) $this->predefinedTemplates = $templates;
}
/**
* Set predefined allowed parents
*
* @param PageArray $parents
*
*/
public function setPredefinedParents(PageArray $parents) {
$this->predefinedParents = $parents;
}
/**
* Set the primary editor, if not ProcessPageEdit
*
* @param WirePageEditor $editor
*
*/
public function setEditor(WirePageEditor $editor) {
$this->editor = $editor;
}
/**
* Called on save requests, sets the next redirect URL for the next request
*
* @param string $url URL to redirect to
* @since 3.0.142 Was protected in previous versions
*
*/
public function setRedirectUrl($url) {
$this->redirectUrl = $url;
}
/**
* Get the current redirectUrl
*
* @param array $extras Any extra parts you want to add as array of strings like "key=value"
* @return string
* @since 3.0.142 Was protected in previous versions
*
*/
public function getRedirectUrl(array $extras = array()) {
$url = $this->redirectUrl;
if(!strlen($url)) $url = "./?id=$this->id";
if($this->requestModal && strpos($url, 'modal=') === false) {
$extras[] = "modal=$this->requestModal";
}
if(strpos($url, '&field=') === false && strpos($url, '&fields=') === false) {
if(count($this->fields)) {
$names = array();
foreach($this->fields as $field) {
$names[] = "$field";
}
$extras[] = "fields=" . implode(',', $names);
} else if($this->field) {
$extras[] = "field=$this->field";
}
}
if(strpos($url, './') === 0 || (strpos($url, '/') !== 0 && strpos($url, '../') !== 0)) {
if($this->requestLanguage && strpos($url, 'language=') === false) {
$extras[] = "language=$this->requestLanguage";
}
if($this->requestContext && preg_match('/\bid=' . $this->id . '\b/', $url)) {
$extras[] = "context=$this->requestContext";
}
}
if(count($extras)) {
$url .= strpos($url, '?') === false ? "?" : "&";
$url .= implode('&', $extras);
}
return $url;
}
/**
* Add a tab with HTML id attribute and label
*
* Label may contain markup, and thus you should entity encode text labels as appropriate.
*
* @param string $id
* @param string $label
*
*/
public function addTab($id, $label) {
$this->tabs[$id] = $label;
}
/**
* Remove the tab with the given id
*
* @param string $id
*
*/
public function removeTab($id) {
unset($this->tabs[$id]);
}
/**
* Returns associative array of tab ID => tab Label
*
* @return array
*
*/
public function ___getTabs() {
return $this->tabs;
}
/**
* Get allowed page statuses
*
* @return array Array of [ statusFlagInteger => 'Status flag label' ]
* @since 3.0.181
*
*/
public function getAllowedStatuses() {
$page = $this->page;
$config = $this->wire()->config;
$statuses = array();
$superuser = $this->user->isSuperuser();
if(!$this->page->template->noUnpublish && $this->page->publishable()) {
$statuses[Page::statusUnpublished] = $this->_('Unpublished: Not visible on site'); // Settings: Unpublished status checkbox label
}
if($this->user->hasPermission('page-hide', $this->page)) {
$statuses[Page::statusHidden] = $this->_('Hidden: Excluded from lists and searches'); // Settings: Hidden status checkbox label
}
if($this->user->hasPermission('page-lock', $this->page)) {
$statuses[Page::statusLocked] = $this->_('Locked: Not editable'); // Settings: Locked status checkbox label
}
if($superuser) {
$uniqueNote = ($this->wire('languages') ? ' ' . $this->_('(in default language only)') : '');
$statuses[Page::statusUnique] = sprintf($this->_('Unique: Require page name “%s” to be globally unique'), $this->page->name) . $uniqueNote;
}
if($superuser && $config->advanced) {
// additional statuses available to superuser in advanced mode
$hasSystem = $page->hasStatus(Page::statusSystem) || $page->hasStatus(Page::statusSystemID) || $page->hasStatus(Page::statusSystemOverride);
$statuses[Page::statusSystem] = "System: Non-deleteable and locked ID, name, template, parent (status not removeable without override)";
$statuses[Page::statusSystemID] = "System ID: Non-deleteable and locked ID (status not removeable without override)";
if($hasSystem) $statuses[Page::statusSystemOverride] = "System Override: Override (must be added temporarily in its own save before system status can be removed)";
$statuses[Page::statusDraft] = "Draft: Page has a separate draft version";
$statuses[Page::statusOn] = "On: Internal toggle when combined with other statuses (only for specific cases, otherwise ignored)";
/*
* Additional statuses that are possible but shouldn't be editable (uncomment temporarily if needed)
*
* $statuses[Page::statusTemp] = "Temp: Unpublished page more than 1 day old may be automatically deleted";
* $statuses[Page::statusFlagged] = "Flagged: Page is flagged as incomplete, needing review, or having some issue";
* $statuses[Page::statusTrash] = "Internal trash: Indicates that page is in the trash";
* $statuses[Page::statusReserved] = "Internal-reserved: Status reserved for future use";
* $statuses[Page::statusInternal] = "Internal-internal: Status for internal or future use";
*
*/
}
return $statuses;
}
/**
* Get PageBookmarks array
*
* @return PageBookmarks
*
*/
protected function getPageBookmarks() {
static $bookmarks = null;
if(is_null($bookmarks)) {
require_once(dirname(__FILE__) . '/PageBookmarks.php');
$bookmarks = $this->wire(new PageBookmarks($this));
}
return $bookmarks;
}
/**
* navJSON action
*
* @param array $options
* @return string
* @throws Wire404Exception
* @throws WireException
*
*/
public function ___executeNavJSON(array $options = array()) {
$bookmarks = $this->getPageBookmarks();
$options['edit'] = $this->wire('config')->urls->admin . 'page/edit/?id={id}';
$options['defaultIcon'] = 'pencil';
$options = $bookmarks->initNavJSON($options);
return parent::___executeNavJSON($options);
}
/**
* Bookmarks action
*
* @return string
*
*/
public function ___executeBookmarks() {
$bookmarks = $this->getPageBookmarks();
return $bookmarks->editBookmarks();
}
/**
* Set the headline used in the UI
*
*/
public function setupHeadline() {
$titlePage = null;
$page = $this->page;
if($page && $page->id) {
$title = $page->get('title');
if(is_object($title) && !strlen("$title") && wireInstanceOf($title, 'LanguagesPageFieldValue')) {
/** @var LanguagesPageFieldValue $title */
$title = $title->getNonEmptyValue($page->name);
} else {
$title = (string) $title;
}
if(empty($title)) {
if($this->wire('pages')->names()->isUntitledPageName($page->name)) {
$title = $page->template->getLabel();
} else {
$title = $page->get('name');
}
}
if(empty($title)) $title = $page->name;
} else if($this->parent && $this->parent->id) {
$titlePage = $this->parent;
$title = rtrim($this->parent->path, '/') . '/[...]';
} else {
$titlePage = new NullPage();
$title = '[...]';
}
$browserTitle = sprintf($this->_('Edit Page: %s'), $title);
$headline = '';
if($this->field) {
if(count($this->fields) == 1) {
$headline = $this->field->getLabel();
} else {
$labels = array();
foreach($this->fields as $field) {
$labels[] = $field->getLabel();
}
$headline = implode(', ', $labels);
}
$browserTitle .= " ($headline)";
} else if($titlePage) {
$headline = $titlePage->get('title|name');
}
if(empty($headline)) $headline = $title;
$this->headline($headline);
$this->browserTitle($browserTitle);
}
/**
* Setup the breadcrumbs used in the UI
*
*/
public function setupBreadcrumbs() {
if($this->input->urlSegment1) return;
if($this->wire('page')->process != $this->className()) return;
$this->wire('breadcrumbs')->shift(); // shift off the 'Admin' breadcrumb
if($this->page && $this->page->id != 1) $this->wire('breadcrumbs')->shift(); // shift off the 'Pages' breadcrumb
$page = $this->page ? $this->page : $this->parent;
if($this->masterPage) $page = $this->masterPage;
$lastID = (int) $this->session->get('ProcessPageList', 'lastID');
$editCrumbs = !empty($this->configSettings['editCrumbs']);
$numParents = $page->parents->count();
foreach($page->parents() as $cnt => $p) {
$url = $editCrumbs && $p->editable() ? "./?id=$p->id" : "../?open=$p->id";
if(!$editCrumbs && $cnt == $numParents-1 && $p->id == $lastID) $url = "../";
$this->breadcrumb($url, $p->get("title|name"));
}
if($this->page && $this->field) {
$this->breadcrumb("./?id={$this->page->id}", $page->get("title|name"));
}
}
/**
* Are we processing a submitted page edit form in this request?
*
* @return bool
* @since 3.0.170
*
*/
public function isSubmit() {
return $this->isPost;
}
/**
* URL to redirect to after non-authenticated user is logged-in, or false if module does not support
*
* @param Page $page
* @return bool|string
* @sine 3.0.167
*
*/
public static function getAfterLoginUrl(Page $page) {
$sanitizer = $page->wire()->sanitizer;
$input = $page->wire()->input;
if($input->urlSegmentStr === 'bookmarks') return $page->url . 'bookmarks/';
$qs = array(
'id' => (int) $input->get('id'),
'language' => (int) $input->get('language'),
'template' => (int) $input->get('template'), // used by executeTemplate() only
'uploadOnlyMode' => (int) $input->get('uploadOnlyMode'),
'fnsx' => $input->get->fieldName('fnsx'),
'context' => $input->get->name('context'),
'field' => $input->get->fieldName('field'),
'fields' => array(),
);
if($input->urlSegmentStr === 'template') {
return "$page->url?id=$qs[id]&template=$qs[template]";
}
if($input->get('fields')) {
foreach(explode(',', $input->get('fields')) as $value) {
$qs['fields'] .= ($qs['fields'] ? ',' : '') . $sanitizer->fieldName($value);
}
}
$a = array();
foreach($qs as $name => $value) {
if(!empty($value)) $a[] = "$name=$value";
}
return "$page->url?" . implode('&', $a);
}
/**
* Module config
*
* @param array $data
* @return InputfieldWrapper
* @throws WireException
*
*/
public function getModuleConfigInputfields(array $data) {
$config = $this->wire()->config;
$pages = $this->wire()->pages;
$modules = $this->wire()->modules;
$inputfields = new InputfieldWrapper();
$this->wire($inputfields);
/** @var InputfieldRadios $f */
$f = $modules->get('InputfieldRadios');
$f->name = 'viewAction';
$f->label = $this->_('Default "view" location/action');
$f->description = $this->_('The default type of action used when the "view" tab is clicked on in the page editor.');
$f->icon = 'eye';
foreach($this->getViewActions(array(), true) as $name => $label) {
$f->addOption($name, $label);
}
/** @var array $configData */
$configData = $config->pageEdit;
if(isset($data['viewAction'])) {
$f->attr('value', $data['viewAction']);
} else if(is_array($configData) && !empty($configData['viewNew'])) {
$f->attr('value', 'new');
} else {
$f->attr('value', 'this');
}
$inputfields->add($f);
$bookmarks = $this->getPageBookmarks();
$bookmarks->addConfigInputfields($inputfields);
$admin = $pages->get($config->adminRootPageID);
$page = $pages->get($admin->path . 'page/edit/');
$bookmarks->checkProcessPage($page);
return $inputfields;
}
}