| @@ -1,16 +1,15 @@ | ||
| 1 | 1 | <?php |
| 2 | - | |
| 3 | 2 | if (!defined('ABSPATH')) exit; |
| 4 | -if (!class_exists('BVFSCallback')) : | |
| 3 | +if (!class_exists('WPRFSCallback')) : | |
| 5 | 4 | require_once dirname( __FILE__ ) . '/../streams.php'; |
| 6 | 5 | |
| 7 | -class BVFSCallback extends BVCallbackBase { | |
| 6 | +class WPRFSCallback extends WPRCallbackBase { | |
| 8 | 7 | public $stream; |
| 9 | 8 | public $account; |
| 10 | 9 | |
| 11 | 10 | public static $cwAllowedFiles = array(".htaccess", ".user.ini", "malcare-waf.php"); |
| 12 | - const FS_WING_VERSION = 1.2; | |
| 11 | + const FS_WING_VERSION = 1.4; | |
| 13 | 12 | |
| 14 | 13 | public function __construct($callback_handler) { |
| 15 | 14 | $this->account = $callback_handler->account; |
| 16 | 15 | } |
| @@ -18,8 +17,15 @@ | ||
| 18 | 17 | function fileStat($relfile, $md5 = false) { |
| 19 | 18 | $absfile = ABSPATH.$relfile; |
| 20 | 19 | $fdata = array(); |
| 21 | 20 | $fdata["filename"] = $relfile; |
| 21 | + | |
| 22 | + if (@is_readable($absfile) === false) { | |
| 23 | + $fdata["failed"] = true; | |
| 24 | + $fdata["error"] = "NOT_READABLE"; | |
| 25 | + return $fdata; | |
| 26 | + } | |
| 27 | + | |
| 22 | 28 | $stats = @stat($absfile); |
| 23 | 29 | if ($stats) { |
| 24 | 30 | foreach (preg_grep('#size|uid|gid|mode|mtime#i', array_keys($stats)) as $key ) { |
| 25 | 31 | $fdata[$key] = $stats[$key]; |
| @@ -133,8 +139,105 @@ | ||
| 133 | 139 | |
| 134 | 140 | return $links; |
| 135 | 141 | } |
| 136 | 142 | |
| 143 | + function getDirectoryPath($dir, $traversal_stack) { | |
| 144 | + $base_path = rtrim($dir, '/'); | |
| 145 | + $sub_path = empty($traversal_stack) ? '' : '/' . implode('/', array_column($traversal_stack, 0)); | |
| 146 | + return $base_path . $sub_path . '/'; | |
| 147 | + } | |
| 148 | + | |
| 149 | + function seekDirectoryHandle($directory_handle, $offset) { | |
| 150 | + while ($offset > 0 && ($file = @readdir($directory_handle)) !== false) { | |
| 151 | + if ($file === "." || $file === "..") continue; | |
| 152 | + $offset--; | |
| 153 | + } | |
| 154 | + } | |
| 155 | + | |
| 156 | + function scanFilesDfs($dir = "/", $traversal_stack = array(), $folder_offset = 0, $limit = 0, $traversal_stack_max_size = 100, | |
| 157 | + $batch_size = 512, $is_recursive = true, $include_md5 = false) { | |
| 158 | + $links = []; | |
| 159 | + $batch_count = 0; | |
| 160 | + $batch_files = []; | |
| 161 | + $count = 0; | |
| 162 | + $traversal_stack_max_size_reached_count = 0; | |
| 163 | + | |
| 164 | + $base_path = $this->getDirectoryPath($dir, $traversal_stack); | |
| 165 | + $directory_handle = @opendir(ABSPATH . $base_path); | |
| 166 | + | |
| 167 | + $this->seekDirectoryHandle($directory_handle, $folder_offset); | |
| 168 | + | |
| 169 | + while ($limit == 0 || ($limit > 0 && $count < $limit)) { | |
| 170 | + if (($file = @readdir($directory_handle)) !== false) { | |
| 171 | + if ($file === "." || $file === "..") continue; | |
| 172 | + | |
| 173 | + $relative_path = $base_path . $file; | |
| 174 | + $absolute_path = ABSPATH . $relative_path; | |
| 175 | + | |
| 176 | + $count++; | |
| 177 | + $folder_offset++; | |
| 178 | + | |
| 179 | + $batch_files[] = $this->fileStat($relative_path, $include_md5); | |
| 180 | + $batch_count++; | |
| 181 | + | |
| 182 | + if ($batch_count >= $batch_size) { | |
| 183 | + $this->stream->writeStream(serialize($batch_files)); | |
| 184 | + $batch_count = 0; | |
| 185 | + $batch_files = []; | |
| 186 | + } | |
| 187 | + | |
| 188 | + if (is_link($absolute_path)) { | |
| 189 | + $links[] = $relative_path; | |
| 190 | + } elseif ($is_recursive && is_dir($absolute_path)) { | |
| 191 | + if (count($traversal_stack) >= $traversal_stack_max_size) { | |
| 192 | + $traversal_stack_max_size_reached_count += 1; | |
| 193 | + continue; | |
| 194 | + } | |
| 195 | + | |
| 196 | + closedir($directory_handle); | |
| 197 | + | |
| 198 | + array_push($traversal_stack, [$file, $folder_offset]); | |
| 199 | + $base_path = $this->getDirectoryPath($dir, $traversal_stack); | |
| 200 | + | |
| 201 | + $directory_handle = @opendir(ABSPATH . $base_path); | |
| 202 | + $folder_offset = 0; | |
| 203 | + } | |
| 204 | + | |
| 205 | + continue; | |
| 206 | + } | |
| 207 | + | |
| 208 | + if ($directory_handle !== false) { | |
| 209 | + closedir($directory_handle); | |
| 210 | + } | |
| 211 | + | |
| 212 | + if (empty($traversal_stack)) { | |
| 213 | + break; | |
| 214 | + } | |
| 215 | + $current_info = array_pop($traversal_stack); | |
| 216 | + | |
| 217 | + $base_path = $this->getDirectoryPath($dir, $traversal_stack); | |
| 218 | + $directory_handle = @opendir(ABSPATH . $base_path); | |
| 219 | + | |
| 220 | + if ($directory_handle === false) { | |
| 221 | + continue; | |
| 222 | + } | |
| 223 | + | |
| 224 | + $this->seekDirectoryHandle($directory_handle, $current_info[1]); | |
| 225 | + $folder_offset = $current_info[1]; | |
| 226 | + } | |
| 227 | + | |
| 228 | + if ($batch_count > 0) { | |
| 229 | + $this->stream->writeStream(serialize($batch_files)); | |
| 230 | + } | |
| 231 | + | |
| 232 | + return [ | |
| 233 | + 'links' => $links, | |
| 234 | + 'traversal_stack' => $traversal_stack, | |
| 235 | + 'folder_offset' => $folder_offset, | |
| 236 | + 'traversal_stack_max_size_reached_count' => $traversal_stack_max_size_reached_count | |
| 237 | + ]; | |
| 238 | + } | |
| 239 | + | |
| 137 | 240 | function calculateMd5($absfile, $fdata, $offset, $limit, $bsize) { |
| 138 | 241 | if ($offset == 0 && $limit == 0) { |
| 139 | 242 | $md5 = md5_file($absfile); |
| 140 | 243 | } else { |
| @@ -141,8 +244,9 @@ | ||
| 141 | 244 | if ($limit == 0) |
| 142 | 245 | $limit = $fdata["size"]; |
| 143 | 246 | if ($offset + $limit < $fdata["size"]) |
| 144 | 247 | $limit = $fdata["size"] - $offset; |
| 248 | + // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen | |
| 145 | 249 | $handle = fopen($absfile, "rb"); |
| 146 | 250 | $ctx = hash_init('md5'); |
| 147 | 251 | fseek($handle, $offset, SEEK_SET); |
| 148 | 252 | $dlen = 1; |
| @@ -148,13 +252,15 @@ | ||
| 148 | 252 | $dlen = 1; |
| 149 | 253 | while (($limit > 0) && ($dlen > 0)) { |
| 150 | 254 | if ($bsize > $limit) |
| 151 | 255 | $bsize = $limit; |
| 256 | + // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fread -- Required for handling partial file reads with offset and limit | |
| 152 | 257 | $d = fread($handle, $bsize); |
| 153 | 258 | $dlen = strlen($d); |
| 154 | 259 | hash_update($ctx, $d); |
| 155 | 260 | $limit -= $dlen; |
| 156 | 261 | } |
| 262 | + // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose | |
| 157 | 263 | fclose($handle); |
| 158 | 264 | $md5 = hash_final($ctx); |
| 159 | 265 | } |
| 160 | 266 | return $md5; |
| @@ -161,20 +267,22 @@ | ||
| 161 | 267 | } |
| 162 | 268 | |
| 163 | 269 | function getFilesContent($files, $withContent = true) { |
| 164 | 270 | $result = array(); |
| 271 | + | |
| 165 | 272 | foreach ($files as $file) { |
| 166 | 273 | $fdata = $this->fileStat($file); |
| 167 | - $absfile = ABSPATH.$file; | |
| 274 | + $absfile = ABSPATH . $file; | |
| 168 | 275 | |
| 169 | - if (is_dir($absfile) && !is_link($absfile)) { | |
| 276 | + if ((WPRWPFileSystem::getInstance()->isDir($absfile) === true) && !is_link($absfile)) { | |
| 170 | 277 | $fdata['is_dir'] = true; |
| 171 | 278 | } else { |
| 172 | - if (!is_readable($absfile)) { | |
| 279 | + if (isset($fdata["error"]) && $fdata["error"] === "NOT_READABLE") { | |
| 173 | 280 | $fdata['error'] = 'file not readable'; |
| 174 | 281 | } else { |
| 175 | 282 | if ($withContent === true) { |
| 176 | - if ($content = file_get_contents($absfile)) { | |
| 283 | + $content = WPRWPFileSystem::getInstance()->getContents($absfile); | |
| 284 | + if ($content !== false) { | |
| 177 | 285 | $fdata['content'] = $content; |
| 178 | 286 | } else { |
| 179 | 287 | $fdata['error'] = 'unable to read file'; |
| 180 | 288 | } |
| @@ -181,8 +289,12 @@ | ||
| 181 | 289 | } |
| 182 | 290 | } |
| 183 | 291 | } |
| 184 | 292 | |
| 293 | + $fs_error = WPRWPFileSystem::getInstance()->checkForErrors(); | |
| 294 | + if (isset($fs_error)) { | |
| 295 | + $fdata['fs_error'] = $fs_error; | |
| 296 | + } | |
| 185 | 297 | $result[$file] = $fdata; |
| 186 | 298 | } |
| 187 | 299 | |
| 188 | 300 | return $result; |
| @@ -192,9 +304,9 @@ | ||
| 192 | 304 | $result = array(); |
| 193 | 305 | foreach ($files as $file) { |
| 194 | 306 | $fdata = $this->fileStat($file); |
| 195 | 307 | $absfile = ABSPATH.$file; |
| 196 | - if (!is_readable($absfile)) { | |
| 308 | + if (isset($fdata["error"]) && $fdata["error"] === "NOT_READABLE") { | |
| 197 | 309 | $result["missingfiles"][] = $file; |
| 198 | 310 | continue; |
| 199 | 311 | } |
| 200 | 312 | if ($md5 === true && !is_dir($absfile)) { |
| @@ -207,12 +319,13 @@ | ||
| 207 | 319 | |
| 208 | 320 | function uploadFiles($files, $offset = 0, $limit = 0, $bsize = 102400) { |
| 209 | 321 | $result = array(); |
| 210 | 322 | foreach ($files as $file) { |
| 211 | - if (!is_readable(ABSPATH.$file)) { | |
| 323 | + if (WPRWPFileSystem::getInstance()->isReadable(ABSPATH.$file) === false) { | |
| 212 | 324 | $result["missingfiles"][] = $file; |
| 213 | 325 | continue; |
| 214 | 326 | } |
| 327 | + // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen -- Required for binary-safe chunked reading | |
| 215 | 328 | $handle = fopen(ABSPATH.$file, "rb"); |
| 216 | 329 | if (($handle != null) && is_resource($handle)) { |
| 217 | 330 | $fdata = $this->fileStat($file); |
| 218 | 331 | $_limit = $limit; |
| @@ -228,13 +341,15 @@ | ||
| 228 | 341 | $dlen = 1; |
| 229 | 342 | while (($_limit > 0) && ($dlen > 0)) { |
| 230 | 343 | if ($_bsize > $_limit) |
| 231 | 344 | $_bsize = $_limit; |
| 345 | + // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fread -- Required for binary-safe chunked reading | |
| 232 | 346 | $d = fread($handle, $_bsize); |
| 233 | 347 | $dlen = strlen($d); |
| 234 | 348 | $this->stream->writeStream($d); |
| 235 | 349 | $_limit -= $dlen; |
| 236 | 350 | } |
| 351 | + // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- Required for cleanup | |
| 237 | 352 | fclose($handle); |
| 238 | 353 | } else { |
| 239 | 354 | $result["unreadablefiles"][] = $file; |
| 240 | 355 | } |
| @@ -244,9 +359,9 @@ | ||
| 244 | 359 | } |
| 245 | 360 | |
| 246 | 361 | function process($request) { |
| 247 | 362 | $params = $request->params; |
| 248 | - $stream_init_info = BVStream::startStream($this->account, $request); | |
| 363 | + $stream_init_info = WPRStream::startStream($this->account, $request); | |
| 249 | 364 | |
| 250 | 365 | if (array_key_exists('stream', $stream_init_info)) { |
| 251 | 366 | $this->stream = $stream_init_info['stream']; |
| 252 | 367 | switch ($request->method) { |
| @@ -285,8 +400,37 @@ | ||
| 285 | 400 | $_links = $this->scanFiles($dir, $offset, $limit, $bsize, $recurse, $md5); |
| 286 | 401 | $links = array_merge($links, $_links); |
| 287 | 402 | } |
| 288 | 403 | $resp = array("status" => "done", "links" => $links); |
| 404 | + break; | |
| 405 | + case "scanfilesdfs": | |
| 406 | + $resp = array(); | |
| 407 | + $dir_options = array(); | |
| 408 | + if (array_key_exists('dir_options', $params)) { | |
| 409 | + $dir_options = $params['dir_options']; | |
| 410 | + } | |
| 411 | + $bsize = intval($params['bsize']); | |
| 412 | + $traversal_stack_max_size = intval($params['traversal_stack_max_size']); | |
| 413 | + foreach($dir_options as $option) { | |
| 414 | + $dir = $option['dir']; | |
| 415 | + $traversal_stack = $option['traversal_stack']; | |
| 416 | + $folder_offset = intval($option['folder_offset']); | |
| 417 | + $limit = intval($option['limit']); | |
| 418 | + | |
| 419 | + $recurse = true; | |
| 420 | + if (array_key_exists('recurse', $option) && $option["recurse"] == "false") { | |
| 421 | + $recurse = false; | |
| 422 | + } | |
| 423 | + | |
| 424 | + $md5 = true; | |
| 425 | + if (array_key_exists('md5', $option) && $option["md5"] == "false") { | |
| 426 | + $md5 = false; | |
| 427 | + } | |
| 428 | + | |
| 429 | + $resp[$dir] = $this->scanFilesDfs($dir, $traversal_stack, $folder_offset, $limit, | |
| 430 | + $traversal_stack_max_size, $bsize, $recurse, $md5); | |
| 431 | + } | |
| 432 | + $resp["status"] = "done"; | |
| 289 | 433 | break; |
| 290 | 434 | case "getfilesstats": |
| 291 | 435 | $files = $params['files']; |
| 292 | 436 | $offset = intval($params['offset']); |