You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1538 lines
50 KiB
Plaintext

<?php
/**
* Page Lister Pro: Process
* __ _ __
* / / (_)____/ /____ _____
* / / / / ___/ __/ _ \/ ___/
* / /___/ (__ ) /_/ __/ /
* /_____/_/____/\__/\___/_/ PRO
*
* Provides an alternative listing view for pages using specific templates (Professional Version)
*
* This is a commercial module, please do not distribute.
*
* ListerPro for ProcessWire
* Copyright 2017 by Ryan Cramer
* https://processwire.com/ListerPro/
*
* PWLPID17
*
* @todo: deleted field that is present in editColumns property can cause exceptions (https://processwire.com/talk/topic/15935-add-hook-method-on-fields-delete-and-name-change/)
* @todo: add support for user-specific bookmarks (https://processwire.com/talk/topic/13117-request-add-permission-to-createedit-bookmarks/)
* @todo: allow for subfields of "modified user" and "created user"
* @todo: include=unpublished overrides status "does not have" unpublished
* @todo: allow for option of locking the field select boxes
* @todo: limitFields option is limiting all column fields, even config ones.
* @todo: option to merge config and bookmarks per here https://processwire.com/talk/topic/10944-filters-reset-in-bookmarks/
*
* @property array $editColumns Field IDs that are editable in the Lister
* @property array $editFieldtypes Fieldtype classnames allowed for editColumns
* @property int $editOption See editOption constants
* @property bool $editPreload Whether or not to preload editors in rendered table
* @property int|bool $useColumnLabels Whether or not to use column labels (vs. names)
* @property array $settings Where all Listers config data is stored
* @property array $allowActions Action class names that are allowed for this Lister
* @property array $limitFields Limit selectable fields to only those present here (names)
* @property array $toggles Toggles from ListerPro config $settings, specific to this Lister
* @property array $InputfieldSelectorSettings Settings to pass along to InputfieldSelector
* @property string $licenseKey
*
*
*/
class ProcessPageListerPro extends ProcessPageLister implements ConfigurableModule, WirePageEditor {
const editOptionNone = 0;
const editOptionSome = 1;
const editOptionAll = 2;
/**
* @var ListerProActions|null
*
*/
protected $actions = null;
/**
* For editable columns and WirePageEditor interface getPage() method
*
* @var null|Page
*
*/
protected $editorPage = null;
/**
* Parent class name either \\ProcessWire\\ProcessPageLister or just ProcessPageLister
*
* @var string
*
*/
protected $parentClass;
/**
* Construct ListerPro
*
*/
public function __construct() {
$url = $this->wire('config')->urls->ProcessPageLister;
$this->wire('config')->styles->add($url . "ProcessPageLister.css");
$this->wire('config')->scripts->add($url . "ProcessPageLister.js");
$this->parentClass = "\\ProcessWire\\ProcessPageLister";
if(!class_exists($this->parentClass)) $this->parentClass = "ProcessPageLister";
parent::__construct();
// the following will be overridden per ListerPro config
$this->set('useColumnLabels', 0);
// where all listers config data is stored
$this->set('settings', array());
// action class names that are allowed for this Lister
$this->set('allowActions', array());
// IDs of fields that are allowed to be edited in the Lister
$this->set('editColumns', array());
// If true, then all editable columns can be used (bypassing editColumns setting)
$this->set('editOption', self::editOptionNone);
// List of all Fieldtypes modules allowed for editable columns (empty=allow all)
$this->set('editFieldtypes', array());
// If true, editors will always be rendered
$this->set('editPreload', false);
$this->set('limitFields', array());
$this->set('licenseKey', '');
$this->set('initSelector', '');
$this->set('toggles', array());
$this->set('imageStyle', 0); // 0=image only, 1=detailed
$dirname = dirname(__FILE__) . '/';
require_once($dirname . '/ListerProConfig.php');
require_once($dirname . '/ListerProActions.php');
}
/**
* Initalize lister variables
*
*/
public function init() {
$name = $this->page->name;
$settings = $this->settings;
if($settings && isset($settings[$name])) {
$settings = $settings[$name]; // convert to localized settings specific to this lister
//$this->parent = empty($settings['parent']) || ((int) $settings['parent']) === 1 ? new NullPage() : $this->pages->get((int) $settings['parent']);
$this->parent = empty($settings['parent']) ? new NullPage() : $this->pages->get((int) $settings['parent']);
if(isset($settings['defaultSort'])) $this->defaultSort = $settings['defaultSort'];
if(isset($settings['columns'])) $this->columns = $settings['columns'];
if(isset($settings['initSelector'])) $this->initSelector = $settings['initSelector'];
if(isset($settings['defaultSelector'])) $this->defaultSelector = $settings['defaultSelector'];
if(isset($settings['delimiters'])) $this->delimiters = $settings['delimiters'];
if(isset($settings['allowActions'])) $this->allowActions = $settings['allowActions'];
if(isset($settings['limitFields'])) $this->limitFields = $settings['limitFields'];
if(isset($settings['imageWidth'])) $this->imageWidth = (int) $settings['imageWidth'];
if(isset($settings['imageHeight'])) $this->imageHeight = (int) $settings['imageHeight'];
if(isset($settings['imageFirst'])) $this->imageFirst = $settings['imageFirst'];
if(isset($settings['imageStyle'])) $this->imageStyle = $settings['imageStyle'];
if(isset($settings['viewMode'])) $this->viewMode = (int) $settings['viewMode'];
if(isset($settings['editMode'])) $this->editMode = (int) $settings['editMode'];
if(isset($settings['toggles'])) $this->toggles = $settings['toggles'];
if(isset($settings['useColumnLabels'])) $this->useColumnLabels = (int) $settings['useColumnLabels'];
if(isset($settings['InputfieldSelectorSettings'])) $this->InputfieldSelectorSettings = $settings['InputfieldSelectorSettings'];
if(isset($settings['editOption'])) $this->editOption = $settings['editOption'];
if(isset($settings['editColumns'])) $this->editColumns = $settings['editColumns'];
if(isset($settings['editPreload'])) $this->editPreload = $settings['editPreload'];
} else if($this->input->urlSegment1 == 'config') {
// defaults for unconfigured lister
if($name != 'lister') $this->initSelector = 'template=';
$this->columns = $this->defaultColumns;
$this->parent = new NullPage();
} else {
// send them to configure this lister
// $this->session->redirect($this->page->url . 'config/');
}
if($this->isValid()) $this->actions = new ListerProActions($this);
$this->config->js($this->className(), array(
'closeLabel' => $this->_('Close'),
'openNewLabel' => $this->_('Open in new window'),
));
if(!$this->wire('config')->ajax) {
foreach($this->editColumns as $fieldID) {
if($fieldID == 'name') {
$inputfield = $this->wire('modules')->get('InputfieldPageName');
} else {
$field = $this->wire('fields')->get($fieldID);
if(!$field) continue;
// load Inputfields now to pre-load of any needed css/js files
$inputfield = $field->getInputfield(new NullPage());
}
$inputfield->renderReady();
$inputfield->render();
}
} else if(isset($_SERVER['HTTP_X_FIELDNAME'])) {
// ajax file upload gets sent directly to ProcessPageEdit
$this->processAjaxUpload();
}
parent::init();
}
protected function processAjaxUpload() {
if(!preg_match('/^(.+)_LPID(\d+)$/', $_SERVER['HTTP_X_FIELDNAME'], $matches)) return;
if(!$this->wire('user')->hasPermission('page-edit')) return;
$fieldName = $this->wire('sanitizer')->fieldName($matches[1]);
$_SERVER['HTTP_X_FIELDNAME'] = $fieldName;
$pageID = (int) $matches[2];
$_POST['id'] = $pageID;
$this->wire('input')->post->id = $pageID;
$pageEdit = $this->wire('modules')->get('ProcessPageEdit');
ob_start();
$pageEdit->execute();
$out = ob_get_contents();
ob_end_clean();
// adjust returned markup for our LPID namespace
$out = str_replace("_{$fieldName}_", "_{$fieldName}_LPID{$pageID}_", $out);
echo $out;
exit;
}
/**
* getModuleInfo interface for required permission
*
* @param array $data
* @return bool
*
*/
public static function hasListerPermission(array $data) {
/** @var Page $page */
$page = $data['page'];
/** @var User $user */
$user = $data['user'];
//$info = $data['info'];
$wire = $data['wire'];
$permission = $wire->permissions->get("page-lister-$page->name");
if(!$permission->id) $permission = 'page-lister';
return $user->hasPermission($permission);
}
/**
* Get the InputfieldSelector instance for this Lister
*
* @return InputfieldSelector
*
*/
public function getInputfieldSelector() {
$s = parent::getInputfieldSelector();
$s->allowSubfieldGroups = true; // we only support in ListerPro
$s->allowSubselectors = true; // we only support in ListerPro
if($this->allowSystem) {
$s->allowSystemCustomFields = true;
$s->allowSystemTemplates = true;
}
if($this->InputfieldSelectorSettings) {
// populate user settings to InputfieldSelector
foreach(explode("\n", $this->InputfieldSelectorSettings) as $line) {
$line = trim($line);
$pos = strpos($line, '=');
if(!$pos) continue;
$key = substr($line, 0, $pos);
$value = substr($line, $pos+1);
$s->$key = $value;
// we will re-use 'exclude' for 'disallowColumns' Lister config option
if($key == 'exclude') {
$this->disallowColumns = array_merge($this->disallowColumns, explode(',', str_replace(' ', '', $value)));
}
}
}
return $s;
}
public function ___execute() {
/*
// @todo section hook should be enabled only if sticky-header.js is loaded
if($this->get('responsiveTable') === null) {
$this->addHookBefore('MarkupAdminDataTable::render', function($event) {
$table = $event->object;
$table->setResponsive(false);
});
} else {
$this->set('responsiveTable', false);
}
// @todo end
*/
$out = parent::___execute();
$config = $this->wire('config');
if(!$config->ajax) {
if(!method_exists($this->parentClass, 'prepareExternalAssets')) {
$this->prepareExternalAssets();
}
}
return $out;
}
/**
* Find and render the results (ajax)
*
* This is only called if the request comes from ajax
*
* @param string|null $selector
* @return string
*
*/
protected function ___renderResults($selector = null) {
$out = parent::___renderResults($selector);
if($this->editOption != self::editOptionNone) {
$confirmText = $this->_('There are unsaved changes. If you proceed, you will lose these changes:');
$formClass =
'Inputfield InputfieldWrapper InputfieldForm InputfieldFormConfirm ' .
//'InputfieldFormNoWidths InputfieldFormNoHeights ' .
'InputfieldFormNoDependencies InputfieldAllowAjaxUpload';
if($this->editPreload) $formClass .= " ListerEditPreload";
$out =
"<form id='table_editable' action='./' method='post' class='$formClass' data-confirm='$confirmText'>" .
//"<div class='Inputfields'>$out</div>" .
$out .
$this->wire('session')->CSRF->renderInput() .
"</form>";
}
if($this->wire('config')->ajax) {
if(strpos($out, "<div id='ProcessListerScript'>") === false) {
$out .= $this->renderExternalAssets();
}
} else if(!method_exists($this->parentClass, 'prepareExternalAssets')) {
$this->prepareExternalAssets();
}
return $out;
}
/**
* Prepare the session values for external assets, to be called during non-ajax request only
*
* This method can be removed once this version of ListerPro is always used with PW 2.6.14+
* as the method has been moved to the core ProcessPageLister.module file
*
*/
public function prepareExternalAssets() {
$config = $this->wire('config');
/*
// @todo make these available as extras that can be enabled (or not) from config tab
$url = $config->urls->ProcessPageListerPro;
$config->scripts->add($url . 'extras/multi-row-select.js');
$config->scripts->add($url . 'extras/sticky-header.js');
$config->styles->add($url . 'extras/sticky-header.css');
*/
if(method_exists($this->parentClass, "prepareExternalAssets")) {
parent::prepareExternalAssets();
return;
}
$loadedFiles = array();
$loadedJSConfig = array();
$regex = '!(Inputfield|Fieldtype|Language|Process|Jquery|Markup)!';
foreach($config->scripts as $file) {
if(!preg_match($regex, $file)) continue;
$loadedFiles[] = $file;
}
foreach($config->styles as $file) {
if(!preg_match($regex, $file)) continue;
$loadedFiles[] = $file;
}
foreach($config->js() as $key => $value) {
$loadedJSConfig[] = $key;
}
$this->sessionSet('loadedFiles', $loadedFiles);
$this->sessionSet('loadedJSConfig', $loadedJSConfig);
}
/**
* Return a markup string with scripts that load external assets for an ajax request
*
* @return string
*
*/
public function renderExternalAssets() {
if(method_exists($this->parentClass, 'renderExternalAssets')) {
return parent::renderExternalAssets();
}
// all the following can be removed after this version of ListerPro is used with PW 2.6.14+
// because this method has been moved to the core ProcessPageLister.module file
$script = '';
$regex = '!(Inputfield|Fieldtype|Language|Process|Jquery|Markup)!';
$scriptClose = '';
$loadedFiles = $this->sessionGet('loadedFiles');
if(is_null($loadedFiles)) $loadedFiles = array();
$loadedFilesAdd = array();
$loadedJSConfig = $this->sessionGet('loadedJSConfig');
$config = $this->wire('config');
$out = '';
foreach($config->scripts as $file) {
if(strpos($file, 'ProcessPageLister')) continue;
if(!preg_match($regex, $file)) continue;
if(in_array($file, $loadedFiles)) {
// script was already loaded and can be skipped
// if($this->wire('config')->debug) $script .= "\nconsole.log('skip: $file');";
} else {
// new script that needs loading
//$script .= "\n<script src='$file'></script>";
if($script) $script .= "\n";
$script .= "$.getScript('$file', function(data, textStatus, jqxhr){";
// "console.log(textStatus); ";
$scriptClose .= "})";
$loadedFilesAdd[] = $file;
}
}
$script .= $scriptClose;
foreach($config->styles as $file) {
if(strpos($file, 'ProcessPageLister')) continue;
if(!preg_match($regex, $file)) continue;
if(!in_array($file, $loadedFiles)) {
$script .= "\n$('<link rel=\"stylesheet\" type=\"text/css\" href=\"$file\">').appendTo('head');"; // console.log('$file');</script>";
$loadedFilesAdd[] = $file;
}
}
if(count($loadedFilesAdd)) {
$loadedFiles = array_merge($loadedFiles, $loadedFilesAdd);
$this->sessionSet('loadedFiles', $loadedFiles);
}
$jsConfig = array();
foreach($this->wire('config')->js() as $property => $value) {
if(!in_array($property, $loadedJSConfig)) {
$loadedJSConfig[] = $property;
$jsConfig[$property] = $value;
}
}
if(count($jsConfig)) {
$script .= "\n\n" . 'var configAdd=';
$script .= json_encode($jsConfig) . ';';
$script .= "\n\n" . '$.extend(config, configAdd);';
//$script .= "console.log(configAdd);";
$this->sessionSet('loadedJSConfig', $loadedJSConfig);
}
$out .= "<div id='ProcessListerScript'>$script</div>";
return $out;
}
/**
* Save editColumns
*
* Looks for a $_POST['_changes'] var containing CSV list of changes in format:
* 123.456,123.456,123.456 where 123 is page ID and 456 is field ID and commas
* separate each page.field combination.
*
*/
public function ___executeSave() {
if($this->editOption == self::editOptionNone) return '';
$result = array('debug' => array());
$changes = $this->wire('input')->post('_changes');
if(empty($changes)) return $result;
$changes = array_unique(explode(',', $changes));
$pages = array();
$pagesChanges = array();
foreach($changes as $change) {
if(!strpos($change, '.')) continue;
list($pageID, $fieldID) = explode('.', $change);
$pageID = (int) $pageID;
if(!$pageID) continue;
$page = isset($pages[$pageID]) ? $pages[$pageID] : $this->wire('pages')->get($pageID);
if(!$page->id || !$page->editable() || $page->isLocked()) continue;
if(!isset($pages[$pageID])) $pages[$pageID] = $page;
$suffix = $this->getInputfieldSuffix($page);
$field = null;
if($fieldID === 'name' || $fieldID === 'parent') {
$fieldName = $fieldID;
} else {
$fieldID = (int) $fieldID;
if(!$fieldID) continue;
$field = $this->wire('fields')->get($fieldID);
if(!$field) continue;
$fieldName = $field->name;
}
if(!$page->id || !$page->editable($fieldName)) continue;
if($this->editOption == self::editOptionSome && !in_array($fieldID, $this->editColumns)) continue;
if($page->isLocked()) continue;
$page->of(false);
$this->editorPage = $page;
$inputfield = null;
if($field) {
$inputfield = $field->getInputfield($page, $suffix);
} else if($fieldName == 'name') {
$inputfield = $this->getNameInputfield($page);
} else if($fieldName == 'parent') {
$inputfield = $this->getParentInputfield($page);
}
if(!$inputfield) continue;
$value = $page->getUnformatted($fieldName);
if($fieldName == 'parent') $value = $value->id;
$inputfield->attr('value', $value);
$inputfield->resetTrackChanges(true);
$inputfield->processInput($this->wire('input')->post);
if(!isset($pagesChanges[$page->id])) $pagesChanges[$page->id] = array();
if($inputfield->isChanged()) {
// $valuePrevious = $value;
if(is_object($value) && $value instanceof LanguagesValueInterface) {
$value->setFromInputfield($inputfield);
} else if($fieldName == 'parent') {
$newParent = $this->wire('pages')->get((int) $inputfield->attr('value'));
if($newParent->id && $newParent->id != $value && $newParent->addable($page)) $value = $newParent;
} else {
$value = $inputfield->attr('value');
}
$result['debug'][] = "$fieldName=$value";
$page->set($fieldName, $value);
$page->trackChange($fieldName);
if($value instanceof WireArray) $value->trackChange('value');
$pagesChanges[$page->id][$fieldName] = $fieldName;
/*
array(
// TMP: change back to just $fieldName after debugging
'old' => (string) $valuePrevious,
'new' => (string) $value,
'new2' => (string) $page->get($fieldName),
'process' => (string) $this->wire('process')
);
*/
}
}
// commit any temporary files to be permanent
foreach($pages as $page) {
/** @var Page $page */
$this->editorPage = $page;
foreach($page->template->fieldgroup as $f) {
if(!$f->type instanceof FieldtypeFile) continue;
$pagefiles = $page->get($f->name);
foreach($pagefiles as $pagefile) {
/** @var Pagefile $pagefile */
if(!$pagefile->isTemp()) continue;
$pagefile->isTemp(false);
$pagesChanges[$page->id][$f->name] = $f->name;
$page->trackChange($f->name);
}
// if limited to 1 file, remove leading file(s) so that only the last remains
if($f->maxFiles == 1) while(count($pagefiles) > 1) {
$item = $pagefiles->first();
$pagefiles->remove($item);
$page->trackChange($f->name);
$pagesChanges[$page->id][$f->name] = $f->name;
}
}
}
$numErrors = 0;
$result['changes'] = $pagesChanges;
foreach($pagesChanges as $pageID => $changes) {
$page = $pages[$pageID];
$this->editorPage = $page;
$numChanges = count($changes);
if(!$numChanges) continue;
if($numChanges == 1 && $this->wire('fields')->get(reset($changes))) {
reset($changes);
$change = key($changes);
if(!$this->wire('pages')->saveField($page, $change)) $numErrors++;
} else {
if(!$this->wire('pages')->save($page, array('uncacheAll' => false))) $numErrors++;
}
}
if($numErrors) {
$result['success'] = false;
$result['message'] = $this->wire('pages')->errors('all string');
} else {
$result['success'] = true;
}
header("Content-type: application/json");
return json_encode($result);
}
public function renderExtras() {
$out = '';
if($this->actions) $out .= $this->actions->render();
if($this->wire('user')->isSuperuser() && $this->isValid()) {
$out .= "<div id='ProcessListerConfigTab' title='" . $this->_x('Config', 'tab') . "' class='WireTab'></div>";
}
$out .= parent::renderExtras();
return $out;
}
/**
* Find what parent templates are active with the current init selectors
*
* @return array
*
*/
public function findParentTemplates() {
static $cache = array();
// if a specific parent is defined, limit to that
if($this->parent && $this->parent->id) return array($this->parent->template);
// if this method was previously called, return cached value
if(isset($cache[$this->initSelector])) return $cache[$this->initSelector];
$parentTemplates = array();
$pageTemplates = $this->getSelectorTemplates($this->initSelector, true);
if(!count($pageTemplates)) {
$cache[$this->initSelector] = $parentTemplates;
return $parentTemplates;
}
$allTemplates = $this->wire('templates');
// first determine parent templates from family 'parentTemplates' settings
foreach($pageTemplates as $key => $template) {
if(empty($template->parentTemplates)) continue;
foreach($template->parentTemplates as $id) {
$id = (int) $id;
if(isset($parentTemplates[$id])) continue;
$template = $allTemplates->get($id);
if($template) $parentTemplates[$id] = $template;
}
unset($pageTemplates[$key]);
}
// next determine parent templates from family 'childTemplates' settings
if(count($pageTemplates)) foreach($allTemplates as $template) {
if(empty($template->childTemplates)) continue;
foreach($pageTemplates as $key => $pageTemplate) {
if(!in_array($pageTemplate->id, $template->childTemplates)) continue;
$parentTemplates[$template->id] = $template;
unset($pageTemplates[$key]);
}
}
// if we have anything leftover, determine it from existing data in the DB
foreach($pageTemplates as $template) {
$sql =
'SELECT parents.templates_id FROM pages ' .
'JOIN pages AS parents ON parents.id=pages.parent_id ' .
'WHERE pages.templates_id=:templateID ' .
'AND pages.status<=:pageStatus ' .
'AND parents.status<=:parentStatus ' .
'GROUP BY parents.templates_id LIMIT 100';
$query = $this->wire('database')->prepare($sql);
$query->bindValue(':templateID', $template->id, PDO::PARAM_INT);
$query->bindValue(':pageStatus', Page::statusUnpublished, PDO::PARAM_INT);
$query->bindValue(':parentStatus', Page::statusUnpublished, PDO::PARAM_INT);
$query->execute();
while($row = $query->fetch(PDO::FETCH_NUM)) {
list($id) = $row;
$id = (int) $id;
if(isset($parentTemplates[$id])) continue;
$template = $allTemplates->get($id);
if($template) $parentTemplates[$id] = $template;
}
}
$cache[$this->initSelector] = $parentTemplates;
return $parentTemplates;
}
/**
* Build the Lister filters form
*
* @return InputfieldForm
*
*/
protected function buildFiltersForm() {
$form = parent::buildFiltersForm();
/** @var InputfieldSelector $f */
$f = $form->getChildByName('filters');
if(in_array('collapseFilters', $this->toggles)) $f->collapsed = Inputfield::collapsedYes;
if(in_array('noNewFilters', $this->toggles)) $f->allowAddRemove = false;
$f->showFieldLabels = (int) $this->useColumnLabels;
return $form;
}
/**
* Build the columns asmSelect
*
*/
public function buildColumnsField() {
/** @var InputfieldAsmSelect $f */
$f = parent::buildColumnsField();
if(!$this->isValid()) return $f;
$nullPage = new NullPage();
$options = $f->getOptions();
$options2 = array();
$systemColumns = $this->getSystemColumns();
$systemLabels = $this->systemLabels;
$useLabels = (bool) $this->useColumnLabels;
$parentLabel = $this->_('Parent');
$limitFields = count($this->limitFields) ? $this->limitFields : null;
/** @var Languages $languages */
$languages = $this->wire('languages');
// specific to the parent.[subfield] properties
foreach($systemColumns as $name) {
$value = "parent.$name";
if($limitFields && !in_array($value, $limitFields)) continue;
$label = isset($systemLabels[$name]) ? $systemLabels[$name] : $name;
$label = $parentLabel . ' > ' . $label;
$options2[$value] = $label;
}
$parentTemplates = $this->findParentTemplates();
foreach($parentTemplates as $template) {
foreach($template->fieldgroup as $field) {
/** @var Field $field */
if(count($parentTemplates) == 1) {
$_field = $template->fieldgroup->getField($field->name, true); // get in context
if($_field) $field = $_field;
}
$value = "parent.$field->name";
if($limitFields && !in_array($value, $limitFields)) continue;
$label = $parentLabel . ' > ' . $field->getLabel();
$options2[$value] = $label;
}
}
$initTemplate = $this->template;
/** @var Fields $fields */
$fields = $this->wire('fields');
// all other fields
foreach($fields as $field) {
if(!isset($options[$field->name])) {
if(!$this->allowColumnField($field)) continue;
// if paired with an older Lister that may have missed the field (in certain cases) on the first pass
$options2[$field->name] = $field->getLabel();
}
if($initTemplate) {
$_field = $initTemplate->fieldgroup->getField($field->name, true); // context
if($_field) $field = $_field;
}
$info = $field->type->getSelectorInfo($field);
//$typeName = $field->type->className();
if(count($info['subfields'])) {
foreach($info['subfields'] as $name => $subinfo) {
$value = "$field->name.$name";
$label = $field->getLabel() . ' > ';
if(isset($systemLabels[$name])) {
$label .= $systemLabels[$name];
} else if($languages && $field->type instanceof FieldtypeLanguageInterface && strpos($name, 'data') === 0) {
if($name == 'data') {
$language = $languages->getDefault();
$label .= $language->get('title|name');
} else if(preg_match('/^data(\d+)$/', $name, $matches)) {
$language = $languages->get((int) $matches[1]);
$label .= $language->get('title|name');
} else {
$label .= $name;
}
} else {
$_f = $this->wire('fields')->get($name);
if($_f) {
$label .= $_f->getLabel();
} else {
$label .= $name;
}
}
$options2[$value] = $label;
}
$blankValue = $field->type->getBlankValue($nullPage, $field);
if($blankValue instanceof Page || $blankValue instanceof PageArray) {
foreach($systemColumns as $name) {
$label = $field->getLabel() . ' > ';
$label .= isset($systemLabels[$name]) ? $systemLabels[$name] : $name;
$options2["$field->name.$name"] = $label;
}
}
}
}
ksort($options2);
foreach($options2 as $value => $label) {
if($useLabels) $f->addOption($value, $label, array('data-desc' => $value));
else $f->addOption($value, $value, array('data-desc' => $label));
}
$f->attr('value', $this->columns);
if(in_array('collapseColumns', $this->toggles)) $f->collapsed = Inputfield::collapsedYes;
return $f;
}
public function renderButtons() {
$out = parent::renderButtons();
if($this->editOption != self::editOptionNone) {
if($out) $out .= "&nbsp;";
$btn = $this->wire('modules')->get('InputfieldButton');
$btn->attr('id', 'save_edits');
$btn->attr('value', $this->_('Save Edits'));
$btn->addClass('save_edits head_button_clone');
$btn->icon = 'save';
$out .= $btn->render();
}
return $out;
}
/**
* Build the Lister table column from a Page and column name
*
* @param Page $p
* @param array $fields
* @param string $name
* @param mixed $value
* @return string
*
*/
protected function buildListerTableCol(Page $p, array $fields, $name, $value = null) {
// whether or not to render the editor output in this request
if($this->editPreload) {
$getEditor = true;
} else {
static $getEditor = null;
if(is_null($getEditor)) $getEditor = (int) $this->wire('input')->post('editor');
}
if(strpos($name, '.') !== false) {
list($basename, $subname) = explode('.', $name);
if($subname == 'data') $subname = '';
} else {
$basename = $name;
$subname = '';
}
if(is_null($value)) $value = $p->getUnformatted($basename);
if($value instanceof Pageimages && $this->imageStyle == 0 && !$subname) {
// this block can be removed when this version of LP requires PW 2.7+
$value = $value->getArray();
if($this->imageFirst && count($value) > 1) $value = array_slice($value, 0, 1);
}
$colPreview = parent::buildListerTableCol($p, $fields, $name, $value);
if($this->editOption == self::editOptionNone) return $colPreview;
if($this->editOption == self::editOptionSome && !count($this->editColumns)) return $colPreview;
$tryEditor = true;
if(!strlen($colPreview)) {
$field = $this->wire('fields')->get($name);
$colPreview .= "<span class='ui-priority-secondary detail'>";
if($field && !$p->template->fieldgroup->hasField($field)) {
$colPreview .= $this->_x('N/A', 'na-col-preview'); // Field is not applicable to page
$tryEditor = false;
} else {
$colPreview .= $this->_x('Blank', 'blank-col-preview'); // Field is blank on page
}
$colPreview .= "</span>";
}
if($p->isLocked()) $tryEditor = false;
if(!$tryEditor) return $colPreview;
if($getEditor) {
$colEditor = $this->buildListerTableColEditor($p, $fields, $name, $value);
} else {
$colEditor = "<div class='col_editor col_editor_inactive'></div>";
}
if(!strlen($colEditor)) return $colPreview;
$out =
"<div class='col_editable'>" .
"<div class='col_preview'>$colPreview</div>" .
"$colEditor" .
"</div>";
return $out;
}
/**
* Build the Lister table column editor
*
* @param Page $p
* @param array $fields
* @param string $name
* @param mixed $value
* @return string Returns blank string when editor not available
*
*/
protected function buildListerTableColEditor(Page $p, array $fields, $name, $value) {
/** @var Inputfield $inputfield */
$inputfield = null;
/** @var Field $field */
$field = null;
$suffix = $this->getInputfieldSuffix($p);
if($name == 'name') {
$inputfield = $this->getNameInputfield($p);
if(!$inputfield) return '';
$fieldID = 'name';
} else if($name == 'parent') {
$inputfield = $this->getParentInputfield($p);
if(!$inputfield) return '';
$fieldID = 'parent';
} else {
$field = isset($fields[$name]) ? $fields[$name] : $this->wire('fields')->get($name);
if(!$p->editable($name)) return '';
$fieldID = $field ? $field->id : '';
if(!$inputfield && empty($field)) return '';
if($field && $this->editOption == self::editOptionSome && !in_array($field->id, $this->editColumns)) return '';
if($field && count($this->editFieldtypes) && !in_array($field->type->className(), $this->editFieldtypes)) return '';
}
$this->editorPage = $p;
if(is_null($value)) $value = $p->getUnformatted($name);
if(!$inputfield && $field) $inputfield = $field->getInputfield($p, $suffix);
if(!$inputfield) return '';
$inputfield->attr('value', $value);
$inClass = $inputfield->className();
$header = '';
$outReplacements = array(); // text replacements in rendered markup, when applicable
// i.e. InputfieldCheckboxes, InputfieldRadios: not enough room for option columns in Lister table
/** @var InputfieldCheckboxes|InputfieldRadios $inputfield */
if($inputfield->optionColumns) {
$inputfield->optionColumns = 0;
}
if($inClass == 'InputfieldPage') {
/** @var InputfieldPage $inputfield */
// special handling for Page fields using a findPagesSelector
$findPagesSelector = $field->get('findPagesSelector');
if($findPagesSelector && strpos($findPagesSelector, '=page.') !== false) {
if(preg_match('!([^,\s=]+)\s*=\s*page\.([^,\s]+)[,\s]*!', $findPagesSelector, $matches)) {
$f = $this->wire('fields')->get($matches[2]);
if($f) {
if(!in_array($f->name, $this->columns) || !in_array($f->id, $this->editColumns)) {
// remove the dependency requirement if dependency field isn't present and editable in the columns
$inputfield->findPagesSelector = str_replace($matches[0], '', $findPagesSelector);
} else {
$outReplacements['!' . $matches[1] . '=\s*page.' . $f->name . '\b!'] = "$matches[1]$suffix=page.{$f->name}$suffix";
}
}
}
}
} else if($inClass == 'InputfieldCKEditor') {
/** @var InputfieldCKEditor $inputfield */
$inputfield->configName = 'InputfieldCKEditor_' . $field->name;
} else if($inputfield instanceof InputfieldItemList) {
$inClass .= ' InputfieldItemList';
$header = "<div class='InputfieldHeader'>$inputfield->label</div>";
}
if($field && $field->type instanceof FieldtypeImage) {
/** @var InputfieldImage $inputfield */
$inputfield->useImageEditor = false;
}
// render the Inputfield
$inputfield->renderReady();
$label = $this->wire('sanitizer')->entities1($inputfield->label);
$desc = $field ? $field->getDescription() : '';
$desc = $desc ? "<p class='description'>" . $this->wire('sanitizer')->entities($desc) . '</p>' : '';
$notes = $field ? $field->getNotes() : '';
$notes = $notes ? "<p class='notes'>" . $this->wire('sanitizer')->entities($notes) . '</p>' : '';
$out = $inputfield->render();
$inClass = trim("$inClass Inputfield_$name $inputfield->wrapClass");
// apply any applicable text replacements
foreach($outReplacements as $find => $replace) {
if(strpos($find, '!') === 0) {
$out = preg_replace($find, $replace, $out); // regex
} else {
$out = str_replace($find, $replace, $out); // regular
}
}
// return rendered Inputfield markup
return "<ul class='Inputfields'><li " .
"id='wrap_Inputfield_$inputfield->name' " .
"class='col_editor Inputfield $inClass' " .
"data-pid='$p->id' " .
"data-fid='$fieldID' " .
"data-label='$label'>" .
$header .
"<div class='InputfieldContent'>$desc$out$notes</div>" .
"</li></ul>";
}
/**
* Get the ListerPro specific suffix to use for an Inputfield for a given Page
*
* @param Page $p
* @return string
*
*/
protected function getInputfieldSuffix(Page $p) {
return "_LPID$p->id";
}
/**
* Get the Inputfield module to edit the 'name' property of a Page
*
* @param Page $p
* @return null|Inputfield
*
*/
protected function getNameInputfield(Page $p) {
if($this->editOption == self::editOptionSome && !in_array('name', $this->editColumns)) return null;
if(!$p->editable('name')) return null;
$suffix = $this->getInputfieldSuffix($p);
$inputfield = $this->wire('modules')->get('InputfieldPageName');
$inputfield->attr('id+name', "name" . $suffix . '__');
$inputfield->required = $p->id != 1;
$inputfield->slashUrls = $p->template->slashUrls;
$inputfield->checkboxSuffix = $suffix;
$inputfield->parentPage = $p->parent;
return $inputfield;
}
/**
* Get the Inputfield module to edit the 'parent' property of a Page
*
* @param Page $p
* @param int $maxParents
* @return null|Inputfield
*
*/
protected function getParentInputfield(Page $p, $maxParents = 100) {
if($p->id == 1) return null;
if($this->editOption == self::editOptionSome && !in_array('parent', $this->editColumns)) return null;
if(!$p->editable('parent')) return null;
$parents = $this->getAllowedParents($p, $maxParents);
if($parents->count()) {
// limited number of parents available, so we can use a regular select
$inputfield = $this->wire('modules')->get('InputfieldSelect');
foreach($parents as $parent) $inputfield->addOption($parent->id, $parent->get('title|name'));
$inputfield->attr('value', $p->parent->id);
} else {
// large quantity of parents available, so use a PageListSelect
$inputfield = $this->wire('modules')->get('InputfieldPageListSelect');
$inputfield->attr('value', $p->parent);
}
$inputfield->label = $this->_('Parent');
$inputfield->attr('id+name', 'parent' . $this->getInputfieldSuffix($p));
$inputfield->required = true;
return $inputfield;
}
/**
* Return the parent pages allowed for given $page
*
* If there are too many possibilities, an empty PageArray is returned.
*
* @param Page $page
* @param int $maxParents Maximum number of parents you will accept
* @return PageArray
*
*/
protected function getAllowedParents(Page $page, $maxParents = 100) {
$template = $page->template;
$parents = new PageArray();
$parents->add($page->parent);
// if page isn't allowed to be moved, then just return current parent
if($template->noMove) return $parents;
// if user doesn't have permission to move pages then only return current parent
if(!$this->wire('user')->hasPermission('page-move', $page)) return $parents;
/** @var Templates $templates */
$templates = $this->wire('templates');
$parentTemplates = array();
// this section populates the parentTemplates array
if(count($template->parentTemplates)) {
// page has specific parent templates that are allowed
foreach($template->parentTemplates as $tid) {
$tid = (int) $tid;
$t = $templates->get($tid);
if(!$t) continue;
// verify that parent template also accepts our $page as a child
if(count($t->childTemplates) && !in_array($template->id, $t->childTemplates)) continue;
$parentTemplates[$tid] = $tid;
}
// there are specified parent templates, but none resolved, so just return current parent
if(!count($parentTemplates)) return $parents;
} else {
// no parent templates specified, iterate all templates to see if any specify page's template as allowed for children
foreach($templates as $t) {
if(!count($t->childTemplates)) continue;
if(!in_array($template->id, $t->childTemplates)) continue;
$parentTemplates[$t->id] = $t->id;
}
}
if(!count($parentTemplates)) {
// anything may be possible, so return blank PageArray
return new PageArray();
}
// determine how many possible parents there are
$selector = "include=all, template=" . implode('|', $parentTemplates);
$qty = $this->wire('pages')->count($selector);
if($qty > $maxParents) return new PageArray(); // too many possibilities
$parents = $this->wire('pages')->find("$selector, sort=title, sort=name");
$hasCurrent = false;
foreach($parents as $parent) {
// check if parent is the same one we currently have, then it's always allowed
if($parent->id == $page->parent->id) {
$hasCurrent = true;
continue;
}
// double check: if user isn't allowed to add this $page to this parent, remove it
if(!$parent->addable($page)) $parents->remove($parent);
}
if(!$hasCurrent) $parents->prepend($page->parent);
return $parents;
}
public function getPage() {
return $this->editorPage ? $this->editorPage : new NullPage();
}
/**
* Execute Page Actions
*
*/
public function ___executeActions() {
if(!$this->isValid()) return 'Product key required';
return $this->actions->execute();
}
/**
* Execute individual Lister config
*
*/
public function ___executeConfig() {
if(!$this->wire('user')->isSuperuser()) throw new WireException('This feature is only available to superuser.');
if(!$this->isValid()) return '';
$this->wire('breadcrumbs')->add(new Breadcrumb('../', $this->page->title));
$this->wire('processHeadline', $this->_('Configure Lister'));
$listerConfig = new ListerProConfig($this);
return $listerConfig->buildForm()->render();
}
public function isValid() {
if(strpos($this->licenseKey, 'PWLP') === 0) return true;
$this->error("Please provide a valid product key in the ListerPro module settings");
return false;
}
public function getListerPageByID($id) {
$moduleID = $this->wire('modules')->getModuleID($this);
return $this->wire('pages')->get("process=$moduleID, template=admin, id=" . (int) $id);
}
public function getAllListerPages() {
$moduleID = $this->wire('modules')->getModuleID($this);
return $this->wire('pages')->find("process=$moduleID, template=admin, sort=created, include=hidden");
}
/**
* ListerPro Module Configuration Screen
*
* @param array $data
* @return InputfieldWrapper
*
*/
public static function getModuleConfigInputfields(array $data) {
$form = new InputfieldWrapper();
/** @var InputfieldText $f */
$f = wire('modules')->get('InputfieldText');
$f->attr('id+name', 'licenseKey');
$licenseKey = isset($data['licenseKey']) ? $data['licenseKey'] : '';
if(wire('input')->post->licenseKey && wire('input')->post->licenseKey != wire('session')->ListerLicenseKey) {
// validate
$http = new WireHttp();
$license = wire('sanitizer')->text(wire('input')->post->licenseKey);
$data = array(
'action' => 'validate',
'license' => $license,
'host' => wire('config')->httpHost,
'ip' => ip2long(wire('session')->getIP())
);
$result = $http->post('http://processwire.com/validate-product/', $data);
if($result === 'valid') {
$licenseKey = $license;
$f->notes = "Validated!";
wire()->message("ListerPro product key has been validated!");
} else {
$licenseKey = '';
$error = "Unable to validate product key: $result";
$f->error($error);
wire()->error($error);
}
}
if(empty($licenseKey)) wire('input')->post->__unset('licenseKey');
$f->attr('value', $licenseKey);
$f->required = true;
$f->label = "Product Key";
if($licenseKey) $f->label .= " - VALIDATED!";
$f->attr('value', wire('config')->demo ? 'disabled for demo mode' : $licenseKey);
$f->icon = $licenseKey ? 'check-square-o' : 'question-circle';
$f->description = "Paste in your ListerPro product support key.";
$f->notes = "If you did not purchase the ListerPro for this site, please [purchase a product key here](http://processwire.com/ListerPro/).";
if($licenseKey) $f->collapsed = Inputfield::collapsedYes;
$form->add($f);
wire('session')->set('ListerLicenseKey', $licenseKey);
/** @var InputfieldAsmSelect $f */
$f = wire('modules')->get('InputfieldAsmSelect');
$f->attr('name', 'editFieldtypes');
$f->label = __('Fieldtypes allowed for column editing in ListerPro');
$f->collapsed = Inputfield::collapsedYes;
$f->icon = 'pencil';
$defaultSelected = array();
$skipDefaults = array('FieldtypePassword', 'FieldtypeRepeater', 'FieldtypePageTable');
foreach(wire('fieldtypes') as $fieldtype) {
/** @var Fieldtype $fieldtype */
$className = $fieldtype->className();
if(strpos($className, 'FieldtypeFieldset') !== false) continue;
$info = wire('modules')->getModuleInfoVerbose($fieldtype);
if(!empty($info['core']) && !in_array($className, $skipDefaults)) $defaultSelected[] = $className;
$f->addOption($className, $info['title']);
}
if(empty($data['editFieldtypes'])) {
$f->attr('value', $defaultSelected);
} else {
$f->attr('value', $data['editFieldtypes']);
}
$form->add($f);
if($licenseKey) {
/** @var ProcessPageListerPro $listerPro */
$listerPro = wire('modules')->get('ProcessPageListerPro');
$pagesWithListers = $listerPro->getAllListerPages();
if(wire('input')->post('licenseKey')) {
wire('modules')->addHookAfter('saveModuleConfigData', $listerPro, 'processConfigActions');
}
// input to add new lister
$f = wire('modules')->get('InputfieldText');
$f->attr('id+name', '_new_lister_title');
$f->label = __('Add a Lister');
$f->icon = 'plus-circle';
$f->description = __('Enter the title for the lister you want to create. A new page containing your Lister will be created in your admin Pages navigation. You may move the page elsewhere if you prefer.'); // Add new Lister description
$form->add($f);
// input to clone existing lister
if(count($pagesWithListers)) {
$f = wire('modules')->get('InputfieldSelect');
$f->attr('name', '_clone_lister');
$f->label = __('Clone a Lister');
$f->description = __('To clone a Lister, select the Lister you want to clone here and enter the title of the new Lister in the "Add a Lister" field above.');
$f->icon = 'copy';
$f->attr('onchange', "if($(this).val().length) $('#_new_lister_title').focus()");
$f->collapsed = Inputfield::collapsedYes;
foreach($pagesWithListers as $p) {
$f->addOption($p->id, $p->title);
}
$form->add($f);
}
// show all EXISTING listers
if(count($pagesWithListers)) {
$f = wire('modules')->get('InputfieldMarkup');
$f->attr('name', '_lister_list');
$f->label = __('Your Listers');
/** @var MarkupAdminDataTable $table */
$table = wire('modules')->get('MarkupAdminDataTable');
$table->setEncodeEntities(false);
$table->headerRow(array(
__('Lister Title'),
__('Created'),
__('Actions'),
'&nbsp;',
));
foreach($pagesWithListers as $p) {
$table->row(array(
"<a href='$p->url'>$p->title</a>",
"<span style='white-space:nowrap;'>" . date(wire('config')->dateFormat, $p->created) . "</span>",
"<a href='{$p->url}config/'>" . __('configure') . "</a>",
"<label style='float: right;'>" .
"<input type='checkbox' onclick='$(\"#delete_confirm\").show()' name='_delete_lister[]' value='$p->id' />&nbsp;" .
"<i class='fa fa-trash-o'></i>" .
"</label>"
));
}
$f->value = $table->render() .
"<p id='delete_confirm' style='display: none; margin-top: -1em;'>" .
"<strong>" .
__('Are you sure you want to delete the checked Lister(s) above?') .
"</strong><br />" .
__('Check this box to confirm:') . ' ' .
"<label style='display:inline;'><input type='checkbox' name='_delete_confirm' value='1' />&nbsp;" .
"<i class='fa fa-trash-o'></i></label>" .
"</p>";
$form->add($f);
}
}
return $form;
}
public function processConfigActions(HookEvent $e) {
static $level = 0;
$level++;
if($level > 1) return;
if($e) {}
// check for NEW listers
$title = wire('input')->post('_new_lister_title');
if($title) {
$cloneID = wire('input')->post('_clone_lister');
if($cloneID) {
$clonePage = $this->getListerPageByID($cloneID);
if($clonePage->id) {
$this->cloneLister($clonePage, $title);
}
} else {
$this->addNewLister($title);
}
}
// check for DELETED Listers
$deleteIDs = wire('input')->post('_delete_lister');
if(count($deleteIDs)) {
if(wire('input')->post('_delete_confirm')) {
foreach($deleteIDs as $pageID) {
$deletePage = $this->getListerPageByID($pageID);
if($deletePage->id) $this->deleteLister($deletePage);
}
} else {
$this->error(__('Delete was not confirmed'));
}
}
}
/**
* Placeholder for the viewport iframe
*
* @param bool $exit If true, execution will stop after this method call.
*
*/
public function executeViewport($exit = true) {
echo "<pre>
__ _ __
/ / (_)____/ /____ _____
/ / / / ___/ __/ _ \/ ___/
/ /___/ (__ ) /_/ __/ /
/_____/_/____/\__/\___/_/ PRO
\n";
if($exit) exit;
}
/**
* Create a new Lister
*
* @param string $title Title of Lister to create
* @return Page Returns the page where the new Lister resides or NullPage on failure
*
*/
public function addNewLister($title) {
$page = new Page();
$page->template = 'admin';
$admin = $this->wire('pages')->get($this->wire('config')->adminRootPageID);
$parent = $admin->child('name=page, include=all');
$page->parent = $parent->id ? $parent : $admin;
$page->process = $this->wire('modules')->get('ProcessPageListerPro');
$page->title = $title;
try {
$page->save();
$this->message($this->_('Created Lister') . ' - ' . $page->title);
} catch(Exception $e) {
$this->error("Error creating lister - " . $e->getMessage());
$page = new NullPage();
}
return $page;
}
/**
* Clone the Lister living on $page to a new lister titled $newListerTitle
*
* @param Page $page
* @param $newListerTitle
* @return Page Returns the new Lister page
*
*/
public function cloneLister(Page $page, $newListerTitle) {
$newPage = $this->addNewLister($newListerTitle);
if(!$newPage->id) return $newPage; // NullPage
if($newPage->parent_id != $page->parent_id) {
$newPage->parent = $page->parent;
$newPage->save();
}
$configData = $this->wire('modules')->getModuleConfigData($this);
$settings = isset($configData['settings'][$page->name]) ? $configData['settings'][$page->name] : array();
$settings['pagename'] = $newPage->name;
$configData['settings'][$newPage->name] = $settings;
$this->wire('modules')->saveModuleConfigData($this, $configData);
$this->message(sprintf($this->_('Cloned Lister: %1$s => %2$s'), $page->name, $newPage->name));
return $newPage;
}
/**
* Delete the Lister that exists on the given Page
*
* Also deletes the page.
*
* @param Page $page
* @return bool Returns true on success, false on fail.
*
*/
public function deleteLister(Page $page) {
$configData = $this->wire('modules')->getModuleConfigData($this);
if(isset($configData['settings'][$page->name])) {
unset($configData['settings'][$page->name]);
$this->wire('modules')->saveModuleConfigData($this, $configData);
}
$className = $this->className();
if(((string) $page->getUnformatted('process')) == $className) {
$this->wire('pages')->delete($page);
$this->message(sprintf($this->_('Deleted Lister at %s'), $page->path));
return true;
} else {
$this->error(sprintf($this->_('Page %1$s does not appear to be a %2$s'), $page->path, $className));
return false;
}
}
/**
* Install ListerPro: Convert existing pages using Lister to use ListerPro
*
*/
public function ___install() {
$data = $this->wire('modules')->getModuleConfigData('ProcessPageLister');
if(!empty($data)) $this->wire('modules')->saveModuleConfigData('ProcessPageListerPro', $data);
$moduleID = $this->wire('modules')->getModuleID('ProcessPageLister');
if(!$moduleID) return;
$pages = $this->wire('pages')->find("template=admin, process=$moduleID, include=all");
foreach($pages as $page) {
$page->of(false);
$page->process = $this;
$page->save('process');
$this->message("Updated $page->path to use $this");
}
}
/**
* Uninstall ListerPro: Convert pages using ListerPro back to use Lister
*
*/
public function ___uninstall() {
$moduleID = $this->wire('modules')->getModuleID($this);
if(!$moduleID) return;
$pages = $this->wire('pages')->find("template=admin, process=$moduleID, include=all");
foreach($pages as $page) {
$page->of(false);
$page->process = 'ProcessPageLister';
$page->save('process');
$this->message("Reverted $page->path to use ProcessPageLister (rather than ProcessPageListerPro).");
}
}
public function ___upgrade($fromVersion, $toVersion) {
// also trigger any pending upgrades from ProcessPageLister
$this->wire('modules')->get('ProcessPageLister');
}
}