From 6f4590404c7742f9d43236e2f4912079dc17c62e Mon Sep 17 00:00:00 2001 From: Samuel Georges Date: Sat, 11 Jul 2015 08:29:23 +1000 Subject: [PATCH] Add logic to look for !!! in plugin updates - Fixes #785 --- modules/system/assets/css/updates/details.css | 53 ++++++ modules/system/assets/css/updates/install.css | 2 +- modules/system/assets/css/updates/updates.css | 60 +++++- modules/system/assets/js/updates/updates.js | 49 +++++ .../system/assets/less/updates/details.less | 51 +++++ .../system/assets/less/updates/updates.less | 63 ++++++- modules/system/classes/UpdateManager.php | 13 +- modules/system/controllers/Updates.php | 177 +++++++++++++++++- .../controllers/updates/_column_code.htm | 19 ++ .../controllers/updates/_disable_form.htm | 15 ++ .../updates/_list_manage_toolbar.htm | 3 + .../controllers/updates/_update_form.htm | 4 +- .../controllers/updates/_update_list.htm | 76 ++++++-- .../updates/config_manage_list.yaml | 3 +- .../system/controllers/updates/details.htm | 76 ++++++++ ...10_01_000015_Db_System_Add_Frozen_Flag.php | 21 +++ modules/system/lang/en/lang.php | 10 +- modules/system/models/PluginVersion.php | 1 - .../models/pluginversion/columns_manage.yaml | 15 ++ 19 files changed, 670 insertions(+), 41 deletions(-) create mode 100644 modules/system/assets/css/updates/details.css create mode 100644 modules/system/assets/less/updates/details.less create mode 100644 modules/system/controllers/updates/_column_code.htm create mode 100644 modules/system/controllers/updates/details.htm create mode 100644 modules/system/database/migrations/2015_10_01_000015_Db_System_Add_Frozen_Flag.php create mode 100644 modules/system/models/pluginversion/columns_manage.yaml diff --git a/modules/system/assets/css/updates/details.css b/modules/system/assets/css/updates/details.css new file mode 100644 index 000000000..4df6b3f9f --- /dev/null +++ b/modules/system/assets/css/updates/details.css @@ -0,0 +1,53 @@ +.plugin-details-content { + padding: 0 0; +} +.plugin-details-content > *:first-child { + margin-top: 0; +} +.plugin-details-content h1 { + font-size: 28px; +} +.plugin-details-content h2 { + font-size: 24px; +} +.plugin-details-content h3 { + font-size: 20px; +} +.plugin-details-content h4 { + font-size: 17px; +} +.plugin-details-content h1, +.plugin-details-content h2 { + padding-bottom: 10px; + border-bottom: 1px solid #ccc; +} +.plugin-details-content ul, +.plugin-details-content ol { + padding-left: 20px; +} +.plugin-details-content pre { + display: block; + padding: 10px 10px 10px 20px; + font-size: 13px; + word-break: break-all; + word-wrap: break-word; + color: #fff; + background-color: #333; + margin-top: 10px; + margin-bottom: 20px; + margin-left: -20px; + margin-right: -20px; +} +.plugin-details-content pre code { + padding: 0; + font-size: inherit; + color: inherit; + white-space: pre-wrap; + background-color: transparent; + border-radius: 0; +} +.plugin-details-content ul pre, +.plugin-details-content ol pre { + margin-left: -40px; + padding-left: 40px; +} diff --git a/modules/system/assets/css/updates/install.css b/modules/system/assets/css/updates/install.css index 904a3bdd1..3d687efe1 100644 --- a/modules/system/assets/css/updates/install.css +++ b/modules/system/assets/css/updates/install.css @@ -196,7 +196,7 @@ display: block; width: 24px; height: 24px; - background-image: url(../../../../system/assets/ui/images/loader-transparent.svg); + background-image: url('../../../../system/assets/ui/images/loader-transparent.svg'); background-size: 24px 24px; background-position: 50% 50%; -webkit-animation: spin 1s linear infinite; diff --git a/modules/system/assets/css/updates/updates.css b/modules/system/assets/css/updates/updates.css index c7f3b1507..fdbf72d7a 100644 --- a/modules/system/assets/css/updates/updates.css +++ b/modules/system/assets/css/updates/updates.css @@ -1,3 +1,6 @@ +.important-update-label { + margin: 7px 0; +} .control-updatelist { border: 1px solid #ccc; margin-bottom: 20px; @@ -8,11 +11,11 @@ .control-updatelist .update-item .item-header { background-color: #f5f5f5; border-bottom: 1px solid #ccc; - padding: 15px 10px; + padding: 0 10px; } .control-updatelist .update-item .item-header h5 { margin: 0; - padding: 0; + padding: 15px 0; text-transform: uppercase; font-size: 13px; } @@ -26,10 +29,13 @@ line-height: 13px; margin-right: 5px; } +.control-updatelist .update-item .item-header .important-update { + padding: 7px 0 0 0; + float: right; +} .control-updatelist .update-item dl { padding: 10px; margin-bottom: 0; - font-size: 12px; } .control-updatelist .update-item dl:before, .control-updatelist .update-item dl:after { @@ -43,12 +49,37 @@ .control-updatelist .update-item dl dd { float: left; padding: 5px 0; + line-height: 20px; +} +.control-updatelist .update-item dl dt.text-muted, +.control-updatelist .update-item dl dd.text-muted { + color: #999 !important; } .control-updatelist .update-item dl dt { width: 15%; + clear: left; + font-size: 14px; + font-weight: 400; + color: #2b3e50; } .control-updatelist .update-item dl dd { width: 85%; + font-size: 12px; + color: #455152; +} +.control-updatelist .update-item dl .important-update-label { + position: relative; + top: -1px; + background-color: #ab2a1c; + color: #fff; + display: inline-block; + padding: .2em .6em .3em; + font-size: 75%; + line-height: 1; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + border-radius: .25em; } .control-updatelist .update-item:last-child { border-bottom: none; @@ -56,6 +87,27 @@ .control-updatelist .update-item.item-danger .item-header { background-color: #f2dede; } -.control-updatelist .update-item.item-danger dl { +.control-updatelist .update-item.item-danger .item-header h5, +.control-updatelist .update-item.item-danger .item-header i, +.control-updatelist .update-item.item-danger .item-header { color: #a94442; } +.control-updatelist .update-item.item-success .item-header { + background-color: #dff0d8; +} +.control-updatelist .update-item.item-success .item-header h5, +.control-updatelist .update-item.item-success .item-header i, +.control-updatelist .update-item.item-success .item-header { + color: #3c763d; +} +.control-updatelist .update-item.item-muted { + border-bottom: none; +} +.control-updatelist .update-item.item-muted .item-header h5, +.control-updatelist .update-item.item-muted .item-header i, +.control-updatelist .update-item.item-muted .item-header { + color: rgba(0, 0, 0, 0.35); +} +.control-updatelist .update-item.item-muted dl { + display: none; +} diff --git a/modules/system/assets/js/updates/updates.js b/modules/system/assets/js/updates/updates.js index c2402bf49..9665c60e3 100644 --- a/modules/system/assets/js/updates/updates.js +++ b/modules/system/assets/js/updates/updates.js @@ -19,6 +19,55 @@ this.updateSteps = null } + UpdateProcess.prototype.check = function() { + var $form = $('#updateForm'), + self = this + + $form.request('onCheckForUpdates').done(function() { + self.evalConfirmedUpdates() + }) + + $form.on('change', '[data-important-update-select]', function() { + var $el = $(this), + selectedValue = $el.val(), + $updateItem = $el.closest('.update-item') + + $updateItem.removeClass('item-danger item-muted item-success') + + if (selectedValue == 'confirm') { + $updateItem.addClass('item-success') + } + else if (selectedValue == 'ignore' || selectedValue == 'skip') { + $updateItem.addClass('item-muted') + } + else { + $updateItem.addClass('item-danger') + } + + self.evalConfirmedUpdates() + }) + } + + UpdateProcess.prototype.evalConfirmedUpdates = function() { + var $form = $('#updateForm'), + hasConfirmed = false + + $('[data-important-update-select]', $form).each(function() { + if ($(this).val() == '') { + hasConfirmed = true + } + }) + + if (hasConfirmed) { + $('#updateListUpdateButton').prop('disabled', true) + $('#updateListImportantLabel').show() + } + else { + $('#updateListUpdateButton').prop('disabled', false) + $('#updateListImportantLabel').hide() + } + } + UpdateProcess.prototype.execute = function(steps) { this.updateSteps = steps this.runUpdate() diff --git a/modules/system/assets/less/updates/details.less b/modules/system/assets/less/updates/details.less new file mode 100644 index 000000000..e3a6d3ece --- /dev/null +++ b/modules/system/assets/less/updates/details.less @@ -0,0 +1,51 @@ +@import "../../../../backend/assets/less/core/boot.less"; + +@padding-standard: 20px; +@padding-container: 0; + +.plugin-details-content { + padding: 0 @padding-container; + + > *:first-child { + margin-top: 0; + } + + h1 { font-size: @font-size-h1 - 8; } + h2 { font-size: @font-size-h2 - 6; } + h3 { font-size: @font-size-h3 - 4; } + h4 { font-size: @font-size-h4 - 1; } + + h1, h2 { padding-bottom: 10px; border-bottom: 1px solid #ccc; } + + ul, ol { + padding-left: @padding-standard; + } + + pre { + display: block; + padding: 10px 10px 10px (@padding-standard + @padding-container); + font-size: 13px; + word-break: break-all; + word-wrap: break-word; + color: #fff; + background-color: #333; + margin-top: 10px; + margin-bottom: 20px; + margin-left: -(@padding-standard + @padding-container); + margin-right: -(@padding-standard + @padding-container); + code { + padding: 0; + font-size: inherit; + color: inherit; + white-space: pre-wrap; + background-color: transparent; + border-radius: 0; + } + } + + ul pre, + ol pre { + margin-left: -((@padding-standard * 2) + @padding-container); + padding-left: ((@padding-standard * 2) + @padding-container); + } +} \ No newline at end of file diff --git a/modules/system/assets/less/updates/updates.less b/modules/system/assets/less/updates/updates.less index e8ca85ab5..5bc9adb51 100644 --- a/modules/system/assets/less/updates/updates.less +++ b/modules/system/assets/less/updates/updates.less @@ -1,5 +1,9 @@ @import "../../../../backend/assets/less/core/boot.less"; +.important-update-label { + margin: 7px 0; +} + .control-updatelist { border: 1px solid #ccc; @@ -12,11 +16,11 @@ .item-header { background-color: #f5f5f5; border-bottom: 1px solid #ccc; - padding: 15px 10px; + padding: 0 10px; h5 { margin: 0; - padding: 0; + padding: 15px 0; text-transform: uppercase; font-size: 13px; @@ -32,23 +36,53 @@ margin-right: 5px; } } + + .important-update { + padding: 7px 0 0 0; + float: right; + } } dl { padding: 10px; margin-bottom: 0; - font-size: 12px; .clearfix; dt, dd { float: left; padding: 5px 0; + line-height: 20px; + &.text-muted { + color: #999 !important; + } } + dt { width: 15%; + clear: left; + font-size: 14px; + font-weight: 400; + color: #2b3e50; } dd { width: 85%; + font-size: 12px; + color: #455152; + } + + .important-update-label { + position: relative; + top: -1px; + background-color: #ab2a1c; + color: #fff; + display: inline-block; + padding: .2em .6em .3em; + font-size: 75%; + line-height: 1; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + border-radius: .25em; } } @@ -59,11 +93,28 @@ &.item-danger { .item-header { background-color: @state-danger-bg; - } - dl { - color: @state-danger-text; + h5, i, & { color: @state-danger-text; } } } + + &.item-success { + .item-header { + background-color: @state-success-bg; + h5, i, & { color: @state-success-text; } + } + } + + &.item-muted { + border-bottom: none; + + .item-header { + h5, i, & { color: rgba(0,0,0,.35); } + } + dl { + display: none; + } + } + } } \ No newline at end of file diff --git a/modules/system/classes/UpdateManager.php b/modules/system/classes/UpdateManager.php index c42021401..ee3984098 100644 --- a/modules/system/classes/UpdateManager.php +++ b/modules/system/classes/UpdateManager.php @@ -209,6 +209,7 @@ class UpdateManager $versions = $installed->lists('version', 'code'); $names = $installed->lists('name', 'code'); $icons = $installed->lists('icon', 'code'); + $frozen = $installed->lists('is_frozen', 'code'); $build = Parameters::get('system::core.build'); $params = [ @@ -241,7 +242,17 @@ class UpdateManager $info['name'] = isset($names[$code]) ? $names[$code] : $code; $info['old_version'] = isset($versions[$code]) ? $versions[$code] : false; $info['icon'] = isset($icons[$code]) ? $icons[$code] : false; - $plugins[$code] = $info; + + /* + * If plugin has updates frozen, do not add it to the list + * and discount an update unit. + */ + if (isset($frozen[$code]) && $frozen[$code]) { + $updateCount = max(0, --$updateCount); + } + else { + $plugins[$code] = $info; + } } $result['plugins'] = $plugins; diff --git a/modules/system/controllers/Updates.php b/modules/system/controllers/Updates.php index 39d00edd8..dddd6b744 100644 --- a/modules/system/controllers/Updates.php +++ b/modules/system/controllers/Updates.php @@ -2,10 +2,12 @@ use Str; use Lang; +use Html; use File; use Flash; use Config; use Backend; +use Markdown; use Redirect; use Response; use BackendMenu; @@ -98,6 +100,71 @@ class Updates extends Controller } } + public function details($urlCode = null, $tab = null) + { + try { + $this->pageTitle = 'Plugin details'; + $this->addCss('/modules/system/assets/css/updates/details.css', 'core'); + + $readmeFiles = ['README.md', 'readme.md']; + $upgradeFiles = ['UPGRADE.md', 'upgrade.md']; + + $upgrades = $readme = $name = null; + $code = str_replace('-', '.', $urlCode); + + /* + * Lookup the plugin + */ + $manager = PluginManager::instance(); + $plugin = $manager->findByIdentifier($code); + $code = $manager->getIdentifier($plugin); + $path = $manager->getPluginPath($plugin); + + if ($path && $plugin) { + $details = $plugin->pluginDetails(); + $readme = $this->getPluginMarkdownFile($path, $readmeFiles); + $upgrades = $this->getPluginMarkdownFile($path, $upgradeFiles); + + $pluginVersion = PluginVersion::whereCode($code)->first(); + $this->vars['pluginName'] = array_get($details, 'name', 'system::lang.plugin.unnamed'); + $this->vars['pluginVersion'] = $pluginVersion->version; + $this->vars['pluginAuthor'] = array_get($details, 'author'); + $this->vars['pluginIcon'] = array_get($details, 'icon', 'icon-leaf'); + $this->vars['pluginHomepage'] = array_get($details, 'homepage'); + } + else { + throw new ApplicationException('Plugin not found'); + } + + $this->vars['activeTab'] = $tab ?: 'readme'; + $this->vars['urlCode'] = $urlCode; + $this->vars['upgrades'] = $upgrades; + $this->vars['readme'] = $readme; + } + catch (Exception $ex) { + $this->handleError($ex); + } + } + + protected function getPluginMarkdownFile($path, $filenames) + { + $contents = null; + foreach ($filenames as $file) { + if (!File::exists($path . '/'.$file)) continue; + + $contents = File::get($path . '/'.$file); + + /* + * Parse markdown, clean HTML, remove first H1 tag + */ + $contents = Markdown::parse($contents); + $contents = Html::clean($contents); + $contents = preg_replace('@]*?>.*?<\/h1>@si', '', $contents, 1); + } + + return $contents; + } + /** * {@inheritDoc} */ @@ -119,6 +186,10 @@ class Updates extends Controller return 'negative'; } + if ($record->is_frozen) { + return 'frozen'; + } + return 'positive'; } @@ -193,8 +264,11 @@ class Updates extends Controller $manager = UpdateManager::instance(); $result = $manager->requestUpdateList(); + $result = $this->processImportantUpdates($result); + $this->vars['core'] = array_get($result, 'core', false); $this->vars['hasUpdates'] = array_get($result, 'hasUpdates', false); + $this->vars['hasImportantUpdates'] = array_get($result, 'hasImportantUpdates', false); $this->vars['pluginList'] = array_get($result, 'plugins', []); $this->vars['themeList'] = array_get($result, 'themes', []); } @@ -205,6 +279,31 @@ class Updates extends Controller return ['#updateContainer' => $this->makePartial('update_list')]; } + /** + * Loops the update list and checks for actionable updates. + */ + protected function processImportantUpdates($result) + { + $hasImportantUpdates = false; + foreach (array_get($result, 'plugins', []) as $code => $plugin) { + $isImportant = false; + + foreach (array_get($plugin, 'updates', []) as $version => $description) { + if (strpos($description, '!!!') === false) continue; + + $isImportant = $hasImportantUpdates = true; + $detailsUrl = Backend::url('system/updates/details/'.strtolower(str_replace('.', '-', $code)).'/upgrades'); + $description = str_replace('!!!', '', $description); + $result['plugins'][$code]['updates'][$version] = [$description, $detailsUrl]; + } + + $result['plugins'][$code]['isImportant'] = $isImportant ? '1' : '0'; + } + + $result['hasImportantUpdates'] = $hasImportantUpdates; + return $result; + } + /** * Contacts the update server for a list of necessary updates. */ @@ -258,20 +357,69 @@ class Updates extends Controller public function onApplyUpdates() { try { + /* + * Process core + */ $coreHash = post('hash'); $coreBuild = post('build'); $core = [$coreHash, $coreBuild]; - $plugins = post('plugins', []); - if (!is_array($plugins)) { + /* + * Process plugins + */ + $plugins = post('plugins'); + if (is_array($plugins)) { + $pluginCodes = []; + foreach ($plugins as $code => $hash) { + $pluginCodes[] = $this->decodeCode($code); + } + + $plugins = array_combine($pluginCodes, $plugins); + } + else { $plugins = []; } - $themes = post('themes', []); - if (!is_array($themes)) { + /* + * Process themes + */ + $themes = post('themes'); + if (is_array($themes)) { + $themeCodes = []; + foreach ($themes as $code => $hash) { + $themeCodes[] = $this->decodeCode($code); + } + + $themes = array_combine($themeCodes, $themes); + } + else { $themes = []; } + /* + * Process important update actions + */ + $pluginActions = (array) post('plugin_actions'); + foreach ($plugins as $code => $hash) { + $_code = $this->encodeCode($code); + if (!array_key_exists($_code, $pluginActions)) continue; + $pluginAction = $pluginActions[$_code]; + + if (!$pluginAction) { + throw new ApplicationException('Please select an action for plugin '. $code); + } + + if ($pluginAction != 'confirm') { + unset($plugins[$code]); + } + + if ($pluginAction == 'ignore') { + PluginVersion::whereCode($code)->update([ + 'is_frozen' => true + ]); + } + } + /* * Update steps */ @@ -546,6 +694,7 @@ class Updates extends Controller public function onDisablePlugins() { $disable = post('disable', false); + $freeze = post('freeze', false); if (($checkedIds = post('checked')) && is_array($checkedIds) && count($checkedIds)) { $manager = PluginManager::instance(); @@ -563,6 +712,7 @@ class Updates extends Controller } $object->is_disabled = $disable; + $object->is_frozen = $freeze; $object->save(); } @@ -727,4 +877,23 @@ class Updates extends Controller return array_values($popular); } + // + // Helpers + // + + /** + * Encode HTML safe product code. + */ + protected function encodeCode($code) + { + return str_replace('.', '_', $code); + } + + /** + * Decode HTML safe product code. + */ + protected function decodeCode($code) + { + return str_replace('_', '.', $code); + } } diff --git a/modules/system/controllers/updates/_column_code.htm b/modules/system/controllers/updates/_column_code.htm new file mode 100644 index 000000000..1aa7f0e24 --- /dev/null +++ b/modules/system/controllers/updates/_column_code.htm @@ -0,0 +1,19 @@ +is_disabled) { + $icon = 'eye-slash'; + } + elseif ($record->disabledBySystem) { + $icon = 'exclamation'; + } + elseif ($record->orphaned) { + $icon = 'question'; + } + elseif ($record->is_frozen) { + $icon = 'lock'; + } +?> + + + \ No newline at end of file diff --git a/modules/system/controllers/updates/_disable_form.htm b/modules/system/controllers/updates/_disable_form.htm index 1bb8fd4e1..997ed6247 100644 --- a/modules/system/controllers/updates/_disable_form.htm +++ b/modules/system/controllers/updates/_disable_form.htm @@ -28,6 +28,21 @@ +
+ +
+ + +

+
+
+ diff --git a/modules/system/controllers/updates/_list_manage_toolbar.htm b/modules/system/controllers/updates/_list_manage_toolbar.htm index f0bc448c5..29a981773 100644 --- a/modules/system/controllers/updates/_list_manage_toolbar.htm +++ b/modules/system/controllers/updates/_list_manage_toolbar.htm @@ -1,4 +1,7 @@
+ + +