From 5663803e0531e7e36b977eef7f0166c863bd0b5c Mon Sep 17 00:00:00 2001 From: Ryan Cramer Date: Thu, 4 Jul 2019 10:43:24 -0400 Subject: [PATCH] Move post-login superuser system checks from ProcessLogin into their own class SystemUpdaterChecks as part of the SystemUpdater module --- wire/core/Notices.php | 16 +- wire/core/User.php | 1 + .../Process/ProcessLogin/ProcessLogin.module | 113 +---- .../System/SystemUpdater/SystemUpdater.module | 38 ++ .../SystemUpdater/SystemUpdaterChecks.php | 441 ++++++++++++++++++ 5 files changed, 515 insertions(+), 94 deletions(-) create mode 100644 wire/modules/System/SystemUpdater/SystemUpdaterChecks.php diff --git a/wire/core/Notices.php b/wire/core/Notices.php index 68ef7d71..b8f1f8c3 100644 --- a/wire/core/Notices.php +++ b/wire/core/Notices.php @@ -58,6 +58,14 @@ abstract class Notice extends WireData { * */ const allowMarkup = 32; + + /** + * Flag indicates notice should prepend (rather than append) to any existing notices + * + * @since 3.0.135 + * + */ + const prepend = 64; /** * Create the Notice @@ -212,7 +220,7 @@ class Notices extends WireArray { $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->flag | Notice::allowMarkup; + $item->flags = $item->flags | Notice::allowMarkup; } else if(is_object($item->text)) { $item->text = (string) $item->text; } @@ -240,7 +248,11 @@ class Notices extends WireArray { if($item->flags & Notice::logOnly) return $this; } - return parent::add($item); + if($item->flags & Notice::prepend) { + return parent::prepend($item); + } else { + return parent::add($item); + } } protected function addLog($item) { diff --git a/wire/core/User.php b/wire/core/User.php index 8a07b97f..22997ff0 100644 --- a/wire/core/User.php +++ b/wire/core/User.php @@ -16,6 +16,7 @@ * @property string|Password $pass Set the user’s password. * @property PageArray $roles Get the roles this user has. * @property Language $language User language, applicable only if LanguageSupport installed. + * @property string $admin_theme Admin theme class name * * @method bool hasPagePermission($name, Page $page = null) #pw-internal * @method bool hasTemplatePermission($name, $template) #pw-internal diff --git a/wire/modules/Process/ProcessLogin/ProcessLogin.module b/wire/modules/Process/ProcessLogin/ProcessLogin.module index e145067d..0ef132e9 100644 --- a/wire/modules/Process/ProcessLogin/ProcessLogin.module +++ b/wire/modules/Process/ProcessLogin/ProcessLogin.module @@ -382,98 +382,27 @@ class ProcessLogin extends Process implements ConfigurableModule { * */ protected function ___afterLogin() { - if(!$this->user->isSuperuser()) return; - - $indexVersion = ProcessWire::indexVersion; - $htaccessVersion = ProcessWire::htaccessVersion; - - if(PROCESSWIRE < $indexVersion) { - $this->warning( - "Not urgent, but note that your root index.php file is not up-to-date with this ProcessWire version - please update it when possible. " . - "
Required version: $indexVersion, Found version: " . PROCESSWIRE . "", Notice::log | Notice::allowMarkup - ); - } - - $htaccessFile = $this->wire('config')->paths->root . '.htaccess'; - if(is_readable($htaccessFile)) { - $htaccessData = file_get_contents($htaccessFile); - if(!preg_match('/@htaccessVersion\s+(\d+)\b/', $htaccessData, $matches) || ((int) $matches[1]) < $htaccessVersion) { - $this->warning( - "Please note that your root .htaccess file is not up-to-date with this ProcessWire version - update it when possible.
" . - "To suppress this warning, replace or add the following in the top of your existing .htaccess file: " . - "# @htaccessVersion $htaccessVersion", Notice::log | Notice::allowMarkup - ); - } - } - - // if($this->config->showSecurityWarnings === false) return; - // if(is_writable($this->config->paths->root . "site/config.php")) $this->error("Security Warning: /site/config.php is writable and ideally should not be."); - // if(is_writable($this->config->paths->root . "index.php")) $this->error("Security Warning: /index.php is writable and ideally should not be."); - $warningText = $this->_("Security Warning: %s exists and should be deleted as soon as possible."); - if(is_file($this->config->paths->root . "install.php")) $this->error(sprintf($warningText, '/install.php'), Notice::log); - - $file = $this->config->paths->assets . "active.php"; - if(!is_file($file)) { - $data = "config->paths->root}]"; - file_put_contents($file, $data); - } - - // warnings about 0666/0777 file permissions - if($this->config->chmodWarn && ($this->config->chmodDir == '0777' || $this->config->chmodFile == '0666')) { - $warning = - $this->_('Warning, your /site/config.php specifies file permissions that are too loose for many environments:') . '
' . - "" . - "\$config->chmodFile = '{$this->config->chmodFile}';
" . - "\$config->chmodDir = '{$this->config->chmodDir}';" . - "

" . - "" . - $this->_('Read "Securing file permissions" for more details') . "
" . - "" . - $this->_('To suppress this warning, set $config->chmodWarn = false; in your /site/config.php file.') . - ""; - $warning = str_replace(array('0666', '0777'), array('0666', '0777'), $warning); - $this->warning($warning, Notice::allowMarkup | Notice::log); - } - - if($this->wire('fields')->get('published')) { - $this->error("Warning: you have a field named 'published' that conflicts with the page 'published' property. Please rename your field field to something else and update any templates referencing it."); - } - - // warning about servers with locales that break UTF-8 strings called by basename - // and other file functions, due to a long running PHP bug - if(basename("§") === "") { - $example = stripos(PHP_OS, 'WIN') === 0 ? 'en-US' : 'en_US.UTF-8'; - $localeLabel = $this->_('Your current locale setting is “%s”.') . ' '; - $msg = $this->_('Note: your current server locale setting isn’t working as expected with the UTF-8 charset and may cause minor issues.') . ' '; - if($this->wire('modules')->isInstalled('LanguageSupport')) { - $textdomain = 'wire--modules--languagesupport--languagesupport-module'; - $locale = __('C', $textdomain); - if(empty($locale)) $locale = setlocale(LC_CTYPE, 0); - $msg .= sprintf($localeLabel, $locale); - $msg .= sprintf($this->_('Please translate the “C” locale setting for each language to the compatible locale in %s'), - '/wire/modules/LanguageSupport/LanguageSupport.module:'); - foreach($this->wire('languages') as $language) { - $url = $this->wire('config')->urls->admin . "setup/language-translator/edit/?language_id=$language->id&" . - "textdomain=$textdomain&" . - "filename=wire/modules/LanguageSupport/LanguageSupport.module"; - $msg .= "
" . $language->get('title|name') . ""; + if($this->wire('user')->isSuperuser()) { + /** @var SystemUpdater $systemUpdater */ + $systemUpdater = $this->wire('modules')->get('SystemUpdater'); + if($systemUpdater) { + $updatesApplied = $systemUpdater->getUpdatesApplied(); + $checks = $systemUpdater->getChecks(); + $checks->setShowNotices(true); + //$checks->setTestAll(true); + + if(count($updatesApplied)) { + $checks->checkWelcome(); + $this->message( + sprintf( + $this->_('Skipping after-login system checks because updates were applied (%s)'), + implode(', ', $updatesApplied) + ), + Notice::debug + ); + } else { + $checks->execute(); } - $msg .= "
" . - sprintf($this->_('For example, the locale setting for US English might be: %s'), "$example") . - ""; - $this->warning($msg, Notice::allowMarkup); - } else { - $locale = setlocale(LC_CTYPE, 0); - $msg .= - sprintf($localeLabel, $locale) . - sprintf( - $this->_('Please add this to your %1$s file (adjust “%2$s” as needed):'), - '/site/config.php', $example - ) . " setlocale(LC_ALL,'$example');"; - $this->warning($msg, Notice::allowMarkup); } } } @@ -699,7 +628,7 @@ class ProcessLogin extends Process implements ConfigurableModule { /** @var Session $session */ $session = $this->wire('session'); - $session->message($user->name . ' - ' . $this->_("Successful login")); + // $session->message($user->name . ' - ' . $this->_("Successful login")); if($this->isAdmin) { $copyVars = $session->getFor($this, 'copyVars'); diff --git a/wire/modules/System/SystemUpdater/SystemUpdater.module b/wire/modules/System/SystemUpdater/SystemUpdater.module index a0ef1cf8..3178ac4f 100644 --- a/wire/modules/System/SystemUpdater/SystemUpdater.module +++ b/wire/modules/System/SystemUpdater/SystemUpdater.module @@ -43,6 +43,14 @@ class SystemUpdater extends WireData implements Module, ConfigurableModule { */ protected $numUpdatesApplied = 0; + /** + * Get array of updates that were applied + * + * @var array Array of update version numbers + * + */ + protected $updatesApplied = array(); + /** * Is an update being applied manually? * @@ -96,6 +104,7 @@ class SystemUpdater extends WireData implements Module, ConfigurableModule { // then already applied updates won't be applied again $this->saveSystemVersion($systemVersion); $this->numUpdatesApplied++; + $this->updatesApplied[] = ($systemVersion-1); } if($this->numUpdatesApplied > 0) { @@ -216,6 +225,7 @@ class SystemUpdater extends WireData implements Module, ConfigurableModule { * * @param int $version Update version number * @return null|SystemUpdate Returns SystemUpdate instance of available or null if not + * @since 3.0.135 * */ public function getUpdate($version) { @@ -246,6 +256,7 @@ class SystemUpdater extends WireData implements Module, ConfigurableModule { * * @param int|SystemUpdate $version Update version number or instance of SystemUpdate you want to apply * @return bool True on success, false on fail + * @since 3.0.135 * */ public function apply($version) { @@ -273,6 +284,33 @@ class SystemUpdater extends WireData implements Module, ConfigurableModule { return $success; } + /** + * Get instance of SystemUpdaterChecks for performing system checks + * + * #pw-internal + * + * @return SystemUpdaterChecks + * @since 3.0.135 + * + */ + public function getChecks() { + require_once(dirname(__FILE__) . '/SystemUpdaterChecks.php'); + $checks = new SystemUpdaterChecks(); + $this->wire($checks); + return $checks; + } + + /** + * Get array of updates (update version numbers) that were automatically applied during this request + * + * @return array + * @since 3.0.135 + * + */ + public function getUpdatesApplied() { + return $this->updatesApplied; + } + /** * Message notice * diff --git a/wire/modules/System/SystemUpdater/SystemUpdaterChecks.php b/wire/modules/System/SystemUpdater/SystemUpdaterChecks.php new file mode 100644 index 00000000..15befcef --- /dev/null +++ b/wire/modules/System/SystemUpdater/SystemUpdaterChecks.php @@ -0,0 +1,441 @@ +showNotices = $showNotices; + } + + /** + * Set whether or not to test all checks (as if all checks failed) + * + * @param bool $testAll + * + */ + public function setTestAll($testAll = true) { + $this->testAll = $testAll; + } + + /** + * Run all system checks and return array of results + * + * @return array + * + */ + public function execute() { + + $this->checkAll = true; + $results = array(); + $checks = array( + 'checkWelcome', + 'checkIndexFile', + 'checkHtaccessFile', + //'checkOtherHtaccessFiles', + 'checkInstallerFiles', + 'checkFilePermissions', + 'checkPublishedField', + 'checkLocale', + 'checkDebugMode', + ); + + foreach($checks as $method) { + try { + $results[$method] = $this->$method(); + } catch(\Exception $e) { + if($this->showNotices) $this->warning("$method: " . $e->getMessage()); + $results[$method] = false; + } + } + + $this->checkAll = false; + + $numWarnings = count($this->warnings); + if($this->showNotices && $numWarnings) { + if($numWarnings > 1) $this->warning($this->_('Multiple issues detected, please review:')); + foreach($this->warnings as $warning) { + $this->warning($warning[0], $warning[1]); + } + } + + $this->warnings = array(); + + return $results; + } + + /** + * Check that index.php file is the correct version + * + * @return bool + * + */ + public function checkIndexFile() { + + $requiredVersion = ProcessWire::indexVersion; + $actualVersion = PROCESSWIRE; + + if(PROCESSWIRE < $requiredVersion || $this->testAll) { + if($this->showNotices) { + $warning = sprintf( + $this->_('Please note that your root %s file is not up-to-date with this ProcessWire version, please update it when possible.'), + $this->location('index.php') + ); + $details = $this->versionsLabel($requiredVersion, $actualVersion); + $this->warning($warning . $this->small($details), Notice::log | Notice::allowMarkup); + } + return false; + } + + return true; + } + + /** + * Check that main htaccess file is the correct version + * + * @return bool + * @throws WireException + * + */ + public function checkHtaccessFile() { + + $requiredVersion = ProcessWire::htaccessVersion; + $htaccessFile = $this->wire('config')->paths->root . '.htaccess'; + + if(is_readable($htaccessFile)) { + $data = file_get_contents($htaccessFile); + if(!preg_match('/@(?:htaccess|index)Version\s+(\d+)\b/', $data, $matches) || ((int) $matches[1]) < $requiredVersion || $this->testAll) { + if($this->showNotices) { + $foundVersion = isset($matches[1]) ? (int) $matches[1] : '?'; + $warning = sprintf( + $this->_('Please note that your root %s file is not up-to-date with this ProcessWire version, please update it when possible.'), + $this->location('.htaccess') + ); + $details = $this->small( + $this->versionsLabel($requiredVersion, $foundVersion) . ' ' . + $this->_('To suppress this warning, replace or add the following in the top of your existing .htaccess file:') . + $this->code("# @htaccessVersion $requiredVersion") + ); + $this->warning("$warning$details", Notice::log | Notice::allowMarkup); + } + return false; + } + } else { + if($this->showNotices) $this->warning($this->fileNotFoundLabel($htaccessFile)); + return false; + } + + return true; + } + + /** + * Check that other useful htaccess files are present + * + * @return bool + * + */ + public function checkOtherHtaccessFiles() { + /** @var SystemUpdater $systemUpdater */ + $systemUpdater = $this->wire('modules')->get('SystemUpdater'); + if(!$systemUpdater) return false; + $result = true; + + /** @var SystemUpdate17 $update */ + // update 17 verifies that fallback .htaccess files are in place for 2nd layer protections + $update = $systemUpdater->getUpdate(17); + if($update) { + if($update->isUseful() || $this->testAll) $result = $update->update(); + } + + return $result; + } + + /** + * Check if this is the first call to checkWelcome and show a welcome message and add an active.php file if so + * + * @return bool Returns false if active.php does not yet exist or true if it does + * + */ + public function checkWelcome() { + + $activeFile = $this->wire('config')->paths->assets . 'active.php'; + $exists = is_file($activeFile); + + if($this->showNotices && ((!$exists && !$this->wire('config')->debug) || $this->testAll)) { + $this->message( + $this->strong($this->_('Welcome to ProcessWire!')) . ' ' . + $this->_('If this installation is currently being used for development or testing, we recommend enabling debug mode.') . ' ' . + $this->_('Debug mode ensures all errors are visible, which can be helpful during development or troubleshooting.') . ' ' . + $this->_('It also enables additional developer information to appear here in the admin.') . ' ' . + sprintf($this->_('You can enable debug mode by editing your %s file and adding the following:'), $this->location('/site/config.php')) . + $this->code('$config->debug = true;') . + $this->small($this->_('Please note: this notification will not be shown again. Remember to disable debug mode before going live.')), + Notice::allowMarkup | Notice::prepend + ); + } + + if(!$exists) { + $data = + "config->paths->root}]"; + $this->wire('files')->filePutContents($activeFile, $data); + return false; + } + + return true; + } + + /** + * Check if unnecessary installer files are present + * + * @return bool + * + */ + public function checkInstallerFiles() { + if(is_file($this->wire('config')->paths->root . "install.php") || $this->testAll) { + if($this->showNotices) { + $warning = $this->_("Security Warning: file '%s' exists and should be deleted as soon as possible."); + $this->warning(sprintf($warning, '/install.php'), Notice::log); + } + return false; + } + return true; + } + + /** + * Check for insecure file permissions + * + * @return bool + * + */ + public function checkFilePermissions() { + + // warnings about 0666/0777 file permissions + if($this->config->chmodDir != '0777' && $this->config->chmodFile != '0666' && !$this->testAll) return true; + if(!$this->config->chmodWarn || !$this->showNotices) return false; + + $warning = sprintf( + $this->_('Warning, your %s file specifies file permissions that are too loose for many environments:'), + $this->location('/site/config.php') + ); + + $code = + $this->code("\$config->chmodFile = '{$this->config->chmodFile}';") . + $this->code("\$config->chmodDir = '{$this->config->chmodDir}';"); + + $link = $this->link( + 'https://processwire.com/docs/security/file-permissions/', + $this->_('Read "Securing file permissions" for more details') + ); + + $details = $this->small( + sprintf($this->_('To suppress this warning, add the following to your %s file:'), $this->location('/site/config.php')) . ' ' . + $this->code('$config->chmodWarn = false;') + ); + + $code = str_replace(array('0666', '0777'), array('0666', '0777'), $code); + + $this->warning("$warning$code$link$details", Notice::allowMarkup | Notice::log); + + return false; + } + + /** + * Check if there is a field named 'published' that should not be present + * + * @return bool + * + */ + public function checkPublishedField() { + if(!$this->wire('fields')->get('published') && !$this->testAll) return true; + if($this->showNotices) $this->warning( + $this->_('Warning: you have a field named “published” that conflicts with the page “published” property.') . ' ' . + $this->_('Please rename your field field to something else and update any templates referencing it.') + ); + return false; + } + + /** + * Check locale setting + * + * Warning about servers with locales that break UTF-8 strings called by basename + * and other file functions, due to a long running PHP bug + * + * @return bool + * + */ + public function checkLocale() { + + if(basename("§") !== "" && !$this->testAll) return true; + if(!$this->showNotices) return false; + + $example = stripos(PHP_OS, 'WIN') === 0 ? 'en-US' : 'en_US.UTF-8'; + $localeLabel = $this->_('Your current locale setting is “%s”.') . ' '; + $warning = $this->_('Note: your current server locale setting isn’t working as expected with the UTF-8 charset and may cause minor issues.'); + $msg = ''; + + if($this->wire('modules')->isInstalled('LanguageSupport')) { + // language support installed + $textdomain = 'wire--modules--languagesupport--languagesupport-module'; + $locale = __('C', $textdomain); + if(empty($locale)) $locale = setlocale(LC_CTYPE, 0); + $msg .= ' ' . + sprintf($localeLabel, $locale) . ' ' . + sprintf( + $this->_('Please translate the “C” locale setting for each language to the compatible locale in %s'), + $this->location('/wire/modules/LanguageSupport/LanguageSupport.module') . ':' + ); + + foreach($this->wire('languages') as $language) { + $url = $this->wire('config')->urls->admin . + "setup/language-translator/edit/?" . + "language_id=$language->id&" . + "textdomain=$textdomain&" . + "filename=wire/modules/LanguageSupport/LanguageSupport.module"; + $msg .= "
" . $this->link($url, $language->get('title|name')) . "
"; + } + + $msg .= $this->small( + sprintf( + $this->_('For example, the locale setting for US English might be: %s'), + $this->strong($example) + ) + ); + + } else { + // no language support installed + $locale = setlocale(LC_CTYPE, 0); + $msg .= + sprintf($localeLabel, $locale) . + sprintf( + $this->_('Please add this to your %1$s file (adjust “%2$s” as needed):'), + $this->location('/site/config.php'), + $example + ) . + $this->code("setlocale(LC_ALL, '$example');"); + } + + $this->warning("$warning $msg", Notice::allowMarkup); + + return false; + + } + + /** + * Check for debug mode + * + * return bool Always returns true, as there is no way to fail this test + * + */ + public function checkDebugMode() { + if(!$this->wire('config')->debug && !$this->testAll) return true; + if($this->showNotices) $this->warning( + $this->_('The site is in debug mode, suitable for sites in development') . + $this->small( + sprintf( + $this->_('If this is a live/production site, you should disable debug mode in %1$s with: %2$s'), + $this->location('/site/config.php'), + $this->code('$config->debug = false;') + ) + ), + Notice::allowMarkup + ); + return true; + } + + /*********************************************************************************************/ + + public function warning($text, $flags = 0) { + if($this->checkAll && $this->showNotices) { + $this->warnings[] = array($text, $flags); + } else { + parent::warning($text, $flags); + } + return $this; + } + + protected function versionsLabel($requiredVersion, $foundVersion) { + return sprintf($this->_('Required version: %1$s, Found version: %2$s.'), "$requiredVersion", "$foundVersion"); + } + + protected function fileNotFoundLabel($file) { + return sprintf($this->_('Unable to locate required file: %s'), $file); + } + + protected function code($str, $block = true) { + $style = 'font-family:monospace;font-size:14px;'; + if($block) $style .= "background:rgba(255,255,255,0.3);padding:5px 7px;border:1px solid rgba(0,0,0,0.1)"; + $out = "$str"; + if($block) $out = "
$out
"; + return $out; + } + + protected function small($str, $block = true) { + $tag = $block ? 'div' : 'span'; + return "<$tag>$str"; + } + + protected function strong($str) { + return "$str"; + } + + protected function link($href, $label, $newWindow = true) { + $target = $newWindow ? "target='_blank'" : ""; + return wireIconMarkup('angle-right') . " $label "; + } + + protected function location($str) { + return "$str"; + } + + +} \ No newline at end of file