alias = $alias; $this->path = $path; } /** * Execute the job. * * @return string */ public function handle() { if (empty($this->path)) { throw new \Exception(trans('modules.errors.file_copy', ['module' => $this->alias])); } $source = storage_path('app/temp/' . $this->path); // Prevent path traversal: ensure source is strictly within the temp directory $temp_base = realpath(storage_path('app/temp')); $real_source = realpath($source); if (! $this->isPathWithinTempBase($real_source, $temp_base)) { throw new \Exception(trans('modules.errors.file_copy', ['module' => $this->alias])); } $destination = $this->getDestination($source); // Move all files/folders from temp path if (!File::copyDirectory($source, $destination)) { throw new \Exception(trans('modules.errors.file_copy', ['module' => $this->alias])); } // Delete temp directory File::deleteDirectory($source); } protected function isPathWithinTempBase($real_source, $temp_base) { if (($real_source === false) || ($temp_base === false)) { return false; } $normalized_source = str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $real_source); $normalized_base = str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $temp_base); if (DIRECTORY_SEPARATOR === '\\') { $normalized_source = mb_strtolower($normalized_source); $normalized_base = mb_strtolower($normalized_base); } return str_starts_with(rtrim($normalized_source, DIRECTORY_SEPARATOR), rtrim($normalized_base, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR); } protected function getDestination($source) { if ($this->alias == 'core') { return base_path(); } if (!is_file($source . '/module.json')) { throw new \Exception(trans('modules.errors.file_copy', ['module' => $this->alias])); } $modules_path = config('module.paths.modules'); // Create modules directory if (!File::isDirectory($modules_path)) { File::makeDirectory($modules_path); } $module_path = $modules_path . '/' . Str::studly($this->alias); // Create module directory if (!File::isDirectory($module_path)) { File::makeDirectory($module_path); } return $module_path; } }