1<?php 2/** 3 * e107 website system 4 * 5 * Copyright (C) 2008-2020 e107 Inc (e107.org) 6 * Released under the terms and conditions of the 7 * GNU General Public License (http://www.gnu.org/licenses/gpl.txt) 8 */ 9 10require_once("e_file_inspector_interface.php"); 11 12/** 13 * File Inspector 14 * 15 * Tool to validate application files for consistency by comparing hashes of files with those in a database 16 */ 17abstract class e_file_inspector implements e_file_inspector_interface 18{ 19 protected $database; 20 protected $currentVersion; 21 private $validatedBitmask; 22 23 protected $defaultDirsCache; 24 protected $customDirsCache; 25 private $undeterminable = array(); 26 27 // FIXME: Better place for the insecure file list 28 public $insecureFiles = [ 29 e_ADMIN . "ad_links.php", 30 e_PLUGIN . "tinymce4/e_meta.php", 31 e_THEME . "bootstrap3/css/bootstrap_dark.css", 32 e_PLUGIN . "search_menu/languages/English.php", 33 e_LANGUAGEDIR . e_LANGUAGE . "/lan_parser_functions.php", 34 e_LANGUAGEDIR . e_LANGUAGE . "/admin/help/theme.php", 35 e_HANDLER . "np_class.php", 36 e_CORE . "shortcodes/single/user_extended.sc", 37 e_ADMIN . "download.php", 38 e_PLUGIN . "banner/config.php", 39 e_PLUGIN . "forum/newforumposts_menu_config.php", 40 e_PLUGIN . "forum/e_latest.php", 41 e_PLUGIN . "forum/e_status.php", 42 e_PLUGIN . "forum/forum_post_shortcodes.php", 43 e_PLUGIN . "forum/forum_shortcodes.php", 44 e_PLUGIN . "forum/forum_update_check.php", 45 e_PLUGIN . "online_extended_menu/online_extended_menu.php", 46 e_PLUGIN . "online_extended_menu/images/user.png", 47 e_PLUGIN . "online_extended_menu/languages/English.php", 48 e_PLUGIN . "pm/sendpm.sc", 49 e_PLUGIN . "pm/shortcodes/", 50 e_PLUGIN . "social/e_header.php", 51 ]; 52 53 private $existingInsecureFiles = array(); 54 private $existingInsecureDirectories = array(); 55 56 /** 57 * e_file_inspector constructor 58 * @param string $database The database from which integrity data may be read or to which integrity data may be 59 * written. This should be an URL or absolute file path for most implementations. 60 */ 61 public function __construct($database) 62 { 63 $this->database = $database; 64 65 $this->checkDeprecatedFilesLog(); 66 67 $appRoot = e107::getInstance()->file_path; 68 $this->undeterminable = array_map(function ($path) 69 { 70 return realpath($path) ? realpath($path) : $path; 71 }, [ 72 $appRoot . "e107_config.php", 73 $appRoot . e107::getFolder('system_base') . "core_image.phar", 74 ] 75 ); 76 $this->existingInsecureFiles = array_filter($this->insecureFiles, function ($path) 77 { 78 return is_file($path); 79 }); 80 $this->existingInsecureFiles = array_map('realpath', $this->existingInsecureFiles); 81 $this->existingInsecureDirectories = array_filter($this->insecureFiles, function ($path) 82 { 83 return is_dir($path); 84 }); 85 $this->existingInsecureDirectories = array_map('realpath', $this->existingInsecureDirectories); 86 } 87 88 /** 89 * Populate insecureFiles list if deprecatedFiles.log found. 90 * @return void 91 */ 92 private function checkDeprecatedFilesLog() 93 { 94 $log = e_LOG.'fileinspector/deprecatedFiles.log'; 95 96 if(!file_exists($log)) 97 { 98 return; 99 } 100 101 $content = file_get_contents($log); 102 103 if(empty($content)) 104 { 105 return; 106 } 107 108 $tmp = explode("\n", $content); 109 $this->insecureFiles = []; 110 foreach($tmp as $line) 111 { 112 if(empty($line)) 113 { 114 continue; 115 } 116 117 $this->insecureFiles[] = e_BASE.$line; 118 } 119 120 121 } 122 123 /** 124 * Convert validation code to string. 125 * @param $validationCode 126 * @return string 127 */ 128 public static function getStatusForValidationCode($validationCode) 129 { 130 $status = 'unknown'; 131 if ($validationCode & self::VALIDATED) 132 $status = 'check'; 133 elseif (!($validationCode & self::VALIDATED_FILE_EXISTS)) 134 $status = 'missing'; 135 elseif (!($validationCode & self::VALIDATED_FILE_SECURITY)) 136 $status = 'warning'; 137 elseif (!($validationCode & self::VALIDATED_PATH_KNOWN)) 138 $status = 'unknown'; 139 elseif (!($validationCode & self::VALIDATED_PATH_VERSION)) 140 $status = 'old'; 141 elseif (!($validationCode & self::VALIDATED_HASH_CALCULABLE)) 142 $status = 'uncalc'; 143 elseif (!($validationCode & self::VALIDATED_HASH_CURRENT)) 144 if ($validationCode & self::VALIDATED_HASH_EXISTS) 145 $status = 'old'; 146 else 147 $status = 'fail'; 148 return $status; 149 } 150 151 /** 152 * Prepare the provided database for reading or writing 153 * 154 * Should tolerate a non-existent database and try to create it if a write operation is executed. 155 * 156 * @return void 157 */ 158 abstract public function loadDatabase(); 159 160 /** 161 * Check the integrity of the provided path 162 * 163 * @param $path string Relative path of the file to look up 164 * @param $version string The desired software release to match. 165 * Leave blank for the current version. 166 * Do not prepend the version number with "v". 167 * @return int Validation code (see the constants of this class) 168 */ 169 public function validate($path, $version = null) 170 { 171 if ($version === null) $version = $this->getCurrentVersion(); 172 173 $bits = 0x0; 174 $absolutePath = $this->relativePathToAbsolutePath($path); 175 $dbChecksums = $this->getChecksums($path); 176 $dbChecksum = $this->getChecksum($path, $version); 177 $actualChecksum = !empty($dbChecksums) ? $this->checksumPath($absolutePath) : null; 178 179 if (!empty($dbChecksums)) $bits |= self::VALIDATED_PATH_KNOWN; 180 if ($dbChecksum !== false) $bits |= self::VALIDATED_PATH_VERSION; 181 if (file_exists($absolutePath)) $bits |= self::VALIDATED_FILE_EXISTS; 182 if (!$this->isInsecure($path)) $bits |= self::VALIDATED_FILE_SECURITY; 183 if ($this->isDeterminable($absolutePath)) $bits |= self::VALIDATED_HASH_CALCULABLE; 184 if ($actualChecksum === $dbChecksum) $bits |= self::VALIDATED_HASH_CURRENT; 185 186 foreach ($dbChecksums as $dbChecksum) 187 { 188 if ($dbChecksum === $actualChecksum) $bits |= self::VALIDATED_HASH_EXISTS; 189 } 190 191 if ($bits + self::VALIDATED === $this->getValidatedBitmask()) $bits |= self::VALIDATED; 192 193 $this->log($path, $bits); 194 195 return $bits; 196 } 197 198 /** 199 * Log old file paths. (may be expanded to other types in future) 200 * 201 * @param string $relativePath 202 * @param int $status 203 * @return null 204 */ 205 private function log($relativePath, $status) 206 { 207 if(empty($relativePath) || self::getStatusForValidationCode($status) !== 'old') // deprecated-file status 208 { 209 return null; 210 } 211 212 $message = $relativePath."\n"; 213 214 $logPath = e_LOG."fileinspector/"; 215 216 if(!is_dir($logPath)) 217 { 218 mkdir($logPath, 0775); 219 } 220 221 file_put_contents($logPath."deprecatedFiles.log", $message, FILE_APPEND); 222 223 return null; 224 } 225 226 /** 227 * Get the file integrity hash for the provided path and version 228 * 229 * @param $path string Relative path of the file to look up 230 * @param $version string The software release version corresponding to the file hash. 231 * Leave blank for the current version. 232 * Do not prepend the version number with "v". 233 * @return string|bool The database hash for the path and version specified. FALSE if the record does not exist. 234 */ 235 public function getChecksum($path, $version = null) 236 { 237 if ($version === null) $version = $this->getCurrentVersion(); 238 $checksums = $this->getChecksums($path); 239 return isset($checksums[$version]) ? $checksums[$version] : false; 240 } 241 242 /** 243 * Calculate the hash of a path to compare with the hash database 244 * 245 * @param $absolutePath string Absolute path of the file to hash 246 * @return string|bool The actual hash for the path. FALSE if the hash was incalculable. 247 */ 248 public function checksumPath($absolutePath) 249 { 250 if (!$this->isDeterminable($absolutePath)) return false; 251 252 return $this->checksum(file_get_contents($absolutePath)); 253 } 254 255 /** 256 * Calculate the hash of a string, which would be used to compare with the hash database 257 * 258 * @param $content string Full content to hash 259 * @return string 260 */ 261 public function checksum($content) 262 { 263 return md5(str_replace(array(chr(13), chr(10)), "", $content)); 264 } 265 266 /** 267 * @inheritDoc 268 */ 269 public function getVersions($path) 270 { 271 return array_keys($this->getChecksums($path)); 272 } 273 274 /** 275 * @inheritDoc 276 */ 277 public function getCurrentVersion() 278 { 279 if ($this->currentVersion) return $this->currentVersion; 280 281 $checksums = $this->getChecksums("index.php"); 282 $versions = array_keys($checksums); 283 usort($versions, 'version_compare'); 284 return $this->currentVersion = array_pop($versions); 285 } 286 287 /** 288 * Get the matching version of the provided path 289 * 290 * Useful for looking up the versions of old files that no longer exist in the latest image 291 * 292 * @param $path string Relative path of the file to look up 293 * @return string|bool PHP-standardized version of the file. FALSE if there is no match. 294 */ 295 public function getVersion($path) 296 { 297 $actualChecksum = $this->checksumPath($path); 298 foreach ($this->getChecksums($path) as $dbVersion => $dbChecksum) 299 { 300 if ($actualChecksum === $dbChecksum) return $dbVersion; 301 } 302 return false; 303 } 304 305 /** 306 * @inheritDoc 307 */ 308 public function isInsecure($path) 309 { 310 $absolutePath = $this->relativePathToAbsolutePath($path); 311 if (in_array($absolutePath, $this->existingInsecureFiles)) return true; 312 foreach ($this->existingInsecureDirectories as $existingInsecureDirectory) 313 { 314 $existingInsecureDirectory .= '/'; 315 if (substr($absolutePath, 0, strlen($existingInsecureDirectory)) === $existingInsecureDirectory) return true; 316 } 317 return false; 318 } 319 320 /** 321 * Convert a custom site path to a default path 322 * @param string $path Custom path 323 * @return string 324 */ 325 public function customPathToDefaultPath($path) 326 { 327 if (!is_array($this->customDirsCache)) $this->populateDirsCache(); 328 foreach ($this->customDirsCache as $dirType => $customDir) 329 { 330 if (!isset($this->defaultDirsCache[$dirType])) continue; 331 332 $defaultDir = $this->defaultDirsCache[$dirType]; 333 if ($customDir === $defaultDir) continue; 334 335 if (substr($path, 0, strlen($customDir)) === $customDir) 336 $path = $defaultDir . substr($path, strlen($customDir)); 337 } 338 return $path; 339 } 340 341 public function defaultPathToCustomPath($path) 342 { 343 if (!is_array($this->customDirsCache)) $this->populateDirsCache(); 344 foreach ($this->customDirsCache as $dirType => $customDir) 345 { 346 if (!isset($this->defaultDirsCache[$dirType])) continue; 347 348 $defaultDir = $this->defaultDirsCache[$dirType]; 349 if ($customDir === $defaultDir) continue; 350 351 if (substr($path, 0, strlen($defaultDir)) === $defaultDir) 352 $path = $customDir . substr($path, strlen($defaultDir)); 353 } 354 return $path; 355 } 356 357 private function getValidatedBitmask() 358 { 359 if ($this->validatedBitmask !== null) return $this->validatedBitmask; 360 $constants = (new ReflectionClass(self::class))->getConstants(); 361 $validated_constants = array_filter($constants, function ($key) 362 { 363 $str = 'VALIDATED_'; 364 return substr($key, 0, strlen($str)) === $str; 365 }, ARRAY_FILTER_USE_KEY); 366 367 $this->validatedBitmask = (max($validated_constants) << 0x1) - 0x1; 368 return $this->validatedBitmask; 369 } 370 371 /** 372 * @param $absolutePath 373 * @return bool 374 */ 375 private function isDeterminable($absolutePath) 376 { 377 return is_file($absolutePath) && is_readable($absolutePath) && !in_array($absolutePath, $this->undeterminable); 378 } 379 380 protected function populateDirsCache() 381 { 382 $this->defaultDirsCache = e107::getInstance()->defaultDirs(); 383 $customDirs = e107::getInstance()->e107_dirs ? e107::getInstance()->e107_dirs : []; 384 $this->customDirsCache = array_diff_assoc($customDirs, $this->defaultDirsCache); 385 } 386 387 /** 388 * @param $path 389 * @return false|string 390 */ 391 private function relativePathToAbsolutePath($path) 392 { 393 return realpath(e_BASE . $path); 394 } 395}