'Page Edit', 'summary' => 'Edit a Page', 'version' => 112, '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, 'ajaxTemplate' => true, 'ajaxCreatedUser' => 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->location('./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 && ($this->input->requestMethod('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 = $languages->hasPageNames(); 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($this->_('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']) || $this->input->requestMethod('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 && $this->form->isSubmitted()) $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() { $config = $this->config; $class = ''; $numFields = count($this->fields); $out = "

{$this->page->id}

"; $description = $this->form->getSetting('description'); if($description) { $out .= "

" . $this->form->entityEncode($description, Inputfield::textFormatBasic) . "

"; $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)) { $file = $config->debug ? 'dropdown.js' : 'dropdown.min.js'; $config->scripts->add($config->urls('InputfieldSubmit') . $file); $input = ""; $out = str_replace('', "$input", $out); $out .= ""; } } if(!$numFields && !$this->requestModal && $this->page->viewable()) { // this supports code in the buildFormView() method $out .= ""; } $func = 'initPageEditForm();'; // to prevent IDE from flagging as unknown function $out .= "$func"; // 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) { $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 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] = " " . $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" => "$labels[panel]", "modal" => "$labels[modal]", "new" => "$labels[new]", "this" => "$labels[this]", ), $actions); foreach($actions as $name => $action) { if(count($languageUrls) > 1) { $ul = ""; $actions[$name] = str_replace('', '  ', $action) . $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 user’s 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) { $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) { /** @var Inputfield $inputfield */ if(!$this->page->editable($field->name, false)) continue; $skipCollapsed = array( Inputfield::collapsedHidden, Inputfield::collapsedNoLocked, Inputfield::collapsedBlankLocked, Inputfield::collapsedYesLocked, Inputfield::collapsedTabLocked, ); $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; $fieldsetTab = null; $collapsedTabTypes = array( Inputfield::collapsedTab => 1, Inputfield::collapsedTabAjax => 1, Inputfield::collapsedTabLocked => 1, ); // identify fields displayed as tabs and add fieldset open/close around them foreach($contentTab as $inputfield) { /** @var Inputfield $inputfield */ if(!isset($collapsedTabTypes[$inputfield->collapsed])) continue; /** @var InputfieldFieldsetTabOpen $tab */ if(!$fieldsetTab) { /** @var FieldtypeFieldsetTabOpen $fieldsetTab */ $fieldsetTab = $this->modules->get('FieldtypeFieldsetTabOpen'); $this->modules->get('FieldtypeFieldsetClose'); } $tab = new InputfieldFieldsetTabOpen(); $this->wire($tab); $tab->attr('name', '_tab_' . $inputfield->attr('name')); $tab->attr('id', '_tab_' . $inputfield->attr('id')); $tab->label = $inputfield->getSetting('tabLabel|label'); if($inputfield->collapsed === Inputfield::collapsedTabAjax) { $tab->collapsed = Inputfield::collapsedYesAjax; $inputfield->collapsed = Inputfield::collapsedNo; if($this->isPost && !$contentTab->isProcessable($tab)) { $contentTab->remove($inputfield); continue; } } $contentTab->insertBefore($tab, $inputfield); $tabClose = new InputfieldFieldsetClose(); $this->wire($tabClose); $tabClose->attr('id+name', $tab->attr('name') . '_END'); $contentTab->insertAfter($tabClose, $inputfield); } foreach($contentTab as $inputfield) { /** @var Inputfield $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; /** @var InputfieldWrapper $tabWrap */ $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->getSetting('modal')) { $href = $this->getEditUrl(array('field' => $inputfield->name, 'modal' => 1)); $this->addTab($tabOpen->id, "" . $this->sanitizer->entities1($tabOpen->label) . "", $tabWrap ); /** @var JqueryUI $jqueryUI */ $jqueryUI = $this->modules->get('JqueryUI'); $jqueryUI->use('modal'); $tabOpen = null; } else { $this->addTab($tabOpen->id, $this->sanitizer->entities1($tabOpen->label), $tabWrap); } } 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); $this->addTab($id, $title, $fields); 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()); /** @var InputfieldWrapper $wrapper */ $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', "$title"); } else { $wrapper->attr('title', $title); } $this->addTab($id, $title, $wrapper); $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) { $modules = $caller->wire()->modules; /** @var InputfieldFieldset $fieldset */ $fieldset = $modules->get("InputfieldFieldset"); if(!$sortfield) $fieldset->collapsed = Inputfield::collapsedYes; /** @var InputfieldSelect $field */ $field = $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(); $fields = $caller->wire()->fields; $fieldsInfo = $fields->getAllValues(array('flags', 'type'), 'name'); foreach($fieldsInfo as $name => $f) { //if(!($f->flags & Field::flagAutojoin)) continue; if($f['flags'] & Field::flagSystem && $name != 'title' && $name != 'email') continue; if(wireInstanceOf($f['type'], 'FieldtypeFieldsetOpen')) continue; $customOptions[$name] = $name; } ksort($customOptions); $field->addOption(__('Custom Fields', __FILE__), $customOptions); // Optgroup label for sorting by custom fields $fieldset->append($field); /** @var InputfieldCheckbox $f */ $f = $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() { $user = $this->wire()->user; $superuser = $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, $wrapper); // 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 || $user->hasPermission('page-edit-redirects', $this->page)) { $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)) { $languages = $this->wire()->languages; $language = $this->user->language; /** @var Language|null $language */ $input = $this->wire()->input; $ajax = $this->configSettings['ajaxTemplate']; /** @var InputfieldSelect $field */ $field = $this->modules->get('InputfieldSelect'); $field->attr('id+name', 'template'); $field->attr('value', $this->page->template->id); $field->required = true; $field->collapsed = Inputfield::collapsedYesAjax; if(!$ajax || $input->get('renderInputfieldAjax') === 'template' || $input->post('template') !== null) { 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', "

" . $this->wire()->sanitizer->entities1($this->page->template->getLabel()) . "

"); } $field->label = $this->_('Template') . ' (' . $this->page->template->getLabel() . ')'; // 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 * * Hookable as of 3.0.194 * * #pw-hooker * * @return Inputfield * */ protected function ___buildFormCreatedUser() { $templates = $this->wire()->templates; $modules = $this->wire()->modules; $config = $this->wire()->config; $pages = $this->wire()->pages; $selector = "parent_id=$config->usersPageID, include=all, limit=100"; $usersPageIDs = $config->usersPageIDs; $createdUser = $this->page->createdUser; if(count($usersPageIDs) < 2 && $pages->count($selector) < 100) { /** @var InputfieldPageListSelect $f */ $f = $modules->get('InputfieldPageListSelect'); $f->parent_id = $config->usersPageID; $f->showPath = false; $f->labelFieldName = 'name'; } else { $searchFields = array('name', 'email'); $selector = "include=all, parent_id=" . implode('|', $usersPageIDs); if($createdUser->id != $config->guestUserPageID) $selector .= ", id!=$config->guestUserPageID"; $userTemplate = null; foreach($config->userTemplateIDs as $templateID) { $template = $templates->get((int) $templateID); if(!$userTemplate && $template->id === $config->userTemplateID) { $userTemplate = $template; } if($template && $template->hasField('title')) { $searchFields[] = 'title'; break; } } /** @var InputfieldPageAutocomplete $f */ $f = $modules->get('InputfieldPageAutocomplete'); $f->labelFieldName = 'name'; if($userTemplate && $userTemplate->pageLabelField) { $f->labelFieldFormat = $userTemplate->pageLabelField; } else { $f->labelFieldFormat = '{name} ({email}) [{roles.name}]'; } $f->searchFields = implode(' ', $searchFields); $f->maxSelectedItems = 1; $f->findPagesSelector = $selector; $f->template_ids = $config->userTemplateIDs; $f->required = true; $f->useList = false; $f->addClass('InputfieldNoFocus', 'wrapClass'); } $f->val($createdUser); $f->label = $this->_('Created by User') . ($createdUser->id ? " ($createdUser->name)" : ""); $f->icon = 'user-circle'; $f->attr('id+name', 'created_users_id'); if($this->configSettings['ajaxCreatedUser']) $f->collapsed = Inputfield::collapsedYesAjax; $f->required = true; return $f; } /** * 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 = ""; if($numNotShown) { $out .= "
" . sprintf($this->_('%d additional pages not shown.'), $numNotShown) . "
"; } } else { $out = "

" . $this->_('Did not find any other pages pointing to this one in page fields or href links.') . "

"; } $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; $pages = $this->wire()->pages; 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 && $languages->hasPageNames(); $slashUrls = $this->page->template->slashUrls; $deleteIDs = array(); $rootUrl = $this->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 && $this->form->isSubmitted('_prevpath_delete')) { $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[] = ' '; if(!$multilang) { $row = array( $sanitizer->entities($this->page->path), $this->_x('Current', 'prev-path-current'), ' ', ); $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( "$path", 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 = "$langLabel"; $row[] = $langLabel; } else { $row[] = '?'; } } if(empty($item['virtual'])) { $delete->attr('name', '_prevpath_delete[]'); $delete->attr('value', $id); $row[] = "
" . $delete->render() . "
"; } else { $parentLabel = $this->_x('Parent', 'prev-path-parent'); $parent = $pages->get((int) $item['virtual']); if($parent->id) $parentLabel = "$parentLabel"; $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 && $this->form->isSubmitted('_prevpath_add')) { $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 = "$createdName"; if($modifiedName != $unknown && $page->modifiedUser instanceof User) $modifiedName = "$modifiedName"; } $lowestDate = strtotime('1974-10-10'); $createdDate = $page->created > $lowestDate ? date($dateFormat, $page->created) . " " . "(" . wireRelativeTimeStr($page->created) . ")" : $unknown; $modifiedDate = $page->modified > $lowestDate ? date($dateFormat, $page->modified) . " " . "(" . wireRelativeTimeStr($page->modified) . ")" : $unknown; $publishedDate = $page->published > $lowestDate ? date($dateFormat, $page->published) . " " . "(" . wireRelativeTimeStr($page->published) . ")" : $unknown; $info = "\n

" . sprintf($this->_('Created by %1$s on %2$s'), $createdName, $createdDate) . "
" . // Settings: created user/date information line sprintf($this->_('Last modified by %1$s on %2$s'), $modifiedName, $modifiedDate) . "
" . // Settings: modified user/date information line sprintf($this->_('Published on %s'), $publishedDate) . // Settings: published information line "

"; $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() { /** @var InputfieldWrapper $wrapper */ $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, $wrapper); /** @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())) { /** @var InputfieldButton $field */ $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, $wrapper); /** @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 = "$label" . "" . ""; $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) { /** @var Role $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 page’s 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() { $input = $this->wire()->input; $pages = $this->wire()->pages; $page = $this->page; $user = $this->user; $form = $this->form; if($page->hasStatus(Page::statusLocked)) { $inputStatus = $input->post('status'); if(!$user->hasPermission('page-lock', $page) || (is_array($inputStatus) && in_array(Page::statusLocked, $inputStatus))) { $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($page->hasStatus(Page::statusTemp)) $page->removeStatus(Page::statusTemp); if($form->isSubmitted('submit_delete')) { if(((int) $input->post('delete_page')) === $this->page->id) $this->deletePage(); } else { $this->processInput($form); $changes = array_unique($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($form->getErrors())) { // add flagged status when form had errors $page->addStatus(Page::statusFlagged); } else if($page->hasStatus(Page::statusFlagged)) { // if no errors, remove incomplete status $page->removeStatus(Page::statusFlagged); $this->message($this->_('Removed flagged status because no errors reported during save')); } $isUnpublished = $page->hasStatus(Page::statusUnpublished); if($input->post('submit_publish') || $input->post('submit_save')) { try { $options = array(); $name = ''; if($page->isChanged('name')) { if(!strlen($page->name) && $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 $page->name = $page->namePrevious; } else { $name = $page->name; } $options['adjustName'] = true; } $numChanges = $numChanges > 0 ? ' (' . sprintf($this->_n('%d change', '%d changes', $numChanges) . ')', $numChanges) : ''; if($input->post('submit_publish') && $isUnpublished && $this->page->publishable() && !$formErrors) { $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 && $input->post('submit_publish')) { $message .= ' - ' . $this->_('Cannot be published until errors are corrected'); } } $restored = false; if($input->post('restore_page') && $page->isTrash() && $page->restorable()) { if($formErrors) { $this->warning($this->_('Page cannot be restored while errors are present')); } else if($pages->restore($page, false)) { $message = sprintf($this->_('Restored Page: %s'), '{path}') . $numChanges; $restored = true; } else { $this->warning($this->_('Error restoring page')); } } $pages->save($page, $options); if($restored) $pages->restored($page); $message = str_replace('{path}', $page->path, $message); $this->message($message); if($name && $name != $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 = $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->location($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) { $input = $this->wire()->input; $languages = $this->wire()->languages; $page = $this->page; static $skipFields = array( 'sortfield_reverse', 'submit_publish', 'submit_save', 'delete_page', ); if(!$level) { $form->processInput($input->post); $formRoot = $form; $page->setQuietly('_forceAddStatus', 0); } $errorAction = (int) $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(!$page->editable($name, false)) { $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 $page"); continue; } } if($name == 'status' && $this->processInputStatus($inputfield)) continue; } if($this->processInputErrorAction($page, $inputfield, $name, $errorAction)) continue; if($name && $inputfield->isChanged()) { 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->val()); } } else { $page->set($name, $inputfield->val()); } } if($inputfield instanceof InputfieldWrapper && count($inputfield->getChildren())) { $this->processInput($inputfield, $level + 1, $formRoot); } } if(!$level) { $forceAddStatus = $page->get('_forceAddStatus'); if($forceAddStatus && !$page->hasStatus($forceAddStatus)) { $page->addStatus($forceAddStatus); } } } /** * Process required error actions as configured with page’s 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) { $page = $this->page; if(!$this->user->isSuperuser() || !$page->id || !$page->template->allowChangeUser) return; $userID = $inputfield->val(); if(is_array($userID)) $userID = reset($userID); if($userID instanceof PageArray) $userID = $userID->first(); if($userID instanceof Page) $userID = $userID->id; $userID = (int) $userID; if(!$userID) return; if($userID == $this->page->created_users_id) return; // no change $user = $this->pages->get($userID); if(!$user->id) return; 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 $page->created_users_id = $user->id; $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->val()); 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->val()); 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->wire()->session->CSRF->validate(); // throws exception when invalid /** @var InputfieldWrapper $form */ $form = $this->wire(new InputfieldWrapper()); $form->useDependencies = false; $keys = array(); $error = ''; $message = ''; if(isset($_SERVER['HTTP_X_FIELDNAME'])) { $keys[] = $this->sanitizer->fieldName($_SERVER['HTTP_X_FIELDNAME']); } else if(count($this->fields)) { $keys = array_keys($this->fields); } 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); $pageClass = $page->className(); $numFields = 0; $lastFieldName = null; $savedNames = array(); $saved = false; // was page saved? $languages = $this->wire()->languages; $changes = array(); foreach($form->children() as $inputfield) { /** @var Inputfield $inputfield */ $name = $inputfield->attr('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->val()); } } else { $page->set($name, $inputfield->val()); } $numFields++; $lastFieldName = $inputfield->name; $savedNames[] = $lastFieldName; if($inputfield instanceof InputfieldFile) $page->trackChange($name); } if($page->isChanged()) { $changes = $page->getChanges(); if($numFields === 1) { if($page->save((string) $lastFieldName)) { $saved = true; $message = "Ajax saved $pageClass $page->id field: $lastFieldName"; } else { $error = "Ajax error saving $pageClass $page->id field: $lastFieldName"; } } else { if($page->save()) { $saved = true; $message = "Ajax saved $pageClass $page->id fields: " . implode(', ', $savedNames); } else { $error = "Ajax error saving $pageClass $page->id fields: " . implode(', ', $savedNames); } } } else { $message = "Ajax $pageClass $page->id not saved (no changes)"; $savedNames = array(); } $data = array( 'fields' => $savedNames, 'changes' => $changes, 'error' => strlen($error) > 0, 'message' => ($error ? $error : $message), 'saved' => $saved, ); if(!$this->ajaxSaveDone($page, $data)) { if($message) $this->message($message); if($error) $this->error($error); } } /** * Ajax save done - send output * * When a hook overrides this, it should hook after and set `$event->return = true;` * to indicate that it has handled the output. * ~~~~~ * $wire->addHookAfter('ProcessPageEdit::ajaxSaveDone', function($event) { * if($event->return === true) return; // another hook already handled output * $page = $event->arguments(0); // Page * $data = $event->arguments(1); // array * $data['page'] = $page->id; * header('Content-Type', 'application/json'); * echo json_encode($data); * $event->return = true; // tell ProcessPageEdit we handled output * }); * ~~~~~ * * #pw-hooker * * @param Page $page * @param array $data * @return bool Return true if hook has handled output, false if not (default) * @since 3.0.188 * */ protected function ___ajaxSaveDone(Page $page, array $data) { return false; } /*************************************************************************************************************** * OTHER ACTIONS * */ /** * Build template form * * @param Template $template Proposed template to change to * @return InputfieldForm * @throws WireException * @since 3.0.205 * */ protected function buildTemplateForm(Template $template) { if($this->page->template->noChangeTemplate) { throw new WireException("Template changes not allowed by pages using template: $template"); } 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."); } 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->name); // 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) { /** @var Field $field */ if(!$template->fieldgroup->has($field)) { $list[] = $this->sanitizer->entities($field->getLabel()) . " ($field->name)"; } } if(count($list)) { $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 = wireIconMarkup('times-circle'); $f->attr('value', "

$icon " . implode("
$icon ", $list) . '

'); $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'; if(count($list)) $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"); $f->attr('name', 'submit_change_template'); $form->append($f); return $form; } /** * Execute a template change for a page, building an info + confirmation form (handler for /template/ action) * * @return string * @throws WireException * */ public function ___executeTemplate() { $page = $this->page; $editUrl = "./?id=$page->id"; $templateId = (int) $this->input->get('template'); try { if($templateId < 1) throw new WireException("Missing a 'template' GET variable"); $template = $this->templates->get($templateId); if(!$template) throw new WireException("Unknown template"); $form = $this->buildTemplateForm($template); } catch(\Exception $e) { $this->error($e->getMessage()); $this->session->location($editUrl); return ''; } $this->breadcrumb($editUrl, $page->get("title|name")); return $form->render(); } /** * Save a template change for a page (handler for /saveTemplate/ action) * * @throws WireException * */ public function ___executeSaveTemplate() { $page = $this->page; $editUrl = "./?id=$page->id"; $templateId = (int) $this->input->post('template'); $template = $templateId > 0 ? $this->templates->get($templateId) : null; if(!$template) { // checkbox not checked, template change aborted $this->session->location($editUrl); return; } try { $form = $this->buildTemplateForm($template); } catch(\Exception $e) { $this->error($e->getMessage()); $form = null; } if(!$form || !$form->isSubmitted('submit_change_template')) { $this->session->location($editUrl); return; } try { $page->template = $template; $page->save(); $this->message(sprintf($this->_("Changed template to '%s'"), $template->name)); // Message: template was changed } catch(\Exception $e) { $this->error($e->getMessage()); } $this->session->location($editUrl); } /** * 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()); $config = $this->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; // note: this triggers load of all templates, fieldgroups and fields 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 * */ public function isAllowedTemplate($id) { // if $id is a template, then convert it to it's numeric ID if($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 = (string) $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 * @param InputfieldWrapper|null $wrapper * */ public function addTab($id, $label, $wrapper = null) { $this->tabs[$id] = $label; if($wrapper) $wrapper->addClass('WireTab'); } /** * 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->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->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 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); } $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; } }