From 562565ff42e9f702561309b4e695c17ec1135a4e Mon Sep 17 00:00:00 2001 From: Ryan Cramer Date: Thu, 4 Jul 2019 10:52:37 -0400 Subject: [PATCH] Add system update #17 which adds a secondary layer of file protection with dedicated .htaccess files in various site directories that take over if the root .htaccess file ever goes missing --- .../System/SystemUpdater/SystemUpdate17.php | 206 ++++++++++++++++++ .../System/SystemUpdater/SystemUpdater.module | 2 +- .../SystemUpdater/SystemUpdaterChecks.php | 2 +- 3 files changed, 208 insertions(+), 2 deletions(-) create mode 100644 wire/modules/System/SystemUpdater/SystemUpdate17.php diff --git a/wire/modules/System/SystemUpdater/SystemUpdate17.php b/wire/modules/System/SystemUpdater/SystemUpdate17.php new file mode 100644 index 00000000..359b50b6 --- /dev/null +++ b/wire/modules/System/SystemUpdater/SystemUpdate17.php @@ -0,0 +1,206 @@ +wire()->addHookAfter('ProcessWire::ready', $this, 'executeAtReady'); + return 0; // indicates we will update system version ourselves when ready + } + + public function executeAtReady() { + $this->auto = true; + if(!$this->update()) return; + $this->message( + "Details: $this->detailsUrl", + Notice::allowMarkup + ); + $this->updater->saveSystemVersion(17); + } + + /** + * Apply the update + * + * @return bool + * @throws WireException + * + */ + public function update() { + + if(!$this->isApache()) { + $this->warning('Update skipped because Apache not detected'); + $this->warning('Please see the details URL below on how to apply this update manually'); + return true; + } + + $blockAll = + "\n\n Require all denied\n" . // Apache 2.4+ + "\n\n Order allow,deny\n Deny from all\n"; // Prior to Apache 2.4 + + $denyAll = str_replace("\n", "\n ", $blockAll); // indented blockAll + + $rules = array( + 'all' => array( + 'label' => 'block all access', + 'content' => ltrim($blockAll), + ), + 'php' => array( + 'label' => 'block all PHP files', + 'content' => "$denyAll\n", + ), + 'rifc' => array( + 'label' => 'block some PHP files', + 'content' => "$denyAll\n", + ) + ); + + $cachePath = $this->wire('config')->paths->cache; + $siteRootDir = rtrim($this->wire('config')->paths->site, '/'); + $siteRootUrl = rtrim($this->wire('config')->urls->site, '/'); + $sitePathRules = array( + '/' => 'rifc', + '/assets/' => 'php', + '/assets/cache/' => 'all', + '/assets/backups/' => 'all', + '/assets/logs/' => 'all', + '/templates/' => 'php', + '/modules/' => 'php', + ); + + /** @var WireFileTools $files */ + $files = $this->wire('files'); + $numErrors = 0; + + foreach($sitePathRules as $dir => $ruleName) { + + $rule = $rules[$ruleName]; + $header = "# Start ProcessWire:pwb$ruleName (update 17)"; + $footer = "# End ProcessWire:pwb$ruleName"; + $summary = "# $rule[label] (optional fallback if root .htaccess missing)"; + $location = "$siteRootUrl$dir.htaccess"; + $content = "$header\n$summary\n$rule[content]\n$footer"; + $file = $siteRootDir . $dir . '.htaccess'; + $url = $siteRootUrl . $dir . '.htaccess'; + $path = dirname($file); + + if(!is_dir($path)) { + if($this->auto) $this->message("Skipped $url (directory not present)"); + continue; + } + + if(file_exists($file)) { + // existing .htaccess file already present + if($this->auto) $this->message("Skipped: $url (already exists)"); + continue; + } + + // no .htaccess file currently present + $writable = is_writable(dirname($file)); + $data = $content; + $actionLabel = 'Created'; + + if(!$writable) { + // file not writable, so we will create a temporary file in cache instead (for optional manual copy) + $file = $cachePath . str_replace(array('/', "\\", '.', '--'), '-', trim(str_replace($siteRootUrl, 'site', $url), '/')) . '.txt'; + $tmpUrl = str_replace($siteRootDir, $siteRootUrl, $file); + if(file_exists($file)) { + // file already exists so we can skip + if($this->auto) $this->message("Ignored: $tmpUrl (already exists)"); + continue; + } + $writable = is_writable(dirname($file)); + if($writable) { + $this->warning("Unable to write updates to '$url', so writing to '$tmpUrl' instead, in case you want to copy manually."); + $data = "# Intended location of this file: $location\n$data"; + $actionLabel = 'Created'; + } + $url = $tmpUrl; + } + + if(!$writable) { + if($ruleName !== 'all') { + if($this->auto) $this->message("Ignored: $url"); + } else { + $this->error("Unable to write: $url"); + $numErrors++; + } + continue; + } + + try { + if($files->filePutContents($file, "$data\n", LOCK_EX)) { + $this->message("$actionLabel: $url"); + } else { + throw new WireException("Unable to write: $url"); + } + } catch(\Exception $e) { + $this->error($e->getMessage()); + $numErrors++; + } + } + + return $numErrors === 0; + } + + /** + * Are we running under Apache? + * + * @return bool + * + */ + public function isApache() { + + $software = isset($_SERVER['SERVER_SOFTWARE']) ? $_SERVER['SERVER_SOFTWARE'] : ''; + + if(stripos($software, 'microsoft-iis') !== false) return false; + if(stripos($software, 'nginx') !== false) return false; + if(stripos($software, 'apache') !== false) return true; + + if(function_exists('apache_get_version') && stripos(apache_get_version(), 'Apache') !== false) return true; + if(function_exists('apache_get_modules') && in_array('mod_rewrite', apache_get_modules())) return true; + if(getenv('HTTP_MOD_REWRITE') == 'On') return true; + + $rootPath = $this->wire('config')->paths->root; + if(file_exists($rootPath . 'Web.config') || file_exists($rootPath . 'web.config')) return false; // IIS + if(file_exists($rootPath . '.htaccess')) return true; + + return false; + } + + /** + * Is this update useful for this installation? + * + * @return bool + * + */ + public function isUseful() { + if(!$this->isApache()) return false; + $f = '.htaccess'; + $paths = $this->wire('config')->paths; + $assets = $paths->assets; + if(!file_exists($assets . $f)) return true; + if(!file_exists($assets . "logs/$f")) return true; + if(!file_exists($assets . "cache/$f")) return true; + if(!file_exists($assets . "backups/$f")) return true; + if(!file_exists($paths->templates . $f)) return true; + if(!file_exists($paths->site . $f)) return true; + if(!file_exists($paths->site . "modules/$f")) return true; + return false; + } + +} + diff --git a/wire/modules/System/SystemUpdater/SystemUpdater.module b/wire/modules/System/SystemUpdater/SystemUpdater.module index 3178ac4f..c96d41a6 100644 --- a/wire/modules/System/SystemUpdater/SystemUpdater.module +++ b/wire/modules/System/SystemUpdater/SystemUpdater.module @@ -26,7 +26,7 @@ class SystemUpdater extends WireData implements Module, ConfigurableModule { * This version number is important, as this updater keeps the systemVersion up with this version * */ - 'version' => 16, + 'version' => 17, ); } diff --git a/wire/modules/System/SystemUpdater/SystemUpdaterChecks.php b/wire/modules/System/SystemUpdater/SystemUpdaterChecks.php index 15befcef..d7a9f577 100644 --- a/wire/modules/System/SystemUpdater/SystemUpdaterChecks.php +++ b/wire/modules/System/SystemUpdater/SystemUpdaterChecks.php @@ -79,7 +79,7 @@ class SystemUpdaterChecks extends Wire { 'checkWelcome', 'checkIndexFile', 'checkHtaccessFile', - //'checkOtherHtaccessFiles', + 'checkOtherHtaccessFiles', 'checkInstallerFiles', 'checkFilePermissions', 'checkPublishedField',