chmodDir`, and it can create directories recursively. Meaning, if you want to create directory /a/b/c/ * and directory /a/ doesn't yet exist, this method will take care of creating /a/, /a/b/, and /a/b/c/. * * The `$recursive` and `$chmod` arguments may optionally be swapped (since 3.0.34). * * ~~~~~ * // Create a new directory in ProcessWire's cache dir * if($files->mkdir($config->paths->cache . 'foo-bar/')) { * // 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" * If omitted, then ProcessWire's `$config->chmodDir` setting is used instead. * @return bool True on success, false on failure * */ public function mkdir($path, $recursive = false, $chmod = null) { if(!strlen($path)) return false; if(is_string($recursive) && strlen($recursive) > 2) { // chmod argument specified as $recursive argument or arguments swapped $_chmod = $recursive; $recursive = is_bool($chmod) ? $chmod : false; $chmod = $_chmod; } if(!is_dir($path)) { if($recursive) { $parentPath = substr($path, 0, strrpos(rtrim($path, '/'), '/')); if(!is_dir($parentPath) && !$this->mkdir($parentPath, true, $chmod)) return false; } if(!@mkdir($path)) { return $this->filesError(__FUNCTION__, "Unable to mkdir $path"); } } $this->chmod($path, false, $chmod); return true; } /** * Remove a directory and optionally everything within it (recursively) * * Unlike PHP's `rmdir()` function, this method provides a recursive option, which can be enabled by specifying true * 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); * * // 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, $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; // 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 == '..' || strpos($file, '..') !== false) continue; $pathname = rtrim($path, '/') . '/' . $file; if(is_dir($pathname)) { $this->rmdir($pathname, $recursive, $options); } else { $this->unlink($pathname, $options['limitPath'], $options['throw']); } } } if(@rmdir($path)) { return true; } else { return $this->filesError(__FUNCTION__, "Unable to rmdir: $path", $options); } } /** * Change the read/write mode of a file or directory, consistent with ProcessWire's configuration settings * * Unless a specific mode is provided via the `$chmod` argument, this method uses the `$config->chmodDir` * and `$config->chmodFile` settings in /site/config.php. * * This method also provides the option of going recursive, adjusting the read/write mode for an entire * file/directory tree at once. * * The `$recursive` or `$chmod` arguments may be optionally swapped in order (since 3.0.34). * * ~~~~~ * // 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, * you may override it by specifying it here. Ignored otherwise. Format should be a string, like "0755". * @return bool Returns true if all changes were successful, or false if at least one chmod failed. * @throws WireException when it receives incorrect chmod format * */ public function chmod($path, $recursive = false, $chmod = null) { if(is_string($recursive) && strlen($recursive) > 2) { // chmod argument specified as $recursive argument or arguments swapped $_chmod = $recursive; $recursive = is_bool($chmod) ? $chmod : false; $chmod = $_chmod; } if(is_null($chmod)) { // default: pull values from PW config $chmodFile = $this->wire()->config->chmodFile; $chmodDir = $this->wire()->config->chmodDir; } else { // optional, manually specified string if(!is_string($chmod)) { $this->filesException(__FUNCTION__, "chmod must be specified as a string like '0755'"); } $chmodFile = $chmod; $chmodDir = $chmod; } $numFails = 0; if(is_dir($path)) { // $path is a directory if($chmodDir) if(!@chmod($path, octdec($chmodDir))) $numFails++; // change mode of files in directory, if recursive if($recursive) foreach(new \DirectoryIterator($path) as $file) { if($file->isDot()) continue; $mod = $file->isDir() ? $chmodDir : $chmodFile; if($mod) if(!@chmod($file->getPathname(), octdec($mod))) $numFails++; if($file->isDir()) { if(!$this->chmod($file->getPathname(), true, $chmod)) $numFails++; } } } else { // $path is a file $mod = $chmodFile; if($mod) if(!@chmod($path, octdec($mod))) $numFails++; } return $numFails == 0; } /** * Copy all files recursively from one directory ($src) to another directory ($dst) * * Unlike PHP's `copy()` function, this method performs a recursive copy by default, * ensuring that all files and directories in the source ($src) directory are duplicated * in the destination ($dst) directory. * * This method can also be used to copy single files. If a file is specified for $src, and * only a path is specified for $dst, then the original filename will be retained in $dst. * * ~~~~~ * // Copy everything from /site/assets/cache/foo/ to /site/assets/cache/bar/ * $copyFrom = $config->paths->cache . "foo/"; * $copyTo = $config->paths->cache . "bar/"; * $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` (bool): Whether to copy directories within recursively. (default=true) * - `allowEmptyDirs` (boolean): Copy directories even if they are empty? (default=true) * - `limitPath` (bool|string|array): Limit copy to within path given here, or true for site assets path. * The limitPath option requires core 3.0.118+. (default=false). * - `hidden` (bool): Also copy hidden files/directories within given $src directory? (applies only if $src is dir) * The hidden option requires core 3.0.180+. (default=true) * - 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()) { $defaults = array( 'recursive' => true, 'hidden' => 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 if(!file_exists($src)) return false; if(is_dir($dst)) { // if only a directory was specified for $dst, then keep same filename but in new dir $dir = rtrim($dst, '/'); $dst .= '/' . basename($src); } else { $dir = dirname($dst); } if(!is_dir($dir)) $this->mkdir($dir); if(!copy($src, $dst)) return false; $this->chmod($dst); return true; } if(substr($src, -1) != '/') $src .= '/'; if(substr($dst, -1) != '/') $dst .= '/'; $dir = opendir($src); if(!$dir) return false; if(!$options['allowEmptyDirs']) { $isEmpty = true; while(false !== ($file = readdir($dir))) { if($file == '.' || $file == '..') continue; if(!$options['hidden'] && strpos(basename($file), '.') === 0) continue; $isEmpty = false; break; } if($isEmpty) return true; } if(!$this->mkdir($dst)) return false; while(false !== ($file = readdir($dir))) { if($file == '.' || $file == '..') continue; if(!$options['hidden'] && strpos(basename($file), '.') === 0) continue; $isDir = is_dir($src . $file); if($options['recursive'] && $isDir) { $this->copy($src . $file, $dst . $file, $options); } else if($isDir) { // skip it, because not recursive } else { copy($src . $file, $dst . $file); $this->chmod($dst . $file); } } closedir($dir); 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 be limited to unlink files only 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 $this->filesError(__FUNCTION__, "Given filename is not a file or link: $filename"); } if(@unlink($filename)) { return true; } else { return $this->filesError(__FUNCTION__, "Unable to unlink file: $filename", $throw); } } /** * 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) * - `copy` (bool): Use copy-then-delete method rather than a file system rename. (default=false) 3.0.178+ * - `retry` (bool): Retry with 'copy' method if regular rename files, applies only if copy option is false. (default=true) 3.0.178+ * - 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, 'copy' => false, 'retry' => 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) { return $this->filesError(__FUNCTION__, '$oldName path invalid: ' . $e->getMessage(), $options); } try { $this->allowPath($newName, $options['limitPath'], true); } catch(\Exception $e) { return $this->filesError(__FUNCTION__, 'Rename $newName path invalid: ' . $e->getMessage(), $options); } if(!file_exists($oldName)) { return $this->filesError(__FUNCTION__, 'Given pathname ($oldName) that does not exist: ' . $oldName, $options); } if(file_exists($newName)) { return $this->filesError(__FUNCTION__, 'Rename to pathname ($newName) that already exists: ' . $newName, $options); } if($options['copy']) { // will use recursive copy method only $success = false; } else if($options['retry']) { // consider any error/warnings from rename a non-event since we can retry $success = @rename($oldName, $newName); } else { // use real rename only $success = rename($oldName, $newName); } if(!$success && ($options['retry'] || $options['copy'])) { $opt = array( 'limitPath' => $options['limitPath'], 'throw' => $options['throw'], ); if($this->copy($oldName, $newName, $opt)) { $success = true; if(is_dir($oldName)) { if(!$this->rmdir($oldName, true, $opt)) { $this->filesError(__FUNCTION__, 'Unable to rmdir source ($oldName): ' . $oldName); } } else { if(!$this->unlink($oldName, $opt['limitPath'], $opt['throw'])) { $this->filesError(__FUNCTION__, 'Unable to unlink source ($oldName): ' . $oldName); } } } } if($success) { if($options['chmod']) $this->chmod($newName); } else { $this->filesError(__FUNCTION__, "Failed: $oldName => $newName", $options); } return $success; } /** * Rename by first copying files to destination and then deleting source files * * The operation is considered successful so long as the source files were able to be copied to the destination. * If source files cannot be deleted afterwards, the warning is logged, plus a warning notice is also shown when in debug mode. * * @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 $options See options for rename() method * @return bool * @throws WireException * @since 3.0.178 * */ public function renameCopy($oldName, $newName, $options = array()) { $options['copy'] = true; return $this->rename($oldName, $newName, $options); } /** * Does the given file/link/dir exist? * * Thie method accepts an `$options` argument that can be specified as an array * or a string (space or comma separated). The examples here demonstrate usage as * a string since it is the simplest for readability. * * - This function may return false for symlinks pointing to non-existing * files, unless you specify `link` as the `type`. * - Specifying `false` for the `readable` or `writable` argument disables the * option from being used, it doesn’t perform a NOT condition. * - The `writable` option may also be written as `writeable`, if preferred. * * ~~~~~ * // 1. check if exists * $exists = $files->exists('/path/file.ext'); * * // 2. check if exists and is readable (or writable) * $exists = $files->exists('/path/file.ext', 'readable'); * $exists = $files->exists('/path/file.ext', 'writable'); * * // 3. check if exists and is file, link or dir * $exists = $files->exists('/path/file.ext', 'file'); * $exists = $files->exists('/path/file.ext', 'link'); * $exists = $files->exists('/path/file.ext', 'dir'); * * // 4. check if exists and is writable file or dir * $exists = $files->exists('/path/file.ext', 'writable file'); * $exists = $files->exists('/path/dir/', 'writable dir'); * * // 5. check if exists and is readable and writable file * $exists = $files->exists('/path/file.ext', 'readable writable file'); * ~~~~~ * * @param string $filename * @param array|string $options Can be specified as array or string: * - `type` (string): Verify it is of type: 'file', 'link', 'dir' (default='') * - `readable` (bool): Verify it is readable? (default=false) * - `writable` (bool): Also verify the file is writable? (default=false) * - `writeable` (bool): Alias of writable (default=false) * - When specified as string, you can use any combination of the words: * `readable, writable, file, link, dir` (separated by space or comma). * @return bool * @throws WireException if given invalid or unrecognized $options * @since 3.0.180 * * */ public function exists($filename, $options = '') { $defaults = array( 'type' => '', 'readable' => false, 'writable' => false, 'writeable' => false, // alias of writable ); if($options === '') { $options = $defaults; } else if(is_array($options)) { $options = array_merge($defaults, $options); if(!empty($options['type'])) $options['type'] = strtolower(trim($options['type'])); } else if(is_string($options)) { $types = array('file', 'link', 'dir'); if(strpos($options, ',') !== false) $options = str_replace(',', ' ', $options); foreach(explode(' ', $options) as $option) { $option = strtolower(trim($option)); if(empty($option)) continue; if(isset($defaults[$option])) { // readable, writable $defaults[$option] = true; } else if(in_array($option, $types, true)) { // file, dir, link if(empty($defaults['type'])) $defaults['type'] = $option; } else { throw new WireException("Unrecognized option: $option"); } } $options = $defaults; } else { throw new WireException('Invalid $options argument'); } if($options['readable'] && !is_readable($filename)) { $exists = false; } else if(($options['writable'] || $options['writeable']) && !is_writable($filename)) { $exists = false; } else if($options['type'] === '') { $exists = $options['readable'] ? true : file_exists($filename); } else if($options['type'] === 'file') { $exists = is_file($filename); } else if($options['type'] === 'link') { $exists = is_link($filename); } else if($options['type'] === 'dir') { $exists = is_dir($filename); } else { throw new WireException("Unrecognized 'type' option: $options[type]"); } return $exists; } /** * 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) { $this->filesError(__FUNCTION__, "Given pathname is not within any of the paths allowed by limitPath", $throw); } 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) return $this->filesError(__FUNCTION__, "Invalid type for limitPath argument", $throw); } else if(!is_dir($limitPath)) { return $this->filesError(__FUNCTION__, "$limitPath (limitPath) does not exist", $throw); } if($limitPath !== false) try { // if limitPath can't pass allowPath then neither can $pathname $this->allowPath($limitPath, false, true); } catch(\Exception $e) { return $this->filesError(__FUNCTION__, "Validating limitPath reported: " . $e->getMessage(), $throw, $e); } 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 return $this->filesError(__FUNCTION__, "pathname not allowed: $pathname", $throw); } if(strpos($pathname, '..') !== false) { // not allowed to traverse anywhere return $this->filesError(__FUNCTION__, 'pathname may not traverse “../”', $throw); } if(strpos($pathname, '.') === 0 || empty($pathname)) { return $this->filesError(__FUNCTION__, 'pathname may not begin with “.”', $throw); } $pos = strpos($pathname, '//'); if($pos !== false && $pos !== strpos($this->wire('config')->paths->assets, '//')) { // URLs or accidental extra slashes not allowed, unless they also appear in a known safe system path return $this->filesError(__FUNCTION__, 'pathname may not contain double slash “//”', $throw); } if($limitPath !== false && strpos($pathname, $limitPath) !== 0) { // disallow paths that do not begin with limitPath (i.e. /path/to/public_html/site/assets/) return $this->filesError(__FUNCTION__, "Given pathname is not within $limitPath (limitPath)", $throw); } return true; } /** * Return a new temporary directory/path ready to use for files * * - The temporary directory will be automatically removed at the end of the request. * - Temporary directories are not http accessible. * - If you call this with the same non-empty `$name` argument more than once in the * same request, the same `WireTempDir` instance will be returned. * * #pw-advanced * * ~~~~~ * $tempDir = $files->tempDir(); * $path = $tempDir->get(); * file_put_contents($path . 'some-file.txt', 'Hello world'); * ~~~~~ * * @param Object|string $name Any one of the following: (default='') * - Omit this argument for auto-generated name, 3.0.178+ * - Name/word that you specify using fieldName format, i.e. [_a-zA-Z0-9]. * - Object instance that needs the temp dir. * @param array|int $options Deprecated argument. Call `WireTempDir` methods if you need more options. * @return WireTempDir Returns a WireTempDir instance. If you typecast return value to a string, * it is the temp dir path (with trailing slash). * @see WireTempDir * */ public function tempDir($name = '', $options = array()) { static $tempDirs = array(); if($name && isset($tempDirs[$name])) return $tempDirs[$name]; if(is_int($options)) $options = array('maxAge' => $options); $basePath = isset($options['basePath']) ? $options['basePath'] : ''; $tempDir = new WireTempDir(); $this->wire($tempDir); if(isset($options['remove']) && $options['remove'] === false) $tempDir->setRemove(false); $tempDir->init($name, $basePath); if(isset($options['maxAge'])) $tempDir->setMaxAge($options['maxAge']); if($name) $tempDirs[$name] = $tempDir; return $tempDir; } /** * Find all files in the given $path recursively, and return a flat array of all found filenames * * @param string $path Path to start from (required). * @param array $options Options to affect what is returned (optional): * - `recursive` (int|bool): How many levels of subdirectories this method should descend into beyond the 1 given. * Specify 1 to remain at the one directory level given, or 2+ to descend into subdirectories. (default=10) * In 3.0.180+ you may also specify true for no limit, or false to disable descending into any subdirectories. * - `extensions` (array|string): Only include files having these extensions, or omit to include all (default=[]). * In 3.0.180+ the extensions argument may also be a string (space or comma separated). * - `excludeDirNames` (array): Do not descend into directories having these names (default=[]). * - `excludeHidden` (bool): Exclude hidden files? (default=false). * - `allowDirs` (bool): Allow directories in returned files (except for '.' and '..')? Note that returned * directories have a trailing slash. (default=false) 3.0.180+ * - `returnRelative` (bool): Make returned array have filenames relative to given start $path? (default=false) * @return array Flat array of filenames * @since 3.0.96 * */ public function find($path, array $options = array()) { $defaults = array( 'recursive' => 10, 'extensions' => array(), 'excludeExtensions' => array(), 'excludeDirNames' => array(), 'excludeHidden' => false, 'allowDirs' => false, 'returnRelative' => false, ); $path = $this->unixDirName($path); if(!is_dir($path) || !is_readable($path)) return array(); $options = array_merge($defaults, $options); if(empty($options['_level'])) { // this is a non-recursive call $options['_startPath'] = $path; $options['_level'] = 0; if(!is_array($options['extensions'])) { if($options['extensions']) { $options['extensions'] = preg_replace('/[,;\.\s]+/', ' ', $options['extensions']); $options['extensions'] = explode(' ', $options['extensions']); } else { $options['extensions'] = array(); } } foreach($options['extensions'] as $k => $v) { $options['extensions'][$k] = strtolower(trim($v)); } } $options['_level']++; if($options['recursive'] && $options['recursive'] !== true) { if($options['_level'] > $options['recursive']) return array(); } $dirs = array(); $files = array(); foreach(new \DirectoryIterator($path) as $file) { if($file->isDot()) continue; $basename = $file->getBasename(); $ext = strtolower($file->getExtension()); if($file->isDir()) { $dir = $this->unixDirName($file->getPathname()); if($options['allowDirs']) { if($options['returnRelative'] && strpos($dir, $options['_startPath']) === 0) { $dir = substr($dir, strlen($options['_startPath'])); } $files[$dir] = $dir; } if($options['recursive'] === false || $options['recursive'] < 1) continue; if(!in_array($basename, $options['excludeDirNames'])) $dirs[$dir] = $file->getPathname(); continue; } if($options['excludeHidden'] && strpos($basename, '.') === 0) continue; if(!empty($options['extensions']) && !in_array($ext, $options['extensions'])) continue; if(!empty($options['excludeExtensions']) && in_array($ext, $options['excludeExtensions'])) continue; $filename = $this->unixFileName($file->getPathname()); if($options['returnRelative'] && strpos($filename, $options['_startPath']) === 0) { $filename = substr($filename, strlen($options['_startPath'])); } $files[] = $filename; } foreach($dirs as $key => $dir) { $_files = $this->find($dir, $options); if(count($_files)) { foreach($_files as $name) { $files[] = $name; } } else { // no files found in directory if($options['allowDirs'] && count($options['extensions']) && isset($files[$key])) { // remove directory if it didn't match any files having requested extension unset($files[$key]); } } } $options['_level']--; if(!$options['_level']) sort($files); return $files; } /** * Unzips the given ZIP file to the destination directory * * ~~~~~ * // Unzip a file * $zip = $config->paths->cache . "my-file.zip"; * $dst = $config->paths->cache . "my-files/"; * $items = $files->unzip($zip, $dst); * if(count($items)) { * // $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. * @return array Returns an array of filenames (excluding $dst) that were unzipped. * @throws WireException All error conditions result in WireException being thrown. * @see WireFileTools::zip() * */ public function unzip($file, $dst) { $dst = rtrim($dst, '/' . DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; if(!class_exists('\ZipArchive')) $this->filesException(__FUNCTION__, "PHP's ZipArchive class does not exist"); if(!is_file($file)) $this->filesException(__FUNCTION__, "ZIP file does not exist"); if(!is_dir($dst)) $this->mkdir($dst, true); $names = array(); $chmodFile = $this->wire()->config->chmodFile; $chmodDir = $this->wire()->config->chmodDir; $zip = new \ZipArchive(); $res = $zip->open($file); if($res !== true) $this->filesException(__FUNCTION__, "Unable to open ZIP file, error code: $res"); for($i = 0; $i < $zip->numFiles; $i++) { $name = $zip->getNameIndex($i); if(strpos($name, '..') !== false) continue; if($zip->extractTo($dst, $name)) { $names[$i] = $name; $filename = $dst . ltrim($name, '/'); if(is_dir($filename)) { if($chmodDir) chmod($filename, octdec($chmodDir)); } else if(is_file($filename)) { if($chmodFile) chmod($filename, octdec($chmodFile)); } } } $zip->close(); return $names; } /** * Creates a ZIP file * * ~~~~~ * // Create zip of all files in directory $dir to file $zip * $dir = $config->paths->cache . "my-files/"; * $zip = $config->paths->cache . "my-file.zip"; * $result = $files->zip($zip, $dir); * * echo "