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

727 lines
22 KiB
Text

<?php namespace ProcessWire;
/**
* ProcessWire Edit Link Process
*
* Provides the link capability as used by the rich text editor.
*
* ProcessWire 3.x, Copyright 2023 by Ryan Cramer
* https://processwire.com
*
* @property string $relOptions
* @property string $classOptions
* @property string $targetOptions
* @property int $urlType
* @property int $extLinkRel
* @property string $extLinkTarget
* @property string $extLinkClass
* @property int $noLinkTextEdit 3.0.211+
*
* @method InputfieldForm buildForm($currentValue, $currentText)
* @method array getFilesPage(Page $page, $prefix = '') Hookable only in 3.0.222+
*
*/
class ProcessPageEditLink extends Process implements ConfigurableModule {
public static function getModuleInfo() {
return array(
'title' => 'Page Edit Link',
'summary' => 'Provides a link capability as used by some Fieldtype modules (like rich text editors).',
'version' => 112,
'permanent' => true,
'permission' => 'page-edit',
'icon' => 'link',
);
}
/**
* URL type: Absolute path from root (no relative paths)
*
*/
const urlTypeAbsolute = 0;
/**
* URL type: Relative path in same branch only
*
*/
const urlTypeRelativeBranch = 1;
/**
* URL type: Relative path always
*
*/
const urlTypeRelativeAll = 2;
/**
* @var Page|null
*
*/
protected $page = null;
/**
* The "choose page" start label
*
* @var string
*
*/
protected $startLabel = '';
/**
* Language ID
*
* @var int
*
*/
protected $langID = 0;
/**
* Get default configuration settings
*
* @return array
*
*/
public static function getDefaultSettings() {
return array(
'classOptions' => "",
'relOptions' => "nofollow",
'targetOptions' => "_blank",
'urlType' => self::urlTypeAbsolute,
'extLinkRel' => '',
'extLinkTarget' => '',
'extLinkClass' => '',
'noLinkTextEdit' => 0,
);
}
/**
* Construct
*
*/
public function __construct() {
parent::__construct();
foreach(self::getDefaultSettings() as $key => $value) {
parent::set($key, $value);
}
}
/**
* Setup for execute methods
*
*/
public function setup() {
$sanitizer = $this->wire()->sanitizer;
$modules = $this->wire()->modules;
$pages = $this->wire()->pages;
$input = $this->wire()->input;
/** @var ProcessPageList $pageList */
$pageList = $modules->get('ProcessPageList');
$pageList->renderReady();
$this->startLabel = $this->_('Choose page');
$id = (int) $input->get('id');
$this->langID = (int) $input->get('lang');
if($id) $this->page = $pages->get($id);
if($this->page && $this->page->id && !$this->wire()->user->hasPermission("page-view", $this->page)) {
throw new WireException("You don't have access to this page");
}
if(!$this->page) $this->page = $pages->newNullPage();
$this->wire()->config->js('ProcessPageEditLink', array(
'selectStartLabel' => $this->startLabel,
'langID' => $this->langID,
'pageID' => $id,
'pageUrl' => $this->page->url,
'pageName' => $this->page->name,
'rootParentUrl' => $this->page->rootParent->url,
'slashUrls' => $this->page->template ? $this->page->template->slashUrls : 1,
'urlType' => $this->urlType,
'extLinkRel' => $sanitizer->names($this->extLinkRel),
'extLinkTarget' => $this->extLinkTarget,
'extLinkClass' => $sanitizer->names($this->extLinkClass),
'noLinkTextEdit' => (int) $this->noLinkTextEdit
));
}
/**
* Set
*
* @param string $key
* @param string|int|array $value
* @return self
*
*/
public function set($key, $value) {
if($key === 'classOptions' || $key === 'relOptions' || $key === 'targetOptions') {
$value = $this->sanitizeOptions($value);
} else if($key === 'extLinkRel' || $key === 'extLinkClass') {
$value = $this->wire()->sanitizer->htmlClasses($value);
} else if($key === 'extLinkTarget') {
$value = $this->wire()->sanitizer->htmlClass($value);
}
return parent::set($key, $value);
}
/**
* Sanitize single option 'value', 'value=label', or 'value="label"'
*
* @param string $value
* @return string
*
*/
protected function sanitizeOption($value) {
$sanitizer = $this->wire()->sanitizer;
$value = trim($value);
$plus = strpos($value, '+') === 0 ? '+' : '';
if($plus) $value = ltrim($value, '+');
if(strpos($value, '=') === false) return $plus . $sanitizer->htmlClasses($value);
// value=label or value="label"
list($value, $label) = explode('=', $value, 2);
$value = trim($value);
$label = trim($label);
$value = $sanitizer->htmlClasses($value);
if(!strlen($value)) return '';
$quote = strpos($label, '"') === 0 ? '"' : '';
$label = str_replace('"', '', $label);
$label = $sanitizer->text($label);
$value = strlen($label) ? "$plus$value=$quote$label$quote" : "$value";
return $value;
}
/**
* Sanitize multiple newline separated options
*
* @param string $value
* @return string
*
*/
protected function sanitizeOptions($value) {
$value = trim($value);
if(!strlen($value)) return '';
if(strpos($value, "\n") === false) return $this->sanitizeOption($value);
$lines = array();
foreach(explode("\n", $value) as $line) {
$line = $this->sanitizeOption($line);
if(strlen($line)) $lines[] = $line;
}
return implode("\n", $lines);
}
/**
* Build the edit link form
*
* @param string Current href value $currentValue
* @param string Current linked text $currentText
* @since 3.0.217
*
*/
protected function ___buildForm($currentValue, $currentText) {
$sanitizer = $this->wire()->sanitizer;
$modules = $this->wire()->modules;
$config = $this->wire()->config;
$input = $this->wire()->input;
/** @var InputfieldForm $form */
$form = $modules->get("InputfieldForm");
$form->attr('id', 'ProcessPageEditLinkForm');
$modules->get('JqueryWireTabs');
/** @var InputfieldWrapper $fieldset */
$fieldset = $this->wire(new InputfieldWrapper());
$fieldset->attr('title', $this->_('Link'));
$fieldset->addClass('WireTab');
$form->add($fieldset);
if($this->noLinkTextEdit) {
// link text editing disabled
} else if($currentText) {
/** @var InputfieldText $field */
$field = $modules->get("InputfieldText");
$field->label = $this->_('Link text');
$field->icon = 'pencil-square';
$field->attr('id+name', 'link_text');
$field->val($currentText);
$fieldset->add($field);
}
/** @var InputfieldPageAutocomplete $field */
$field = $modules->get("InputfieldPageAutocomplete");
$field->label = $this->_('Link to URL');
$field->attr('id+name', 'link_page_url');
$field->icon = 'external-link-square';
$field->description = $this->_('Enter a URL, email address, anchor, or enter word(s) to find a page.');
$field->labelFieldName = 'url';
if($modules->isInstalled('PagePaths') && !$this->wire('languages')) {
$field->searchFields = 'path title';
} else {
$field->searchFields = 'name title';
}
if($this->langID) $field->lang_id = $this->langID;
$field->maxSelectedItems = 1;
$field->useList = false;
$field->allowAnyValue = true;
$field->disableChars = '/:.#';
$field->useAndWords = true;
$field->findPagesSelector =
"has_parent!=" . $config->adminRootPageID . ", " .
"id!=" . $config->http404PageID;
if($currentValue) $field->attr('value', $currentValue);
$fieldset->add($field);
if(is_array($input->get('anchors'))) {
$field->columnWidth = 60;
/** @var InputfieldSelect $field */
$field = $modules->get('InputfieldSelect');
$field->columnWidth = 40;
$field->attr('id+name', 'link_page_anchor');
$field->label = $this->_('Select Anchor');
$field->description = $this->_('Anchors found in the text you are editing.');
$field->icon = 'flag';
foreach($input->get->array('anchors') as $anchor) {
$anchor = '#' . $sanitizer->text($anchor);
if(strlen($anchor)) $field->addOption($anchor);
if($currentValue && $currentValue == $anchor) $field->attr('value', $currentValue);
}
$fieldset->add($field);
}
/** @var InputfieldInteger $field */
$field = $modules->get('InputfieldInteger');
$field->attr('id+name', 'link_page_id');
$field->label = $this->_("Select Page");
$field->set('startLabel', $this->startLabel);
$field->collapsed = Inputfield::collapsedYes;
$field->icon = 'sitemap';
$fieldset->add($field);
if($this->page->numChildren) {
/** @var InputfieldInteger $field */
$field = $modules->get('InputfieldInteger');
$field->attr('id+name', 'child_page_id');
$field->label = $this->_("Select Child Page");
$field->description = $this->_('This is the same as "Select Page" above, but may quicker to use if linking to children of the current page.');
$field->set('startLabel', $this->startLabel);
$field->collapsed = Inputfield::collapsedYes;
$field->icon = 'sitemap';
$fieldset->append($field);
}
$fieldset->append($this->getFilesField());
/** @var InputfieldWrapper $fieldset */
$fieldset = $this->wire(new InputfieldWrapper());
$fieldset->attr('title', $this->_('Attributes'));
$fieldset->attr('id', 'link_attributes');
$fieldset->addClass('WireTab');
$form->append($fieldset);
/** @var InputfieldText $field */
$field = $modules->get('InputfieldText');
$field->attr('id+name', 'link_title');
$field->label = $this->_('Title');
$field->description = $this->_('Additional text to describe link.');
if($input->get('title')) {
$field->attr('value', $sanitizer->unentities($sanitizer->text($input->get('title'))));
}
$fieldset->add($field);
if($this->targetOptions) {
/** @var InputfieldSelect $field */
$field = $modules->get('InputfieldSelect');
$field->attr('id+name', 'link_target');
$field->label = $this->_('Target');
$field->description = $this->_('Where this link will open.');
$this->addSelectOptions($field, 'target', $this->targetOptions);
if($this->relOptions) $field->columnWidth = 50;
$fieldset->add($field);
if($this->extLinkTarget) {
$options = $field->getOptions();
if(!isset($options[$this->extLinkTarget])) $field->addOption($this->extLinkTarget);
}
}
if($this->relOptions || $this->extLinkRel) {
/** @var InputfieldSelect $field */
$field = $modules->get('InputfieldSelect');
$field->attr('id+name', 'link_rel');
$field->label = $this->_('Rel');
$field->description = $this->_('Relationship of link to document.');
if($this->targetOptions) $field->columnWidth = 50;
$this->addSelectOptions($field, 'rel', $this->relOptions);
$fieldset->add($field);
if($this->extLinkRel) {
$options = $field->getOptions();
if(!isset($options[$this->extLinkRel])) $field->addOption($this->extLinkRel);
}
}
$classOptions = $this->getClassOptions();
if($classOptions) {
/** @var InputfieldCheckboxes $field */
$field = $modules->get('InputfieldCheckboxes');
$field->attr('id+name', 'link_class');
$field->label = $this->_('Class');
$field->description = $this->_('Additional classes that can affect the look or behavior of the link.');
$field->optionColumns = 1;
$this->addSelectOptions($field, 'class', $classOptions);
if($this->extLinkClass) {
$options = $field->getOptions();
if(!isset($options[$this->extLinkClass])) $field->addOption($this->extLinkClass);
}
$fieldset->add($field);
}
if($this->wire()->user->isSuperuser()) $fieldset->notes =
sprintf(
$this->_('You may customize available attributes shown above in the %s module settings.'),
"[ProcessPageEditLink](" . $config->urls->admin . "module/edit?name=ProcessPageEditLink)"
);
return $form;
}
/**
* Primary execute
*
* @return string
*
*/
public function ___execute() {
$sanitizer = $this->wire()->sanitizer;
$input = $this->wire()->input;
$this->setup();
if($input->get('href')) {
$currentValue = $sanitizer->url($input->get('href'), array(
'stripQuotes' => false,
'allowIDN' => true,
));
} else {
$currentValue = '';
}
$currentText = $input->get('text');
$currentText = $currentText === null ? '' : $this->wire()->sanitizer->text($currentText);
$form = $this->buildForm($currentValue, $currentText);
return $form->render() . "<p class='detail ui-priority-secondary'><code id='link_markup'></code></p>";
}
/**
* Get class options string
*
* This gets class options specified with module and those specified in input.get[class].
*
* @return string Newline separated string of class options
* @since 3.0.212
*
*/
protected function getClassOptions() {
$sanitizer = $this->wire()->sanitizer;
$inputClass = $this->wire()->input->get->text('class');
if(empty($inputClass)) return $this->classOptions;
$inputClass = $sanitizer->htmlClasses($inputClass, true);
if(!count($inputClass)) return $this->classOptions;
sort($inputClass);
$inputClasses = $inputClass;
$inputClass = implode(' ', $inputClass);
$classOptions = array();
if($this->classOptions) {
foreach(explode("\n", $this->classOptions) as $line) {
$value = ltrim(trim($line), '+');
if(strpos($value, '=')) {
list($value, /*$label*/) = explode('=', $value, 2);
}
if(strpos($value, ' ')) {
$value = $sanitizer->htmlClasses($value, true);
sort($value);
$value = implode(' ', $value);
}
$classOptions[$value] = $line;
}
}
if(isset($classOptions[$inputClass])) {
// class already appears as-is, i.e. "uk-text-muted" or "uk-text-muted uk-text-small", etc.
} else {
// add new classes from input
foreach($inputClasses as $class) {
if(!isset($classOptions[$class])) $classOptions[$class] = $class;
}
}
return count($classOptions) ? implode("\n", $classOptions) : '';
}
/**
* @param InputfieldSelect $field
* @param $attrName
* @param $optionsText
*
*/
protected function addSelectOptions(InputfieldSelect $field, $attrName, $optionsText) {
$input = $this->wire()->input;
$isExisting = $input->get('href') != '';
$existingValueStr = $this->wire()->sanitizer->text($input->get($attrName));
$existingValueArray = strlen($existingValueStr) ? explode(' ', $existingValueStr) : array();
$values = array();
if($field instanceof InputfieldRadios) {
$field->addOption('', $this->_('None'));
}
foreach(explode("\n", $optionsText) as $value) {
$value = trim($value);
$isDefault = strpos($value, '+') !== false;
if($isDefault) $value = trim($value, '+');
$attr = array();
$value = trim($value, '+ ');
$label = '';
if(strpos($value, '=') !== false) {
list($value, $label) = explode('=', $value, 2);
$value = trim($value);
$label = trim($label);
} else {
if($value == '_blank') $label = $this->_('open in new window');
if($value == 'nofollow') $label = $this->_('tell search engines not to follow');
}
if(strpos($label, '"') === 0 || strpos($label, "'") === 0) {
$label = trim($label, "\"'");
} else if($label) {
$label = "$value ($label)";
} else {
$label = $value;
}
if(($isDefault && !$isExisting) || (in_array($value, $existingValueArray) || $existingValueStr === $value)) {
if($field instanceof InputfieldCheckboxes) {
$attr['checked'] = 'checked';
} else {
$attr['selected'] = 'selected';
}
}
$field->addOption($value, $label, $attr);
$values[] = $value;
}
}
/**
* Return JSON containing files list for ajax use
*
* @return string
* @throws WireException
*
*/
public function ___executeFiles() {
$this->setup();
if(!$this->page->id) throw new WireException("A page id must be specified");
$files = $this->getFiles();
return wireEncodeJSON($files);
}
/**
* Get array of info about files attached to given Page
*
* @return array Associative array of "/url/to/file.pdf" => "Field label: basename"
*
*/
protected function getFiles() {
$files = array();
$page = $this->page;
// As the link generator might be called in a repeater, we need to find the containing page
$n = 0;
while(wireInstanceOf($page, 'RepeaterPage') && ++$n < 10) {
/** @var RepeaterPage $page */
$page = $page->getForPage();
}
if($page && $page->id) {
$files = $this->getFilesPage($page);
}
asort($files);
return $files;
}
/**
* Get array of info about files attached to given Page, including any repeater items
*
* Hookable in 3.0.222+ only
*
* @param Page $page
* @param string $prefix Optional prefix to prepend to "Field label:" portion of label
* @return array Associative array of "/url/to/file.pdf" => "Field label: basename"
*
*/
protected function ___getFilesPage(Page $page, $prefix = '') {
$files = array();
foreach($page->template->fieldgroup as $field) {
/** @var Fieldtype $type */
$type = $field->type;
if($type instanceof FieldtypeFile) {
$value = $page->get($field->name);
if($value) foreach($page->get($field->name) as $file) {
$files[$file->url] = $prefix . $field->getLabel() . ': ' . $file->basename;
}
} else if(wireInstanceOf($type, 'FieldtypeRepeater')) {
$value = $page->get($field->name);
if($value) {
if($value instanceof Page) $value = array($value);
if(WireArray::iterable($value)) {
foreach($value as $repeaterPage) {
$files = array_merge($this->getFilesPage($repeaterPage, $field->getLabel() . ': '), $files);
}
}
}
}
}
return $files;
}
/**
* @return InputfieldSelect
*
*/
protected function getFilesField() {
/** @var InputfieldSelect $field */
$field = $this->wire()->modules->get("InputfieldSelect");
$field->label = $this->_("Select File");
$field->attr('id+name', 'link_page_file');
$files = $this->getFiles();
$field->addOption('');
$field->addOptions($files);
$field->collapsed = Inputfield::collapsedYes;
if($this->page && $this->page->id) $field->notes = $this->_('Showing files on page:') . ' **' . $this->page->url . '**';
$field->description =
$this->_('Select the file from this page that you want to link to.') . ' ' .
$this->_("To select a file from another page, click 'Select Page' above and choose the page you want to select a file from."); // Instruction on how to select a file from another page
$field->icon = 'file-text-o';
return $field;
}
/**
* Module configuration
*
* @param InputfieldWrapper $inputfields
*
*/
public function getModuleConfigInputfields(InputfieldWrapper $inputfields) {
$modules = $this->wire()->modules;
/** @var InputfieldFieldset $fieldset */
$fieldset = $modules->get('InputfieldFieldset');
$fieldset->label = $this->_('Attribute options');
$fieldset->description =
$this->_('Enter one of attribute `value`, `value=label`, or `value="label"` per line (see notes for details).') . ' ' .
$this->_('The user will be able to select these as options when adding links.') . ' ' .
$this->_('To make an option selected by default (for new links), precede the value with a plus “+”.');
$fieldset->detail =
$this->_('To include labels, specify `value=label` to show **“value (label)”** for each selectable option.') . ' ' .
$this->_('Or specify `value="label"` (label in quotes) to show just **“label”** (hiding the value) for each selectable option.');
$fieldset->icon = 'sliders';
/** @var InputfieldTextarea $f */
$f = $modules->get('InputfieldTextarea');
$f->attr('name', 'classOptions');
$f->label = 'class';
$f->attr('value', $this->classOptions);
$f->columnWidth = 34;
$fieldset->add($f);
/** @var InputfieldTextarea $f */
$f = $modules->get('InputfieldTextarea');
$f->attr('name', 'relOptions');
$f->label = 'rel';
$f->attr('value', $this->relOptions);
$f->columnWidth = 33;
$fieldset->add($f);
/** @var InputfieldTextarea $f */
$f = $modules->get('InputfieldTextarea');
$f->attr('name', 'targetOptions');
$f->label = 'target';
$f->attr('value', $this->targetOptions);
$f->columnWidth = 33;
$fieldset->add($f);
$inputfields->add($fieldset);
/** @var InputfieldFieldset $fieldset */
$fieldset = $modules->get('InputfieldFieldset');
$fieldset->label = $this->_('External link attributes');
$fieldset->description = $this->_('Specify the default selected attributes that will be automatically populated when an external link is detected.');
$fieldset->description .= ' ' . $this->_('If used, the value must be one you have predefined above.');
$fieldset->icon = 'external-link';
$fieldset->collapsed = Inputfield::collapsedBlank;
/** @var InputfieldText $f */
$f = $modules->get('InputfieldText');
$f->attr('name', 'extLinkClass');
$f->label = 'class';
$f->attr('value', $this->extLinkClass);
$f->required = false;
$f->columnWidth = 34;
$fieldset->add($f);
/** @var InputfieldText $f */
$f = $modules->get('InputfieldText');
$f->attr('name', 'extLinkRel');
$f->notes = $this->_('Example: Specifying **nofollow** would make external links default to be not followed by search engines.');
$f->label = 'rel';
$f->required = false;
$f->attr('value', $this->extLinkRel);
$f->columnWidth = 33;
$fieldset->add($f);
/** @var InputfieldName $f */
$f = $modules->get('InputfieldName');
$f->attr('name', 'extLinkTarget');
$f->label = 'target';
$f->notes = $this->_('Example: Specifying **_blank** would make external links default to open in a new window.');
$f->attr('value', $this->extLinkTarget);
$f->required = false;
$f->columnWidth = 33;
$fieldset->add($f);
$inputfields->add($fieldset);
/** @var InputfieldRadios $f */
$f = $modules->get('InputfieldRadios');
$f->attr('name', 'urlType');
$f->label = $this->_('URL type for page links');
$f->addOption(self::urlTypeAbsolute, $this->_('Full/absolute path from root (default)'));
$f->addOption(self::urlTypeRelativeBranch, $this->_('Relative URLs in the same branches only') . '*');
$f->addOption(self::urlTypeRelativeAll, $this->_('Relative URLs always') . '*');
$f->attr('value', $this->urlType ? $this->urlType : self::urlTypeAbsolute);
$f->notes = $this->_('*Currently experimental');
$f->collapsed = Inputfield::collapsedYes;
$inputfields->add($f);
/** @var InputfieldCheckbox $f */
$f = $modules->get('InputfieldCheckbox');
$f->attr('name', 'noLinkTextEdit');
$f->label = $this->_('Disable link text edit feature?');
$f->description = $this->_('Disables the “Edit Link Text” feature, enabling you to support links that can contain existing markup.');
if($this->noLinkTextEdit) {
$f->attr('checked', 'checked');
} else {
$f->collapsed = Inputfield::collapsedYes;
}
$inputfields->add($f);
}
}