artabro/wire/modules/Process/ProcessPageAdd/ProcessPageAdd.module
2024-08-27 11:35:37 +02:00

1514 lines
46 KiB
Text
Raw Permalink Blame History

This file contains ambiguous Unicode characters

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

<?php namespace ProcessWire;
/**
* ProcessWire Page Add Process
*
* Provides the UI for adding a page
*
* For more details about how Process modules work, please see:
* /wire/core/Process.php
*
* ProcessWire 3.x, Copyright 2023 by Ryan Cramer
* https://processwire.com
*
* @method string executeTemplate()
* @method bool processQuickAdd(Page $parent, Template $template)
* @method InputfieldForm buildForm()
* @method bool processInput(InputfieldForm $form)
* @method array getAllowedTemplates($parent = null)
* @method string nameChangedWarning(Page $page, $namePrevious)
*
* @property bool|int $noAutoPublish Disable automatic publishing?
* @property-write Template $template
* @property-write int $parent_id
*
*/
class ProcessPageAdd extends Process implements ConfigurableModule, WirePageEditor {
/**
* @var InputfieldForm
*
*/
protected $form;
/**
* @var Page|null
*
*/
protected $parent = null;
/**
* @var int
*
*/
protected $parent_id = 0;
/**
* @var Page
*
*/
protected $page;
/**
* @var Template|null
*
*/
protected $template = null;
/**
* @var array|null
*
*/
protected $allowedTemplates = null; //cache
/**
* @var array
*
*/
protected $predefinedTemplates = array();
/**
* @var PageArray|array
*
*/
protected $predefinedParents = array();
/**
* @var WirePageEditor|ProcessPageAdd
*
*/
protected $editor; // WirePageEditor
/**
* Settings that may be specified with $config->pageAdd array
*
* @var array
*
*/
protected $settings = array(
'noSuggestTemplates' => '', // Disable suggestions: 1|true=disable all, or space-separated template names
);
/**
* @return array
*
*/
public static function getModuleInfo() {
return array(
'title' => __('Page Add', __FILE__),
'summary' => __('Add a new page', __FILE__),
'version' => 109,
'permanent' => true,
'permission' => 'page-edit',
'icon' => 'plus-circle',
'useNavJSON' => true,
);
}
/**
* Construct and populate default config
*
*/
public function __construct() {
$this->editor = $this;
parent::__construct();
$this->set('noAutoPublish', false);
$this->set('shortcutSort', array());
$settings = $this->wire()->config->pageAdd;
if(is_array($settings)) $this->settings = array_merge($this->settings, $settings);
}
/**
* Module init
*
*/
public function init() {
$this->page = null;
parent::init();
}
/**
* Set property
*
* @param string $key
* @param mixed $value
* @return Process|ProcessPageAdd
*
*/
public function set($key, $value) {
if($key === 'parent_id') {
$this->parent_id = (int) "$value";
} else if($key === 'template') {
if(is_string($value) || is_int($value)) $value = $this->wire()->templates->get($value);
if($value instanceof Template) $this->template = $value;
} else {
return parent::set($key, $value);
}
return $this;
}
/**
* Return list of addable templates and links to add them
*
* Returns JSON by default, specify $options['getArray'] = true; to return an array.
*
* @param array $options
* @return array|string
*
*/
public function ___executeNavJSON(array $options = array()) {
$page = $this->wire()->page;
$user = $this->wire()->user;
$config = $this->wire()->config;
$session = $this->wire()->session;
$modules = $this->wire()->modules;
$templates = $this->wire()->templates;
$sanitizer = $this->wire()->sanitizer;
$data = $session->getFor($this, 'nav');
if(empty($data)) {
$data = array(
'url' => $config->urls->admin . 'page/add/',
'label' => $this->_((string) $page->get('title|name')),
'icon' => 'plus-circle',
'add' => null,
'list' => array(),
'modified' => time(),
);
$items = array();
if(!$user->isGuest() && $user->hasPermission('page-edit')) {
foreach($templates as $template) {
$parent = $template->getParentPage(true);
if(!$parent) continue;
if($parent->id) {
// one parent possible
if(!$this->isAllowedParent($parent, false, $template)) continue; // double check
$qs = "?parent_id=$parent->id&template_id=$template->id";
} else {
// multiple parents possible
$qs = "?template_id=$template->id";
}
$icon = $template->getIcon();
if(!$icon) $icon = "plus-circle";
$label = $template->getLabel();
$key = strtolower($label);
$label = $sanitizer->entities1($label);
if(isset($items[$key])) $key .= $template->name;
$items[$key] = array(
'url' => $qs,
'label' => $label,
'icon' => $icon,
'parent_id' => $parent->id, // for internal use only
'template_id' => $template->id, // for internal use only
);
}
ksort($items);
$configData = $modules->getConfig($this); // because admin theme calls with noInit option
$shortcutSort = isset($configData['shortcutSort']) ? $configData['shortcutSort'] : array();
if(!is_array($shortcutSort)) $shortcutSort = array();
if(!empty($shortcutSort)) {
$sorted = array();
foreach($shortcutSort as $templateID) {
foreach($items as $key => $item) {
if($item['template_id'] == $templateID) {
$sorted[$key] = $item;
break;
}
}
}
foreach($items as $key => $item) {
if(!isset($sorted[$key])) $sorted[$key] = $item;
}
$items = $sorted;
}
}
$data['list'] = array_values($items);
$session->setFor($this, 'nav', $data);
}
unset($data['modified']);
// get additional from PageBookmarks
$bookmarks = $this->getPageBookmarks();
$options2 = $bookmarks->initNavJSON(array('add' => 'ignore-me'));
$lastItem = null;
$listLength = count($data['list']);
$n = 0;
foreach($options2['items'] as $p) {
/** @var Page $p */
if($p->id == 'bookmark' && !$user->isSuperuser()) continue;
$item = array(
'url' => ($p->id == 'bookmark' ? 'bookmarks/?role=0' : "?parent_id=$p->id"),
'label' => $p->get('title|name') . ($p instanceof Page ? ' &hellip;' : ''),
'icon' => $p->get('_icon') ? $p->get('_icon') : 'arrow-circle-right',
'className' => $p->get('_class') . (!$n ? ' separator' : '')
);
if($p->id == 'bookmark') {
$lastItem = $item;
} else {
$n++;
$data['list'][] = $item;
}
}
if($lastItem) $data['list'][] = $lastItem;
$session->setFor($this, 'numAddable', $listLength + $n);
if(!empty($options['getArray'])) return $data;
if($config->ajax) header("Content-Type: application/json");
return json_encode($data);
}
/**
* Clear "add new" session caches
*
* @since 3.0.194
*
*/
public function clearSessionCaches() {
$session = $this->wire()->session;
$session->removeFor($this, 'nav');
$session->removeFor($this, 'numAddable');
$this->message("Clearing 'Add New' page cache", Notice::debug);
}
/**
* Ask user to select template and parent
*
* @return string
* @throws WireException
*
*/
public function ___executeTemplate() {
$modules = $this->wire()->modules;
$pages = $this->wire()->pages;
$input = $this->wire()->input;
$templateID = (int) $this->input->get('template_id');
if(!$templateID) throw new WireException("No template specified");
$template = $this->templates->get($templateID);
if(!$template) throw new WireException("Unknown template");
$parentTemplates = new TemplatesArray();
foreach($template->parentTemplates as $templateID) {
$t = $this->templates->get((int) $templateID);
if($t) $parentTemplates->add($t);
}
if(!count($parentTemplates)) throw new WireException("No parent templates defined for $template->name");
$parentTemplateIDs = $parentTemplates->implode('|', 'id');
$parents = $pages->find("templates_id=$parentTemplateIDs, include=hidden, limit=500, sort=name");
if(!count($parents)) throw new WireException("No usable parents match this template");
if(count($parents) == 1) {
$url = "./?parent_id=" . $parents->first()->id;
if($input->get('modal')) $url .= "&modal=1";
$this->redirect($url);
}
$templateLabel = $this->getTemplateLabel($template);
/** @var InputfieldForm $form */
$form = $modules->get('InputfieldForm');
$form->description = $this->getTemplateLabel($template);
$form->method = 'get';
$form->action = './';
$form->attr('id', 'select_parent_form');
if($input->get('modal')) {
/** @var InputfieldHidden $f */
$f = $modules->get('InputfieldHidden');
$f->attr('name', 'modal');
$f->attr('value', 1);
$form->add($f);
}
/** @var InputfieldSelect $f */
$f = $modules->get('InputfieldSelect');
$f->attr('name', 'parent_id');
$f->attr('id', 'select_parent_id');
$f->label = sprintf($this->_('Where do you want to add the new %s?'), "“{$templateLabel}”");
$f->description = sprintf($this->_('Please select a parent %s page below:'), ''); // Select parent label // you can omit the '%s' (no longer used)
$options = array();
foreach($parents as $parent) {
if(!$parent->addable()) continue;
$key = $parent->parent->title ? $parent->parent->title . " - " . $parent->parent->path : $parent->parent->path;
if(!isset($options[$key])) $options[$key] = array();
$options[$key][$parent->id] = $parent->get('title|name');
}
ksort($options);
foreach($options as $optgroupLabel => $optgroup) {
$f->addOption($optgroupLabel, $optgroup);
}
$form->add($f);
/** @var InputfieldHidden $f */
$f = $modules->get('InputfieldHidden');
$f->attr('name', 'template_id');
$f->attr('value', $template->id);
$form->add($f);
/** @var InputfieldSubmit $f */
$f = $modules->get('InputfieldSubmit');
$f->attr('id', 'select_parent_submit');
$form->add($f);
return $form->render();
}
/**
* Render an HTML definition list template selection for when no parent/template is known
*
* @return string
*
*/
public function renderChooseTemplate() {
$pages = $this->wire()->pages;
$templates = $this->wire()->templates;
/** @var array $data */
$data = $this->executeNavJSON(array('getArray' => true));
$out = '';
$bookmarkItem = null;
$rightIcon = "<span class='ui-priority-secondary'>" . wireIconMarkup('angle-right', 'fw') . "</span>";
foreach($data['list'] as $item) {
if(strpos($item['url'], '?role=0') !== false) {
$bookmarkItem = $item;
continue;
}
if(!empty($item['parent_id'])) {
$parents = $pages->find("id=$item[parent_id]");
} else if(!empty($item['template_id'])) {
$template = $templates->get($item['template_id']);
$parentTemplates = implode('|', $template->parentTemplates);
if(empty($parentTemplates)) continue;
$parents = $pages->find("template=$parentTemplates, include=unpublished, limit=100, sort=-modified");
} else {
$parents = array();
}
$icon = wireIconMarkup($item['icon'], 'fw');
$out .= "<dt><a class='label' href='./$item[url]'>$icon $item[label]</a></dt><dd>";
if(count($parents)) {
$out .= "<ul>";
foreach($parents as $parent) {
$url = $item['url'];
if(strpos($url, 'parent_id') === false) $url .= "&parent_id=$parent->id";
$out .= "<li><a href='./$url'>";
$parentParents = $parent->parents()->and($parent);
foreach($parentParents as $p) {
/** @var Page $p */
if($p->id == 1 && $parentParents->count() > 1) continue;
$out .= $p->title . $rightIcon;
}
$out .= "</a></li>";
}
$out .= "</ul>";
}
$out .= "</dd>";
}
if($out) {
$out = "<dl class='nav'>$out</dl>";
} else {
// Text shown when no templates use family settings
$out =
"<h2>" .
$this->_('There are currently no templates with defined parent/child relationships needed to show “Add New” shortcuts here.') . ' ' .
$this->_('To configure this, edit any template (Setup > Templates) and click on the “Family” tab.') .
"</h2>";
}
if($bookmarkItem) {
/** @var InputfieldButton $button */
$button = $this->wire()->modules->get('InputfieldButton');
$button->href = $bookmarkItem['url'];
$button->value = $bookmarkItem['label'];
$button->showInHeader();
$button->icon = $bookmarkItem['icon'];
$out .= $button->render();
}
return $out;
}
/**
* Method to handle AJAX call to check of a given page name exists for a parent
*
* Returns error or OK message in HTML
*
* @return string
*
*/
public function executeExists() {
$pages = $this->wire()->pages;
$input = $this->wire()->input;
$sanitizer = $this->wire()->sanitizer;
$parentID = (int) $input->get('parent_id');
if(!$parentID) return '';
$parent = $pages->get($parentID);
if(!$parent->addable()) return '';
$name = $sanitizer->pageNameUTF8($input->get('name'));
if(!strlen($name)) return '';
$parentID = count($this->predefinedParents) ? $this->predefinedParents : $parentID;
$test = new Page();
$test->parent_id = $parentID;
$test->name = $name;
$reason = $pages->names()->pageNameHasConflict($test);
if($reason) {
$out = "<span class='taken ui-state-error-text'>" . wireIconMarkup('exclamation-triangle') . " $reason</span>";
} else {
$out = "<span class='ui-priority-secondary'>" . wireIconMarkup('check-square-o') . ' ' . $this->_('Ok') . "</span>";
}
return $out;
}
/**
* Main execution, first screen of adding a Page
*
* @return string
* @throws Wire404Exception
* @throws WireException
*
*/
public function ___execute() {
$input = $this->wire()->input;
$config = $this->wire()->config;
$this->headline($this->_('Add New')); // Headline
if(!$this->parent_id) {
if($input->post('parent_id')) {
$this->parent_id = (int) $input->post('parent_id');
} else if($input->get('parent_id')) {
$this->parent_id = (int) $input->get('parent_id');
}
}
if($input->get('template_id') && !$this->parent_id) {
return $this->executeTemplate();
}
$template_id = (int) $input->post('template'); // note POST uses 'template' and GET uses 'template_id'
if(!$template_id) $template_id = (int) $input->get('template_id');
if($template_id) $this->template = $this->wire()->templates->get($template_id);
if(!$this->parent_id && count($this->predefinedParents)) {
$this->parent_id = $this->predefinedParents->first()->id;
}
if(!$this->parent_id) return $this->renderChooseTemplate();
$this->parent = $this->wire()->pages->get((int) $this->parent_id);
if(!$this->parent->id) {
throw new Wire404Exception("Unable to load parent page $this->parent_id", Wire404Exception::codeSecondary);
}
if(!$this->isAllowedParent($this->parent, true, $this->template)) {
throw new WireException($this->errors('string'));
}
// if page has a custom ProcessPageType for editing/adding then redirect to its add URL instead
if($this->parent->template->name === 'admin' && $this->wire()->page->id != $this->parent->id) {
$process = $this->parent->process;
if($process && wireInstanceOf($process, 'ProcessPageType')) {
$this->redirect($this->parent->url . 'add/');
}
}
// the following does the same as the block of code above for a custom ProcessPageType
// but is necessary for the case of when custom $config->usersPageIDs is in use since
// the parent template may not be 'admin' and user pages may be outside of admin structure
if(in_array($this->parent_id, $config->usersPageIDs) && $this->wire()->process != 'ProcessUser') {
$url = $config->urls->admin . "access/users/add/?parent_id=$this->parent_id";
if($template_id && in_array($template_id, $config->userTemplateIDs)) $url .= "&template_id=$template_id";
$this->redirect($url);
}
if(count($this->parent->template->childTemplates) == 1) {
// only one type of template is allowed for the parent
$childTemplates = $this->parent->template->childTemplates;
$template = $this->wire()->templates->get(reset($childTemplates));
if($this->template && $template->id != $this->template->id) {
throw new WireException("Template $template is required for parent {$this->parent->path}");
}
$this->template = $template;
if(!$this->isAllowedTemplate($this->template, $this->parent)) {
throw new WireException("You don't have access to the template required to add pages here");
}
} else if($this->template) {
// initial request specifying a template id
if(!$this->isAllowedTemplate($this->template, $this->parent)) {
throw new WireException("Template {$this->template->name} is not allowed here ({$this->parent->path})");
}
}
if($this->template && $this->parent) {
// determine whether quick-add can be used
if(strlen($this->parent->template->childNameFormat) || $input->get('name_format')) {
$this->processQuickAdd($this->parent, $this->template);
}
}
$this->form = $this->buildForm();
$this->form->setTrackChanges();
if($input->post('submit_save') || $input->post('submit_publish') || $input->post('submit_publish_add')) {
if($this->processInput($this->form)) {
// redirect occurs within processInput
} else {
// errors occurred during process input, re-render form
}
}
$this->setupBreadcrumbs();
return $this->form->render();
}
/**
* Returns an array of templates that are allowed to be used here
*
* @param Page|null $parent
* @return array
*
*/
protected function ___getAllowedTemplates($parent = null) {
$config = $this->wire()->config;
if(is_null($parent)) $parent = $this->parent;
if(!$parent) return array();
if(is_array($this->allowedTemplates)) return $this->allowedTemplates;
$user = $this->wire()->user;
$templates = array();
$allTemplates = count($this->predefinedTemplates) ? $this->predefinedTemplates : $this->wire()->templates;
$allParents = $this->getAllowedParents();
$usersPageIDs = $config->usersPageIDs;
$userTemplateIDs = $config->userTemplateIDs;
if($parent->hasStatus(Page::statusUnpublished)) {
$parentEditable = $parent->editable();
} else {
// temporarily put the parent in an unpublished status so that we can check it from
// the proper context: when page-publish permission exists, a page not not editable
// if a user doesn't have page-publish permission to it, even though it may still
// be editable if it was unpublished.
$parent->addStatus(Page::statusUnpublished);
$parentEditable = $parent->editable();
$parent->removeStatus(Page::statusUnpublished);
}
foreach($allTemplates as $t) {
if($t->noParents == -1) {
// only 1 of this type allowed
if($t->getNumPages() > 0) continue;
} else if($t->noParents) {
continue;
}
if($t->useRoles && !$user->hasPermission('page-create', $t)) continue;
if(!$t->useRoles && !$parentEditable) continue;
if(!$t->useRoles && !$user->hasPermission('page-create', $parent)) continue;
if(count($allParents) == 1) {
if(count($parent->template->childTemplates)) {
// check that this template is allowed by the defined parent
if(!in_array($t->id, $parent->template->childTemplates)) continue;
}
}
if(count($t->parentTemplates)) {
// this template is only allowed for certain parents
$allow = false;
foreach($allParents as $_parent) {
if(in_array($_parent->template->id, $t->parentTemplates)) {
$allow = true;
break;
}
}
if(!$allow) continue;
}
if(in_array($t->id, $userTemplateIDs)) {
// this is a user template: allow any parents defined in $config->usersPageIDs
$allow = false;
foreach($allParents as $_parent) {
if(in_array($_parent->id, $usersPageIDs)) {
$allow = true;
break;
}
}
if(!$allow) continue;
} else if($t->name == 'role' && $parent->id != $config->rolesPageID) {
// only allow role templates below rolesPageID
continue;
} else if($t->name == 'permission' && $parent->id != $config->permissionsPageID) {
// only allow permission templates below permissionsPageID
continue;
}
$templates[$t->id] = $t;
}
if($this->template || count($this->predefinedTemplates)) {
$predefinedTemplates = count($this->predefinedTemplates) ? $this->predefinedTemplates : array($this->template);
foreach($predefinedTemplates as $t) {
$isUserTemplate = in_array($t->id, $userTemplateIDs);
if($isUserTemplate && !isset($templates[$t->id]) && $user->hasPermission('user-admin')) {
// account for the unique situation of user-admin permission
// where all user-based templates are allowed
$templates[$t->id] = $t;
}
}
}
$this->allowedTemplates = $templates;
return $templates;
}
/**
* Is the given template or template ID allowed here?
*
* @param Template|int Template ID or object
* @param Page $parent Optionally parent page to filter by
* @return bool
* @throws WireException of template argument can't be resolved
*
*/
protected function isAllowedTemplate($template, Page $parent = null) {
if(!is_object($template)) $template = $this->wire()->templates->get($template);
if(!$template) throw new WireException('Unknown template');
$templates = $this->getAllowedTemplates($parent);
$allowed = isset($templates[$template->id]);
if($allowed && $parent) {
if(count($parent->template->childTemplates) && !in_array($template->id, $parent->template->childTemplates)) {
$allowed = false;
} else if($parent->template->noChildren) {
$allowed = false;
} else if(count($template->parentTemplates) && !in_array($parent->template->id, $template->parentTemplates)) {
$allowed = false;
} else if($template->noParents == -1) {
$allowed = $template->getNumPages() == 0;
} else if($template->noParents) {
$allowed = false;
}
}
return $allowed;
}
/**
* Is the given parent page allowed?
*
* @param Page $parent
* @param bool $showError
* @param Template $template Optionally limit condition to a specific template
* @return bool
*
*/
protected function isAllowedParent(Page $parent, $showError = false, Template $template = null) {
if($parent->template->noChildren) {
if($showError) {
$this->error($this->_('The parent template has specified that no children may be added here'));
}
return false;
}
if($template && count($template->parentTemplates)) {
if(!in_array($parent->template->id, $template->parentTemplates)) {
if($showError) {
$this->error(sprintf(
$this->_('The template "%1$s" does not allow parents of type "%2$s"'),
$template->name,
$parent->template->name
));
}
return false;
}
}
if($template && count($parent->template->childTemplates)) {
if(!in_array($template->id, $parent->template->childTemplates)) {
if($showError) {
$this->error(sprintf(
$this->_('The parent of type "%1$s" does not allow children of type "%2$s"'),
$parent->template->name,
$template->name
));
}
return false;
}
}
if(!$parent->addable()) {
if($showError) {
$this->error(sprintf(
$this->_('You dont have access to add pages to parent %s'),
$parent->path
));
}
return false;
}
if(count($this->predefinedParents)) {
$allowed = false;
foreach($this->predefinedParents as $p) {
if($p->id == $parent->id) {
$allowed = true;
}
}
if(!$allowed) {
if($showError) {
$this->error(sprintf(
$this->_('Specified parent is not allowed (%s)'),
$parent->path
));
}
return false;
}
}
return true;
}
/**
* Get allowed parents
*
* This will always be 1-parent, unless predefinedParents was populated.
*
* @param Template $template Optionally specify a template to filter parents by
* @return PageArray
*
*/
protected function getAllowedParents(Template $template = null) {
if(count($this->predefinedParents)) {
$parents = $this->predefinedParents;
} else {
$parents = $this->wire()->pages->newPageArray();
if($this->parent) $parents->add($this->parent);
}
foreach($parents as $parent) {
if(!$parent->addable()) $parents->remove($parent);
if($parent->template->noChildren) $parents->remove($parent);
if($template && count($parent->template->childTemplates)) {
// parent only allows certain templates for children
// if a template was given in the arguments, check that it is allowed
if(!in_array($template->id, $parent->template->childTemplates)) {
$parents->remove($parent);
}
}
}
if($template && count($template->parentTemplates)) {
// given template only allows certain parents
foreach($parents as $parent) {
if(!in_array($parent->template->id, $template->parentTemplates)) {
$parents->remove($parent);
}
}
}
return $parents;
}
/**
* Build the form fields for adding a page
*
* @return InputfieldForm
* @throws WireException
*
*/
protected function ___buildForm() {
$modules = $this->wire()->modules;
$input = $this->wire()->input;
$config = $this->wire()->config;
$pages = $this->wire()->pages;
$fields = $this->wire()->fields;
$user = $this->wire()->user;
$lockedStates = array(Inputfield::collapsedNoLocked, Inputfield::collapsedYesLocked, Inputfield::collapsedBlankLocked);
/** @var InputfieldForm $form */
$form = $modules->get('InputfieldForm');
$form->attr('id', 'ProcessPageAdd');
$form->addClass('InputfieldFormFocusFirst');
$form->attr('action', './' . ($input->get('modal') ? "?modal=1" : ''));
$form->attr('data-ajax-url', $config->urls->admin . 'page/add/');
$form->attr('data-dup-note', $this->_('The name entered is already in use. If you do not modify it, the name will be made unique automatically after you save.'));
$form->attr('method', 'post');
$page = $pages->newNullPage(); // for getInputfield
if(is_null($this->template) || !$this->template->noGlobal) {
foreach($fields as $field) {
if($field->flags & Field::flagGlobal && ($field->type instanceof FieldtypePageTitle || $field->type instanceof FieldtypePageTitleLanguage)) {
if($this->template) {
$_field = $this->template->fieldgroup->getField($field->id, true); // get in context of fieldgroup
if($_field) $field = $_field;
}
if(in_array($field->collapsed, $lockedStates)) continue;
$inputfield = $field->getInputfield($page);
if($inputfield) {
if($this->template && $this->template->noLang) $inputfield->useLanguages = false;
$inputfield->columnWidth = 100;
$form->append($inputfield);
}
break;
}
}
} else if($this->template) {
/** @var Field $field */
$field = $this->template->fieldgroup->getField('title', true);
if($field) {
if(in_array($field->collapsed, $lockedStates)) {
// skip it
} else {
$inputfield = $field->getInputfield($page);
$inputfield->columnWidth = 100;
if($inputfield) $form->append($inputfield);
}
}
}
/** @var InputfieldPageName $field */
$field = $modules->get('InputfieldPageName');
$field->parentPage = $this->parent;
$field->attr('name', '_pw_page_name');
$field->required = true;
if($this->template) {
$field->slashUrls = $this->template->slashUrls;
$label = $this->template->getNameLabel();
if($label) $field->label = $label;
$languages = $this->template->getLanguages();
} else {
$languages = $this->wire()->languages;
}
/** @var Languages $languages */
if($languages && $this->parent && $this->parent_id > 0) {
if($this->template) {
// dummy edit page for examination by InputfieldPageName
$editPage = $pages->newPage(array(
'template' => $this->template,
'parent' => $this->parent,
));
$field->editPage = $editPage;
}
}
$form->append($field);
$defaultTemplateId = (int) $input->get('template_id');
if(!$defaultTemplateId && $this->parent->numChildren > 0) {
$sibling = $this->parent->child('sort=-created, include=hidden');
if($sibling && $sibling->id) $defaultTemplateId = $sibling->template->id;
}
if(!$defaultTemplateId) $defaultTemplateId = $this->parent->template->id;
$allowedTemplates = $this->getAllowedTemplates();
if(!count($allowedTemplates)) throw new WireException($this->_('No templates allowed for adding new pages here.'));
if($this->template && !isset($allowedTemplates[$this->template->id])) throw new WireException(sprintf($this->_('Template "%s" is not allowed here.'), $this->template->name));
if(!isset($allowedTemplates[$defaultTemplateId])) $defaultTemplateId = 0;
$numPublishable = 0;
if(count($allowedTemplates) < 2) {
// only 1 template can be used here, so store it in a hidden field (no need for selection)
$template = $this->template ? $this->template : reset($allowedTemplates);
/** @var InputfieldHidden $field */
$field = $modules->get('InputfieldHidden');
$field->attr('value', $template->id);
if(count($template->fieldgroup) == 1 && $template->fieldgroup->hasField('title')) $numPublishable++;
$field->attr('data-publish', $numPublishable);
if($template->noLang) $field->attr('data-nolang', 1);
} else {
// multiple templates are possible so give them a select
/** @var InputfieldSelect $field */
$field = $modules->get('InputfieldSelect');
$noSuggest = $this->settings['noSuggestTemplates'];
if(empty($noSuggest)) {
$noSuggest = false;
} else {
// determine whether to show blank option at top
$ptpl = $this->parent ? $this->parent->template->name : '';
$noSuggestArray = is_array($noSuggest) ? $noSuggest : explode(' ', $noSuggest);
if(((string) $noSuggest) === "1" || ($ptpl && in_array($ptpl, $noSuggestArray))) {
$field->addOption('', '', array('data-publish' => false, 'data-nolang' => false));
$noSuggest = true;
} else {
$noSuggest = false;
}
}
foreach($allowedTemplates as $template) {
if(!count($this->predefinedTemplates) && $this->template && $template->id != $this->template->id) continue;
$numFields = count($template->fieldgroup);
if($numFields == 1 && $template->fieldgroup->hasField('title')) {
$isPublishable = 1;
$numPublishable++;
} else {
$isPublishable = 0;
}
$field->addOption($template->id, $this->getTemplateLabel($template), array(
'data-publish' => $isPublishable,
'data-nolang' => (int) $template->noLang
));
}
if(!$noSuggest) $field->attr('value', $defaultTemplateId);
}
$field->label = $this->_('Template'); // Template field label
$field->attr('id+name', 'template');
$field->icon = 'cubes';
$field->required = true;
$field instanceof InputfieldHidden ? $form->append($field) : $form->prepend($field);
if(count($this->predefinedParents) > 1) {
/** @var InputfieldSelect $field */
$field = $modules->get('InputfieldSelect');
$field->attr('name', 'parent_id');
$field->label = $this->_('Parent');
$field->required = true;
$field->icon = 'folder-open-o';
$value = 0;
foreach($this->predefinedParents as $parent) {
$field->addOption($parent->id, $parent->path);
if($parent->id == $this->parent_id) $value = $parent->id;
}
if($value) $field->attr('value', $value);
$form->prepend($field);
} else {
/** @var InputfieldHidden $field */
$field = $modules->get('InputfieldHidden');
$field->attr('name', 'parent_id');
$value = count($this->predefinedParents) == 1 ? $this->predefinedParents->first()->id : $this->parent_id;
$field->attr('value', $value);
$form->append($field);
}
/** @var InputfieldSubmit $field */
$field = $modules->get('InputfieldSubmit');
$field->attr('name', 'submit_save');
$field->attr('value', $this->_('Save'));
$field->showInHeader();
$form->append($field);
if($numPublishable && !$this->noAutoPublish) {
$allowPublish = true;
if(!$user->isSuperuser()) {
$publishPermission = $this->wire()->permissions->get('page-publish');
if($publishPermission->id && !$user->hasPermission('page-publish')) $allowPublish = false;
}
if($allowPublish) {
/** @var InputfieldSubmit $field */
$field = $modules->get('InputfieldSubmit');
$field->attr('id+name', 'submit_publish');
$field->attr('value', $this->_('Save + Publish'));
$field->setSecondary();
$form->append($field);
if(!$input->get('modal')) {
/** @var InputfieldSubmit $field */
$field = $modules->get('InputfieldSubmit');
$field->attr('id+name', 'submit_publish_add');
$field->attr('value', $this->_('Save + Publish + Add Another'));
$field->setSecondary();
$form->append($field);
}
}
}
if(count($allowedTemplates) == 1) {
$t = reset($allowedTemplates);
$form->description = $this->getTemplateLabel($t);
}
return $form;
}
/**
* Return the label for the given Template
*
* @param Template $template
* @return string
*
*/
protected function getTemplateLabel(Template $template) {
$label = '';
$user = $this->wire()->user;
$language = $this->wire()->languages && $user->language->id && !$user->language->isDefault() ? $user->language : null;
if($language) $label = $template->get('label' . $language->id);
if(!$label) $label = $template->label ? $template->label : $template->name;
return $label;
}
/**
* Delete old 'quick add' pages that were never saved
*
*/
protected function deleteOldTempPages() {
$old = time() - 86400;
$selector = "include=all, modified<$old, limit=10, status&" . Page::statusTemp . ", status<" . Page::statusTrash;
$items = $this->wire()->pages->find($selector);
foreach($items as $item) {
$this->message("Checking temporary item: $item->path", Notice::debug);
if(!$item->hasStatus(Page::statusUnpublished)) continue;
if(!$item->hasStatus(Page::statusTemp)) continue;
if($item->modified > $old) continue;
if($item->numChildren > 0) continue;
$msg = "Automatically trashed unused page: $item->path";
$this->message($msg, Notice::debug);
$this->wire()->log->message($msg);
try {
if(!$item->title) $item->title = $this->_('Unused temp page') . ' - ' . $item->name;
$this->wire()->pages->trash($item);
} catch(\Exception $e) {
$this->error($e->getMessage());
}
}
}
/**
* Perform a 'quick add' of a page and redirect to edit the page
*
* @param Page $parent
* @param Template $template
* @return bool Returns false if not success. Redirects if success.
*
*/
protected function ___processQuickAdd(Page $parent, Template $template) {
$pages = $this->wire()->pages;
$input = $this->wire()->input;
$sanitizer = $this->wire()->sanitizer;
$this->deleteOldTempPages();
// allow for nameFormat to come from a name_format GET variable
$nameFormat = (string) $input->get('name_format');
if(strlen($nameFormat)) {
$nameFormat = $sanitizer->chars($sanitizer->text($nameFormat), '-_:./| [alpha][digit]', '-');
} else {
if(count($parent->template->childTemplates) > 1) return false;
$nameFormat = '';
}
$nameFormatTemplate = $parent->template->childNameFormat;
if(strlen($nameFormat)) {
// temporarily assign to the template->childNameFormat property
$parent->template->childNameFormat = $nameFormat;
} else {
// if not specified in get variable, next check parent template for setting
$nameFormat = $nameFormatTemplate;
}
$page = $pages->newPage(array(
'template' => $template,
'parent' => $parent,
));
$pages->setupNew($page);
if(!strlen($page->name)) return false;
if(!$this->isAllowedTemplate($template)) return false;
$page->addStatus(Page::statusUnpublished);
$page->addStatus(Page::statusTemp); // ProcessPageEdit will remove this status the first time the page is saved
// if languages are in use, make the new page inherit the parent's language status (active vs. inactive)
$languages = $template->getLanguages();
if($languages) {
foreach($languages as $language) {
/** @var Language $language */
if($language->isDefault()) continue;
$languageStatus = $parent->get("status$language");
if($languageStatus) $page->set("status$language", $languageStatus);
}
}
try {
$pages->save($page);
$this->createdPageMessage($page);
} catch(\Exception $e) {
$this->error($e->getMessage());
return false;
}
if(strlen($nameFormat) && $nameFormat != $nameFormatTemplate) {
$parent->template->childNameFormat = $nameFormatTemplate; // restore original name format
}
if($page->id) {
// allow for classes descending Page to redirect to alternate editor if $this->editor is not the right kind
$page->setEditor($this->editor);
// redirect to edit the page
$this->redirect("../edit/?id=$page->id&new=1" . ($input->get('modal') ? '&modal=1' : ''));
return true;
} else {
return false;
}
}
/**
* Populate a session message indicating info about created page
*
* @param Page $page
*
*/
protected function createdPageMessage(Page $page) {
$this->wire()->session->message(
sprintf(
$this->_('Created page %1$s using template: %2$s'),
$page->parent->url . $page->name, $page->template->getLabel()
)
);
}
/**
* Hook called when the page's name changed during save
*
* @param Page $page
* @param $namePrevious
* @return string Warning message
*
*/
protected function ___nameChangedWarning(Page $page, $namePrevious) {
return sprintf(
$this->_('Warning, the name you selected "%1$s" was already in use and has been changed to "%2$s".'),
$namePrevious, $page->name
);
}
/**
* Save the submitted page add form
*
* @param InputfieldForm $form
* @throws WireException
* @return bool
*
*/
protected function ___processInput(InputfieldForm $form) {
$pages = $this->wire()->pages;
$input = $this->wire()->input;
$template = $this->template;
$this->page = $pages->newPage($template ? $template : array()); // must exist before processInput for language hooks
$form->processInput($input->post);
/** @var InputfieldPageName $nameField */
$nameField = $form->getChildByName('_pw_page_name');
$name = $nameField->value;
if(!strlen($name)) {
$nameField->error($this->_("Missing required field: name"));
return false;
}
if(is_null($template)) {
/** @var InputfieldSelect $templateField */
$templateField = $form->getChildByName('template');
$templatesId = (int) $templateField->val();
$template = $templatesId ? $this->templates->get($templatesId) : null;
if(!$template) {
$templateField->error($this->_('Template selection is required'));
return false;
}
}
if(!$this->isAllowedTemplate($template, $this->parent)) {
throw new WireException("You don't have access to add pages with template '$template'");
} else {
// $this->message("Template $template is allowed for {$this->parent->template}");
}
if(!$this->isAllowedParent($this->parent, true, $template)) {
throw new WireException($this->errors('string'));
} else {
// $this->message("Parent {$this->parent->path} is allowed for $template");
}
$this->page->template = $template;
$this->page->parent = $this->parent;
$this->page->name = $name;
$this->page->sort = $this->parent->numChildren;
$publishAdd = $input->post('submit_publish_add');
$publishNow = $this->page->publishable() && ($input->post('submit_publish') || $publishAdd);
$languages = $template->getLanguages();
foreach($this->page->fieldgroup as $field) {
/** @var Field $field */
if(!$this->page->hasField($field)) continue;
$f = $form->children->get($field->name);
if($f) {
/** @var Inputfield $f */
if($languages && $f->getSetting('useLanguages')) {
// account for language fields (most likely FieldtypePageTitleLanguage)
$value = $this->page->get($field->name);
if(is_object($value)) $value->setFromInputfield($f);
} else {
$value = $f->attr('value');
}
$this->page->set($field->name, $value);
} else {
$publishNow = false; // non-global fields means we won't publish yet
}
}
if($publishNow && $this->noAutoPublish) $publishNow = false;
// if more fields are going to be present in this page's template, then don't make this page available until the user has
// had the opportunity to edit those fields in ProcessPageEdit. But if they've already seen all the fields that will be here (global),
// like just a title field, then go ahead and publish now.
if(!$publishNow) $this->page->addStatus(Page::statusUnpublished);
$pageName = $this->page->name;
$this->page->setEditor($this->editor);
try {
$this->pages->save($this->page, array('adjustName' => true));
} catch(\Exception $e) {
$this->error($e->getMessage());
return false;
}
$this->createdPageMessage($this->page);
if($pages->names()->hasAdjustedName($this->page)) {
$warning = $this->nameChangedWarning($this->page, $pageName);
if($warning) $this->warning($warning);
}
if($publishNow && $publishAdd) {
$this->redirect("./?parent_id={$this->page->parent_id}&template_id={$this->page->template->id}");
} else {
$this->redirect("../edit/?id={$this->page->id}&new=1" . ($input->get('modal') ? "&modal=1" : ''));
}
return true;
}
/**
* Redirect
*
* @param string $url
* @param bool $permanent
*
*/
protected function redirect($url, $permanent = false) {
if($permanent) $this->wire()->session->redirect($url);
$this->wire()->session->location($url);
}
/**
* Setup the UI breadcrumb trail
*
*/
public function setupBreadcrumbs() {
$config = $this->wire()->config;
if($this->wire()->page->process != $this->className()) return;
$breadcrumbs = $this->wire(new Breadcrumbs());
$breadcrumbs->add(new Breadcrumb($config->urls->admin . 'page/list/', "Pages"));
foreach($this->parent->parents()->append($this->parent) as $p) {
/** @var Page $p */
$breadcrumbs->add(new Breadcrumb($config->urls->admin . "page/list/?open=" . $p->id, $p->get("title|name")));
}
$this->wire('breadcrumbs', $breadcrumbs);
}
/**
* Get the Page that is being edited
*
* @return Page|null
*
*/
public function getPage() {
return $this->page ? $this->page : $this->wire()->pages->newNullPage();
}
/**
* Set the WirePageEditor that is calling this Process
*
* @param WirePageEditor $editor
*
*/
public function setEditor(WirePageEditor $editor) {
$this->editor = $editor;
}
/**
* Predefine the allowed templates, separately from family/auto-detect
*
* @param array|WireArray $templates array of Template objects
*
*/
public function setPredefinedTemplates($templates) {
$this->predefinedTemplates = $templates;
}
/**
* Predefine the allowed parents, separately from family/auto-detect
*
* @param PageArray $parents
*
*/
public function setPredefinedParents(PageArray $parents) {
$this->predefinedParents = $parents;
}
/**
* Get an instance of PageBookmarks
*
* @return PageBookmarks
*
*/
protected function getPageBookmarks() {
require_once($this->wire()->config->paths('ProcessPageEdit') . 'PageBookmarks.php');
return $this->wire(new PageBookmarks($this));
}
/**
* Execute the Page Bookmarks
*
* @return string
* @throws WireException
* @throws WirePermissionException
*
*/
public function ___executeBookmarks() {
$input = $this->wire()->input;
$user = $this->wire()->user;
$modules = $this->wire()->modules;
if(is_array($input->post('shortcutSort')) && $user->isSuperuser()) {
$data = $modules->getConfig($this);
$data['shortcutSort'] = $input->post->intArray('shortcutSort');
$modules->saveConfig($this, $data);
}
$bookmarks = $this->getPageBookmarks();
$form = $bookmarks->editBookmarksForm();
$roleID = $input->get('role'); // no integer sanitization is intentional
if(!is_null($roleID) && $roleID == 0 && $user->isSuperuser()) {
$f = $this->getShortcutSortField();
$form->insertBefore($f, $form->getChildByName('submit_save_bookmarks'));
}
$f = $form->getChildByName('bookmarks');
if($f->notes) $f->notes .= "\n\n";
$f->notes .= $this->_('The pages you select above represent bookmarks to the parent pages where you want children added. Note that if a user does not have permission to add a page to a given parent page (whether due to access control or template family settings), the bookmark will not appear.'); // Notes for bookmarks
$this->wire()->session->remove($this, 'numAddable');
return $form->render();
}
/**
* Get Inputfield that lets you define shorcuts and sort order
*
* @return InputfieldAsmSelect|InputfieldHidden
*
*/
public function getShortcutSortField() {
$this->wire()->session->remove($this, 'nav');
/** @var array $data */
$data = $this->executeNavJSON(array('getArray' => true));
$name = 'shortcutSort';
/** @var InputfieldAsmSelect $f */
$f = $this->wire()->modules->get('InputfieldAsmSelect');
$f->label = $this->_('Template shortcut sort order');
$f->description = $this->_('To change the order of the "Add New" page-template shortcuts, drag and drop the options to the order you want them in.');
$f->notes = $this->_('To add or remove templates from these shortcuts, see the Template editor Family tab.');
$f->attr('name', $name);
$f->icon = 'sort';
$f->setAsmSelectOption('removeLabel', '');
$value = array();
foreach($data['list'] as $item) {
if(empty($item['template_id'])) continue;
$template = $this->wire()->templates->get($item['template_id']);
if(!$template) continue;
$f->addOption($template->id, $template->getLabel());
$value[] = $template->id;
}
if(!count($f->getOptions())) {
/** @var InputfieldHidden $f */
$f = $this->wire()->modules->get('InputfieldHidden');
$f->attr('name', $name);
return $f;
}
$f->attr('value', $value);
$f->collapsed = Inputfield::collapsedBlank;
return $f;
}
/**
* Get module configuration inputs
*
* @param array $data
* @return InputfieldWrapper
*
*/
public function getModuleConfigInputfields(array $data) {
/** @var InputfieldWrapper $form */
$form = $this->wire(new InputfieldWrapper());
$form->add($this->getShortcutSortField());
/** @var InputfieldCheckbox $f */
$f = $this->wire()->modules->get('InputfieldCheckbox');
$f->label = $this->_('Disable automatic publishing');
$f->description =
$this->_('By default, pages with nothing but global fields (most commonly “title”) will be published automatically when added.') . ' ' .
$this->_('This bypasses the unpublished state, which can be a desirable time saver in some instances.') . ' ' .
$this->_('You may optionally cancel this behavior by checking the box below.');
$f->attr('name', 'noAutoPublish');
$f->attr('value', 1);
if(!empty($data['noAutoPublish'])) $f->attr('checked', 'checked');
$form->add($f);
return $form;
}
}