From eb299ee5986b561b2a02551625a43caa70c75097 Mon Sep 17 00:00:00 2001 From: Ryan Cramer Date: Wed, 12 Jun 2019 15:37:26 -0400 Subject: [PATCH] Add support for Lister user-specific bookmarks. This enables users to create their own private or shared bookmarks in Lister. --- .../ProcessPageLister/ListerBookmarks.php | 763 ++++++++++++++++++ .../ProcessPageLister/ProcessPageLister.css | 2 +- .../ProcessPageLister.module | 95 ++- .../ProcessPageLister/ProcessPageLister.scss | 15 +- .../ProcessPageListerBookmarks.php | 664 ++++++++++----- 5 files changed, 1289 insertions(+), 250 deletions(-) create mode 100644 wire/modules/Process/ProcessPageLister/ListerBookmarks.php diff --git a/wire/modules/Process/ProcessPageLister/ListerBookmarks.php b/wire/modules/Process/ProcessPageLister/ListerBookmarks.php new file mode 100644 index 00000000..fa7c7f3c --- /dev/null +++ b/wire/modules/Process/ProcessPageLister/ListerBookmarks.php @@ -0,0 +1,763 @@ +page = $page; + $this->user = $user; + parent::__construct(); + } + + /** + * Set the Lister page that bookmarks will be for + * + * @param Page $page + * + */ + public function setPage(Page $page) { + $this->page = $page; + } + + /** + * Set user that bookmarks will be for + * + * @param User $user + * + */ + public function setUser(User $user) { + $this->user = $user; + } + + /** + * Get owned bookmarks + * + * @param int $userID + * @return array + * + */ + public function getOwnedBookmarks($userID = 0) { + + $settings = $this->getUserSettings($userID); + $bookmarks = array(); + + if(!isset($settings['bookmarks'])) $settings['bookmarks'] = array(); + + foreach($settings['bookmarks'] as $bookmarkID => $bookmark) { + if(empty($bookmarkID) || empty($bookmark['title'])) continue; + $bookmarkID = $this->bookmarkStrID($bookmarkID, self::typeOwned); + $bookmark = $this->wakeupBookmark($bookmark, $bookmarkID, self::typeOwned); + if(!$bookmark) continue; + if($userID && !$userID != $this->user->id && empty($bookmark['share'])) continue; + $bookmarks[$bookmarkID] = $bookmark; + } + + return $bookmarks; + } + + /** + * Save owned bookmarks + * + * @param array $bookmarks + * + */ + public function saveOwnedBookmarks(array $bookmarks) { + + $settings = $this->getUserSettings(); + $saveBookmarks = array(); + + if(!isset($settings['bookmarks'])) $settings['bookmarks'] = array(); + + // prep for save + foreach($bookmarks as $bookmarkID => $bookmark) { + if(empty($bookmark['title'])) continue; + $bookmark = $this->sleepBookmark($bookmark); + $bookmarkID = $this->bookmarkStrID($bookmarkID, self::typeOwned); + $saveBookmarks[$bookmarkID] = $bookmark; + } + + $bookmarks = $saveBookmarks; + + if($settings['bookmarks'] !== $bookmarks) { + $settings['bookmarks'] = $bookmarks; + if(empty($bookmarks)) unset($settings['bookmarks']); + $this->message('Updated owned bookmarks', Notice::debug); + $this->saveUserSettings($settings); + } + } + + /** + * Get user’s lister settings for current page + * + * @param int $userID + * @return array + * + */ + public function getUserSettings($userID = 0) { + + $pageKey = $this->strID($this->page->id); + $userSettings = array(); + + if($userID === $this->user->id) $userID = 0; + + if($userID) { + // other user + $user = $this->wire('users')->get((int) $userID); + if($user && $user->id) { + $userSettings = $user->meta('lister'); + if(!is_array($userSettings)) $userSettings = array(); + } + } else if($this->userSettings !== null && $this->user->id === $this->userSettingsID) { + $userSettings = $this->userSettings; + } else { + $userSettings = $this->user->meta('lister'); + if(!is_array($userSettings)) $userSettings = array(); + $this->userSettings = $userSettings; + $this->userSettingsID = $this->user->id; + } + + if(!isset($userSettings[$pageKey])) { + $userSettings[$pageKey] = array(); + if(!$userID) $this->userSettings = $userSettings; + } + + return $userSettings[$pageKey]; + } + + /** + * Save user settings for current page + * + * @param array $settings + * @return bool + * + */ + public function saveUserSettings(array $settings) { + + $pageKey = $this->strID($this->page->id); + $userSettings = $this->getUserSettings(); + $userSettings[$pageKey] = $settings; + + foreach($userSettings as $key => $value) { + + if(!$this->isID($key)) continue; // not a pageKey setting + + // remove empty keys and settings + if(is_array($value)) { + foreach($value as $k => $v) { + if(empty($v)) unset($value[$k]); // i.e. an empty $value['bookmarks'] + } + } + + $userSettings[$key] = $value; + if(!$this->isValidPageKey($key)) $value = array(); // maintenance + if(empty($value)) unset($userSettings[$key]); + } + + // if no changes, exit now + if($userSettings === $this->userSettings) return false; + + // save user settings + $this->user->meta('lister', $userSettings); + $this->userSettings = $userSettings; + $this->message('Updated user settings', Notice::debug); + + return true; + } + + /** + * Get public bookmarks (from module config) + * + * @return array + * + */ + public function getPublicBookmarks() { + + $pageKey = $this->strID($this->page->id); + $moduleConfig = $this->getModuleConfig(); + $bookmarks = array(); + + if(!isset($moduleConfig['bookmarks'][$pageKey])) { + $moduleConfig['bookmarks'][$pageKey] = array(); + } + + foreach($moduleConfig['bookmarks'][$pageKey] as $bookmarkID => $bookmark) { + if(empty($bookmarkID) || empty($bookmark['title'])) continue; + $bookmarkID = $this->bookmarkStrID($bookmarkID, self::typePublic); + $bookmark = $this->wakeupBookmark($bookmark, $bookmarkID, self::typePublic); + if($bookmark) $bookmarks[$bookmarkID] = $bookmark; + } + + $moduleConfig['bookmarks'][$pageKey] = $bookmarks; + $this->setModuleConfig($moduleConfig); + + return $bookmarks; + } + + /** + * Save public bookmarks (to module config) + * + * @param array $bookmarks + * @return bool + * + */ + public function savePublicBookmarks(array $bookmarks) { + + $pageKey = $this->strID($this->page->id); + $moduleConfig = $this->getModuleConfig(); + $saveBookmarks = array(); + + if(isset($moduleConfig['bookmarks'][$pageKey])) { + // if given bookmarks are identical to what is in module config, there are no changes to save + if($bookmarks === $moduleConfig['bookmarks'][$pageKey]) return false; + } + + // prep bookmarks for save + foreach($bookmarks as $bookmarkID => $bookmark) { + + // don't save bookmarks that lack a title or of the wrong type + if(empty($bookmark['title'])) continue; + if($bookmark['type'] != self::typePublic) continue; + + // assign IDs for any bookmarks that don't have them + if(empty($bookmarkID)) { + $bookmarkID = time(); + while(isset($bookmarks["_$bookmarkID"])) $bookmarkID++; + } + + $bookmarkID = $this->bookmarkStrID($bookmarkID, self::typePublic); + $saveBookmarks[$bookmarkID] = $this->sleepBookmark($bookmark); + } + + $bookmarks = $saveBookmarks; + + if(empty($bookmarks)) { + // remove if empty... + unset($moduleConfig['bookmarks'][$pageKey]); + } else { + // ...otherwise populate + $moduleConfig['bookmarks'][$pageKey] = $bookmarks; + } + + // check if any bookmarks in module config are for pages that no longer exist + foreach($moduleConfig['bookmarks'] as $key => $bookmarks) { + if(!$this->isValidPageKey($key)) { + $this->warning("Removed expired bookmark for page $key", Notice::debug); + unset($moduleConfig['bookmarks'][$key]); + } + } + + // if there are changes, save them + return $this->saveModuleConfig($moduleConfig); + } + + /** + * Save all bookmarks (whether public or owned) + * + * @param array $allBookmarks + * + */ + public function saveBookmarks(array $allBookmarks) { + + // save owned (user) bookmarks + $ownedBookmarks = $this->filterBookmarksByType($allBookmarks, self::typeOwned); + $this->saveOwnedBookmarks($ownedBookmarks); + + if($this->user->isSuperuser()) { + $publicBookmarks = $this->filterBookmarksByType($allBookmarks, self::typePublic); + $this->savePublicBookmarks($publicBookmarks); + } + } + + /** + * Get all bookmarks (public and owned) + * + * @return array + * + */ + public function getAllBookmarks() { + $publicBookmarks = $this->getPublicBookmarks(); + $ownedBookmarks = $this->getOwnedBookmarks(); + $allBookmarks = array_merge($publicBookmarks, $ownedBookmarks); + return $allBookmarks; + } + + /** + * Get configured bookmarks allowed for current user, indexed by bookmark ID (int) + * + * @return array + * + */ + public function getBookmarks() { + $bookmarks = array(); + foreach($this->getAllBookmarks() as $bookmarkID => $bookmark) { + if(!$this->isBookmarkViewable($bookmark)) continue; + $bookmarks[$bookmarkID] = $bookmark; + } + return $bookmarks; + } + + + /** + * Get a bookmark by ID (whether public or owned) + * + * @param string|int $bookmarkID + * @param int|null $type + * @return array|null + * + */ + public function getBookmark($bookmarkID, $type = null) { + if($type === null && strpos($bookmarkID, $this->typePrefix(self::typeOwned)) !== false) { + $type = self::typeOwned; + } + if($type === self::typeOwned) { + $prefix = $this->typePrefix(self::typeOwned); + if(strpos($bookmarkID, $prefix) > 0) { + // 123O456 where 123 is user ID and 456 is bookmark ID + list($userID, $bookmarkID) = explode($prefix, $bookmarkID); + $userID = (int) $userID; + if($userID === $this->user->id) $userID = 0; + $bookmarkID = $prefix . ((int) $bookmarkID); + } else { + $bookmarkID = $this->_bookmarkID($bookmarkID); + $userID = 0; + } + $bookmarks = $this->getOwnedBookmarks($userID); + $bookmarkID = $this->bookmarkStrID($bookmarkID, self::typeOwned); + + } else { + $bookmarks = $this->getPublicBookmarks(); + $bookmarkID = $this->bookmarkStrID($bookmarkID, self::typePublic); + } + + $bookmark = isset($bookmarks[$bookmarkID]) ? $bookmarks[$bookmarkID] : null; + + return $bookmark; + } + + /** + * Get the URL for a bookmark + * + * @param string $bookmarkID + * @param User|null $user + * @return string + * + */ + public function getBookmarkUrl($bookmarkID, $user = null) { + if(strpos($bookmarkID, $this->typePrefix(self::typeOwned)) === 0) { + if($user) $bookmarkID = $user->id . $bookmarkID; + } else { + $bookmarkID = $this->intID($bookmarkID); + } + return $this->page->url . "bm$bookmarkID"; + } + + /** + * Get the URL for a bookmark + * + * @param string $bookmarkID + * @return string + * + */ + public function getBookmarkEditUrl($bookmarkID) { + return $this->page->url . "edit-bookmark/?bookmark=$bookmarkID"; + } + + /** + * Get the title for the given bookmark ID or bookmark array + * + * @param int|array $bookmarkID + * @return mixed|string + * @throws WireException + * + */ + public function getBookmarkTitle($bookmarkID) { + if(is_array($bookmarkID)) { + $bookmark = $bookmarkID; + } else { + $bookmark = $this->getBookmark($bookmarkID); + if(empty($bookmark)) return ''; + } + $languages = $this->wire('languages'); + $title = $bookmark['title']; + if($languages) { + $user = $this->wire('user'); + if(!$user->language->isDefault() && !empty($bookmark["title$user->language"])) { + $title = $bookmark["title$user->language"]; + } + } + return $title; + } + + /** + * Delete a bookmark by ID + * + * @param int $bookmarkID + * @return bool + * + */ + public function deleteBookmarkByID($bookmarkID) { + + $bookmark = $this->getBookmark($bookmarkID); + + if(!$bookmark) return false; + if(!$this->isBookmarkDeletable($bookmark)) return false; + + if($bookmark['type'] == self::typeOwned) { + $bookmarks = $this->getOwnedBookmarks(); + unset($bookmarks[$bookmarkID]); + $this->saveOwnedBookmarks($bookmarks); + } else { + $bookmarks = $this->getPublicBookmarks(); + unset($bookmarks[$bookmarkID]); + $this->savePublicBookmarks($bookmarks); + } + + return true; + } + + /** + * Filter bookmarks, removing those that are not of the requested type + * + * @param array $allBookmarks + * @param int $type + * @return array + * + */ + public function filterBookmarksByType(array $allBookmarks, $type) { + $filteredBookmarks = array(); + foreach($allBookmarks as $key => $bookmark) { + if(!isset($bookmark['type'])) $bookmark['type'] = self::typePublic; + if($bookmark['type'] != $type) continue; + $filteredBookmarks[$key] = $bookmark; + } + return $filteredBookmarks; + } + + /** + * Filter bookmarks, removing those user does not have access to + * + * @param array $bookmarks + * @return array + * + */ + public function filterBookmarksByAccess(array $bookmarks) { + foreach($bookmarks as $key => $bookmark) { + if(!$this->isBookmarkViewable($bookmark)) unset($bookmarks[$key]); + } + return $bookmarks; + } + + /** + * Is the given bookmark editable? + * + * @param array $bookmark + * @return bool + * + */ + public function isBookmarkEditable(array $bookmark) { + if($this->user->isSuperuser()) return true; + if($bookmark['type'] == self::typePublic) return false; + return true; + } + + /** + * Is the given bookmark viewable? + * + * @param array $bookmark + * @return bool + * + */ + public function isBookmarkViewable(array $bookmark) { + + if(empty($bookmark['roles'])) return true; + if($this->user->isSuperuser()) return true; + + $userRoles = $this->user->roles; + $viewable = false; + + foreach($bookmark['roles'] as $roleID) { + foreach($userRoles as $userRole) { + if($userRole->id == $roleID) { + $viewable = true; + break; + } + } + } + + return $viewable; + } + + /** + * Is the given bookmark deletable? + * + * @param array $bookmark + * @return bool + * + */ + public function isBookmarkDeletable(array $bookmark) { + return $this->isBookmarkEditable($bookmark); + } + + /** + * Get a template array for a bookmark + * + * @param array $bookmark + * @return array + * + */ + public function _bookmark(array $bookmark = array()) { + $template = array( + 'id' => '', + 'title' => '', + 'desc' => '', + 'selector' => '', + 'columns' => array(), + 'sort' => '', + 'type' => self::typePublic, + 'roles' => array(), + 'share' => false, + ); + + return empty($bookmark) ? $template : array_merge($template, $bookmark); + } + + /** + * Sanitize a bookmark ID + * + * @param string|array $bookmarkID + * @return string + * + */ + public function _bookmarkID($bookmarkID) { + if(is_array($bookmarkID)) { + $bookmark = $bookmarkID; + $type = $bookmark['type']; + $bookmarkID = $bookmark['id']; + } else { + $type = self::typePublic; + $ownedPrefix = $this->typePrefix(self::typeOwned); + if(strpos($bookmarkID, $ownedPrefix) !== false) { + list($userID, $bookmarkID) = explode($ownedPrefix, $bookmarkID); + $userID = empty($userID) ? '' : (int) $userID; + $bookmarkID = $userID . $ownedPrefix . ((int) $bookmarkID); + return $bookmarkID; + } else { + $bookmarkID = ltrim($bookmarkID, $this->typePrefix(self::typePublic)); + } + } + if(!ctype_digit("$bookmarkID")) return ''; + return $this->typePrefix($type) . ((int) $bookmarkID); + } + + protected function getModuleConfig() { + if($this->moduleConfig === null) { + $this->moduleConfig = $this->wire('modules')->getConfig('ProcessPageLister'); + } + if(!isset($this->moduleConfig['bookmarks'])) $this->moduleConfig['bookmarks'] = array(); + return $this->moduleConfig; + } + + protected function setModuleConfig(array $moduleConfig) { + $this->moduleConfig = $moduleConfig; + } + + protected function saveModuleConfig(array $moduleConfig) { + if($moduleConfig === $this->moduleConfig) return false; + $this->wire('modules')->saveConfig('ProcessPageLister', $moduleConfig); + $this->moduleConfig = $moduleConfig; + $this->message('Updated module config (bookmarks)', Notice::debug); + return true; + } + + protected function wakeupBookmark(array $bookmark, $bookmarkID, $type = null) { + + if(empty($bookmarkID) || empty($bookmark['title'])) return false; + + if($type === null) $type = $this->idType($bookmarkID); + $bookmarkID = $this->bookmarkStrID($bookmarkID, $type); + + $bookmark = $this->_bookmark($bookmark); + $bookmark['type'] = $type; + $bookmark['id'] = $bookmarkID; + $bookmark['url'] = $this->getBookmarkUrl($bookmarkID); + $bookmark['editUrl'] = $this->getBookmarkEditUrl($bookmarkID); + $bookmark['share'] = empty($bookmark['share']) ? false : true; + //$bookmark['shareUrl'] = $bookmark['url']; + + return $bookmark; + } + + protected function sleepBookmark(array $bookmark) { + unset($bookmark['id'], $bookmark['url'], $bookmark['editUrl']); + if($bookmark['type'] === self::typeOwned) unset($bookmark['roles']); + if(empty($bookmark['share'])) unset($bookmark['share']); + return $bookmark; + } + + /** + * Given an id or string key, return an int ID + * + * @param string|int $val + * @return int + * + */ + public function intID($val) { + return (int) ltrim($val, '_O'); + } + + /** + * Given an id or string key, return an string ID (with leading underscore) + * + * @param string|int $val + * @return int + * + */ + public function strID($val) { + return '_' . ltrim($val, '_O'); + } + + /** + * Given an id or string key, return an bookmark string ID + * + * @param string|int $val + * @param int $type + * @return int + * + */ + public function bookmarkStrID($val, $type) { + return ($type === self::typeOwned ? 'O' : '_') . ltrim($val, '_O'); + } + + /** + * Does the given string value represent an ID? If yes, return ID, otherwise return false. + * + * @param string $val + * @return bool|int + * + */ + public function isID($val) { + $val = trim($val, '_O'); + return ctype_digit($val) ? (int) $val : false; + } + + /** + * Get the type from the given id string + * + * @param string $val + * @return int + * + */ + public function idType($val) { + if(strpos($val, 'O') === 0) return self::typeOwned; + return self::typePublic; + } + + /** + * Get the prefix for the given bookmark type + * + * @param int $type + * @return string + * + */ + public function typePrefix($type) { + if($type == self::typePublic) return '_'; + if($type == self::typeOwned) return 'O'; + return ''; + } + + /** + * Is the given page ID or key valid and existing? + * + * @param int|string $val + * @return bool + * + */ + public function isValidPageKey($val) { + $id = $this->intID($val); + return $id === $this->page->id || $this->wire('pages')->get($id)->id > 0; + } + + /** + * Return a readable selector from bookmark for output purposes + * + * @param array $bookmark + * @return string + * + */ + public function readableBookmarkSelector(array $bookmark) { + + $selector = $bookmark['selector']; + if(strpos($selector, 'template=') !== false && preg_match('/template=([\d\|]+)/', $selector, $matches)) { + // make templates readable, for output purposes + $t = ''; + foreach(explode('|', $matches[1]) as $templateID) { + $template = $this->wire('templates')->get((int) $templateID); + $t .= ($t ? '|' : '') . ($template ? $template->name : $templateID); + } + $selector = str_replace($matches[0], "template=$t", $selector); + } + + if(!empty($bookmark['sort'])) $selector .= ($selector ? ", " : "") . "sort=$bookmark[sort]"; + + return $selector; + } + + +} \ No newline at end of file diff --git a/wire/modules/Process/ProcessPageLister/ProcessPageLister.css b/wire/modules/Process/ProcessPageLister/ProcessPageLister.css index ba15ff05..13f35f4f 100644 --- a/wire/modules/Process/ProcessPageLister/ProcessPageLister.css +++ b/wire/modules/Process/ProcessPageLister/ProcessPageLister.css @@ -1 +1 @@ -#ProcessLister{margin-top:-2px}#ProcessListerResultsTab{padding-top:.5em}#ProcessListerResults>form.InputfieldForm{margin-bottom:0}#ProcessListerResults #ProcessListerTable{clear:both;overflow-x:auto}#ProcessListerResults #ProcessListerTable>div{margin-top:0}#ProcessListerResults #ProcessListerTable table.ProcessListerTable{clear:both;margin-top:0}#ProcessListerResults #ProcessListerTable table.ProcessListerTable td table{width:100%}#ProcessListerResults #ProcessListerTable table.ProcessListerTable>thead th{font-size:.8571428571em}#ProcessListerResults #ProcessListerTable table.ProcessListerTable>thead th i{font-size:14px}#ProcessListerResults #ProcessListerTable table.ProcessListerTable>thead th strong{white-space:nowrap}#ProcessListerResults #ProcessListerTable table.ProcessListerTable>thead th b,#ProcessListerResults #ProcessListerTable table.ProcessListerTable>thead .th b{display:none}#ProcessListerResults #ProcessListerTable table.ProcessListerTable>thead th:first-child{padding-left:0}#ProcessListerResults #ProcessListerTable table.ProcessListerTable>tbody>tr>td{font-size:.9285714286em}#ProcessListerResults #ProcessListerTable table.ProcessListerTable>tbody>tr>td:first-child,#ProcessListerResults #ProcessListerTable table.ProcessListerTable>tbody>tr>td:first-child>a{padding-left:0}#ProcessListerResults #ProcessListerTable table.ProcessListerTable>tbody>tr>td ul.MarkupFieldtype{margin:0;padding-left:0}#ProcessListerResults #ProcessListerTable table.ProcessListerTable>tbody>tr>td ul.MarkupFieldtype>li{list-style:none;margin:0;padding-left:0}#ProcessListerResults #ProcessListerTable table.ProcessListerTable>tbody>tr>td ul.MarkupFieldtype>li+li{border-top:1px solid #eee}#ProcessListerResults #ProcessListerTable table.ProcessListerTable>tbody>tr>td>*:first-child,#ProcessListerResults #ProcessListerTable table.ProcessListerTable>tbody>tr>td .col_preview>*:first-child{margin-top:0}#ProcessListerResults #ProcessListerTable table.ProcessListerTable>tbody>tr>td>*:last-child,#ProcessListerResults #ProcessListerTable table.ProcessListerTable>tbody>tr>td .col_preview>*:last-child{margin-bottom:0}#ProcessListerResults .PageListerActions{display:none;line-height:1.3em;text-transform:lowercase}#ProcessListerResults .PageListerActions a.PageExtra{margin-bottom:3px}#ProcessListerResults .PageListerActions a.PageExtras i{margin-left:3px;margin-right:2px}#ProcessListerResults .PageListerActions a.PageExtras.open i{margin-left:2px;margin-right:3px}#ProcessListerResults .row_message{display:inline}#ProcessListerResults .PageListStatusUnpublished{text-decoration:line-through;opacity:.5}#ProcessListerResults .PageListStatusHidden{opacity:.5}#ProcessListerResults .MarkupPagerNav{float:right}#ProcessListerResults .nobr{white-space:nowrap}#ProcessListerResults table+.MarkupPagerNav{margin:0}#ProcessListerResults .datetime{white-space:nowrap}#ProcessListerResults td:not(.col_editing) .InputfieldHasFileList .InputfieldHeader{display:none}#ProcessListerResults td:not(.col_editing) .InputfieldHasFileList .InputfieldContent{padding:5px;border:none;margin-top:5px;margin-bottom:5px}@media only screen and (max-width: 767px){#ProcessListerResults table.ProcessListerTable+.MarkupPagerNav{margin-bottom:1em}#ProcessListerResults .MarkupPagerNav{float:none}}.AdminDataTable p{margin:1em 0}#ProcessListerResults+a button{float:left;margin-right:0;margin-top:0}#ProcessListerSpinner{margin-left:.5em;font-size:20px;position:relative}#ProcessListerSpinner i{position:absolute;top:-15px;left:0}.pw-content .lister_headline,#content .lister_headline{float:left;margin-top:1em}@media only screen and (max-width: 767px){.pw-content .lister_headline,#content .lister_headline{float:none;clear:both}}#lister_open_cnt{display:none}#filters_spinner{float:right;margin-top:4px}#ProcessListerRefreshTab{float:right}#ProcessListerSelector{display:inline-block}p.version{clear:both;padding-top:1em}#ProcessListerScript{display:none}#table_bookmarks{margin-top:.5em}.AdminThemeReno a.lister-lightbox{padding:0 !important}.AdminThemeReno a.lister-lightbox img{margin:0 !important}.AdminThemeReno #content .lister_headline{margin-top:.5em}#ProcessListerTable .InputfieldFile .InputfieldContent,#ProcessListerTable .InputfieldFile .InputfieldHeader,#ProcessListerTable .InputfieldImage .InputfieldContent,#ProcessListerTable .InputfieldImage .InputfieldHeader{background:inherit}#ProcessListerTable .InputfieldFile .InputfieldHeader,#ProcessListerTable .InputfieldImage .InputfieldHeader{padding-left:0;padding-right:0;padding-top:0}#ProcessListerTable .InputfieldFile .gridImage__tooltip table th,#ProcessListerTable .InputfieldFile .gridImage__tooltip table td,#ProcessListerTable .InputfieldImage .gridImage__tooltip table th,#ProcessListerTable .InputfieldImage .gridImage__tooltip table td{padding:0} +#ProcessLister{margin-top:-2px}#ProcessListerResultsTab{padding-top:.5em}#ProcessListerResults>form.InputfieldForm{margin-bottom:0}#ProcessListerResults #ProcessListerTable{clear:both;overflow-x:auto}#ProcessListerResults #ProcessListerTable>div{margin-top:0}#ProcessListerResults #ProcessListerTable table.ProcessListerTable{clear:both;margin-top:0}#ProcessListerResults #ProcessListerTable table.ProcessListerTable td table{width:100%}#ProcessListerResults #ProcessListerTable table.ProcessListerTable>thead th{font-size:.8571428571em}#ProcessListerResults #ProcessListerTable table.ProcessListerTable>thead th i{font-size:14px}#ProcessListerResults #ProcessListerTable table.ProcessListerTable>thead th strong{white-space:nowrap}#ProcessListerResults #ProcessListerTable table.ProcessListerTable>thead th b,#ProcessListerResults #ProcessListerTable table.ProcessListerTable>thead .th b{display:none}#ProcessListerResults #ProcessListerTable table.ProcessListerTable>thead th:first-child{padding-left:0}#ProcessListerResults #ProcessListerTable table.ProcessListerTable>tbody>tr>td{font-size:.9285714286em}#ProcessListerResults #ProcessListerTable table.ProcessListerTable>tbody>tr>td:first-child,#ProcessListerResults #ProcessListerTable table.ProcessListerTable>tbody>tr>td:first-child>a{padding-left:0}#ProcessListerResults #ProcessListerTable table.ProcessListerTable>tbody>tr>td ul.MarkupFieldtype{margin:0;padding-left:0}#ProcessListerResults #ProcessListerTable table.ProcessListerTable>tbody>tr>td ul.MarkupFieldtype>li{list-style:none;margin:0;padding-left:0}#ProcessListerResults #ProcessListerTable table.ProcessListerTable>tbody>tr>td ul.MarkupFieldtype>li+li{border-top:1px solid #eee}#ProcessListerResults #ProcessListerTable table.ProcessListerTable>tbody>tr>td>*:first-child,#ProcessListerResults #ProcessListerTable table.ProcessListerTable>tbody>tr>td .col_preview>*:first-child{margin-top:0}#ProcessListerResults #ProcessListerTable table.ProcessListerTable>tbody>tr>td>*:last-child,#ProcessListerResults #ProcessListerTable table.ProcessListerTable>tbody>tr>td .col_preview>*:last-child{margin-bottom:0}#ProcessListerResults .PageListerActions{display:none;line-height:1.3em;text-transform:lowercase}#ProcessListerResults .PageListerActions a.PageExtra{margin-bottom:3px}#ProcessListerResults .PageListerActions a.PageExtras i{margin-left:3px;margin-right:2px}#ProcessListerResults .PageListerActions a.PageExtras.open i{margin-left:2px;margin-right:3px}#ProcessListerResults .row_message{display:inline}#ProcessListerResults .PageListStatusUnpublished{text-decoration:line-through;opacity:.5}#ProcessListerResults .PageListStatusHidden{opacity:.5}#ProcessListerResults .MarkupPagerNav{float:right}#ProcessListerResults .nobr{white-space:nowrap}#ProcessListerResults table+.MarkupPagerNav{margin:0}#ProcessListerResults .datetime{white-space:nowrap}#ProcessListerResults td:not(.col_editing) .InputfieldHasFileList .InputfieldHeader{display:none}#ProcessListerResults td:not(.col_editing) .InputfieldHasFileList .InputfieldContent{padding:5px;border:none;margin-top:5px;margin-bottom:5px}@media only screen and (max-width: 767px){#ProcessListerResults table.ProcessListerTable+.MarkupPagerNav{margin-bottom:1em}#ProcessListerResults .MarkupPagerNav{float:none}}.AdminDataTable p{margin:1em 0}#ProcessListerResults+a button{float:left;margin-right:0;margin-top:0}#ProcessListerSpinner{margin-left:.5em;font-size:20px;position:relative}#ProcessListerSpinner i{position:absolute;top:-15px;left:0}.pw-content .lister_headline,#content .lister_headline{float:left;margin-top:1em}@media only screen and (max-width: 767px){.pw-content .lister_headline,#content .lister_headline{float:none;clear:both}}#lister_open_cnt{display:none}#filters_spinner{float:right;margin-top:4px}#ProcessListerRefreshTab{float:right}#ProcessListerSelector{display:inline-block}#ProcessListerResultNotes+#ProcessListerSelector{margin-top:0}p.version{clear:both;padding-top:1em}#ProcessListerScript{display:none}#tab_bookmarks table tr>td:first-child{width:30%}#tab_bookmarks table tr>td:nth-child(2){width:40%}.AdminThemeReno a.lister-lightbox{padding:0 !important}.AdminThemeReno a.lister-lightbox img{margin:0 !important}.AdminThemeReno #content .lister_headline{margin-top:.5em}#ProcessListerTable .InputfieldFile .InputfieldContent,#ProcessListerTable .InputfieldFile .InputfieldHeader,#ProcessListerTable .InputfieldImage .InputfieldContent,#ProcessListerTable .InputfieldImage .InputfieldHeader{background:inherit}#ProcessListerTable .InputfieldFile .InputfieldHeader,#ProcessListerTable .InputfieldImage .InputfieldHeader{padding-left:0;padding-right:0;padding-top:0}#ProcessListerTable .InputfieldFile .gridImage__tooltip table th,#ProcessListerTable .InputfieldFile .gridImage__tooltip table td,#ProcessListerTable .InputfieldImage .gridImage__tooltip table th,#ProcessListerTable .InputfieldImage .gridImage__tooltip table td{padding:0} diff --git a/wire/modules/Process/ProcessPageLister/ProcessPageLister.module b/wire/modules/Process/ProcessPageLister/ProcessPageLister.module index 8757c492..1353bdcb 100644 --- a/wire/modules/Process/ProcessPageLister/ProcessPageLister.module +++ b/wire/modules/Process/ProcessPageLister/ProcessPageLister.module @@ -146,6 +146,14 @@ class ProcessPageLister extends Process implements ConfigurableModule { */ protected $openPageIDs = array(); + /** + * Additional notes about the results to display underneath them + * + * @var array + * + */ + protected $resultNotes = array(); + /** * Initalize module config variables * @@ -348,35 +356,25 @@ class ProcessPageLister extends Process implements ConfigurableModule { /** * Check for a bookmark specified in GET variable $n * - * return null|int|bool Returns NULL if not applicable, boolean false if bookmark not found, or integer of bookmark ID if applied + * @param string $bookmarkID + * @return null|int|bool Returns NULL if not applicable, boolean false if bookmark not found, or integer of bookmark ID if applied * */ - public function checkBookmark() { - $n = (int) $this->wire('input')->get('bookmark'); - if(!$n) return null; - $bookmarks = $this->getBookmarksInstance()->getBookmarks(); - if(!isset($bookmarks[$n])) return false; - $bookmark = $bookmarks[$n]; - $user = $this->wire('user'); - if(!$user->isSuperuser() && count($bookmark['roles'])) { - $allow = false; - foreach($bookmark['roles'] as $roleID) { - foreach($user->roles as $role) { - if($role->id == $roleID) { - $allow = true; - break; - } - } - } - if(!$allow) return false; - } + public function checkBookmark($bookmarkID = '') { + if(!$bookmarkID) $bookmarkID = $this->wire('input')->get('bookmark'); + if(!$bookmarkID) return null; + $bookmarks = $this->getBookmarksInstance(); + $bookmarkID = $bookmarks->_bookmarkID($bookmarkID); + if(!$bookmarkID) return false; + $bookmark = $bookmarks->getBookmark($bookmarkID); + if(!$bookmark || !$bookmarks->isBookmarkViewable($bookmark)) return false; $this->sessionClear(); $this->set('defaultSelector', $bookmark['selector']); $this->set('defaultSort', $bookmark['sort']); $this->sessionSet('sort', $bookmark['sort']); $this->set('columns', $bookmark['columns']); - - return $n; + $this->headline($this->wire('page')->title . ' - ' . $bookmark['title']); + return $bookmarkID; } /** @@ -1009,7 +1007,9 @@ class ProcessPageLister extends Process implements ConfigurableModule { // ok, override } else { $selectors->remove($s); - if($value && $showIncludeWarnings) $this->error($this->_("Specified 'include=' mode is not allowed here.")); + if($value && $showIncludeWarnings) { + $this->resultNotes[] = $this->_("Specified 'include=' mode is not allowed here.") . " (include=$value)"; + } $changed = true; } } @@ -1039,7 +1039,9 @@ class ProcessPageLister extends Process implements ConfigurableModule { // include=unpublished is allowed } else if($includeMode == 'unpublished') { // include=unpublished is not allowed - if($showIncludeWarnings) $this->error($this->_("Not all specified templates are editable. Only 'include=hidden' is allowed")); + if($showIncludeWarnings) { + $this->resultNotes[] = $this->_("Not all specified templates are editable. Only 'include=hidden' is allowed"); + } $includeSelector->value = 'hidden'; $changed = true; } @@ -1049,7 +1051,9 @@ class ProcessPageLister extends Process implements ConfigurableModule { // only allow a max include mode of hidden // regardless of edit access if($includeMode != 'hidden') { - if($showIncludeWarnings) $this->error($this->_("No templates specified so 'include=hidden' is max allowed include mode")); + if($showIncludeWarnings) { + $this->resultNotes[] = $this->_("No templates specified so 'include=hidden' is max allowed include mode"); + } $includeSelector->value = 'hidden'; $changed = true; } @@ -1783,9 +1787,15 @@ class ProcessPageLister extends Process implements ConfigurableModule { "
$tableOut
" . $pagerOut; + if(count($this->resultNotes)) { + $notes = array(); + foreach($this->resultNotes as $note) { + $notes[] = wireIconMarkup('warning') . ' ' . $this->wire('sanitizer')->entities1($note); + } + $out .= "

" . implode('
', $notes) . "

"; + } if($this->wire('config')->debug) { $out .= "

" . $this->wire('sanitizer')->entities($this->finalSelector) . "

"; - //$out .= "

" . $this->wire('sanitizer')->entities($findSelector) . "

"; } if(!$this->editOption) { @@ -1889,9 +1899,7 @@ class ProcessPageLister extends Process implements ConfigurableModule { } if($this->allowBookmarks) { $bookmarks = $this->getBookmarksInstance(); - if($this->wire('user')->isSuperuser() || count($bookmarks->getBookmarks())) { - $out .= $bookmarks->buildBookmarkListForm()->render(); - } + $out .= $bookmarks->buildBookmarkListForm()->render(); } $out .= $this->renderExtras(); } @@ -2096,7 +2104,27 @@ class ProcessPageLister extends Process implements ConfigurableModule { */ public function ___executeEditBookmark() { if(!$this->allowBookmarks) throw new WireException("Bookmarks are disabled"); - return $this->getBookmarksInstance()->editBookmark(); + return $this->getBookmarksInstance()->executeEditBookmark(); + } + + /** + * Catch-all for bookmarks + * + * @return string + * @throws Wire404Exception + * @throws WireException + * + */ + public function ___executeUnknown() { + $bookmarkID = $this->wire('input')->urlSegment1; + if(strpos($bookmarkID, 'bm') === 0) { + $bookmarks = $this->getBookmarksInstance(); + $bookmarkID = $bookmarks->_bookmarkID(ltrim($bookmarkID, 'bm')); + } else { + $bookmarkID = ''; + } + if(!$bookmarkID || !$this->checkBookmark($bookmarkID)) throw new Wire404Exception(); + return $this->execute(); } /** @@ -2123,13 +2151,14 @@ class ProcessPageLister extends Process implements ConfigurableModule { $user = $this->wire('user'); $languageID = $languages && !$user->language->isDefault() ? $user->language->id : ''; - foreach($bookmarks as $n => $bookmark) { + foreach($bookmarks as $bookmarkID => $bookmark) { $name = $bookmark['title']; + $icon = $bookmark['type'] ? 'user-circle-o' : 'search'; if($languageID && !empty($bookmark["title$languageID"])) $name = $bookmark["title$languageID"]; $item = array( - 'id' => $n, + 'id' => $bookmarkID, 'name' => $name, - 'icon' => 'search', + 'icon' => $icon, ); $items[] = $item; } diff --git a/wire/modules/Process/ProcessPageLister/ProcessPageLister.scss b/wire/modules/Process/ProcessPageLister/ProcessPageLister.scss index ae526a26..d43768a0 100644 --- a/wire/modules/Process/ProcessPageLister/ProcessPageLister.scss +++ b/wire/modules/Process/ProcessPageLister/ProcessPageLister.scss @@ -201,6 +201,9 @@ #ProcessListerSelector { display: inline-block; } +#ProcessListerResultNotes + #ProcessListerSelector { + margin-top: 0; +} p.version { clear: both; @@ -211,10 +214,18 @@ p.version { display: none; } -#table_bookmarks { - margin-top: 0.5em; +#tab_bookmarks { + table tr { + > td:first-child { + width: 30%; + } + > td:nth-child(2) { + width: 40%; + } + } } + .AdminThemeReno { a.lister-lightbox { padding: 0 !important; diff --git a/wire/modules/Process/ProcessPageLister/ProcessPageListerBookmarks.php b/wire/modules/Process/ProcessPageLister/ProcessPageListerBookmarks.php index 1eb93b85..00e41bf3 100644 --- a/wire/modules/Process/ProcessPageLister/ProcessPageListerBookmarks.php +++ b/wire/modules/Process/ProcessPageLister/ProcessPageListerBookmarks.php @@ -7,52 +7,74 @@ * */ class ProcessPageListerBookmarks extends Wire { - - protected $lister; - + + /** + * @var ProcessPageLister + * + */ + protected $lister; + + /** + * @var ListerBookmarks + * + */ + protected $bookmarks; + + /** + * @var Page + * + */ + protected $page; + + /** + * @var User + * + */ + protected $user; + + /** + * Construct + * + * @param ProcessPageLister $lister + * + */ public function __construct(ProcessPageLister $lister) { + require_once(__DIR__ . '/ListerBookmarks.php'); $this->lister = $lister; + $this->page = $lister->wire('page'); + $this->user = $lister->wire('user'); + $this->bookmarks = new ListerBookmarks($this->page, $this->user); + parent::__construct(); } /** - * Get configured bookmarks allowed for current user - * - * @return array - * + * @return ListerBookmarks + * */ - public function getBookmarks() { + public function bookmarks() { + return $this->bookmarks; + } - $page = $this->wire('page'); - $key = "_$page->id"; - $data = $this->wire('modules')->getModuleConfigData('ProcessPageLister'); - $_bookmarks = isset($data['bookmarks'][$key]) ? $data['bookmarks'][$key] : array(); - $bookmarks = array(); + /** + * Set the Lister page that bookmarks will be for + * + * @param Page $page + * + */ + public function setPage(Page $page) { + $this->page = $page; + $this->bookmarks->setPage($page); + } - foreach($_bookmarks as $n => $bookmark) { - $n = (int) ltrim($n, '_'); - $bookmark['url'] = $this->wire('page')->url . "?bookmark=$n"; - $bookmarks[$n] = $bookmark; - } - - if(!$this->wire('user')->isSuperuser()) { - $userRoles = $this->wire('user')->roles; - foreach($bookmarks as $n => $bookmark) { - $allowBookmark = false; - if(empty($bookmark['roles'])) { - $allowBookmark = true; - } else foreach($bookmark['roles'] as $roleID) { - foreach($userRoles as $userRole) { - if($userRole->id == $roleID) { - $allowBookmark = true; - break; - } - } - } - if(!$allowBookmark) unset($bookmarks[$n]); - } - } - - return $bookmarks; + /** + * Set user that bookmarks will be for + * + * @param User $user + * + */ + public function setUser(User $user) { + $this->user = $user; + $this->bookmarks->setUser($user); } /** @@ -63,6 +85,9 @@ class ProcessPageListerBookmarks extends Wire { */ public function buildBookmarkListForm() { + /** @var Sanitizer $sanitizer */ + $sanitizer = $this->wire('sanitizer'); + /** @var InputfieldForm $form */ $form = $this->modules->get('InputfieldForm'); $form->attr('id', 'tab_bookmarks'); @@ -71,180 +96,318 @@ class ProcessPageListerBookmarks extends Wire { $form->class .= ' WireTab'; $form->attr('title', $this->_x('Bookmarks', 'tab')); - $user = $this->wire('user'); - $bookmarks = $this->getBookmarks(); - $superuser = $user->isSuperuser(); + $publicBookmarks = $this->bookmarks->getPublicBookmarks(); + $ownedBookmarks = $this->bookmarks->getOwnedBookmarks(); + + /** @var Languages $languages */ $languages = $this->wire('languages'); - $languageID = $languages && !$user->language->isDefault() ? $user->language->id : ''; + $languageID = $languages && !$this->user->language->isDefault() ? $this->user->language->id : ''; - if($superuser) { - $fieldset = $this->buildBookmarkEditForm(0, $bookmarks); - if(count($bookmarks)) $fieldset->collapsed = Inputfield::collapsedYes; - $form->add($fieldset); - } + $addBookmarkFieldset = $this->buildBookmarkEditForm(0); + $addBookmarkFieldset->collapsed = Inputfield::collapsedYes; + $form->add($addBookmarkFieldset); - $f = $this->wire('modules')->get('InputfieldMarkup'); - $f->label = $form->attr('title'); - $f->icon = 'bookmark-o'; - $form->add($f); + $bookmarksByType = array( + ListerBookmarks::typeOwned => $ownedBookmarks, + ListerBookmarks::typePublic => $publicBookmarks, + ); - if(!count($bookmarks)) return $form; + $iconsByType = array( + ListerBookmarks::typeOwned => 'user-circle-o', + ListerBookmarks::typePublic => 'bookmark-o', + ); - // render table of current bookmarks - $table = $this->wire('modules')->get('MarkupAdminDataTable'); - $table->setID('table_bookmarks'); - $table->setSortable(false); - $headerRow = array($this->_x('Bookmark', 'bookmark-th')); - if($superuser) { - $headerRow[] = $this->_x('Selector', 'bookmark-th'); - $headerRow[] = $this->_x('Columns', 'bookmark-th'); - $headerRow[] = $this->_x('Access', 'bookmark-th'); - $headerRow[] = $this->_x('Action', 'bookmark-th'); - } - $table->headerRow($headerRow); - foreach($bookmarks as $n => $bookmark) { - $row = array(); - $title = $bookmark['title']; - if($languageID && !empty($bookmark["title$languageID"])) $title = $bookmark["title$languageID"]; - $row["$title\0"] = $bookmark['url']; - if($superuser) { - $selector = $bookmark['selector']; - if(strpos($selector, 'template=') !== false && preg_match('/template=([\d\|]+)/', $selector, $matches)) { - // make templates readable, for output purposes - $t = ''; - foreach(explode('|', $matches[1]) as $templateID) { - $template = $this->wire('templates')->get((int) $templateID); - $t .= ($t ? '|' : '') . ($template ? $template->name : $templateID); + $typeLabels = array( + ListerBookmarks::typeOwned => $this->_('My bookmarks'), + ListerBookmarks::typePublic => $this->_('Public bookmarks') + ); + + foreach($bookmarksByType as $bookmarkType => $bookmarks) { + + if(empty($bookmarks)) continue; + + /** @var InputfieldMarkup $f */ + $f = $this->wire('modules')->get('InputfieldMarkup'); + $f->label = $typeLabels[$bookmarkType]; + $f->icon = $iconsByType[$bookmarkType]; + + $headerRow = array( + 0 => $this->_x('Bookmark', 'bookmark-th'), + 1 => $this->_x('Description', 'bookmark-th'), + 2 => $this->_x('Access', 'bookmark-th'), + 3 => $this->_x('Actions', 'bookmark-th'), + ); + + /** @var MarkupAdminDataTable $table */ + $table = $this->wire('modules')->get('MarkupAdminDataTable'); + $table->setID('table_bookmarks_' . $bookmarkType); + $table->setSortable(false); + $table->setEncodeEntities(false); + $table->headerRow($headerRow); + + foreach($bookmarks as $bookmarkID => $bookmark) { + + $row = array(); + if(!$this->bookmarks->isBookmarkViewable($bookmark)) continue; + + // title column + $title = $bookmark['title']; + if($languageID && !empty($bookmark["title$languageID"])) $title = $bookmark["title$languageID"]; + $title = $sanitizer->entities($title); + $viewUrl = $this->bookmarks->getBookmarkUrl($bookmarkID, $this->user); + $row["$title\0"] = $viewUrl; + + // description column + $desc = $bookmark['desc']; + if($languageID && !empty($bookmark["desc$languageID"])) $desc = $bookmark["desc$languageID"]; + if(empty($desc)) { + $selector = $this->bookmarks->readableBookmarkSelector($bookmark); + $columns = implode(', ', $bookmark['columns']); + $desc = "$selector ($columns)"; + } + $row[] = $sanitizer->entities($desc); + + // access column (public bookmarks only) + if($bookmark['type'] == ListerBookmarks::typePublic) { + if(count($bookmark['roles']) < 2 && ((int) reset($bookmark['roles'])) === 0) { + $row[] = $this->_x('all', 'bookmark-roles'); + } else if(count($bookmark['roles'])) { + $row[] = $this->wire('pages')->getById($bookmark['roles'])->implode(', ', 'name'); } - $selector = str_replace($matches[0], "template=$t", $selector); + } else { + $row[] = $this->_('you'); } - if($bookmark['sort']) $selector .= ($selector ? ", " : "") . "sort=$bookmark[sort]"; - $row[] = $selector; - $row[] = implode(', ', $bookmark['columns']); - if(count($bookmark['roles']) < 2 && ((int) reset($bookmark['roles'])) === 0) { - $row[] = $this->_x('all', 'bookmark-roles'); - } else if(count($bookmark['roles'])) { - $row[] = $this->wire('pages')->getById($bookmark['roles'])->implode(', ', 'name'); + + // actions column + $actions = array(); + $actions[] = "" . $this->_x('View', 'bookmark-action') . ""; + if($this->bookmarks->isBookmarkEditable($bookmark)) { + $editUrl = $this->bookmarks->getBookmarkEditUrl($bookmarkID); + $actions[] = "" . $this->_x('Modify', 'bookmark-action') . ""; + if($this->bookmarks->isBookmarkDeletable($bookmark)) { + $actions[] = "" . $this->_x('Delete', 'bookmark-action') . ""; + } } - $row[$this->_x('Edit', 'bookmark-action')] = "./edit-bookmark/?n=$n"; + + $actions = implode('  /  ', $actions); + $row[] = $actions; + + $table->row($row); } - $table->row($row); + + $f->value = $table->render(); + + $form->add($f); } - if($superuser) $f->appendMarkup = "

" . $this->_('Superuser note: other users can see and click bookmarks, but may not add or edit them.') . "

"; - $f->value = $table->render(); return $form; } + /** + * Build form for deleting a bookmark + * + * @param int $bookmarkID Bookmark ID + * + * @return InputfieldFieldset + * @throws WireException + * @throws WirePermissionException + * + */ + protected function buildBookmarkDeleteForm($bookmarkID) { + + $bookmark = $this->bookmarks->getBookmark($bookmarkID); + if(!$bookmark) throw new WireException('Unknown bookmark'); + + /** @var InputfieldFieldset $fieldset */ + $fieldset = $this->wire('modules')->get('InputfieldFieldset'); + $fieldset->icon = 'trash-o'; + $fieldset->label = $this->_('Please check the box to confirm you want to delete this bookmark'); + + /** @var InputfieldCheckbox $f */ + $f = $this->wire('modules')->get('InputfieldCheckbox'); + $f->attr('name', 'delete_bookmark'); + $f->label = $this->_('Delete this bookmark?'); + $f->icon = 'trash-o'; + $f->attr('value', $bookmarkID); + $fieldset->add($f); + + /** @var InputfieldSubmit $submit */ + $submit = $this->wire('modules')->get('InputfieldSubmit'); + $submit->attr('name', 'submit_delete_bookmark'); + $submit->icon = 'trash-o'; + $fieldset->add($submit); + + /** @var InputfieldButton $cancel */ + $cancel = $this->wire('modules')->get('InputfieldButton'); + $cancel->href = '../#tab_bookmarks'; + $cancel->setSecondary(true); + $cancel->attr('value', $this->_('Cancel')); + $fieldset->add($cancel); + + return $fieldset; + } + /** * Build the form needed to edit/add bookmarks * * @param int $bookmarkID Specify bookmark ID if editing existing bookmark - * @param array $bookmarks Optionally include list of all bookmarks to prevent this method from having to re-load them + * * @return InputfieldWrapper * */ - protected function buildBookmarkEditForm($bookmarkID = 0, $bookmarks = array()) { - + protected function buildBookmarkEditForm($bookmarkID = 0) { + + /** @var Languages $languages */ $languages = $this->wire('languages'); + + /** @var InputfieldFieldset $fieldset */ $fieldset = $this->wire('modules')->get('InputfieldFieldset'); if($bookmarkID) { - if(empty($bookmarks)) $bookmarks = $this->getBookmarks(); - $bookmark = isset($bookmarks[$bookmarkID]) ? $bookmarks[$bookmarkID] : array(); - if(empty($bookmark)) $bookmarkID = 0; + $bookmark = $this->bookmarks->getBookmark($bookmarkID); + if(!$bookmark) throw new WireException("Unknown bookmark"); + if(!$this->bookmarks->isBookmarkEditable($bookmark)) throw new WirePermissionException('Bookmark is not editable'); $fieldset->label = $this->_('Edit Bookmark'); } else { $bookmark = array(); $fieldset->label = $this->_('Add New Bookmark'); + $fieldset->description = $this->_('Creates a new bookmark matching your current filters, columns and order.'); } $fieldset->icon = $bookmarkID ? 'bookmark-o' : 'plus-circle'; - if(!$bookmarkID) $fieldset->description = $this->_('Creates a new bookmark matching your current filters, columns and order.'); - $f = $this->wire('modules')->get('InputfieldText'); - $f->attr('name', 'bookmark_title'); - $f->label = $this->_x('Title', 'bookmark-editor'); // Bookmark title - $f->required = true; - if($languages) $f->useLanguages = true; - $fieldset->add($f); + /** @var InputfieldText $titleField */ + $titleField = $this->wire('modules')->get('InputfieldText'); + $titleField->attr('name', 'bookmark_title'); + $titleField->label = $this->_x('Title', 'bookmark-editor'); // Bookmark title + $titleField->required = true; + if($languages) $titleField->useLanguages = true; + $fieldset->add($titleField); + + /** @var InputfieldText $descField */ + $descField = $this->wire('modules')->get('InputfieldText'); + $descField->attr('name', 'bookmark_desc'); + $descField->label = $this->_x('Description', 'bookmark-editor'); // Bookmark title + if($languages) $descField->useLanguages = true; + $fieldset->add($descField); if($bookmarkID) { // editing existing bookmark - $f->attr('value', $bookmark['title']); - if($languages) foreach($languages as $language) { - if($language->isDefault()) continue; - $f->attr("value$language", isset($bookmark["title$language"]) ? $bookmark["title$language"] : ""); + $titleField->attr('value', $bookmark['title']); + $descField->attr('value', $bookmark['desc']); + + if($languages) { + foreach($languages as $language) { + if($language->isDefault()) continue; + $titleField->attr("value$language", isset($bookmark["title$language"]) ? $bookmark["title$language"] : ""); + $descField->attr("value$language", isset($bookmark["desc$language"]) ? $bookmark["desc$language"] : ""); + } } - $f = $this->wire('modules')->get('InputfieldSelector'); - $f->attr('name', 'bookmark_selector'); - $f->label = $this->_x('What pages should this bookmark show?', 'bookmark-editor'); - $selector = $bookmark['selector']; - if($bookmark['sort']) $selector .= ", sort=$bookmark[sort]"; - if($this->lister->initSelector && strpos($selector, $this->lister->initSelector) !== false) { - $selector = str_replace($this->lister->initSelector, '', $selector); // ensure that $selector does not contain initSelector + if($this->user->isSuperuser()) { + /** @var InputfieldSelector $f */ + $f = $this->wire('modules')->get('InputfieldSelector'); + $f->attr('name', 'bookmark_selector'); + $f->label = $this->_x('What pages should this bookmark show?', 'bookmark-editor'); + $selector = $bookmark['selector']; + if($bookmark['sort']) $selector .= ", sort=$bookmark[sort]"; + if($this->lister->initSelector && strpos($selector, $this->lister->initSelector) !== false) { + $selector = str_replace($this->lister->initSelector, '', $selector); // ensure that $selector does not contain initSelector + } + if($this->lister->template) $f->initTemplate = $this->lister->template; + $default = $this->lister->className() == 'ProcessPageLister'; + $f->preview = false; + $f->allowSystemCustomFields = true; + $f->allowSystemTemplates = true; + $f->allowSubfieldGroups = $default ? false : true; + $f->allowSubselectors = $default ? false : true; + $f->showFieldLabels = $this->lister->useColumnLabels ? 1 : 0; + $f->initValue = $this->lister->initSelector; + $f->attr('value', $selector); + $fieldset->add($f); + + $f = $this->lister->buildColumnsField(); + $f->attr('name', 'bookmark_columns'); + $f->attr('value', $bookmark['columns']); + $f->label = $this->_x('Columns', 'bookmark-editor'); + $fieldset->add($f); } - if($this->lister->template) $f->initTemplate = $this->lister->template; - $default = $this->lister->className() == 'ProcessPageLister'; - $f->preview = false; - $f->allowSystemCustomFields = true; - $f->allowSystemTemplates = true; - $f->allowSubfieldGroups = $default ? false : true; - $f->allowSubselectors = $default ? false : true; - $f->showFieldLabels = $this->lister->useColumnLabels ? 1 : 0; - $f->initValue = $this->lister->initSelector; - $f->attr('value', $selector); + + $f = $this->wire('modules')->get('InputfieldHidden'); + $f->attr('name', 'bookmark_type'); + $f->attr('value', (int) $bookmark['type']); $fieldset->add($f); - $f = $this->lister->buildColumnsField(); - $f->attr('name', 'bookmark_columns'); - $f->attr('value', $bookmark['columns']); - $f->label = $this->_x('Columns', 'bookmark-editor'); + } else { + // add new bookmark + if($this->user->isSuperuser()) { + $f = $this->wire('modules')->get('InputfieldRadios'); + $f->attr('name', 'bookmark_type'); + $f->label = $this->_('Bookmark type'); + $f->addOption(ListerBookmarks::typeOwned, $this->_('Owned') . ' ' . + "[span.detail] " . $this->_('(visible to me only)') . " [/span] "); + $f->addOption(ListerBookmarks::typePublic, $this->_('Public')); + $f->attr('value', isset($bookmark['type']) ? (int) $bookmark['type'] : ListerBookmarks::typeOwned); + $fieldset->add($f); + } + } + + if($this->user->isSuperuser()) { + /** @var InputfieldAsmSelect $f */ + $f = $this->wire('modules')->get('InputfieldAsmSelect'); + $f->attr('name', 'bookmark_roles'); + $f->label = $this->_x('Access', 'bookmark-editor'); + $f->icon = 'key'; + $f->description = $this->_('What user roles will see this bookmark? If no user roles are selected, then all roles with permission to use this Lister can view the bookmark.'); + foreach($this->wire('roles') as $role) { + if($role->name != 'guest') $f->addOption($role->id, $role->name); + } + if($bookmarkID) $f->attr('value', $bookmark['roles']); + $f->collapsed = Inputfield::collapsedBlank; + $f->showIf = 'bookmark_type=' . ListerBookmarks::typePublic; $fieldset->add($f); } - $f = $this->wire('modules')->get('InputfieldAsmSelect'); - $f->attr('name', 'bookmark_roles'); - $f->label = $this->_x('Access', 'bookmark-editor'); - $f->icon = 'key'; - $f->description = $this->_('What user roles will see this bookmark? If no user roles are selected, then all roles with permission to use this Lister can view the bookmark.'); - foreach($this->wire('roles') as $role) { - if($role->name != 'guest') $f->addOption($role->id, $role->name); + /** @var InputfieldCheckbox $f */ + $f = $this->wire('modules')->get('InputfieldCheckbox'); + $f->attr('name', 'bookmark_share'); + $f->label = $this->_('Allow other users to access this bookmark URL?'); + $f->description = $this->_('If you send the bookmark URL to someone else that is already logged in to the admin, they can view the bookmark if you check this box.'); + $f->notes = sprintf( + $this->_('Shareable bookmark URL: [View](%s)'), + $this->page->httpUrl() . str_replace($this->page->url, '', $this->bookmarks->getBookmarkUrl($bookmarkID, $this->user)) + ); + if(empty($bookmark['share'])) { + $f->collapsed = Inputfield::collapsedYes; + } else { + $f->attr('checked', 'checked'); } - if($bookmarkID) $f->attr('value', $bookmark['roles']); - $f->collapsed = Inputfield::collapsedBlank; + $f->showIf = 'bookmark_type=' . ListerBookmarks::typeOwned; $fieldset->add($f); if($bookmarkID) { - - /** @var InputfieldAsmSelect $f */ - $f = $this->wire('modules')->get('InputfieldAsmSelect'); - $f->attr('name', 'bookmarks_sort'); - $f->label = $this->_('Bookmarks sort order'); - $f->icon = 'sort'; - $f->setAsmSelectOption('removeLabel', ''); - $value = array(); - foreach($bookmarks as $n => $b) { - $f->addOption($n, $b['title']); - $value[] = $n; + $bookmarks = $bookmark['type'] == ListerBookmarks::typePublic ? $this->bookmarks->getPublicBookmarks() : $this->bookmarks->getOwnedBookmarks(); + // option for changing the order of bookmarks + if(count($bookmarks) > 1) { + /** @var InputfieldAsmSelect $f */ + $f = $this->wire('modules')->get('InputfieldAsmSelect'); + $f->attr('name', 'bookmarks_sort'); + $f->label = $this->_('Order'); + $f->icon = 'sort'; + $f->setAsmSelectOption('removeLabel', ''); + $value = array(); + foreach($bookmarks as $bmid => $bm) { + $f->addOption($bmid, $bm['title']); + $value[] = $bmid; + } + $f->attr('value', $value); + $fieldset->add($f); } - $f->attr('value', $value); - $f->collapsed = Inputfield::collapsedYes; - $fieldset->add($f); - - $f = $this->wire('modules')->get('InputfieldCheckbox'); - $f->attr('name', 'delete_bookmark'); - $f->label = $this->_x('Delete', 'bookmark-editor'); - $f->label2 = $this->_('Delete this bookmark?'); - $f->icon = 'trash-o'; - $f->attr('value', $bookmarkID); - $f->collapsed = Inputfield::collapsedYes; - $fieldset->add($f); } + /** @var InputfieldSubmit $submit */ $submit = $this->wire('modules')->get('InputfieldSubmit'); - $submit->attr('name', 'submit_bookmark'); + $submit->attr('name', 'submit_save_bookmark'); $submit->icon = 'bookmark-o'; $fieldset->add($submit); @@ -258,21 +421,46 @@ class ProcessPageListerBookmarks extends Wire { * @throws WirePermissionException * */ - public function editBookmark() { + public function executeEditBookmark() { - if(!$this->wire('user')->isSuperuser()) throw new WirePermissionException("Only superuser can edit bookmarks"); + /** @var WireInput $input */ + $input = $this->wire('input'); - if($this->wire('input')->post('bookmark_title')) return $this->saveBookmark(); + $deleteBookmarkID = $this->bookmarks->_bookmarkID($input->post('delete_bookmark')); + if($deleteBookmarkID) { + if($this->bookmarks->deleteBookmarkByID($deleteBookmarkID)) { + $this->message($this->_('Deleted bookmark')); + } else { + $this->error($this->_('Bookmark is not deletable')); + } + $this->redirectToBookmarks(); + return ''; + } - $bookmarkID = $this->wire('input')->get->int('n'); + if($input->post('bookmark_title')) { + return $this->executeSaveBookmark(); + } + $bookmarkID = $this->bookmarks->_bookmarkID($input->get('bookmark')); + $bookmark = $this->bookmarks->getBookmark($bookmarkID); + + if(!$bookmark) $this->redirectToBookmarks(); + if(!$this->bookmarks->isBookmarkEditable($bookmark)) throw new WirePermissionException("Bookmark not editable"); + + /** @var InputfieldForm $form */ $form = $this->wire('modules')->get('InputfieldForm'); $form->attr('action', './'); - $fieldset = $this->buildBookmarkEditForm($bookmarkID); + if($input->get('delete')) { + $fieldset = $this->buildBookmarkDeleteForm($bookmarkID); + $this->lister->headline($bookmark['title']); + } else { + $fieldset = $this->buildBookmarkEditForm($bookmarkID); + $this->lister->headline($fieldset->label); + } $form->add($fieldset); - $this->lister->headline($fieldset->label); + /** @var InputfieldHidden $f */ $f = $this->wire('modules')->get('InputfieldHidden'); $f->attr('name', 'bookmark_id'); $f->attr('value', $bookmarkID); @@ -290,30 +478,52 @@ class ProcessPageListerBookmarks extends Wire { * Performs redirect after saving * */ - protected function saveBookmark() { + protected function executeSaveBookmark() { $input = $this->wire('input'); $sanitizer = $this->wire('sanitizer'); - $page = $this->wire('page'); + $languages = $this->wire('languages'); - $bookmarkID = $input->post->int('bookmark_id'); + $bookmarkID = $this->bookmarks->_bookmarkID($input->post('bookmark_id')); $bookmarkTitle = $input->post->text('bookmark_title'); + $bookmarkDesc = $input->post->text('bookmark_desc'); if(!$bookmarkID && empty($bookmarkTitle)) { - $this->wire('session')->redirect('../#tab_bookmarks'); + $this->redirectToBookmarks(); return; } + if($bookmarkID) { + $existingBookmark = $this->bookmarks->getBookmark($bookmarkID); + if(!$existingBookmark || !$this->bookmarks->isBookmarkEditable($existingBookmark)) { + throw new WirePermissionException("Bookmark not editable"); + } + } else { + $existingBookmark = null; + } + $bookmarkSort = ''; - $textOptions = array('maxLength' => 1024, 'stripTags' => false); - $bookmarkSelector = $bookmarkID ? $input->post->text('bookmark_selector', $textOptions) : $this->lister->getSelector(); + $textOptions = array( + 'maxLength' => 1024, + 'stripTags' => false + ); + + if($this->user->isSuperuser()) { + $bookmarkSelector = $bookmarkID ? $input->post->text('bookmark_selector', $textOptions) : $this->lister->getSelector(); + } else { + $bookmarkSelector = $existingBookmark ? $existingBookmark['selector'] : $this->lister->getSelector(); + } + if(preg_match('/\bsort=([-_.a-zA-Z]+)/', $bookmarkSelector, $matches)) $bookmarkSort = $matches[1]; $bookmarkSelector = preg_replace('/\b(include|sort|limit)=[^,]+,?/', '', $bookmarkSelector); + if($this->lister->initSelector && strpos($bookmarkSelector, $this->lister->initSelector) !== false) { // ensure that $selector does not contain initSelector $bookmarkSelector = str_replace($this->lister->initSelector, '', $bookmarkSelector); } + $bookmarkSelector = str_replace(', , ', ', ', $bookmarkSelector); + $bookmarkSelector = trim($bookmarkSelector, ', '); if($bookmarkID) { $bookmarkColumns = $input->post('bookmark_columns'); @@ -330,63 +540,89 @@ class ProcessPageListerBookmarks extends Wire { $bookmarkColumns = $this->lister->columns; } - $bookmark = array( + $bookmark = $this->bookmarks->_bookmark(array( + 'id' => $bookmarkID, 'title' => $bookmarkTitle, - 'selector' => trim($bookmarkSelector, ", "), + 'desc' => $bookmarkDesc, + 'selector' => $bookmarkSelector, 'columns' => $bookmarkColumns, 'sort' => $bookmarkSort, - 'roles' => $input->post->intArray('bookmark_roles') - ); - - $languages = $this->wire('languages'); - if($languages) foreach($languages as $language) { - if($language->isDefault()) continue; - $bookmark["title$language"] = $input->post->text("bookmark_title__$language"); - } + 'share' => $input->post('bookmark_share') ? true : false + )); - $data = $this->wire('modules')->getModuleConfigData('ProcessPageLister'); - $_bookmarks = isset($data['bookmarks']) ? $data['bookmarks'] : array(); - - foreach($_bookmarks as $pageID => $bookmarks) { - // remove bookmarks for Lister pages that no longer exist - $pageID = (int) ltrim($pageID, '_'); - if($pageID == $page->id) continue; - if(!$this->wire('pages')->get($pageID)->id) unset($_bookmarks[$pageID]); - } - - $bookmarks = isset($_bookmarks["_$page->id"]) ? $_bookmarks["_$page->id"] : array(); - - if($bookmarkID) { - $n = $bookmarkID; + if($this->user->isSuperuser()) { + $bookmark['type'] = $input->post->int('bookmark_type'); + $bookmark['roles'] = $input->post->intArray('bookmark_roles'); } else { - $n = time(); - while(isset($bookmarks[$n])) $n++; + $bookmark['type'] = ListerBookmarks::typeOwned; + $bookmark['roles'] = array(); } - $bookmarks["_$n"] = $bookmark; + if($languages) { + foreach($languages as $language) { + if($language->isDefault()) continue; + $bookmark["title$language"] = $input->post->text("bookmark_title__$language"); + $bookmark["desc$language"] = $input->post->text("bookmark_desc__$language"); + } + } + + if($bookmark['type'] == ListerBookmarks::typeOwned) { + $bookmarks = $this->bookmarks->getOwnedBookmarks(); + } else { + $bookmarks = $this->bookmarks->getPublicBookmarks(); + } + + $typePrefix = $this->bookmarks->typePrefix($bookmark['type']); + if(!$bookmarkID) $bookmarkID = $typePrefix . time(); // new bookmark + $bookmarks[$bookmarkID] = $bookmark; // update sort order of all bookmarks - if($input->post->bookmarks_sort) { + if($input->post('bookmarks_sort')) { $sorted = array(); - foreach($input->post->intArray('bookmarks_sort') as $bmid) { - $bm = $bookmarks["_$bmid"]; - $sorted["_$bmid"] = $bm; + foreach($input->post->array('bookmarks_sort') as $bmid) { + $bmid = $this->bookmarks->_bookmarkID($bmid); + if(!isset($bookmarks[$bmid])) continue; + $bm = $bookmarks[$bmid]; + $sorted[$bmid] = $bm; } $bookmarks = $sorted; } - if($bookmarkID && $input->post('delete_bookmark') == $bookmarkID) { - unset($bookmarks["_$n"]); - $this->message(sprintf($this->_('Deleted bookmark: %s'), $bookmarkTitle)); - $bookmarkID = 0; + if($bookmark['type'] == ListerBookmarks::typeOwned) { + $this->bookmarks->saveOwnedBookmarks($bookmarks); } else { - $this->message(sprintf($this->_('Saved bookmark: %s'), $bookmarkTitle)); + $this->bookmarks->savePublicBookmarks($bookmarks); } - $_bookmarks["_$page->id"] = $bookmarks; - $data['bookmarks'] = $_bookmarks; - - $this->wire('modules')->saveModuleConfigData('ProcessPageLister', $data); - $this->wire('session')->redirect("../?bookmark=$bookmarkID#tab_bookmarks"); + $this->redirectToBookmarks($bookmarkID); } -} \ No newline at end of file + + protected function redirectToBookmarks($bookmarkID = '') { + $url = $this->page->url; + if($bookmarkID) { + $this->wire('session')->redirect($url . "?bookmark=$bookmarkID#tab_bookmarks"); + } else { + $this->wire('session')->redirect($url . '#tab_bookmarks'); + } + } + + public function _bookmark(array $bookmark = array()) { + return $this->bookmarks->_bookmark($bookmark); + } + + public function _bookmarkID($bookmarkID) { + return $this->bookmarks->_bookmarkID($bookmarkID); + } + + public function getBookmark($bookmarkID, $type = null) { + return $this->bookmarks->getBookmark($bookmarkID, $type); + } + + public function getBookmarks() { + return $this->bookmarks->getBookmarks(); + } + + public function isBookmarkViewable($bookmark) { + return $this->bookmarks->isBookmarkViewable($bookmark); + } +}