diff --git a/wire/core/Notices.php b/wire/core/Notices.php index 5bcbbc0b..6878c7ea 100644 --- a/wire/core/Notices.php +++ b/wire/core/Notices.php @@ -10,14 +10,18 @@ * Base class that holds a message, source class, and timestamp. * Contains notices/messages used by the application to the user. * - * ProcessWire 3.x, Copyright 2019 by Ryan Cramer + * ProcessWire 3.x, Copyright 2020 by Ryan Cramer * https://processwire.com * * @property string $text Text of notice * @property string $class Class of notice - * @property int $timestamp When the notice was generated - * @property int $flags Optional flags bitmask of Notice::debug and/or Notice::warning - * @property string $icon + * @property int $timestamp Unix timestamp of when the notice was generated + * @property int $flags Bitmask using any of the Notice::constants + * @property-read $flagsArray Current flags as an array where indexes are int flags and values are flag names (since 3.0.149) + * @property-read $flagsStr Current flags as string of flag names (since 3.0.149) + * @property string $icon Name of icon to use with Notice + * @property string $idStr Unique ID string for Notice + * @property int $qty Number of times this Notice was added. * */ abstract class Notice extends WireData { @@ -81,40 +85,255 @@ abstract class Notice extends WireData { * */ const noGroup = 131072; + + /** + * Ignore notice unless it will be seen by a logged-in user + * + * @since 3.0.149 + * + */ + const login = 262144; + + /** + * Ignore notice unless user is somewhere in the admin (login page included) + * + * @since 3.0.149 + * + */ + const admin = 524288; + + /** + * Ignore notice unless current user is a superuser + * + * @since 3.0.149 + * + */ + const superuser = 1048576; + + /** + * Make notice persist in session until removed with $notices->removeNotice() call + * + * (not yet fully implemented) + * + * #pw-internal + * + * @since 3.0.149 + * @todo still needs an interactive way to remove + * + */ + const persist = 2097152; + + /** + * Flag integers to flag names + * + * @var array + * @since 3.0.149 + * + */ + static protected $flagNames = array( + self::prepend => 'prepend', + self::debug => 'debug', + self::log => 'log', + self::logOnly => 'logOnly', + self::allowMarkup => 'allowMarkup', + self::anonymous => 'anonymous', + self::noGroup => 'noGroup', + self::login => 'login', + self::admin => 'admin', + self::superuser => 'superuser', + self::persist => 'persist', + ); /** * Create the Notice + * + * As of version 3.0.149 the $flags argument can also be specified as a space separated + * string or array of flag names. Previous versions only accepted flags integer. * * @param string $text Notification text - * @param int $flags Flags + * @param int|string|array $flags Flags Flags for Notice * */ public function __construct($text, $flags = 0) { + parent::__construct(); $this->set('icon', ''); $this->set('class', ''); $this->set('timestamp', time()); - $this->set('flags', $flags); + $this->set('flags', 0); $this->set('text', $text); + $this->set('qty', 0); + if($flags !== 0) $this->flags($flags); } - + + /** + * Set property + * + * @param string $key + * @param mixed $value + * @return $this|WireData + * + */ public function set($key, $value) { if($key === 'text' && is_string($value) && strpos($value, 'icon-') === 0 && strpos($value, ' ')) { list($icon, $value) = explode(' ', $value, 2); list(,$icon) = explode('-', $icon, 2); $icon = $this->wire('sanitizer')->name($icon); if(strlen($icon)) $this->set('icon', $icon); + } else if($key === 'flags') { + $this->flags($value); + return $this; } return parent::set($key, $value); } /** - * Get the notice log + * Get property + * + * @param string $key + * @return mixed + * + */ + public function get($key) { + if($key === 'flagsArray') return $this->flagNames(parent::get('flags')); + if($key === 'flagsStr') return $this->flagNames(parent::get('flags'), true); + if($key === 'idStr') return $this->getIdStr(); + return parent::get($key); + } + + /** + * Get or set flags + * + * @param string|int|array|null $value Accepts flags integer, or array of flag names, or space-separated string of flag names + * @return int + * @since 3.0.149 + * + */ + public function flags($value = null) { + + if($value === null) return parent::get('flags'); // get flags + + $flags = 0; + + if(is_int($value)) { + $flags = $value; + } else if(is_string($value)) { + if(ctype_digit($value)) { + $flags = (int) $value; + } else { + if(strpos($value, ',') !== false) $value = str_replace(array(', ', ','), ' ', $value); + $value = explode(' ', $value); + } + } + + if(is_array($value)) { + foreach($value as $flag) { + if(empty($flag)) continue; + $flag = $this->flag($flag); + if($flag) $flags = $flags | $flag; + } + } + + parent::set('flags', $flags); + + return $flags; + } + + /** + * Given flag name or int, return flag int + * + * @param string|int $name + * @return int + * + */ + protected function flag($name) { + if(is_int($name)) return $name; + $name = trim($name); + if(ctype_digit("$name")) return (int) $name; + $flag = array_search(strtolower($name), array_map('strtolower', self::$flagNames)); + return $flag ? $flag : 0; + } + + /** + * Get string of names for given flags integer + * + * @param null|int $flags Specify flags integer or omit to return all flag names (default=null) + * @param bool $getString Get a space separated string rather than an array (default=false) + * @return array|string + * @since 3.0.149 + * + */ + protected function flagNames($flags = null, $getString = false) { + if($flags === null) return self::$flagNames; + if(!is_int($flags)) return ''; + $flagNames = array(); + foreach(self::$flagNames as $flag => $flagName) { + if($flags & $flag) $flagNames[$flag] = $flagName; + } + return $getString ? implode(' ', $flagNames) : $flagNames; + } + + /** + * Add a flag + * + * @param int|string $flag + * @since 3.0.149 + * + */ + public function addFlag($flag) { + $flag = $this->flag($flag); + if($flag && !($this->flags & $flag)) $this->flags = $this->flags | $flag; + } + + /** + * Remove a flag + * + * @param int|string $flag + * @since 3.0.149 + * + */ + public function removeFlag($flag) { + $flag = $this->flag($flag); + if($flag && ($this->flags & $flag)) $this->flags = $this->flags & ~$flag; + } + + /** + * Does this Notice have given flag? + * + * @param int|string $flag + * @return bool + * @since 3.0.149 + * + */ + public function hasFlag($flag) { + $flag = $this->flag($flag); + return $flag ? $this->flags & $flag : false; + } + + /** + * Get the name for this type of Notice + * + * This name is used for notice logs when Notice::log or Notice::logOnly flag is used. * * @return string Name of log (basename) * */ abstract public function getName(); + /** + * Get a unique ID string based on properties of this Notice to identify it among others + * + * #pw-internal + * + * @return string + * @since 3.0.149 + * + */ + public function getIdStr() { + $prefix = substr(str_replace('otice', '', $this->className()), 0, 2); + $idStr = $prefix . md5("$prefix$this->flags$this->class$this->text"); + return $idStr; + } + public function __toString() { return (string) $this->text; } @@ -202,6 +421,17 @@ class Notices extends WireArray { const logAllNotices = false; // for debugging/dev purposes + /** + * Initialize Notices API var + * + * #pw-internal + * + */ + public function init() { + // @todo + // $this->loadStoredNotices(); + } + /** * #pw-internal * @@ -223,6 +453,67 @@ class Notices extends WireArray { return $this->wire(new NoticeMessage('')); } + /** + * Allow given Notice to be added? + * + * @param Notice $item + * @return bool + * + */ + protected function allowNotice(Notice $item) { + + $user = $this->wire('user'); /** @var User $user */ + + if($item->flags & Notice::debug) { + if(!$this->wire('config')->debug) return false; + } + + if($item->flags & Notice::superuser) { + if(!$user || !$user->isSuperuser()) return false; + } + + if($item->flags & Notice::login) { + if(!$user || !$user->isLoggedin()) return false; + } + + if($item->flags & Notice::admin) { + $page = $this->wire('page'); /** @var Page|null $page */ + if(!$page || !$page->template || $page->template->name != 'admin') return false; + } + + if($this->isDuplicate($item)) { + $item->qty = $item->qty+1; + return false; + } + + if(self::logAllNotices || ($item->flags & Notice::log) || ($item->flags & Notice::logOnly)) { + $this->addLog($item); + $item->flags = $item->flags & ~Notice::log; // remove log flag, to prevent it from being logged again + if($item->flags & Notice::logOnly) return false; + } + + return true; + } + + /** + * Format Notice text + * + * @param Notice $item + * + */ + protected function formatNotice(Notice $item) { + $text = $item->text; + if(is_array($text)) { + $item->text = "
" . trim(print_r($this->sanitizeArray($text), true)) . "
"; + $item->flags = $item->flags | Notice::allowMarkup; + } else if(is_object($text) && $text instanceof Wire) { + $item->text = "
" . $this->wire('sanitizer')->entities(print_r($text, true)) . "
"; + $item->flags = $item->flags | Notice::allowMarkup; + } else if(is_object($text)) { + $item->text = (string) $text; + } + } + /** * Add a Notice object * @@ -235,57 +526,142 @@ class Notices extends WireArray { * */ public function add($item) { - - if($item->flags & Notice::debug) { - if(!$this->wire('config')->debug) return $this; + + if(!($item instanceof Notice)) { + $item = new NoticeError("You attempted to add a non-Notice object to \$notices: $item", Notice::debug); } - if(is_array($item->text)) { - $item->text = "
" . trim(print_r($this->sanitizeArray($item->text), true)) . "
"; - $item->flags = $item->flags | Notice::allowMarkup; - } else if(is_object($item->text) && $item->text instanceof Wire) { - $item->text = "
" . $this->wire('sanitizer')->entities(print_r($item->text, true)) . "
"; - $item->flags = $item->flags | Notice::allowMarkup; - } else if(is_object($item->text)) { - $item->text = (string) $item->text; - } + if(!$this->allowNotice($item)) return $this; - // check for duplicates - $dup = false; - foreach($this as $notice) { - /** @var Notice $notice */ - if($notice->text == $item->text && $notice->flags == $item->flags && $notice->icon == $item->icon) $dup = true; - } + $item->qty = $item->qty+1; + $this->formatNotice($item); - if($dup) return $this; - - if(($item->flags & Notice::warning) && !$item instanceof NoticeWarning) { - // if given a warning of either NoticeMessage or NoticeError, convert it to a NoticeWarning - // this is in support of legacy code, as NoticeWarning didn't used to exist - $warning = $this->wire(new NoticeWarning($item->text, $item->flags)); - $warning->class = $item->class; - $warning->timestamp = $item->timestamp; - $item = $warning; - } - - if(self::logAllNotices || ($item->flags & Notice::log) || ($item->flags & Notice::logOnly)) { - $this->addLog($item); - $item->flags = $item->flags & ~Notice::log; // remove log flag, to prevent it from being logged again - if($item->flags & Notice::logOnly) return $this; - } - if($item->flags & Notice::anonymous) { $item->set('class', ''); } + if($item->flags & Notice::persist) { + $this->storeNotice($item); + } + if($item->flags & Notice::prepend) { return parent::prepend($item); } else { return parent::add($item); } } + + /** + * Store a persist Notice in Session + * + * @param Notice $item + * @return bool + * + */ + protected function storeNotice(Notice $item) { + /** @var Session $session */ + $session = $this->wire('session'); + if(!$session) return false; + $items = $session->getFor($this, 'items'); + if(!is_array($items)) $items = array(); + $str = $this->noticeToStr($item); + $idStr = $item->getIdStr(); + if(isset($items[$idStr])) return false; + $items[$idStr] = $str; + $session->setFor($this, 'items', $items); + return true; + } + + /** + * Load persist Notices stored in Session + * + * @return int Number of Notices loaded + * + */ + protected function loadStoredNotices() { + + $session = $this->wire('session'); + $items = $session->getFor($this, 'items'); + $qty = 0; + + if(empty($items) || !is_array($items)) return $qty; + + foreach($items as $idStr => $str) { + if(!is_string($str)) continue; + $item = $this->strToNotice($str); + if(!$item) continue; + $persist = $item->hasFlag(Notice::persist) ? Notice::persist : 0; + // temporarily remove persist flag so Notice does not get re-stored when added + if($persist) $item->removeFlag($persist); + $this->add($item); + if($persist) $item->addFlag($persist); + $item->set('_idStr', $idStr); + $qty++; + } - protected function addLog($item) { + return $qty; + } + + /** + * Remove a Notice + * + * Like the remove() method but also removes persist notices. + * + * @param string|Notice $item Accepts a Notice object or Notice ID string. + * @return self + * @since 3.0.149 + * + */ + public function removeNotice($item) { + if($item instanceof Notice) { + $idStr = $item->get('_idStr|idStr'); + } else if(is_string($item)) { + $idStr = $item; + $item = $this->getByIdStr($idStr); + } else { + return $this; + } + if($item) parent::remove($item); + $session = $this->wire('session'); + $items = $session->getFor($this, 'items'); + if(is_array($items) && isset($items[$idStr])) { + unset($items[$idStr]); + $session->setFor($this, 'items', $items); + } + return $this; + } + + /** + * Is the given Notice a duplicate of one already here? + * + * @param Notice $item + * @return bool|Notice Returns Notice that it duplicate sor false if not a duplicate + * + */ + protected function isDuplicate(Notice $item) { + $duplicate = false; + foreach($this as $notice) { + /** @var Notice $notice */ + if($notice === $item) { + $duplicate = $notice; + break; + } + if($notice->className() === $item->className() && $notice->flags === $item->flags + && $notice->icon === $item->icon && $notice->text === $item->text) { + $duplicate = $notice; + break; + } + } + return $duplicate; + } + + /** + * Add Notice to log + * + * @param Notice $item + * + */ + protected function addLog(Notice $item) { /** @var Notice $item */ $text = $item->text; if(strpos($text, '&') !== false) { @@ -377,4 +753,76 @@ class Notices extends WireArray { } return $n; } + + /** + * Get a Notice by ID string + * + * #pw-internal + * + * @param string $idStr + * @return Notice|null + * @since 3.0.149 + * + */ + protected function getByIdStr($idStr) { + $notice = null; + if(strlen($idStr) < 33) return null; + $prefix = substr($idStr, 0, 1); + foreach($this as $item) { + /** @var Notice $item */ + if(strpos($item->className(), $prefix) !== 0) continue; + if($item->getIdStr() !== $idStr) continue; + $notice = $item; + break; + } + return $notice; + } + + /** + * Export Notice object to string + * + * #pw-internal + * + * @param Notice $item + * @return string + * @since 3.0.149 + * + */ + protected function noticeToStr(Notice $item) { + $type = str_replace('Notice', '', $item->className()); + $a = array( + 'type' => $type, + 'flags' => $item->flags, + 'timestamp' => $item->timestamp, + 'class' => $item->class, + 'icon' => $item->icon, + 'text' => $item->text, + ); + return implode(';', $a); + } + + /** + * Import Notice object from string + * + * #pw-internal + * + * @param string $str + * @return Notice|null + * @since 3.0.149 + * + */ + protected function strToNotice($str) { + if(substr_count($str, ';') < 5) return null; + list($type, $flags, $timestamp, $class, $icon, $text) = explode(';', $str, 6); + $type = __NAMESPACE__ . "\\Notice$type"; + if(!wireClassExists($type)) return null; + /** @var Notice $item */ + $item = new $type($text, (int) $flags); + $item->setArray(array( + 'timestamp' => (int) $timestamp, + 'class' => $class, + 'icon' => $icon, + )); + return $item; + } } diff --git a/wire/core/ProcessWire.php b/wire/core/ProcessWire.php index 8d925ea5..a810efa9 100644 --- a/wire/core/ProcessWire.php +++ b/wire/core/ProcessWire.php @@ -439,10 +439,11 @@ class ProcessWire extends Wire { Debug::timer('boot'); Debug::timer('boot.load'); } - + + $notices = new Notices(); $this->wire('urls', $config->urls); // shortcut API var $this->wire('log', new WireLog(), true); - $this->wire('notices', new Notices(), true); + $this->wire('notices', $notices, true); $this->wire('sanitizer', new Sanitizer()); $this->wire('datetime', new WireDateTime()); $this->wire('files', new WireFileTools()); @@ -520,6 +521,7 @@ class ProcessWire extends Wire { // populate admin URL before modules init() $config->urls->admin = $config->urls->root . ltrim($pages->getPath($config->adminRootPageID), '/'); + $notices->init(); if($this->debug) Debug::saveTimer('boot.load', 'includes all boot.load timers'); $this->setStatus(self::statusInit); } diff --git a/wire/core/Wire.php b/wire/core/Wire.php index dd637561..c444df8c 100644 --- a/wire/core/Wire.php +++ b/wire/core/Wire.php @@ -1197,7 +1197,7 @@ abstract class Wire implements WireTranslatable, WireFuelable, WireTrackable { * Record a Notice, internal use (contains the code for message, warning and error methods) * * @param string|array|Wire $text Title of notice - * @param int $flags Flags bitmask + * @param int|string $flags Flags bitmask or space separated string of flag names * @param string $name Name of container * @param string $class Name of Notice class * @return $this @@ -1230,12 +1230,13 @@ abstract class Wire implements WireTranslatable, WireFuelable, WireTrackable { * #pw-group-notices * * @param string|array|Wire $text Text to include in the notice - * @param int|bool $flags Optional flags to alter default behavior: + * @param int|bool|string $flags Optional flags to alter default behavior: * - `Notice::debug` (constant): Indicates notice should only be shown when debug mode is active. * - `Notice::log` (constant): Indicates notice should also be logged. * - `Notice::logOnly` (constant): Indicates notice should only be logged. * - `Notice::allowMarkup` (constant): Indicates notice should allow the use of HTML markup tags. * - `true` (boolean): Shortcut for the `Notice::log` constant. + * - In 3.0.149+ you may also specify a space-separated string of flag names. * @return $this * @see Wire::messages(), Wire::warning(), Wire::error() * @@ -1260,12 +1261,13 @@ abstract class Wire implements WireTranslatable, WireFuelable, WireTrackable { * #pw-group-notices * * @param string|array|Wire $text Text to include in the notice - * @param int|bool $flags Optional flags to alter default behavior: + * @param int|bool|string $flags Optional flags to alter default behavior: * - `Notice::debug` (constant): Indicates notice should only be shown when debug mode is active. * - `Notice::log` (constant): Indicates notice should also be logged. * - `Notice::logOnly` (constant): Indicates notice should only be logged. * - `Notice::allowMarkup` (constant): Indicates notice should allow the use of HTML markup tags. * - `true` (boolean): Shortcut for the `Notice::log` constant. + * - In 3.0.149+ you may also specify a space-separated string of flag names. * @return $this * @see Wire::warnings(), Wire::message(), Wire::error() * @@ -1292,12 +1294,13 @@ abstract class Wire implements WireTranslatable, WireFuelable, WireTrackable { * #pw-group-notices * * @param string|array|Wire $text Text to include in the notice - * @param int|bool $flags Optional flags to alter default behavior: + * @param int|bool|string $flags Optional flags to alter default behavior: * - `Notice::debug` (constant): Indicates notice should only be shown when debug mode is active. * - `Notice::log` (constant): Indicates notice should also be logged. * - `Notice::logOnly` (constant): Indicates notice should only be logged. * - `Notice::allowMarkup` (constant): Indicates notice should allow the use of HTML markup tags. * - `true` (boolean): Shortcut for the `Notice::log` constant. + * - In 3.0.149+ you may also specify a space-separated string of flag names. * @return $this * @see Wire::errors(), Wire::message(), Wire::warning() * diff --git a/wire/core/WireDatabaseBackup.php b/wire/core/WireDatabaseBackup.php index 2c0704b0..f949ec3b 100644 --- a/wire/core/WireDatabaseBackup.php +++ b/wire/core/WireDatabaseBackup.php @@ -1301,8 +1301,9 @@ class WireDatabaseBackup { /** * Execute an SQL query, either a string or PDOStatement * - * @param string $query + * @param string|\PDOStatement $query * @param bool|array $options May be boolean (for haltOnError), or array containing the property (i.e. $options array) + * - `haltOnError` (bool): Halt execution when error occurs? (default=false) * @return bool Query result * @throws \Exception if haltOnError, otherwise it populates $this->errors * diff --git a/wire/modules/Fieldtype/FieldtypeOptions/FieldtypeOptions.module b/wire/modules/Fieldtype/FieldtypeOptions/FieldtypeOptions.module index 617acbeb..f23ee14b 100644 --- a/wire/modules/Fieldtype/FieldtypeOptions/FieldtypeOptions.module +++ b/wire/modules/Fieldtype/FieldtypeOptions/FieldtypeOptions.module @@ -3,8 +3,10 @@ /** * ProcessWire Select Options Fieldtype * - * ProcessWire 3.x, Copyright 2016 by Ryan Cramer + * ProcessWire 3.x, Copyright 2020 by Ryan Cramer * https://processwire.com + * + * @property SelectableOptionManager $manager * */ diff --git a/wire/modules/Fieldtype/FieldtypeOptions/SelectableOptionConfig.php b/wire/modules/Fieldtype/FieldtypeOptions/SelectableOptionConfig.php index 360c8c93..83858af4 100644 --- a/wire/modules/Fieldtype/FieldtypeOptions/SelectableOptionConfig.php +++ b/wire/modules/Fieldtype/FieldtypeOptions/SelectableOptionConfig.php @@ -3,7 +3,7 @@ /** * Inputfields and processing for Select Options Fieldtype * - * ProcessWire 3.x, Copyright 2016 by Ryan Cramer + * ProcessWire 3.x, Copyright 2020 by Ryan Cramer * https://processwire.com * */ @@ -43,7 +43,8 @@ class SelectableOptionConfig extends Wire { */ public function __construct(Field $field, InputfieldWrapper $inputfields) { $this->field = $field; - $this->fieldtype = $field->type; + $fieldtype = $field->type; /** @var FieldtypeOptions $fieldtype */ + $this->fieldtype = $fieldtype; $this->inputfields = $inputfields; $this->manager = $this->fieldtype->manager; } @@ -200,16 +201,17 @@ class SelectableOptionConfig extends Wire { $inputfields->add($f); $this->process($f); - if($options->count() && $field->inputfieldClass && $f = $modules->get($field->inputfieldClass)) { + $inputfieldClass = $field->get('inputfieldClass'); + if($options->count() && $inputfieldClass && $f = $modules->get($inputfieldClass)) { $f->attr('name', 'initValue'); $f->label = $this->_('What options do you want pre-selected? (if any)'); $f->collapsed = Inputfield::collapsedBlank; - $f->description = sprintf($this->_('This field also serves as a preview of your selected input type (%s) and options.'), $field->inputfieldClass); + $f->description = sprintf($this->_('This field also serves as a preview of your selected input type (%s) and options.'), $inputfieldClass); foreach($options as $option) { $f->addOption($option->id, $option->title); } - $f->attr('value', $field->initValue); - if(!$this->field->required && !$this->field->requiredIf) { + $f->attr('value', $field->get('initValue')); + if(!$field->required && !$field->requiredIf) { $f->notes = $this->_('Please note: your selections here do not become active unless a value is *always* required for this field. See the "required" option on the Input tab of your field settings.'); } else { $f->notes = $this->_('This feature is active since a value is always required.');