Rev 22 | Blame | Compare with Previous | Last modification | View Log | Download
<?php namespace ProcessWire;/*** Class PageFrontEdit** Enables front-end editing of page fields.** @property array $inlineEditFields* @property string $buttonLocation* @property string $buttonType* @property string $editRegionAttr* @property string $editRegionTag* @property bool|int $inlineLimitPage Limit editor to current page only* @property array $inlineAllowFieldtypes* @method Page getPage() Get page being edited (3.0.208+)**/class PageFrontEdit extends WireData implements Module {public static function getModuleInfo() {return array('title' => 'Front-End Page Editor','summary' => 'Enables front-end editing of page fields.','version' => 6,'author' => 'Ryan Cramer','license' => 'MPL 2.0','icon' => 'cube','autoload' => true,'permissions' => array('page-edit-front' => 'Use the front-end page editor',));}/*** Mode for debugging/developing this module, should always be false in production**/const debug = false;/*** Names of fields applying front end editing to** @var array of field names**/protected $inlineEditors = array();/*** Names of fields applying modal editing to** @var array of field names, indexed by "page_id-field_id"**/protected $modalEditors = array();/*** Page this front-end editor is for** @var Page|null**/protected $page = null;/*** Whether or not the editor should be applied for any requested fields** @var bool**/protected $inlineEditorActive = true;/*** The field ID of an field that is allowed for edits, even if not in inlineEditFields** @var int**/protected $inlineEditField = 0;/*** Whether or not the editor is allowed for this request** @var bool**/protected $editorAllowed = false;/*** Priority of Page::render hook** @var int**/protected $renderHookPriority = 102;/*** Priority for Fieldtype::formatValue hook** @var int**/protected $formatHookPriority = 200;/*** Editor number for setting ID attributes on .pw-edit divs** @var int**/protected $editorNum = 0;public function __construct() {parent::__construct();// allowed base Fieldtypes for inline editing (all others go modal)$this->set('inlineAllowFieldtypes', array('FieldtypeText','FieldtypeInteger'));}public function init() {if($this->wire()->config->ajax && $this->wire()->input->post('action') === 'PageFrontEditSave') {$this->addHookAfter('ProcessWire::ready', $this, 'inlineSaveEdits');}}/*** Ready state, attach hooks**/public function ready() {$page = $this->getPage();if(!$page) $page = $this->wire()->page;// check if we should allow editor for current pageif($page->template->name === 'admin') return;$this->addHookBefore('Page::edit', $this, 'hookPageEditor');$this->addHook('Page::editor', $this, 'hookPageEditor');if(isset($_GET['livepreview'])) return; // PWPD$contentType = $page->template->contentType;if($contentType && $contentType != 'html' && $contentType != 'text/html') return;$user = $this->wire()->user;if($user->isGuest()|| ($page->hasStatus(Page::statusDraft) && !$page->get('_isDraft'))|| !$page->editable()|| !$user->hasPermission('page-edit-front', $page)) {// replace any <edit> tags or "edit" attributes in the markup$this->addHookAfter('Page::render', $this, 'hookPageRenderNoEdit', array('priority' => $this->renderHookPriority));return;}// editing is allowed$this->editorAllowed = true;$this->setPage($page);$input = $this->wire()->input;$config = $this->wire()->config;$templates = $this->wire()->templates;if($config->ajax && $input->post('action') == 'PageFrontEditSave') {// skip, this is handled by another hook} else if($templates->get('admin')->https == 1 && !$config->https && !$config->noHTTPS && $page->template->https != -1) {// hooks allowed, but we need the same scheme as admin$url = $input->httpUrl(true);if(strpos($url, 'http://') === 0) {$url = str_replace('http://', 'https://', $url);$this->wire()->session->redirect($url, false);}} else {// we are allowing this page to use the editor, so attach hooksforeach($this->inlineAllowFieldtypes as $fieldtypeClass) {$this->addHookAfter("$fieldtypeClass::formatValue", $this, 'inlineHookFormatValue', array('priority' => $this->formatHookPriority));}$this->addHookAfter('Page::render', $this, 'hookPageRender', array('priority' => $this->renderHookPriority));}}/*** Get the page being edited or null if not yet set** #pw-hooker** @return Page|null* @since 3.0.208**/public function ___getPage() {return $this->page;}/*** Set the page being edited** @param Page $page**/public function setPage(Page $page) {$page->setQuietly('_PageFrontEdit', true);$this->page = $page;}/*** Is the given page and field editable on the front-end?** @param Page $page* @param Field $field* @return bool**/public function inlineIsEditable(Page $page, Field $field) {if(!$this->inlineEditorActive || !$this->editorAllowed) return false;if(!in_array($field->id, $this->inlineEditFields) && $field->id != $this->inlineEditField) return false;if($this->inlineLimitPage && $page->id != $this->page->id && in_array($field->id, $this->inlineEditFields)) return false;return $this->isEditable($page, $field);}/*** Is the given page and field saveable on the front-end?** @param Page $page* @param Field $field* @return bool**/public function inlineIsSaveable(Page $page, Field $field) {if(!$this->editorAllowed) return false;if(!$this->inlineSupported($field)) return false;// if(!in_array($field->id, $this->inlineEditFields) && $field->id != $this->inlineEditField) return false;return $this->isEditable($page, $field);}/*** Is the given page and field front-end editable?** @param Page $page* @param Field $field* @return bool**/protected function isEditable(Page $page, Field $field) {$user = $this->wire()->user;if($page->className() != 'Page' && wireInstanceOf($page, 'RepeaterPage')) {/** @var RepeaterPage $page */$forPage = $page->getForPage();$forField = $page->getForField();if(!$user->hasPermission('page-edit-front', $forPage)) return false;if(!$forPage->editable($forField)) return false;} else {if(!$user->hasPermission('page-edit-front', $page)) return false;}if(!$page->editable($field)) return false;return true;}/*** Is inline mode supported for the given field?** @param Field $field* @return bool**/public function inlineSupported(Field $field) {$classParents = wireClassParents($field->type);$className = $field->type->className();$supported = false;foreach($this->inlineAllowFieldtypes as $allowClass) {if($allowClass == $className) {$supported = true;break;} else if(in_array($allowClass, $classParents)) {$test = $field->type->getBlankValue(new NullPage(), $field);if(!is_object($test)) $supported = true;break;}}return $supported;}/*** Add Page::edit() method with this hook** This method enables any of the following:* - Retrieving the current editor status (enabled or disabled), by supplying no arguments.* - Setting the current editor status by supplying a bool (true or false).* - Retrieving an editable value ready for output by supplying a field name.* - Retrieving a value in non-editable form by supplying a field name and false as 2nd argument.** Examples:* $isActive = $page->edit(); // returns whether editor is currently active* $page->edit(true); // enables the editor (false disables)* $value = $page->edit('field_name'); // retrieve editable value, if field_name is editable* $value = $page->edit('field_name', false); // retrieve non-editable formatted value value* $value = $page->edit('field_name', '<strong>markup</strong>'); // make editable region with markup (inline or modal)* $value = $page->edit('field_name', '<strong>markup</strong>', true); // make editable region, forcing modal** @param HookEvent $event**/public function hookPageEditor(HookEvent $event) {/** @var Page $page */$page = $event->object;$arg1 = $event->arguments(0);$arg2 = $event->arguments(1);$arg3 = $event->arguments(2);$livePreview = isset($_GET['livepreview']); // PWPD$event->replace = true;if(is_null($arg1)) {// no arguments specified$event->return = $livePreview ? false : $this->inlineEditorActive;return;}if(is_bool($arg1)) {// set editor active stateif(!$livePreview) $this->inlineEditorActive = $arg1;$event->return = $page;return;}if($arg2 === false) {// request to retrieve value without editor$editorActive = $this->inlineEditorActive;$this->inlineEditorActive = false;$event->return = $page->getFormatted($arg1);$this->inlineEditorActive = $editorActive;return;}// at this point, we require arg1 to be the field nameif(!is_string($arg1)) {$event->return = 'Invalid argument to $page->edit()';return;}// if given field name doesn't map to a custom field, delegate the request to $page instead$renderField = false;$field = $this->wire()->fields->get($arg1);if(!$field) {if(strpos($arg1, '_') === 0 && substr($arg1, -1) === '_') {$renderField = true;$arg1 = substr($arg1, 1, -1);$field = $this->wire()->fields->get($arg1);if(!$field) $arg1 = "_{$arg1}_"; // restore if it didn't resolve to a field}if(!$field) {$event->return = $page->get($arg1);return;}}if(is_string($arg2)) {// make a markup string editable for indicated field, arg1 is field name, arg2 is markupif($page->editable($field->name) && !$livePreview) {// field is editable, return markup wrapped in modal editor$forceModal = $arg3 === true;if(!$forceModal && $this->inlineSupported($field)) {$this->inlineEditField = $field->id;$event->return = $this->inlineRenderEditor($page, $field, $arg2);$this->inlineEditField = 0;} else {$event->return = $this->modalRenderEditor($page, array($field), $arg2);}} else {// field is not editable by user, just return original markup$event->return = $arg2;}return;}// now try for inline editable fieldif(!$livePreview && $this->inlineIsSaveable($page, $field)) {// if inline can be used, use it...$editorActive = $this->inlineEditorActive;$this->inlineEditorActive = true;if(!in_array($field->id, $this->inlineEditFields)) $this->inlineEditField = $field->id;$event->return = $renderField ? $page->renderField($field->name) : $page->getFormatted($field->name);$this->inlineEditorActive = $editorActive;$this->inlineEditField = 0;} else {// ...otherwise just return the formatted value$event->return = $renderField ? $page->renderField($field->name) : $page->getFormatted($field->name);}}/*** Hook after Page::render for when we are supporting editable regions** @param HookEvent $event**/public function hookPageRender(HookEvent $event) {$this->editorNum = 0;$out = $event->return;if(stripos($out, '<html') === false) return;$hasEditTags = strpos($out, "<$this->editRegionTag") !== false; // i.e. <edit title>$hasEditAttr = strpos($out, " $this->editRegionAttr=") !== false; // i.e. <div edit='title'>if(strpos($out, "id=pw-edit-") === false && !$hasEditTags && !$hasEditAttr) return;/** @var Page $page */$page = $event->object;if(!$this->editorAllowed) {$this->hookPageRenderNoEdit($event);return;}// parse <edit> tags$numEditable = 0;if($hasEditTags) $numEditable += $this->populateEditTags($page, $out);if($hasEditAttr) $numEditable += $this->populateEditAttrs($page, $out);$numEditable += count($this->inlineEditors);if(!$numEditable) {$event->return = $out;return;}header('X-Frame-Options: SAMEORIGIN');// bundle in any needed javascript files and related assetsif(stripos($out, '</body>')) {$out = str_ireplace("</body>", $this->renderAssets() . "</body>", $out);} else {$out .= $this->renderAssets();}$event->return = $out;}/*** Page::render for situations where the page is NOT editable** This removes any <edit> tags.** @param HookEvent $event**/public function hookPageRenderNoEdit(HookEvent $event) {$out = $event->return;$tag = $this->editRegionTag;$attr = $this->editRegionAttr;$hasEditTags = strpos($out, "<$tag") !== false;$hasEditAttr = strpos($out, "$attr=") !== false;if($hasEditTags) {// remove modal edit tags$out = preg_replace('!</?' . $tag . '(?:\s[^>]*>|>)\s*!is', '', $out);}if($hasEditAttr) {// remove modal edit attributes$out = preg_replace('!(<[^>]+?)\s' . $attr . '=["\']?[^"\'\s>]*["\']?!is', '$1', $out);}if($hasEditTags || $hasEditAttr) {// update markup$event->return = $out;}}/*** Populate <edit> tags in the markup** Supported formats (one or more fields on current page):* <edit field_name>...</edit>* <edit field='field_name'>...</edit>* <edit field='field1,field2,field3'>...</edit>** Supported formats (one or more fields, with specific page "123"):* <edit 123.field_name>...</edit>* <edit field='123.field_name'>...</edit>* <edit page='123' field='field_name'>...</edit>* <edit page='123' field='field1,field2,field3'>...</edit>** @param Page $page* @param string $out* @param bool|null $editable* @return int Number of tags populated**/protected function populateEditTags(Page $page, &$out, $editable = null) {$sanitizer = $this->wire()->sanitizer;$pages = $this->wire()->pages;$fields = $this->wire()->fields;$tag = $this->editRegionTag;if(!preg_match_all('!<' . $tag . '([^>]+)>(.*?)</' . $tag . '>!is', $out, $matches)) return 0;if(is_null($editable)) $editable = $page->editable();$numReplaced = 0;$numEditable = 0;foreach($matches[0] as $key => $fullMatch) {$names = '';$pageID = $page->id;$attrs = explode(' ', $matches[1][$key]);$markup = $matches[2][$key];if(strpos($markup, 'pw-edit-') !== false) {$out = str_replace($fullMatch, $markup, $out);continue;}foreach($attrs as $attr) {$attr = trim($attr);if(empty($attr)) continue;if(strpos($attr, '=') !== false) {list($attrName, $attrValue) = explode('=', $attr);$attrName = trim($attrName, ' ');$attrValue = trim($attrValue, ' "\'');if(in_array($attrName, array('name', 'names', 'field', 'fields'))) {$names = $attrValue;} else if($attrName == 'page') {$pageID = $attrValue;}} else if(empty($names)) {$names = $attr;}if(strpos($names, ':') !== false) {list($pageID, $names) = explode(':', $names);} else if(strpos($names, '.') !== false) {list($pageID, $names) = explode('.', $names);}}if(ctype_digit($names) && !ctype_digit($pageID)) {list($names, $pageID) = array($pageID, $names); // swap order detected}$foundFields = array();$inlineSupported = false;$p = new NullPage();if($names) {$p = $page;if($pageID != $page->id && $pageID != $page->path) {if(ctype_digit($pageID)) {$pageID = (int) $pageID;} else {$pageID = $sanitizer->path($pageID);}$p = $pages->get($pageID);if(!$p->id) $p = $page;}foreach(explode(',', $names) as $name) {$name = trim($name);$field = $fields->get($name);if($editable && $p->editable($name)) {$foundFields[$name] = $field;if($this->inlineSupported($field)) $inlineSupported = true;}}}if(count($foundFields) == 1 && $inlineSupported) {$out = str_replace($fullMatch, $this->inlineRenderEditor($p, reset($foundFields), $markup), $out);$numEditable++;} else if(count($foundFields)) {$out = str_replace($fullMatch, $this->modalRenderEditor($p, $foundFields, $markup), $out);$numEditable++;} else {$out = str_replace($fullMatch, $markup, $out);}$numReplaced++;}return $numEditable;}/*** Update "edit" attributes for modal editing** @param Page $page* @param $out* @return int**/protected function populateEditAttrs(Page $page, &$out) {$sanitizer = $this->wire()->sanitizer;$fields = $this->wire()->fields;$pages = $this->wire()->pages;$numEditable = 0;$editRegionAttr = $this->editRegionAttr;if(stripos($out, " $editRegionAttr=") === false) return $numEditable;// tag attr1 data attr2$regex = '!<([a-z0-9]+)([^>]*?)\s+' . $editRegionAttr . '=["\']?([^\s>"\']+)["\']?([^>]*)>!is';if(!preg_match_all($regex, $out, $matches)) return $numEditable;foreach($matches[0] as $key => $fullMatch) {$tag = $matches[1][$key];$attr = $matches[2][$key];$attr .= trim((strlen($attr) ? ' ' : '') . $matches[4][$key]);$data = $matches[3][$key];if(strpos($data, ':') !== false) {list($pageID, $fieldNames) = explode(':', $data);} else if(strpos($data, '.') !== false) {list($pageID, $fieldNames) = explode('.', $data);} else {list($pageID, $fieldNames) = array(0, '');}if($pageID && $fieldNames) {if(ctype_digit($fieldNames) && !ctype_digit($pageID)) {list($pageID, $fieldNames) = array($fieldNames, $pageID); // swap order detected}// check if pageID is actually a page path$pageID = ctype_digit($pageID) ? (int) $pageID : $sanitizer->path($pageID);if(!$pageID) continue; // skip$editPage = $pages->get($pageID);if(!$editPage->id) continue; // skip} else {$fieldNames = $data;$editPage = $page;}$fieldNames = explode(',', $fieldNames);$fieldIDs = array();foreach($fieldNames as $k => $v) {$fieldName = $sanitizer->fieldName(trim($v));$field = $fields->get($fieldName);if(!$field || !$editPage->editable($fieldName)) {unset($fieldNames[$k]);} else {$fieldIDs[] = $field->id;}}if(!count($fieldNames)) continue;$fieldNames = implode(',', $fieldNames);$editURL = $editPage->editUrl() . "&fields=$fieldNames&modal=1";$modalID = 'pw-edit-modal-' . $this->getModalID($page, $fieldIDs);if(strpos(" $attr", " id=") === false) {$attr = "id=$modalID $attr";} else {$attr = "data-id=$modalID $attr";}$attr .= " " ."data-class='pw-edit-attr pw-edit-modal pw-modal pw-modal-dblclick' " ."data-href='$editURL' " ."data-fields='$fieldNames' " ."data-buttons='button.ui-button[type=submit]' " ."data-autoclose=''";$newTag = "<$tag " . trim($attr) . ">";$out = $this->strReplaceOne($fullMatch, $newTag, $out);$this->editorNum++;$numEditable++;}return $numEditable;}/*** Hook to Fieldtype::formatValue** Updates formatted values to include inline markup needed for the editor.** @param HookEvent $event**/public function inlineHookFormatValue(HookEvent $event) {$field = $event->arguments(1);if(!in_array($field->id, $this->inlineEditFields) && $field->id != $this->inlineEditField) return;$page = $event->arguments(0);$formatted = $event->return;if(is_object($formatted)) $formatted = (string) $formatted;if(empty($formatted)) return;if(!$this->inlineIsEditable($page, $field)) return;if(!$this->inlineEditorActive) return;$event->return = $this->inlineRenderEditor($page, $field, $formatted);}/*** Render markup output for javascripts and editor interface** @return string**/public function renderAssets() {$sanitizer = $this->wire()->sanitizer;$modules = $this->wire()->modules;$config = $this->wire()->config;$input = $this->wire()->input;$user = $this->wire()->user;$scripts = array();$className = $this->className();$css = '';$cssFiles = array();$draft = (int) $input->get('draft');$adminTheme = $user->admin_theme;if($modules->isInstalled('InputfieldCKEditor')) {$modules->includeModule('InputfieldCKEditor');$ckeditorUrl = $config->urls('InputfieldCKEditor') . 'ckeditor-' . constant('\ProcessWire\InputfieldCKEditor::CKEDITOR_VERSION') . '/';} else {$ckeditorUrl = '';}if($modules->isInstalled('InputfieldTinyMCE')) {$inputfield = $modules->get('InputfieldTinyMCE');if(method_exists($inputfield, 'getExtraStyles')) $css .= $inputfield->getExtraStyles();$tinymceInputfieldUrl = $config->urls('InputfieldTinyMCE');$tinymceUrl = $tinymceInputfieldUrl . 'tinymce-' . constant('\ProcessWire\InputfieldTinyMCE::mceVersion');$tinymceFile1 = $tinymceUrl . '/tinymce.min.js';$tinymceFile2 = $tinymceInputfieldUrl . 'InputfieldTinyMCE.js';$cssFiles[] = $tinymceInputfieldUrl . 'InputfieldTinyMCE.css';} else {$tinymceFile1 = '';$tinymceFile2 = '';$tinymceUrl = '';}$configJS = $config->js();$configJS['modals'] = $config->modals;$configJS['urls'] = array('admin' => $config->urls->admin,);$configJS['PageFrontEdit'] = array('labels' => array('cancelConfirm' => $this->_('Are you sure you want to cancel?')),'files' => array('modal' => $config->urls('JqueryUI') . 'modal.min.js','ckeditor' => $ckeditorUrl . 'ckeditor.js','tinymce1' => $tinymceFile1, // tinymce.min.js'tinymce2' => $tinymceFile2, // InputfieldTinyMCE.js'css' => ($config->urls($className)) . "$className.css?nocache" . mt_rand(),'fa' => $config->urls->adminTemplates . "styles/font-awesome/css/font-awesome.min.css",),'adminTheme' => $adminTheme ? $adminTheme : 'AdminThemeDefault','pageURL' => $this->wire()->page->url . ($draft ? "?draft=$draft" : ""));$scripts[] ="window.CKEDITOR_BASEPATH = '$ckeditorUrl';" ."window.TINYMCE_BASEURL = '$tinymceUrl';";$scripts[] = $config->urls($className) . $className . 'Load.js';if(self::debug && defined('JSON_PRETTY_PRINT')) {$configJSON = json_encode($configJS, JSON_PRETTY_PRINT); // for debugging} else {$configJSON = json_encode($configJS);}$scripts[] ="var _pwfe={config:$configJSON};" ."if(typeof ProcessWire == 'undefined'){" ."var ProcessWire=_pwfe;" ."}else{" ."for(var _pwfekey in _pwfe.config) ProcessWire.config[_pwfekey]=_pwfe.config[_pwfekey];" ."}" ."_pwfe=null;" ."if(typeof config == 'undefined') var config=ProcessWire.config;"; // legacy$loadItems = array();// jQuery$loadItems[] = array('test' => "function() { return (typeof jQuery == 'undefined'); }",'file' => $config->urls('JqueryCore') . ($config->debug === 'dev' ? 'dev/' : '') . 'JqueryCore.js','after' => "function() { " ."jQuery.noConflict(); " ."}");// jQuery UI$loadItems[] = array('test' => "function() { " ."jQuery('[data-class^=pw-edit-attr]').each(function() { " ."jQuery(this).addClass(jQuery(this).attr('data-class')); " ."});" ."return (typeof jQuery.ui == 'undefined'); " ."}",'file' => $config->urls('JqueryUI') . ($config->debug === 'dev' ? 'dev/' : '') . 'JqueryUI.js');// add in our PageFrontEdit.js file$loadItems[] = array('file' => ($config->urls($className)) . $className . '.js?nc=' . filemtime(dirname(__FILE__) . "/$className.js"));$loadItemsJSON = "{$className}Load(" . json_encode($loadItems) . ");";$loadItemsJSON = str_replace(array('"function(', '}"'), array('function(', '}'), $loadItemsJSON);$scripts[] = $loadItemsJSON;// button labels$saveLabel = $this->_('Save');$savingLabel = $this->_('Saving…');$savedLabel = $this->_('Saved!');$cancelLabel = $this->_('Cancel');// render the scripts to markup$out = '';foreach($scripts as $script) {if(strpos($script, ' ') !== false || strpos($script, '(') !== false) {$out .= "<script>$script</script>";} else {$out .= "<script src='$script?NoMinify=1'></script>";}if(self::debug) $out .= "\n";}// render the editor interface buttons$lang = $user->language;$lang = $lang ? $lang->id : 0;$class = "pw-edit-buttons pw-edit-buttons-type-$this->buttonType pw-edit-buttons-location-$this->buttonLocation";$editURL = $this->page->editUrl();$viewURL = $sanitizer->entities($input->url(true));$out .="<input type='hidden' id='Inputfield_id' class='PageFrontEdit' value='{$this->page->id}' />" . // for CKE plugins"<input type='hidden' id='pw-edit-lang' value='$lang' />" ."<input type='hidden' id='pw-edit-href' value='$editURL' />" . // edit"<input type='hidden' id='pw-url' value='$viewURL' />" . // view$this->wire()->session->CSRF->renderInput() .($css ? "<style style='text/css'>$css</style>" : "") ."<div class='ui-widget $class' style='display:none'>" ."<button class='ui-button pw-edit-save'>" ."<i class='fa fa-check fa-fw'></i><span> $saveLabel</span>" ."</button>" ."<button class='ui-button pw-edit-cancel ui-priority-secondary'>" ."<i class='fa fa-times fa-fw'></i><span> $cancelLabel</span>" ."</button>" ."<button class='ui-button pw-edit-saving' style='display:none'>" ."<i class='fa fa-spin fa-spinner fa-fw'></i> $savingLabel" ."</button>" ."<button class='ui-button pw-edit-saved' style='display:none'>" ."<i class='fa fa-check fa-fw'></i> $savedLabel" ."</button>" ."</div>" ."<i id='pw-fa-test' class='fa fa-fw'></i>"; // to test width to see if font-awesome already loadedforeach($cssFiles as $cssFile) {$out .= "<link rel='stylesheet' href='$cssFile' />";}return $out;}/*** Get Inputfield** @param Page $page* @param Field $field* @return Inputfield**/protected function getInputfield(Page $page, Field $field) {$inputfield = $field->___getInputfield($page);$inputfield->attr('name', $field->name);if(wireInstanceOf($inputfield, 'InputfieldTinyMCE')) {$inputfield->set('inlineMode', 1);$sf = $inputfield->get('settingsField');if($sf && $sf = $this->wire()->fields->get($sf)) $sf->setQuietly('inlineMode', 1);}return $inputfield;}/*** Render the inline editor** @param Page $page* @param Field $field* @param string $formatted Formatted value* @return string**/protected function inlineRenderEditor(Page $page, Field $field, $formatted) {if(strpos($formatted, "id=pw-editor-$field->name")) return $formatted;$unformatted = $this->getUnformattedValue($page, $field);$this->inlineEditors[$field->name] = $field->name;$langID = $this->wire()->languages ? $this->wire()->user->language->id : 0;$attr = '';// make sure we've got any initialization from the Inputfield$inputfield = $this->getInputfield($page, $field);$inputfield->renderReady(null, false);if(wireInstanceOf($inputfield, 'InputfieldTinyMCE')) {// Ensure TinyMCE is in inline mode and we have required attributesforeach($inputfield->wrapAttr() as $attrName => $attrVal) {// i.e. data-configName, data-features, data-settings, etc.if(strpos($attrName, 'data-') !== 0) continue;$attrVal = htmlspecialchars($attrVal);$attr .= "$attrName='$attrVal' ";}}// use div tags for 'textarea' fields and 'span' tags for text fields$tag = $field->type instanceof FieldtypeTextarea ? 'div' : 'span';$this->editorNum++;$attr = trim("id=pw-edit-$this->editorNum " ."class='pw-edit pw-edit-$inputfield' " ."data-name=$field->name " ."data-page=$page->id " ."data-lang='$langID' " ."style='position:relative' " ."$attr ");return"<$tag $attr>" ."<$tag class=pw-edit-orig>" .$formatted ."</$tag>" ."<$tag class=pw-edit-copy id=pw-editor-$field->name-$page->id " ."style='display:none;-webkit-user-select:text;user-select:text;' contenteditable>" .$unformatted ."</$tag>" ."</$tag>";}/*** Render the modal editor** Take the given markup and render a modal editor around it for the given Page and Field** @param Page $page* @param array $fields Array of Field objects* @param string $markup* @return string**/protected function modalRenderEditor(Page $page, array $fields, $markup) {$modalID = $this->getModalID($page, $fields);$tag = 'div';$_fields = array();foreach($fields as $field) $_fields[] = $field->name;$fieldsStr = implode(',', $_fields);$editURL = $page->editUrl() . "&fields=$fieldsStr&modal=1";$this->editorNum++;if($this->wire()->input->get('pw_edit_fields')) {$markup = preg_replace('/\.(gif|png|jpg|jpeg)\b/i', '.$1?nocache=' . time(), $markup);}$out ="<$tag " ."id=pw-edit-modal-$modalID " ."class='pw-modal pw-modal-dblclick pw-edit-modal' " ."data-href='$editURL' " ."data-fields='$fieldsStr' " ."data-buttons='button.ui-button[type=submit]' " ."data-autoclose=''>" .$markup ."</$tag>";return $out;}/*** Get next available modal editor ID attribute** @param Page $page* @param Field|string|array $field* @return string**/protected function getModalID(Page $page, $field) {if(is_array($field)) {$fields = array();foreach($field as $f) {$fields[] = $f instanceof Field ? $f->id : $f;}$field = implode('-', $fields);} else if($field instanceof Field) {$field = $field->id;}$modalID = "$page->id-$field";$n = 0;while(isset($this->modalEditors[$modalID])) {$n++;$modalID = "$page->id-{$field}_$n";}$this->modalEditors[$modalID] = $field;return $modalID;}/*** Like str_replace but only replaces one rather than all** @param string $search* @param string $replace* @param string $subject* @return string**/protected function strReplaceOne($search, $replace, $subject) {$first = strpos($subject, $search);$last = strrpos($subject, $search);if($first !== false) {if($first === $last) return str_replace($search, $replace, $subject);$before = substr($subject, 0, $first);$after = substr($subject, $first + strlen($search));return $before . $replace . $after;} else {return $subject;}}/*** Execute the ajax page save action** Outputs JSON and halts execution.**/protected function inlineSaveEdits() {$this->inlineEditorActive = true;$session = $this->wire()->session;$input = $this->wire()->input;$user = $this->wire()->user;$pageID = (int) $input->post('id');$langID = (int) $input->post('language');$postFields = $input->post('fields');// JSON return data$data = array('status' => 0, // 0=error, 1=success, 2=success but with errors'error' => '', // new line separated errors'changes' => '', // csv field names'formatted' => array(), // formatted field values'unformatted' => array(), // unformatted field values);if(!$this->page || !$this->page->id) {$data['error'] = "Page is not available for front-end edits";} else if($pageID != $this->page->id) {$data['error'] = "Edited page does not match current page ID ($pageID != {$this->page->id})";} else if($this->wire('languages') && ($langID != $user->language->id)) {$data['error'] = "Edited language does not match current language ($langID != {$this->user->language})";} else if(!$this->page->editable()) {$data['error'] = "Page is not editable by this user (page $pageID, user {$this->user->name})";} else if(!is_array($postFields)) {$data['error'] = "No changes to save";} else {// okay to make editstry {$session->CSRF->validate();$data = $this->inlineProcessSaveEdits($postFields, $data);} catch(WireCSRFException $e) {$data['error'] = "Failed CSRF check";}}$http = new WireHttp();$this->wire($http);$http->sendHeader("Content-type: application/json");$http->sendStatusHeader(200);echo json_encode($data);exit;}/*** Save the given fields to the page and populate the result $data array** @param array $postFields* @param array $data* @return array**/protected function inlineProcessSaveEdits(array $postFields, array $data) {$sanitizer = $this->wire()->sanitizer;$languages = $this->wire()->languages;$fields = $this->wire()->fields;$pages = $this->wire()->pages;$input = $this->wire()->input;$user = $this->wire()->user;$language = $languages ? $user->language : null;$pages->uncacheAll();$pages->setOutputFormatting(false);$errors = array();$pagesToSave = array();$names = array();$draft = (int) $input->get('draft');foreach($postFields as $key => $value) {if($sanitizer->name($key) != $key) {unset($postFields[$key]);continue;}list($pageID, $name) = explode('__', $key, 2);$name = $sanitizer->fieldName($name);$names[$key] = $name;$field = $fields->get($name);$useLanguages = in_array('FieldtypeLanguageInterface', wireClassImplements($field->type));$pageID = (int) $pageID;if(!$pageID) continue;if(isset($pagesToSave[$pageID])) {$page = $pagesToSave[$pageID];} else {if($pageID == $this->wire()->page->id) {// ensure we are using same instance as the one loaded$page = $this->wire()->page;} else {$page = $pages->get($pageID);}if(!$page->id) continue;$page->resetTrackChanges(true);$page->setOutputFormatting(false);$pagesToSave[$pageID] = $page;}if(!$field) {$errors[] = "Field '$name' does not exist.";continue;}if(!$this->inlineIsSaveable($page, $field)) {$errors[] = "Field '$name' is not saveable.";continue;}// let the Inputfield process the input$inputfield = $this->getInputfield($page, $field);$postName = $name;// jQuery HTML function entity encodes things like & to & even if they don't appear in the source// so we determine if it's necessary to decode them hereif($inputfield instanceof InputfieldTextarea) {if(wireInstanceOf($inputfield, 'InputfieldCKEditor')) {$decode = false;} else if(wireInstanceOf($inputfield, 'InputfieldTinyMCE')) {$postName = "Inputfield_$name";$decode = false;} else if($field->get('contentType') >= FieldtypeTextarea::contentTypeHTML) {$decode = false;} else {$decode = true;}} else if($inputfield instanceof InputfieldText) {$decode = true;} else {$decode = false;}if($decode) {$value = $sanitizer->unentities($value);}$input->post->$postName = $value;$inputfield->attr('name', $name);$inputfield->val($page->getUnformatted($name));$inputfield->resetTrackChanges(true);$inputfield->processInput($input->post);$_errors = $inputfield->getErrors(true);if(count($_errors)) {foreach($_errors as $error) {if(strpos($error, $name) === false) $error .= " (field=$name)";$errors[] = $error;}continue;}if($inputfield->isChanged()) {$page->of(false);if($language && $useLanguages) {$value = $page->get($name);if(is_object($value) && in_array('LanguagesValueInterface', wireClassImplements($value))) {/** @var LanguagesValueInterface $value */$value->setLanguageValue($language, $inputfield->val());$page->set($name, $value);$page->trackChange($name);} else {$page->set($name, $inputfield->val());}} else {$page->set($name, $inputfield->val());}}}// save the pages that were modifiedforeach($pagesToSave as $page) {$changes = $page->getChanges();if(count($changes)) {try {$page->save();$data['status'] = count($errors) ? 2 : 1;}catch(\Exception $e) {$data['status'] = $data['status'] ? 2 : 0;$error = $e->getMessage();$errors[] = $error;}} else {if(!$data['status']) $data['status'] = 3; // no changes}if(count($errors)) {$data['error'] .= " \n" . implode(" \n", $errors);}if($draft && $page->id == $this->wire()->page->id) {// use existing $page} else {// get fresh copy of page$page = $pages->get((int) $page->id);}$page->of(false);foreach($postFields as $key => $value) {if(strpos($key, $page->id . '__') !== 0) continue;$name = $names[$key];$data['unformatted'][$key] = (string) $this->getUnformattedValue($page, $name);$data['formatted'][$key] = (string) $page->getFormatted($name);}}$data['error'] = trim($data['error']);return $data;}/*** Get an unformatted Page value suitable for inclusion in existing markup** @param Page $page* @param Field|string $name Field object or name* @return string|int|float**/protected function getUnformattedValue(Page $page, $name) {if($name instanceof Field) {$field = $name;$name = $field->name;} else {$field = $this->wire()->fields->get($name);}$unformatted = $page->getUnformatted($name);if(is_object($unformatted)) $unformatted = (string) $unformatted;$purifyHTML = true;if($field && $field->type instanceof FieldtypeTextarea) {$contentType = (int) $field->get('contentType');$cls = $field->get('inputfieldClass');if($cls === 'InputfieldCKEditor' || $cls === 'InputfieldTinyMCE' || $contentType == 1 || $contentType == 2) {// HTML is expected and allowed$purifyHTML = false;}}if(is_string($unformatted) && $purifyHTML && (strpos($unformatted, '<') !== false || strpos($unformatted, '&') !== false)) {// string might have some HTML in it, allow only a purified version through$unformatted = trim($unformatted);$unformatted = $this->wire()->sanitizer->purify(trim($unformatted));}return $unformatted;}}