diff --git a/wire/core/CacheFile.php b/wire/core/CacheFile.php index 9325713a..aefac4f3 100644 --- a/wire/core/CacheFile.php +++ b/wire/core/CacheFile.php @@ -210,7 +210,7 @@ class CacheFile extends Wire { foreach($dir as $file) { if($file->isDir() || $file->isDot()) continue; //if(strpos($file->getFilename(), self::cacheFileExtension)) @unlink($file->getPathname()); - if(self::isCacheFile($file->getPathname())) @unlink($file->getPathname()); + if(self::isCacheFile($file->getPathname())) $this->wire('files')->unlink($file->getPathname()); } return @rmdir($this->path); @@ -223,7 +223,7 @@ class CacheFile extends Wire { * */ protected function removeFilename($filename) { - @unlink($filename); + $this->wire('files')->unlink($filename); } @@ -250,7 +250,7 @@ class CacheFile extends Wire { $numRemoved += self::removeAll($pathname, true); } else if($file->isFile() && (self::isCacheFile($pathname) || ($file->getFilename() == self::globalExpireFilename))) { - if(unlink($pathname)) $numRemoved++; + if(wire('files')->unlink($pathname)) $numRemoved++; } } diff --git a/wire/core/FileCompiler.php b/wire/core/FileCompiler.php index ae586de8..e2814dce 100644 --- a/wire/core/FileCompiler.php +++ b/wire/core/FileCompiler.php @@ -1110,7 +1110,7 @@ class FileCompiler extends Wire { if(!file_exists($sourceFile)) { // source file has been deleted - unlink($targetFile); + $this->wire('files')->unlink($targetFile, true); if($useLog) $this->log("Maintenance/Remove target file: $targetURL$basename"); } else if(filemtime($sourceFile) > filemtime($targetFile)) { diff --git a/wire/core/FileLog.php b/wire/core/FileLog.php index b0510de9..703d18cd 100644 --- a/wire/core/FileLog.php +++ b/wire/core/FileLog.php @@ -451,11 +451,11 @@ class FileLog extends Wire { fclose($fpr); if($cnt) { - unlink($filename); - rename("$filename.new", $filename); + $this->wire('files')->unlink($filename, true); + $this->wire('files')->rename("$filename.new", $filename, true); $this->wire('files')->chmod($filename); } else { - @unlink("$filename.new"); + $this->wire('files')->unlink("$filename.new", true); } return $cnt; @@ -477,8 +477,8 @@ class FileLog extends Wire { 'dateTo' => time(), )); if(file_exists($toFile)) { - unlink($this->logFilename); - rename($toFile, $this->logFilename); + $this->wire('files')->unlink($this->logFilename, true); + $this->wire('files')->rename($toFile, $this->logFilename, true); return $qty; } return 0; @@ -491,7 +491,7 @@ class FileLog extends Wire { * */ public function delete() { - return @unlink($this->logFilename); + return $this->wire('files')->unlink($this->logFilename, true); } public function __toString() { diff --git a/wire/core/ImageSizerEngine.php b/wire/core/ImageSizerEngine.php index f1296923..2546177d 100755 --- a/wire/core/ImageSizerEngine.php +++ b/wire/core/ImageSizerEngine.php @@ -552,9 +552,9 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable $dest = preg_replace('/\.' . $extension . '$/', '_tmp.' . $extension, $filename); if(strlen($content) == @file_put_contents($dest, $content, \LOCK_EX)) { // on success we replace the file - unlink($filename); - rename($dest, $filename); - wireChmod($filename); + $this->wire('files')->unlink($filename); + $this->wire('files')->rename($dest, $filename); + $this->wire('files')->chmod($filename); return true; } else { // it was created a temp diskfile but not with all data in it @@ -1540,8 +1540,8 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable // all went well, copy back the temp file, remove the temp file if(!@copy($this->tmpFile, $this->filename)) return false; // fallback or failed - wireChmod($this->filename); - @unlink($this->tmpFile); + $this->wire('files')->chmod($this->filename); + $this->wire('files')->unlink($this->tmpFile); // post processing: IPTC, setModified and reload ImageInfo $this->writeBackIPTC($this->filename, false); @@ -1590,13 +1590,13 @@ abstract class ImageSizerEngine extends WireData implements Module, Configurable if($result) { // success if($tmpFilename != $dstFilename) { - if(is_file($dstFilename)) unlink($dstFilename); - rename($tmpFilename, $dstFilename); + if(is_file($dstFilename)) $this->wire('files')->unlink($dstFilename); + $this->wire('files')->rename($tmpFilename, $dstFilename); } - wireChmod($dstFilename); + $this->wire('files')->chmod($dstFilename); } else { // fail - if(is_file($tmpFilename)) unlink($tmpFilename); + if(is_file($tmpFilename)) $this->wire('files')->unlink($tmpFilename); } return $result; diff --git a/wire/core/Modules.php b/wire/core/Modules.php index fc781bff..80ab0de6 100644 --- a/wire/core/Modules.php +++ b/wire/core/Modules.php @@ -2068,6 +2068,7 @@ class Modules extends WireArray { $success = false; $reason = $this->isDeleteable($class, true); if($reason !== true) throw new WireException($reason); + $siteModulesPath = $this->wire('config')->paths->siteModules; $filename = $this->installable[$class]; $basename = basename($filename); @@ -2161,7 +2162,7 @@ class Modules extends WireArray { foreach($files as $file) { $file = "$path/$file"; if(!file_exists($file)) continue; - if(unlink($file)) { + if($this->wire('files')->unlink($file, $siteModulesPath)) { $this->message("Removed file: $file", Notice::debug); } else { $this->error("Unable to remove file: $file", Notice::debug); diff --git a/wire/core/Pagefile.php b/wire/core/Pagefile.php index 0de56bf3..e9208cd3 100644 --- a/wire/core/Pagefile.php +++ b/wire/core/Pagefile.php @@ -952,7 +952,7 @@ class Pagefile extends WireData { */ public function unlink() { if(!strlen($this->basename) || !is_file($this->filename)) return true; - return unlink($this->filename); + return $this->wire('files')->unlink($this->filename, true); } /** @@ -968,7 +968,7 @@ class Pagefile extends WireData { */ public function rename($basename) { $basename = $this->pagefiles->cleanBasename($basename, true); - if(rename($this->filename, $this->pagefiles->path . $basename)) { + if($this->wire('files')->rename($this->filename, $this->pagefiles->path . $basename, true)) { $this->set('basename', $basename); return $this->basename(); } diff --git a/wire/core/PagefilesManager.php b/wire/core/PagefilesManager.php index d574371a..2762b83a 100644 --- a/wire/core/PagefilesManager.php +++ b/wire/core/PagefilesManager.php @@ -318,16 +318,16 @@ class PagefilesManager extends Wire { $errors = 0; if($recursive) { // clear out path and everything below it - if(!wireRmdir($path, true)) $errors++; + if(!$this->wire('files')->rmdir($path, true, true)) $errors++; if(!$rmdir) $this->_createPath($path); } else { // only clear out files in path foreach(new \DirectoryIterator($path) as $file) { if($file->isDot() || $file->isDir()) continue; - if(!unlink($file->getPathname())) $errors++; + if(!$this->wire('files')->unlink($file->getPathname(), true)) $errors++; } if($rmdir) { - @rmdir($path); // will not be successful if other dirs within it + $this->wire('files')->rmdir($path, false, true); // will not be successful if other dirs within it } } return $errors === 0; diff --git a/wire/core/Pageimage.php b/wire/core/Pageimage.php index 09a7075f..e310637c 100644 --- a/wire/core/Pageimage.php +++ b/wire/core/Pageimage.php @@ -701,8 +701,8 @@ class Pageimage extends Pagefile { if(!$exists || $options['forceNew']) { // filenameUnvalidated is temporary filename used for resize $filenameUnvalidated = $this->pagefiles->page->filesManager()->getTempPath() . $basename; - if($exists && $options['forceNew']) @unlink($filenameFinal); - if(file_exists($filenameUnvalidated)) @unlink($filenameUnvalidated); + if($exists && $options['forceNew']) $this->wire('files')->unlink($filenameFinal, true); + if(file_exists($filenameUnvalidated)) $this->wire('files')->unlink($filenameUnvalidated, true); if(@copy($this->filename(), $filenameUnvalidated)) { try { @@ -760,8 +760,8 @@ class Pageimage extends Pagefile { // if an error occurred, that error property will be populated with details if($this->error) { // error condition: unlink copied file - if(is_file($filenameFinal)) @unlink($filenameFinal); - if($filenameUnvalidated && is_file($filenameUnvalidated)) @unlink($filenameUnvalidated); + if(is_file($filenameFinal)) $this->wire('files')->unlink($filenameFinal, true); + if($filenameUnvalidated && is_file($filenameUnvalidated)) $this->wire('files')->unlink($filenameUnvalidated); // write an invalid image so it's clear something failed // todo: maybe return a 1-pixel blank image instead? @@ -1235,7 +1235,7 @@ class Pageimage extends Pagefile { // rebuild the variation $o['forceNew'] = true; $o['suffix'] = $info['suffix']; - if(is_file($info['path'])) unlink($info['path']); + if(is_file($info['path'])) $this->wire('files')->unlink($info['path'], true); /* if(!$info['width'] && $info['actualWidth']) { @@ -1477,7 +1477,7 @@ class Pageimage extends Pagefile { $variations = $this->getVariations(); foreach($variations as $variation) { - if(is_file($variation->filename)) unlink($variation->filename); + if(is_file($variation->filename)) $this->wire('files')->unlink($variation->filename, true); } $this->variations = null; diff --git a/wire/core/PagesExportImport.php b/wire/core/PagesExportImport.php index d0017707..4e08a3e1 100644 --- a/wire/core/PagesExportImport.php +++ b/wire/core/PagesExportImport.php @@ -96,7 +96,7 @@ class PagesExportImport extends Wire { $qty++; } } else { - if(unlink($pathname)) { + if($files->unlink($pathname, true)) { $this->message($this->_('Removed old file') . " - $pathname", Notice::debug); $qty++; } @@ -151,8 +151,9 @@ class PagesExportImport extends Wire { 'allowHidden' => false, 'allowEmptyDirs' => false )); + if($zipInfo) {} // ignore - unlink($jsonFile); + $files->unlink($jsonFile, true); return $zipName; } diff --git a/wire/core/WireDatabaseBackup.php b/wire/core/WireDatabaseBackup.php index 6e5419e0..c56ffb89 100644 --- a/wire/core/WireDatabaseBackup.php +++ b/wire/core/WireDatabaseBackup.php @@ -668,7 +668,7 @@ class WireDatabaseBackup { $success = false; if($file && file_exists($file)) { if(!filesize($file)) { - unlink($file); + $this->wire('files')->unlink($file, true); } else { $success = true; } @@ -904,7 +904,7 @@ class WireDatabaseBackup { if(file_exists($file)) { if(filesize($file) > 0) return $file; - unlink($file); + $this->wire('files')->unlink($file); } return false; diff --git a/wire/core/WireFileTools.php b/wire/core/WireFileTools.php index b0cb93a1..e51be2bb 100644 --- a/wire/core/WireFileTools.php +++ b/wire/core/WireFileTools.php @@ -29,7 +29,7 @@ class WireFileTools extends Wire { * // directory created: /site/assets/cache/foo-bar/ * } * ~~~~~ - * + * * @param string $path Directory you want to create * @param bool|string $recursive If set to true, all directories will be created as needed to reach the end. * @param string|null|bool $chmod Optional mode to set directory to (default: $config->chmodDir), format must be a string i.e. "0755" @@ -65,31 +65,54 @@ class WireFileTools extends Wire { * for the `$recursive` argument. You should be careful with this option, as it can easily wipe out an entire * directory tree in a flash. * + * Note that the $options argument was added in 3.0.118. + * * ~~~~~ * // Remove directory /site/assets/cache/foo-bar/ and everything in it - * $files->rmdir($config->paths->cache . 'foo-bar/', true); + * $files->rmdir($config->paths->cache . 'foo-bar/', true); + * + * // Remove directory after ensuring $pathname is somewhere within /site/assets/ + * $files->rmdir($pathname, true, [ 'limitPath' => $config->paths->assets ]); * ~~~~~ * * @param string $path Path/directory you want to remove * @param bool $recursive If set to true, all files and directories in $path will be recursively removed as well (default=false). + * @param array|bool|string $options Optional settings to adjust behavior or (bool|string) for limitPath option: + * - `limitPath` (string|bool|array): Must be somewhere within given path, boolean true for site assets, or false to disable (default=false). + * - `throw` (bool): Throw verbose WireException (rather than return false) when potentially consequential fail (default=false). * @return bool True on success, false on failure * */ - public function rmdir($path, $recursive = false) { + public function rmdir($path, $recursive = false, $options = array()) { + + $defaults = array( + 'limitPath' => false, + 'throw' => false, + ); + + if(!is_array($options)) $options = array('limitPath' => $options); + $options = array_merge($defaults, $options); + + // if there's nothing to remove, exit now if(!is_dir($path)) return false; - if(!strlen(trim($path, '/.'))) return false; // just for safety, don't proceed with empty string + + // verify that path is allowed for this operation + if(!$this->allowPath($path, $options['limitPath'], $options['throw'])) return false; + + // handle recursive rmdir if($recursive === true) { $files = @scandir($path); if(is_array($files)) foreach($files as $file) { - if($file == '.' || $file == '..') continue; + if($file == '.' || $file == '..' || strpos($file, '..') !== false) continue; $pathname = "$path/$file"; if(is_dir($pathname)) { - $this->rmdir($pathname, true); + $this->rmdir($pathname, $recursive, $options); } else { - @unlink($pathname); + $this->unlink($pathname, $options['limitPath'], $options['throw']); } } } + return @rmdir($path); } @@ -109,7 +132,7 @@ class WireFileTools extends Wire { * // Update the mode of /site/assets/cache/foo-bar/ recursively * $files->chmod($config->paths->cache . 'foo-bar/', true); * ~~~~~ - * + * * @param string $path Path or file that you want to adjust mode for (may be a path/directory or a filename). * @param bool|string $recursive If set to true, all files and directories in $path will be recursively set as well (default=false). * @param string|null|bool $chmod If you want to set the mode to something other than ProcessWire's chmodFile/chmodDir settings, @@ -176,16 +199,19 @@ class WireFileTools extends Wire { * // Copy everything from /site/assets/cache/foo/ to /site/assets/cache/bar/ * $copyFrom = $config->paths->cache . "foo/"; * $copyTo = $config->paths->cache . "bar/"; - * copy($copyFrom, $copyTo); + * $files->copy($copyFrom, $copyTo); * ~~~~~ - * + * * @param string $src Path to copy files from, or filename to copy. * @param string $dst Path (or filename) to copy file(s) to. Directory is created if it doesn't already exist. * @param bool|array $options Array of options: * - `recursive` (boolean): Whether to copy directories within recursively. (default=true) * - `allowEmptyDirs` (boolean): Copy directories even if they are empty? (default=true) - * - If a boolean is specified for $options, it is assumed to be the 'recursive' option. + * - `limitPath` (bool|string|array): Limit copy to within path given here, or true for site assets path (default=false). + * - Note that the limitPath option was added in 3.0.118. + * - If a boolean is specified for $options, it is assumed to be the `recursive` option. * @return bool True on success, false on failure. + * @throws WireException if `limitPath` option is used and either $src or $dst is not allowed * */ public function copy($src, $dst, $options = array()) { @@ -193,10 +219,16 @@ class WireFileTools extends Wire { $defaults = array( 'recursive' => true, 'allowEmptyDirs' => true, + 'limitPath' => false, ); if(is_bool($options)) $options = array('recursive' => $options); $options = array_merge($defaults, $options); + + if($options['limitPath'] !== false) { + $this->allowPath($src, $options['limitPath'], true); + $this->allowPath($dst, $options['limitPath'], true); + } if(!is_dir($src)) { // just copy a file @@ -250,6 +282,220 @@ class WireFileTools extends Wire { return true; } + /** + * Unlink/delete file with additional protections relative to PHP unlink() + * + * - This method requires a full pathname to a file to unlink and does not + * accept any kind of relative path traversal. + * + * - This method will only unlink files in /site/assets/ if you specify `true` + * for the `$limitPath` option (recommended). + * + * @param string $filename + * @param string|bool $limitPath Limit only to files within some starting path? (default=false) + * - Boolean true to limit unlink operations to somewhere within /site/assets/ (only known always writable path). + * - Boolean false to disable to security feature. (default) + * - An alternative path (string) that represents the starting path (full disk path) to limit deletions to. + * - An array with multiple of the above string option. + * @param bool $throw Throw exception on error? + * @return bool True on success, false on fail + * @throws WireException If file is not allowed to be removed or unlink fails + * @since 3.0.118 + * + */ + public function unlink($filename, $limitPath = false, $throw = false) { + + if(!$this->allowPath($filename, $limitPath, $throw)) { + // path not allowed + return false; + } + + if(!is_file($filename) && !is_link($filename)) { + // only files or links (that exist) can be deleted + return false; + } + + if(@unlink($filename)) { + return true; + } else { + if($throw) throw new WireException("Unable to unlink file $filename"); + return false; + } + } + + /** + * Rename a file or directory and update permissions + * + * Note that this method will fail if pathname given by $newName argument already exists. + * + * @param string $oldName Old pathname, must be full disk path. + * @param string $newName New pathname, must be full disk path OR can be basename to assume same path as $oldName. + * @param array|bool|string $options Options array to modify behavior or substitute `limitPath` (bool or string) option here. + * - `limitPath` (bool|string|array): Limit renames to within this path, or boolean TRUE for site/assets, or FALSE to disable (default=false). + * - `throw` (bool): Throw WireException with verbose details on error? (default=false) + * - `chmod` (bool): Adjust permissions to be consistent with $config after rename? (default=true) + * - If given a bool or string for $options the `limitPath` option is assumed. + * @return bool True on success, false on fail (or WireException if throw option specified). + * @throws WireException If error occurs and $throw argument was true. + * @since 3.0.118 + * + */ + public function rename($oldName, $newName, $options = array()) { + + $defaults = array( + 'limitPath' => false, + 'throw' => false, + 'chmod' => true, + ); + + if(!is_array($options)) $options = array('limitPath' => $options); + $options = array_merge($defaults, $options); + + // if only basename was specified for the newName then use path from oldName + if(basename($newName) === $newName) { + $newName = dirname($oldName) . '/' . $newName; + } + + try { + $this->allowPath($oldName, $options['limitPath'], true); + } catch(\Exception $e) { + if($options['throw']) throw new WireException("Rename oldName path invalid: " . $e->getMessage()); + return false; + } + + try { + $this->allowPath($newName, $options['limitPath'], true); + } catch(\Exception $e) { + if($options['throw']) throw new WireException("Rename newName path invalid: " . $e->getMessage()); + return false; + } + + if(!file_exists($oldName)) { + if($options['throw']) throw new WireException("Rename given pathname (oldName) that does not exist: $oldName"); + return false; + } + + if(file_exists($newName)) { + if($options['throw']) throw new WireException("Rename to pathname (newName) that already exists: $newName"); + return false; + } + + if(!rename($oldName, $newName)) { + if($options['throw']) throw new WireException("Rename failed: $oldName => $newName"); + } + + if($options['chmod']) { + $this->chmod($newName); + } + + return true; + } + + /** + * Allow path or filename to to be used for file manipulation actions? + * + * Given path must be a full path (no relative references). If given a $limitPath, it must be a + * directory that already exists. + * + * Note that this method does not indicate whether or not the given pathname exists, only that it is allowed. + * As a result this can be used for checking a path before creating something in it too. + * + * #pw-internal + * + * @param string $pathname File or directory name to check + * @param bool|string|array $limitPath Any one of the following (default=false): + * - Full disk path (string) that $pathname must be within (whether directly or in subdirectory of). + * - Array of the above. + * - Boolean false to disable (default). + * - Boolean true for site assets path, which is the only known always-writable path in PW. + * @param bool $throw Throw verbose exceptions on error? (default=false). + * @return bool True if given pathname allowed, false if not. + * @throws WireException when $throw argument is true and function would otherwise return false. + * @since 3.0.118 + * + */ + public function allowPath($pathname, $limitPath = false, $throw = false) { + + if(is_array($limitPath)) { + // run allowPath() for each of the specified limitPaths + $allow = false; + foreach($limitPath as $dir) { + if(!is_string($dir) || empty($dir)) continue; + $allow = $this->allowPath($pathname, $dir, false); + if($allow) break; // found one that is allowed + } + if(!$allow && $throw) { + throw new WireException("Given pathname is not within any of the paths allowed by limitPath"); + } + return $allow; + + } else if($limitPath === true) { + // default limitPath + $limitPath = $this->wire('config')->paths->assets; + + } else if($limitPath === false) { + // no limitPath in use + + } else if(empty($limitPath) || !is_string($limitPath)) { + // invalid limitPath argument (wrong type or path does not exist) + if($throw) throw new WireException("Invalid type for limitPath argument"); + return false; + + } else if(!is_dir($limitPath)) { + if($throw) throw new WireException("$limitPath (limitPath) does not exist"); + return false; + } + + if($limitPath !== false) try { + // if limitPath can't pass allowPath then neither can $pathname + $this->allowPath($limitPath, false, true); + } catch(\Exception $e) { + if($throw) throw new WireException("Validating limitPath reported: " . $e->getMessage()); + return false; + } + + if(DIRECTORY_SEPARATOR != '/') { + $pathname = str_replace(DIRECTORY_SEPARATOR, '/', $pathname); + if(is_string($limitPath)) $limitPath = str_replace(DIRECTORY_SEPARATOR, '/', $limitPath); + $testname = $pathname; + if(strpos($pathname, ':')) list(,$testname) = explode(':', $pathname, 2); // reduce to no drive letter, if present + } else { + $testname = $pathname; + } + + if(!strlen(trim($testname, '/.')) || substr_count($testname, '/') < 2) { + // do not allow paths that consist of nothing but slashes and/or dots + // and do not allow paths off root or lacking absolute path reference + if($throw) throw new WireException("pathname not allowed: $pathname"); + return false; + } + + if(strpos($pathname, '..') !== false) { + // not allowed to traverse anywhere + if($throw) throw new WireException('pathname may not traverse “../”'); + return false; + } + + if(strpos($pathname, '.') === 0 || empty($pathname)) { + if($throw) throw new WireException('pathname may not begin with “.”'); + return false; + } + + if(strpos($pathname, '//') !== false) { + // URLs or accidental extra slashes not allowed + if($throw) throw new WireException('pathname may not contain double slash “//”'); + return false; + } + + if($limitPath !== false && strpos($pathname, $limitPath) !== 0) { + // disallow paths that do not begin with limitPath (i.e. /path/to/public_html/site/assets/) + if($throw) throw new WireException("Given pathname is not within $limitPath (limitPath)"); + return false; + } + + return true; + } + /** * Return a new temporary directory/path ready to use for files * @@ -360,6 +606,8 @@ class WireFileTools extends Wire { * // $items is an array of filenames that were unzipped into $dst * } * ~~~~~ + * + * #pw-group-archives * * @param string $file ZIP file to extract * @param string $dst Directory where files should be unzipped into. Directory is created if it doesn't exist. @@ -424,6 +672,8 @@ class WireFileTools extends Wire { * } * } * ~~~~~ + * + * #pw-group-archives * * @param string $zipfile Full path and filename to create or update (i.e. /path/to/myfile.zip) * @param array|string $files Array of files to add (full path and filename), or directory (string) to add. @@ -474,7 +724,7 @@ class WireFileTools extends Wire { if(!is_dir($zippath)) throw new WireException("Path for ZIP file ($zippath) does not exist"); if(!is_writable($zippath)) throw new WireException("Path for ZIP file ($zippath) is not writable"); if(empty($files)) throw new WireException("Nothing to add to ZIP file $zipfile"); - if(is_file($zipfile) && $options['overwrite'] && !unlink($zipfile)) throw new WireException("Unable to overwrite $zipfile"); + if(is_file($zipfile) && $options['overwrite'] && !$this->unlink($zipfile)) throw new WireException("Unable to overwrite $zipfile"); if(!is_array($files)) $files = array($files); if(!is_array($options['exclude'])) $options['exclude'] = array($options['exclude']); $recursive = false; @@ -547,6 +797,7 @@ class WireFileTools extends Wire { * */ public function send($filename, array $options = array(), array $headers = array()) { + $this->allowPath($filename, false, true); $http = new WireHttp(); $http->sendFile($filename, $options, $headers); } @@ -563,9 +814,11 @@ class WireFileTools extends Wire { * - `FILE_APPEND` (constant): Append to file if it already exists . * - `LOCK_EX` (constant): Acquire exclusive lock to file while writing. * @return int|bool Number of bytes written or boolean false on fail + * @throws WireException if given invalid $filename (since 3.0.118) * */ public function filePutContents($filename, $contents, $flags = 0) { + $this->allowPath($filename, false, true); $result = file_put_contents($filename, $contents, $flags); if($result !== false) $this->chmod($filename); return $result; @@ -585,6 +838,8 @@ class WireFileTools extends Wire { * * Note this function returns the output to you, so that you can send the output wherever you want (delayed output). * For direct output, use the `$files->include()` function instead. + * + * #pw-group-includes * * @param string $filename Assumed relative to /site/templates/ unless you provide a full path name with the filename. * If you provide a path, it must resolve somewhere in site/templates/, site/modules/ or wire/modules/. @@ -671,7 +926,6 @@ class WireFileTools extends Wire { return $t->render(); } - /** * Include a PHP file passing it all API variables and optionally your own specified variables * @@ -684,6 +938,8 @@ class WireFileTools extends Wire { * * Note this function produces direct output. To retrieve output as a return value, use the * `$files->render()` function instead. + * + * #pw-group-includes * * @param string $filename Filename to include * @param array $vars Optional variables you want to hand to the include (associative array) @@ -770,6 +1026,8 @@ class WireFileTools extends Wire { * * See the `WireFileTools::include()` method for details, arguments and options. * + * #pw-group-includes + * * @param string $filename * @param array $vars * @param array $options @@ -884,7 +1142,7 @@ class WireFileTools extends Wire { /** * Compile the given file using ProcessWire’s file compiler * - * #pw-group-compiler + * #pw-internal * * @param string $file File to compile * @param array $options Optional associative array of the following: @@ -916,7 +1174,7 @@ class WireFileTools extends Wire { /** * Compile and include() the given file * - * #pw-group-compiler + * #pw-internal * * @param string $file File to compile and include * @param array $options Optional associative array of the following: @@ -937,7 +1195,7 @@ class WireFileTools extends Wire { /** * Compile and include_once() the given file * - * #pw-group-compiler + * #pw-group-internal * * @param string $file File to compile and include * @param array $options Optional associative array of the following: @@ -958,7 +1216,7 @@ class WireFileTools extends Wire { /** * Compile and require() the given file * - * #pw-group-compiler + * #pw-internal * * @param string $file File to compile and include * @param array $options Optional associative array of the following: @@ -979,7 +1237,7 @@ class WireFileTools extends Wire { /** * Compile and require_once() the given file * - * #pw-group-compiler + * #pw-internal * * @param string $file File to compile and include * @param array $options Optional associative array of the following: @@ -1000,6 +1258,8 @@ class WireFileTools extends Wire { /** * Convert given directory name to use unix slashes and enforce trailing or no-trailing slash * + * #pw-group-filenames + * * @param string $dir Directory name to adust (if it needs it) * @param bool $trailingSlash True to force trailing slash, false to force no trailing slash (default=true) * @return string Adjusted directory name @@ -1016,6 +1276,8 @@ class WireFileTools extends Wire { /** * Convert given file name to use unix slashes (if it isn’t already) + * + * #pw-group-filenames * * @param string $file File name to adjust (if it needs it) * @return string Adjusted file name @@ -1029,7 +1291,9 @@ class WireFileTools extends Wire { * Is given $file name in given $path name? (aka: is $file a subdirectory somewhere within $path) * * This is purely for string comparison purposes, it does not check if file/path actually exists. - * Note that if $file and $path are identical, this method returns false. + * Note that if $file and $path are identical, this method returns false. + * + * #pw-group-filenames * * @param string $file May be a file or a directory * @param string $path diff --git a/wire/core/WireHttp.php b/wire/core/WireHttp.php index db8a76d9..58ca26ab 100644 --- a/wire/core/WireHttp.php +++ b/wire/core/WireHttp.php @@ -744,16 +744,15 @@ class WireHttp extends Wire { $methods = implode(", ", $triedMethods); if(count($this->error) || ($this->httpCode >= 400 && isset($this->httpCodes[$this->httpCode]))) { - unlink($toFile); + $this->wire('files')->unlink($toFile); $error = $this->_('File could not be downloaded') . ' ' . htmlentities("($fromURL) ") . $this->getError() . " (tried: $methods)"; throw new WireException($error); } else { $bytes = filesize($toFile); $this->message("Downloaded " . htmlentities($fromURL) . " => $toFile (using: $methods) [$bytes bytes]", Notice::debug); } - - $chmodFile = $this->wire('config')->chmodFile; - if($chmodFile) chmod($toFile, octdec($chmodFile)); + + $this->wire('files')->chmod($toFile); return $toFile; } diff --git a/wire/core/WireTempDir.php b/wire/core/WireTempDir.php index ef6cb850..6e29d696 100644 --- a/wire/core/WireTempDir.php +++ b/wire/core/WireTempDir.php @@ -369,8 +369,8 @@ class WireTempDir extends Wire { $dir = $this->wire('files')->unixDirName($dir); if(!strlen($dir) || !is_dir($dir)) return true; if(!$this->isTempDir($dir)) return false; - if(is_file($dir . self::hiddenFileName)) unlink($dir . self::hiddenFileName); - return $this->wire('files')->rmdir($dir, $recursive); + if(is_file($dir . self::hiddenFileName)) $this->wire('files')->unlink($dir . self::hiddenFileName, true); + return $this->wire('files')->rmdir($dir, $recursive, true); } /** diff --git a/wire/core/WireUpload.php b/wire/core/WireUpload.php index d845279e..a77e6be0 100644 --- a/wire/core/WireUpload.php +++ b/wire/core/WireUpload.php @@ -179,7 +179,7 @@ class WireUpload extends Wire { public function __destruct() { // cleanup files that were backed up when overwritten foreach($this->overwrittenFiles as $bakDestination => $destination) { - if(is_file($bakDestination)) unlink($bakDestination); + if(is_file($bakDestination)) $this->wire('files')->unlink($bakDestination); } } @@ -491,7 +491,7 @@ class WireUpload extends Wire { if(!$destination || !$filename) $destination = $this->destinationPath . 'invalid-filename'; if(!$error) $error = "Unable to move uploaded file to: $destination"; $this->error($error); - if(is_file($tmp_name)) @unlink($tmp_name); + if(is_file($tmp_name)) $this->wire('files')->unlink($tmp_name); return false; } @@ -533,7 +533,7 @@ class WireUpload extends Wire { } catch(\Exception $e) { $this->error($e->getMessage()); $this->wire('files')->rmdir($tmpDir, true); - unlink($zipFile); + $this->wire('files')->unlink($zipFile); return $files; } @@ -544,7 +544,7 @@ class WireUpload extends Wire { $pathname = $tmpDir . $file; if(!$this->isValidUpload($file, filesize($pathname), UPLOAD_ERR_OK)) { - @unlink($pathname); + $this->wire('files')->unlink($pathname, $tmpDir); continue; } @@ -574,12 +574,12 @@ class WireUpload extends Wire { $this->completedFilenames[] = basename($destination); $cnt++; } else { - @unlink($pathname); + $this->wire('files')->unlink($pathname, $tmpDir); } } $this->wire('files')->rmdir($tmpDir, true); - @unlink($zipFile); + $this->wire('files')->unlink($zipFile); if(!$cnt) return false; return true; diff --git a/wire/modules/Image/ImageSizerEngineIMagick/ImageSizerEngineIMagick.module b/wire/modules/Image/ImageSizerEngineIMagick/ImageSizerEngineIMagick.module index 421b0687..ee8fc37e 100755 --- a/wire/modules/Image/ImageSizerEngineIMagick/ImageSizerEngineIMagick.module +++ b/wire/modules/Image/ImageSizerEngineIMagick/ImageSizerEngineIMagick.module @@ -368,7 +368,7 @@ class ImageSizerEngineIMagick extends ImageSizerEngine { } // save to file - @unlink($dstFilename); + $this->wire('files')->unlink($dstFilename); @clearstatcache(dirname($dstFilename)); ##if(!$this->im->writeImage($this->destFilename)) { // We use this approach for saving so that it behaves the same like core ImageSizer with images that diff --git a/wire/modules/Inputfield/InputfieldFile/InputfieldFile.module b/wire/modules/Inputfield/InputfieldFile/InputfieldFile.module index 3de1ecd8..985392e0 100644 --- a/wire/modules/Inputfield/InputfieldFile/InputfieldFile.module +++ b/wire/modules/Inputfield/InputfieldFile/InputfieldFile.module @@ -760,8 +760,8 @@ class InputfieldFile extends Inputfield implements InputfieldItemList, Inputfiel $err = false; foreach($ul->getOverwrittenFiles() as $bakFile => $newFile) { if(basename($newFile) != $filename) continue; - unlink($newFile); - rename($bakFile, $newFile); // restore + $this->wire('files')->unlink($newFile); + $this->wire('files')->rename($bakFile, $newFile); // restore $ul->error(sprintf($this->_('Refused file %s because it is already on the file system and owned by a different field.'), $filename)); $err = true; } diff --git a/wire/modules/Inputfield/InputfieldImage/InputfieldImage.module b/wire/modules/Inputfield/InputfieldImage/InputfieldImage.module index e973e28c..8b5bca1e 100755 --- a/wire/modules/Inputfield/InputfieldImage/InputfieldImage.module +++ b/wire/modules/Inputfield/InputfieldImage/InputfieldImage.module @@ -439,8 +439,8 @@ class InputfieldImage extends InputfieldFile implements InputfieldItemList, Inpu } $pagefile2 = $pagefile->size($maxWidth, $maxHeight, array('cropping' => false)); if($pagefile->filename != $pagefile2->filename) { - unlink($pagefile->filename); - rename($pagefile2->filename, $pagefile->filename); + $this->wire('files')->unlink($pagefile->filename); + $this->wire('files')->rename($pagefile2->filename, $pagefile->filename); } $pagefile->getImageInfo(true); // force it to reload its dimensions } @@ -510,7 +510,7 @@ class InputfieldImage extends InputfieldFile implements InputfieldItemList, Inpu $exists = is_file($f); if($exists && $remove) { - unlink($f); + $this->wire('files')->unlink($f); $exists = false; } diff --git a/wire/modules/LanguageSupport/ProcessLanguageTranslator.module b/wire/modules/LanguageSupport/ProcessLanguageTranslator.module index 42f1151c..4499a100 100644 --- a/wire/modules/LanguageSupport/ProcessLanguageTranslator.module +++ b/wire/modules/LanguageSupport/ProcessLanguageTranslator.module @@ -648,7 +648,7 @@ class ProcessLanguageTranslator extends Process { if(is_null($this->fp)) { $f = $this->language->filesManager()->path() . '.phrase-index.txt'; // @todo make hidden - if(is_file($f)) unlink($f); + if(is_file($f)) $this->wire('files')->unlink($f); $this->fp = fopen($f, "a"); } diff --git a/wire/modules/LazyCron.module b/wire/modules/LazyCron.module index 2f0021a9..cc8a8821 100644 --- a/wire/modules/LazyCron.module +++ b/wire/modules/LazyCron.module @@ -151,7 +151,7 @@ class LazyCron extends WireData implements Module { // other LazyCron process potentially running if(filemtime($lockfile) < (time() - 3600)) { // expired lock file, some fatal error must have occurred during last LazyCron run - @unlink($lockfile); + $this->wire('files')->unlink($lockfile); } else { // skip running this time as an active lock file exists return; @@ -168,7 +168,7 @@ class LazyCron extends WireData implements Module { // file is probably locked, so skip it this time if($filedata === false) { - @unlink($lockfile); + $this->wire('files')->unlink($lockfile); return; } } else { @@ -210,7 +210,7 @@ class LazyCron extends WireData implements Module { if($this->config->chmodFile) @chmod($filename, octdec($this->config->chmodFile)); } - @unlink($lockfile); + $this->wire('files')->unlink($lockfile); } /** diff --git a/wire/modules/PageRender.module b/wire/modules/PageRender.module index 9368386b..0e74f254 100644 --- a/wire/modules/PageRender.module +++ b/wire/modules/PageRender.module @@ -743,10 +743,10 @@ class PageRender extends WireData implements Module, ConfigurableModule { foreach($d as $f) { if(!$f->isDir() && preg_match('/\.cache$/D', $f->getFilename())) { $numFiles++; - @unlink($f->getPathname()); + $this->wire('files')->unlink($f->getPathname()); } } - @rmdir($file->getPathname()); + $this->wire('files')->rmdir($file->getPathname()); } if($clearNow) { diff --git a/wire/modules/Process/ProcessModule/ProcessModuleInstall.php b/wire/modules/Process/ProcessModule/ProcessModuleInstall.php index 7820a1a6..bab27c3d 100644 --- a/wire/modules/Process/ProcessModule/ProcessModuleInstall.php +++ b/wire/modules/Process/ProcessModule/ProcessModuleInstall.php @@ -234,13 +234,13 @@ class ProcessModuleInstall extends Wire { $mkdirDestination = false; try { - $files = wireUnzipFile($file, $tempDir); - if(is_file($file)) unlink($file); + $files = $this->wire('files')->unzip($file, $tempDir); + if(is_file($file)) $this->wire('files')->unlink($file, true); foreach($files as $f) $this->message("Extracted: $f", Notice::debug); } catch(\Exception $e) { $this->error($e->getMessage()); - if(is_file($file)) unlink($file); + if(is_file($file)) $this->wire('files')->unlink($file, true); return false; } @@ -283,7 +283,7 @@ class ProcessModuleInstall extends Wire { if(!$success) { $this->error($this->_('Unable to copy module files:') . ' ' . $dirLabel); - if($mkdirDestination && !wireRmdir($destinationDir, true)) { + if($mkdirDestination && !$this->wire('files')->rmdir($destinationDir, true)) { $this->error($this->_('Could not delete failed module dir:') . ' ' . $destinationDir, Notice::log); } } @@ -306,11 +306,13 @@ class ProcessModuleInstall extends Wire { // remove symbolic link unlink(rtrim($moduleDir, '/')); $dir = str_replace($this->wire('config')->paths->root, '/', $moduleDir); - $this->error(sprintf($this->_('Please note that %s was a symbolic link and has been converted to a regular directory'), $dir), Notice::warning); + $this->warning(sprintf( + $this->_('Please note that %s was a symbolic link and has been converted to a regular directory'), $dir + )); } else { // module is a regular directory // just rename it to become the new backup dir - if(rename($moduleDir, $backupDir)) $success = true; + if($this->wire('files')->rename($moduleDir, $backupDir)) $success = true; } if($success) { @@ -352,6 +354,7 @@ class ProcessModuleInstall extends Wire { $tempDir = $this->getTempDir(); + /** @var WireUpload $ul */ $ul = $this->wire(new WireUpload($inputName)); $ul->setValidExtensions(array('zip')); $ul->setMaxFiles(1); @@ -416,7 +419,7 @@ class ProcessModuleInstall extends Wire { } catch(\Exception $e) { $this->error($e->getMessage()); - @unlink($tempZIP); + $this->wire('files')->unlink($tempZIP); } return $success ? $destinationDir : false; diff --git a/wire/modules/Process/ProcessPageEditImageSelect/ProcessPageEditImageSelect.module b/wire/modules/Process/ProcessPageEditImageSelect/ProcessPageEditImageSelect.module index 74a7ab5b..e494ad5a 100644 --- a/wire/modules/Process/ProcessPageEditImageSelect/ProcessPageEditImageSelect.module +++ b/wire/modules/Process/ProcessPageEditImageSelect/ProcessPageEditImageSelect.module @@ -1154,15 +1154,15 @@ class ProcessPageEditImageSelect extends Process implements ConfigurableModule { $thumb = $thumb['thumb']; if($thumb->url != $original->url) { // there is a thumbnail, distinct from the original image - unlink($thumb->filename); + $this->wire('files')->unlink($thumb->filename); } } } // replace original image $target = $path . $this->original; - unlink($target); - rename($image2->filename(), $target); + $this->wire('files')->unlink($target); + $this->wire('files')->rename($image2->filename(), $target); $this->wire('pages')->uncacheAll(); $page = $this->wire('pages')->get($this->page->id); @@ -1365,7 +1365,7 @@ class ProcessPageEditImageSelect extends Process implements ConfigurableModule { foreach($delete as $name) { if(!isset($variations[$name])) continue; $info = $variations[$name]; - if(is_file($info['path']) && unlink($info['path'])) { + if(is_file($info['path']) && $this->wire('files')->unlink($info['path'])) { $this->message($this->_('Deleted image variation') . " - $info[url]"); unset($variations[$name]); } else { diff --git a/wire/modules/Process/ProcessPagesExportImport/ProcessPagesExportImport.module b/wire/modules/Process/ProcessPagesExportImport/ProcessPagesExportImport.module index 4f2d4dd4..ab353b49 100644 --- a/wire/modules/Process/ProcessPagesExportImport/ProcessPagesExportImport.module +++ b/wire/modules/Process/ProcessPagesExportImport/ProcessPagesExportImport.module @@ -199,7 +199,7 @@ class ProcessPagesExportImport extends Process { if(!$zipFile || !is_file($zipFile)) throw new WireException('No ZIP file found: ' . $zipFile); $unzipPath = $this->exportImport->getExportPath('import-zip'); $zipFileItems = $this->wire('files')->unzip($zipFile, $unzipPath); - unlink($zipFile); + $this->wire('files')->unlink($zipFile); if(empty($zipFileItems)) throw new WireException("No files found in ZIP"); $jsonFile = $unzipPath . "pages.json"; $this->wire('session')->setFor($this, 'filesPath', $unzipPath); @@ -1109,7 +1109,7 @@ class ProcessPagesExportImport extends Process { 'forceDownload' => true, 'exit' => false )); - unlink($zipFile); + $this->wire('files')->unlink($zipFile); exit; } else { throw new WireException('Export failed during ZIP file generation');