security = tigerstyle_life9()->get_security(); $this->reset_stats(); } /** * Scan directory and return file information * * @param string $path Directory path to scan * @param array $options Scan options * @return array Array of file information */ public function scan_directory($path, $options = []) { $defaults = [ 'show_hidden' => false, 'max_depth' => 1, 'include_stats' => true, 'exclude_patterns' => [] ]; $options = array_merge($defaults, $options); // Validate path if (!$this->security->validate_path($path, ABSPATH)) { throw new Exception('Invalid or unsafe path'); } if (!is_dir($path) || !is_readable($path)) { throw new Exception('Directory not found or not readable'); } $files = []; $this->reset_stats(); try { $iterator = new RecursiveIteratorIterator( new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::SKIP_DOTS), RecursiveIteratorIterator::SELF_FIRST ); if ($options['max_depth'] > 0) { $iterator->setMaxDepth($options['max_depth'] - 1); } foreach ($iterator as $file) { $file_path = $file->getPathname(); // Security check for each file if (!$this->security->validate_path($file_path, ABSPATH)) { continue; } // Skip hidden files if not requested if (!$options['show_hidden'] && $this->is_hidden_file($file_path)) { continue; } // Check exclude patterns if ($this->should_exclude($file_path, $options['exclude_patterns'])) { continue; } $file_info = $this->get_file_info($file, $options['include_stats']); if ($file_info) { $files[] = $file_info; $this->update_stats($file_info); } } } catch (Exception $e) { error_log('TigerStyle Life9: File scan error - ' . $e->getMessage()); throw new Exception('File scan failed: ' . $e->getMessage()); } // Sort files: directories first, then by name usort($files, function($a, $b) { if ($a['type'] !== $b['type']) { return $a['type'] === 'directory' ? -1 : 1; } return strcasecmp($a['name'], $b['name']); }); return $files; } /** * Scan files for backup * * @param array $config Scan configuration * @return array Array of files to backup */ public function scan_files($config = []) { $defaults = [ 'include_paths' => [ABSPATH], 'exclude_patterns' => [], 'follow_symlinks' => false, 'max_file_size' => 1024 * 1024 * 1024, // 1GB 'skip_empty_files' => false ]; $config = array_merge($defaults, $config); $files = []; $this->reset_stats(); foreach ($config['include_paths'] as $path) { // Validate path if (!$this->security->validate_path($path, ABSPATH)) { error_log("TigerStyle Life9: Skipping invalid path: {$path}"); continue; } if (!file_exists($path)) { error_log("TigerStyle Life9: Path does not exist: {$path}"); continue; } if (is_file($path)) { // Single file $file_info = $this->get_file_info_from_path($path, true); if ($file_info && $this->should_include_file($file_info, $config)) { $files[] = $file_info; $this->update_stats($file_info); } } else { // Directory - recursive scan $directory_files = $this->scan_directory_recursive($path, $config); $files = array_merge($files, $directory_files); } } return $files; } /** * Recursively scan directory for backup * * @param string $path Directory path * @param array $config Scan configuration * @return array Array of files */ private function scan_directory_recursive($path, $config) { $files = []; try { $flags = RecursiveDirectoryIterator::SKIP_DOTS; if (!$config['follow_symlinks']) { $flags |= RecursiveDirectoryIterator::FOLLOW_SYMLINKS; } $iterator = new RecursiveIteratorIterator( new RecursiveDirectoryIterator($path, $flags), RecursiveIteratorIterator::SELF_FIRST ); foreach ($iterator as $file) { $file_path = $file->getPathname(); // Security validation if (!$this->security->validate_path($file_path, ABSPATH)) { continue; } // Check exclude patterns if ($this->should_exclude($file_path, $config['exclude_patterns'])) { continue; } $file_info = $this->get_file_info($file, true); if ($file_info && $this->should_include_file($file_info, $config)) { $files[] = $file_info; $this->update_stats($file_info); } } } catch (Exception $e) { error_log('TigerStyle Life9: Recursive scan error - ' . $e->getMessage()); } return $files; } /** * Get file information * * @param SplFileInfo $file File object * @param bool $include_stats Include file statistics * @return array|null File information or null if error */ private function get_file_info($file, $include_stats = true) { try { $file_path = $file->getPathname(); $file_info = [ 'name' => $file->getFilename(), 'path' => $file_path, 'type' => $file->isDir() ? 'directory' : 'file', 'size' => $file->isFile() ? $file->getSize() : 0, 'modified' => date('Y-m-d H:i:s', $file->getMTime()), 'permissions' => substr(sprintf('%o', $file->getPerms()), -4), 'readable' => $file->isReadable(), 'writable' => $file->isWritable() ]; if ($include_stats && $file->isFile()) { $file_info['mime_type'] = $this->get_mime_type($file_path); $file_info['extension'] = strtolower($file->getExtension()); } return $file_info; } catch (Exception $e) { error_log('TigerStyle Life9: Get file info error - ' . $e->getMessage()); return null; } } /** * Get file information from path * * @param string $file_path File path * @param bool $include_stats Include file statistics * @return array|null File information or null if error */ private function get_file_info_from_path($file_path, $include_stats = true) { try { if (!file_exists($file_path)) { return null; } $file_info = [ 'name' => basename($file_path), 'path' => $file_path, 'type' => is_dir($file_path) ? 'directory' : 'file', 'size' => is_file($file_path) ? filesize($file_path) : 0, 'modified' => date('Y-m-d H:i:s', filemtime($file_path)), 'permissions' => substr(sprintf('%o', fileperms($file_path)), -4), 'readable' => is_readable($file_path), 'writable' => is_writable($file_path) ]; if ($include_stats && is_file($file_path)) { $file_info['mime_type'] = $this->get_mime_type($file_path); $file_info['extension'] = strtolower(pathinfo($file_path, PATHINFO_EXTENSION)); } return $file_info; } catch (Exception $e) { error_log('TigerStyle Life9: Get file info from path error - ' . $e->getMessage()); return null; } } /** * Check if file should be excluded * * @param string $file_path File path * @param array $exclude_patterns Exclude patterns * @return bool True if file should be excluded */ private function should_exclude($file_path, $exclude_patterns) { if (empty($exclude_patterns)) { return false; } $relative_path = str_replace(ABSPATH, '', $file_path); $filename = basename($file_path); foreach ($exclude_patterns as $pattern) { // Validate pattern for security $validator = new TigerStyle_Life9_Validator(); if (!$validator->validate_exclude_pattern($pattern)) { continue; } // Convert glob pattern to regex if needed if (strpos($pattern, '*') !== false || strpos($pattern, '?') !== false) { $regex_pattern = $this->glob_to_regex($pattern); if (preg_match($regex_pattern, $relative_path) || preg_match($regex_pattern, $filename)) { return true; } } else { // Exact match or substring match if (strpos($relative_path, $pattern) !== false || strpos($filename, $pattern) !== false) { return true; } } } return false; } /** * Check if file should be included in backup * * @param array $file_info File information * @param array $config Scan configuration * @return bool True if file should be included */ private function should_include_file($file_info, $config) { // Skip empty files if configured if ($config['skip_empty_files'] && $file_info['size'] === 0 && $file_info['type'] === 'file') { return false; } // Check file size limit if ($file_info['size'] > $config['max_file_size']) { return false; } // Skip unreadable files if (!$file_info['readable']) { return false; } // Skip system files and temporary files $system_patterns = [ '/proc/', '/sys/', '/dev/', '/tmp/', '.tmp', '~$', '.swp', '.lock' ]; foreach ($system_patterns as $pattern) { if (strpos($file_info['path'], $pattern) !== false) { return false; } } return true; } /** * Check if file is hidden * * @param string $file_path File path * @return bool True if file is hidden */ private function is_hidden_file($file_path) { $filename = basename($file_path); // Unix hidden files (start with .) if (strpos($filename, '.') === 0 && $filename !== '.' && $filename !== '..') { return true; } // Windows hidden files (check attributes if on Windows) if (PHP_OS_FAMILY === 'Windows' && file_exists($file_path)) { $attrs = fileperms($file_path); return ($attrs & 0x02) !== 0; // FILE_ATTRIBUTE_HIDDEN } return false; } /** * Get MIME type of file * * @param string $file_path File path * @return string MIME type */ private function get_mime_type($file_path) { if (function_exists('finfo_file')) { $finfo = finfo_open(FILEINFO_MIME_TYPE); $mime_type = finfo_file($finfo, $file_path); finfo_close($finfo); if ($mime_type) { return $mime_type; } } if (function_exists('mime_content_type')) { $mime_type = mime_content_type($file_path); if ($mime_type) { return $mime_type; } } // Fallback based on extension $extension = strtolower(pathinfo($file_path, PATHINFO_EXTENSION)); $mime_types = [ 'txt' => 'text/plain', 'php' => 'application/x-php', 'html' => 'text/html', 'css' => 'text/css', 'js' => 'application/javascript', 'json' => 'application/json', 'xml' => 'text/xml', 'jpg' => 'image/jpeg', 'jpeg' => 'image/jpeg', 'png' => 'image/png', 'gif' => 'image/gif', 'pdf' => 'application/pdf', 'zip' => 'application/zip' ]; return $mime_types[$extension] ?? 'application/octet-stream'; } /** * Convert glob pattern to regex * * @param string $pattern Glob pattern * @return string Regex pattern */ private function glob_to_regex($pattern) { $regex = preg_quote($pattern, '/'); // Replace glob wildcards with regex equivalents $regex = str_replace('\*', '.*', $regex); $regex = str_replace('\?', '.', $regex); return '/^' . $regex . '$/i'; } /** * Reset scan statistics */ private function reset_stats() { $this->stats = [ 'total_files' => 0, 'total_directories' => 0, 'total_size' => 0, 'largest_file' => ['size' => 0, 'path' => ''], 'file_types' => [] ]; } /** * Update scan statistics * * @param array $file_info File information */ private function update_stats($file_info) { if ($file_info['type'] === 'file') { $this->stats['total_files']++; $this->stats['total_size'] += $file_info['size']; if ($file_info['size'] > $this->stats['largest_file']['size']) { $this->stats['largest_file'] = [ 'size' => $file_info['size'], 'path' => $file_info['path'] ]; } if (isset($file_info['extension'])) { $ext = $file_info['extension']; $this->stats['file_types'][$ext] = ($this->stats['file_types'][$ext] ?? 0) + 1; } } else { $this->stats['total_directories']++; } } /** * Get scan statistics * * @return array Scan statistics */ public function get_stats() { return $this->stats; } /** * Get disk usage for path * * @param string $path Directory path * @return array Disk usage information */ public function get_disk_usage($path) { if (!$this->security->validate_path($path, ABSPATH)) { throw new Exception('Invalid path'); } $total_size = 0; $file_count = 0; $dir_count = 0; try { $iterator = new RecursiveIteratorIterator( new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::SKIP_DOTS) ); foreach ($iterator as $file) { if ($file->isFile()) { $total_size += $file->getSize(); $file_count++; } else { $dir_count++; } } } catch (Exception $e) { error_log('TigerStyle Life9: Disk usage calculation error - ' . $e->getMessage()); } return [ 'total_size' => $total_size, 'file_count' => $file_count, 'directory_count' => $dir_count, 'formatted_size' => $this->format_bytes($total_size) ]; } /** * Format bytes to human readable format * * @param int $bytes Number of bytes * @return string Formatted size */ private function format_bytes($bytes) { $units = ['B', 'KB', 'MB', 'GB', 'TB']; for ($i = 0; $bytes > 1024; $i++) { $bytes /= 1024; } return round($bytes, 2) . ' ' . $units[$i]; } }