Subversion Repositories web.creative

Rev

Blame | Last modification | View Log | Download

<?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 2017 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' => 108, 
      '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;
    return 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' &&  $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');
    /** @var Session $session */
    $session = $this->wire('session');
    $data = $session->getFor($this, 'nav');
    
    if(!empty($data)) {
      // check that session cache data is still current
      foreach($this->wire('templates') as $template) {
        if($template->modified > $data['modified']) {
          $data = array();
          $session->remove($this, 'nav');
          $session->remove($this, 'numAddable');
          $this->message("Clearing 'Add New' page cache", Notice::debug);
          break;
        }
      }
    }
    
    if(empty($data)) {

      $data = array(
        'url'   => $this->wire('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')) {

        $items = array();

        foreach($this->wire('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 = $this->wire('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 = $this->wire('modules')->getModuleConfigData($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(array_values($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($this->wire('config')->ajax) header("Content-Type: application/json");
    
    return json_encode($data);
  }

  /**
   * Ask user to select template and parent
   * 
   * @return string
   * @throws WireException
   * 
   */
  public function ___executeTemplate() {

    $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 = $this->wire('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($this->wire('input')->get('modal')) $url .= "&modal=1";
      $this->wire('session')->redirect($url);
    }

    $templateLabel = $this->getTemplateLabel($template); 
    $form = $this->wire('modules')->get('InputfieldForm'); 
    $form->description = $this->getTemplateLabel($template); 
    $form->method = 'get';
    $form->action = './';
    $form->attr('id', 'select_parent_form'); 
  
    if($this->wire('input')->get('modal')) {
      $f = $this->wire('modules')->get('InputfieldHidden');
      $f->attr('name', 'modal');
      $f->attr('value', 1);
      $form->add($f);
    }
    
    $f = $this->wire('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);
    
    $f = $this->wire('modules')->get('InputfieldHidden');
    $f->attr('name', 'template_id');
    $f->attr('value', $template->id);
    $form->add($f);

    $f = $this->wire('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() {
    /** @var array $data */
    $data = $this->executeNavJSON(array('getArray' => true)); 
    $out = '';
    $bookmarkItem = null;
    foreach($data['list'] as $item) {
      if(strpos($item['url'], '?role=0') !== false) {
        $bookmarkItem = $item;
        continue;
      }
      if(!empty($item['parent_id'])) {
        $parents = $this->wire('pages')->find("id=$item[parent_id]");
      } else if(!empty($item['template_id'])) {
        $template = $this->wire('templates')->get($item['template_id']);
        $parentTemplates = implode('|', $template->parentTemplates);
        if(empty($parentTemplates)) continue;
        $parents = $this->wire('pages')->find("template=$parentTemplates, include=unpublished, limit=100, sort=-modified");
      } else {
        $parents = array();
      }
      $out .= 
        "<dt><a class='label' href='./$item[url]'>" . 
        "<i class='fa fa-fw fa-$item[icon]'></i> $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) {
            if($p->id == 1 && $parentParents->count() > 1) continue;
            $out .= "$p->title<i class='ui-priority-secondary fa fa-fw fa-angle-right'></i>";
          }
          $out .= "</a></li>";
        }
        $out .= "</ul>";
      }
      $out .= "</dd>";
    }
    if($out) {
      $out = "<dl class='nav'>$out</dl>";
    } else {
      $out = 
        "<h2>" . 
        $this->_('There are currently no templates with defined parent/child relationships needed to show "Add New" shortcuts here. To configure this, edit any template (Setup > Templates) and click on the "Family" tab.') . // Text shown when no templates use family settings
        "</h2>";
    }
    if($bookmarkItem) {
      $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() {
    
    /** @var Pages $pages */
    $pages = $this->wire('pages');
    
    $parentID = (int) $this->wire('input')->get('parent_id'); 
    if(!$parentID) return '';
    
    $parent = $this->wire('pages')->get($parentID); 
    if(!$parent->addable()) return '';
    
    $name = $this->wire('sanitizer')->pageNameUTF8($this->wire('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');
    $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->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(count($this->parent->template->childTemplates) == 1) {
      // only one type of template is allowed for the parent
      $childTemplates = $this->parent->template->childTemplates;
      $template = $this->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) {

    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 = $this->wire('config')->usersPageIDs;
    $userTemplateIDs = $this->wire('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 != $this->config->rolesPageID) {
        // only allow role templates below rolesPageID
        continue;
        
      } else if($t->name == 'permission' && $parent->id != $this->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("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("The template '$template' does not allow parents of type '$parent->template'"); 
        return false;
      }
    }
    
    if($template && count($parent->template->childTemplates)) {
      if(!in_array($template->id, $parent->template->childTemplates)) {
        if($showError) $this->error("The parent of type '$parent->template' does not allow children of type '$template'");
        return false;
      }
    }

    if(!$parent->addable()) {
      if($showError) $this->error("You don't have access to add pages to parent $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("Specified parent is not allowed ($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() {

    /** @var InputfieldForm $form */
    $form = $this->modules->get('InputfieldForm');

    $form->attr('id', 'ProcessPageAdd'); 
    $form->addClass('InputfieldFormFocusFirst');
    $form->attr('action', './' . ($this->input->get('modal') ? "?modal=1" : '')); 
    $form->attr('data-ajax-url', $this->wire('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 = $this->wire('pages')->newNullPage(); // for getInputfield
    if(is_null($this->template) || !$this->template->noGlobal) {
      foreach($this->wire('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, array(Inputfield::collapsedNoLocked, Inputfield::collapsedYesLocked))) 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, array(Inputfield::collapsedNoLocked, Inputfield::collapsedYesLocked))) {
          // skip it
        } else {
          $inputfield = $field->getInputfield($page);
          $inputfield->columnWidth = 100;
          if($inputfield) $form->append($inputfield);
        }
      }
    }

    /** @var InputfieldPageName $field */
    $field = $this->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) {
      foreach($languages as $language) {
        if($language->isDefault()) continue;
        $field->checkboxChecked = $this->parent->get("status$language") ? true : false;
      }
      if($this->template) {
        // dummy edit page for examination by InputfieldPageName
        $editPage = $this->wire('pages')->newPage(array(
          'template' => $this->template,
          'parent' => $this->parent,
        ));
        $field->editPage = $editPage;
      }
    }
    $form->append($field); 

    $defaultTemplateId = (int) $this->wire('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); 
      $field = $this->modules->get('InputfieldHidden');
      $field->attr('id+name', 'template'); 
      $field->attr('value', $template->id); 
      if(count($template->fieldgroup) == 1 && $template->fieldgroup->hasField('title')) $numPublishable++;
      $field->attr('data-publish', $numPublishable);
      
    } else {
      // multiple templates are possible so give them a select

      /** @var InputfieldSelect $field */
      $field = $this->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) {
      $field = $this->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 {
      $field = $this->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 = $this->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(!$this->wire('user')->isSuperuser()) {
        $publishPermission = $this->wire('permissions')->get('page-publish');
        if($publishPermission->id && !$this->wire('user')->hasPermission('page-publish')) $allowPublish = false;
      }
      if($allowPublish) {
        /** @var InputfieldSubmit $field */
        $field = $this->modules->get('InputfieldSubmit');
        $field->attr('id+name', 'submit_publish');
        $field->attr('value', $this->_('Save + Publish'));
        $field->setSecondary();
        $form->append($field);
      
        if(!$this->wire('input')->get('modal')) {
          $field = $this->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) { 

    $this->deleteOldTempPages();
    // allow for nameFormat to come from a name_format GET variable
    $nameFormat = $this->wire('input')->get('name_format');
    if(strlen($nameFormat)) {
      $nameFormat = $this->sanitizer->chars($this->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 = $this->wire('pages')->newPage(array(
      'template' => $template,
      'parent' => $parent,
    )); 
    $this->wire('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) {
      if($language->isDefault()) continue; 
      $languageStatus = $parent->get("status$language"); 
      if($languageStatus) $page->set("status$language", $languageStatus); 
    }

    try {
      $this->wire('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->session->redirect("../edit/?id=$page->id&new=1" . ($this->wire('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->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) {

    $template = $this->template; 
    $this->page = $this->wire('pages')->newPage($template ? $template : array()); // must exist before processInput for language hooks
    $form->processInput($this->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 = $this->wire('input')->post('submit_publish_add');
    $publishNow = $this->page->publishable() && ($this->wire('input')->post('submit_publish') || $publishAdd);
    $languages = $template->getLanguages();

    foreach($this->page->fieldgroup as $field) {
      if(!$this->page->hasField($field)) continue;
      $f = $form->children->get($field->name); 
      if($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($this->wire('pages')->names()->hasAdjustedName($this->page)) {
      $warning = $this->nameChangedWarning($this->page, $pageName);
      if($warning) $this->warning($warning);
    }
    
    if($publishNow && $publishAdd) {
      $this->session->redirect("./?parent_id={$this->page->parent_id}&template_id={$this->page->template->id}");
    } else {
      $this->session->redirect("../edit/?id={$this->page->id}&new=1" . ($this->input->get('modal') ? "&modal=1" : ''));
    }
    
    return true;
  }

  /**
   * Setup the UI breadcrumb trail
   *
   */
  public function setupBreadcrumbs() {
    if($this->wire('page')->process != $this->className()) return;
    $breadcrumbs = $this->wire(new Breadcrumbs());
    $breadcrumbs->add(new Breadcrumb($this->config->urls->admin . 'page/list/', "Pages"));
    foreach($this->parent->parents()->append($this->parent) as $p) {
      /** @var Page $p */
      $breadcrumbs->add(new Breadcrumb($this->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() {
    
    if(is_array($this->wire('input')->post('shortcutSort')) && $this->wire('user')->isSuperuser()) {
      $data = $this->wire('modules')->getModuleConfigData($this); 
      $data['shortcutSort'] = $this->wire('input')->post->intArray('shortcutSort'); 
      $this->wire('modules')->saveModuleConfigData($this, $data);
    }
  
    $bookmarks = $this->getPageBookmarks();
    $form = $bookmarks->editBookmarksForm();

    $roleID = $this->wire('input')->get('role'); // no integer sanitization is intentional
    
    if(!is_null($roleID) && $roleID == 0 && $this->wire('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
   * 
   */
  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())) {
      $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) {
    
    $form = $this->wire(new InputfieldWrapper());
    $form->add($this->getShortcutSortField());
    
    $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, bypassing the usual unpublished state. Usually this is a desirable time saver. But you may cancel this behavior by checking the box below.'); // Description of automatic publishing
    $f->attr('name', 'noAutoPublish');
    $f->attr('value', 1);
    if(!empty($data['noAutoPublish'])) $f->attr('checked', 'checked');
    $form->add($f);

    return $form; 
  }
}