diff --git a/AGENTS.md b/AGENTS.md index 027f644..9264928 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -22,7 +22,7 @@ Packages are plain WordPress **attachments** — there is no custom post type. | Area | Files | |------|-------| -| Bootstrap | `exelearning.php` (`require_once` for every class), `includes/class-exelearning.php` (creates components; most register their own hooks), `includes/class-upgrader.php` (option migrations keyed on `exelearning_db_version`) | +| Bootstrap | `exelearning.php` (`require_once` for every class), `includes/class-exelearning.php` (creates components; most register their own hooks), `includes/class-upgrader.php` (option migrations keyed on `exelearning_db_version`), `includes/class-filesystem.php` (shared recursive delete) | | Ingestion | `includes/class-elp-upload-handler.php` (upload, extraction, delete cleanup), `includes/class-elp-file-service.php` | | Reprocessing | `includes/class-elp-reprocessor.php` (class `ExeLearning_Reprocessor`), used by REST save/reprocess, the Media Library bulk action and `wp exelearning reprocess` (`includes/class-cli-command.php`); `includes/class-content-hash-aliases.php` keeps retired hashes as redirects (ADR-68-01) | | Content delivery | `includes/class-content-proxy.php`: `GET /wp-json/exelearning/v1/content/{hash}/{file}` with security headers and CSP. A generated `.htaccess` blocks direct HTML/SVG/XML access under `uploads/exelearning/` (Apache only); the `exelearning_content_origin` filter serves content from a separate host | diff --git a/exelearning.php b/exelearning.php index 01bebc5..0c0a39b 100644 --- a/exelearning.php +++ b/exelearning.php @@ -39,6 +39,7 @@ require_once EXELEARNING_PLUGIN_DIR . 'includes/class-activator.php'; require_once EXELEARNING_PLUGIN_DIR . 'includes/class-deactivator.php'; require_once EXELEARNING_PLUGIN_DIR . 'includes/class-exelearning.php'; +require_once EXELEARNING_PLUGIN_DIR . 'includes/class-filesystem.php'; require_once EXELEARNING_PLUGIN_DIR . 'includes/class-i18n.php'; require_once EXELEARNING_PLUGIN_DIR . 'includes/class-mime-types.php'; diff --git a/includes/class-elp-file-service.php b/includes/class-elp-file-service.php index e2900b2..d9c3f2a 100644 --- a/includes/class-elp-file-service.php +++ b/includes/class-elp-file-service.php @@ -2,7 +2,7 @@ /** * ELP File Service for eXeLearning. * - * Handles validation, parsing, and extraction of .elp/.elpx files. + * Handles validation, parsing, and extraction of .elpx packages. * Replaces the external exelearning/elp-parser library with inline logic * using native PHP ZipArchive and SimpleXML. * @@ -16,7 +16,7 @@ /** * Class ExeLearning_Elp_File_Service. * - * Provides methods to validate, parse, and extract .elp files. + * Provides methods to validate, parse, and extract .elpx packages. */ class ExeLearning_Elp_File_Service { @@ -51,7 +51,7 @@ class ExeLearning_Elp_File_Service { /** * Parses an ELP file and populates version and metadata. * - * @param string $file_path Path to the .elp file. + * @param string $file_path Path to the .elpx file. * @return true|WP_Error True on success, WP_Error on failure. */ public function parse( string $file_path ) { @@ -170,7 +170,7 @@ private function extract_v3_metadata( SimpleXMLElement $xml ) { /** * Extracts the ELP file contents to a directory. * - * @param string $file_path Path to the .elp file. + * @param string $file_path Path to the .elpx file. * @param string $destination Directory to extract to. * @return true|WP_Error True on success, WP_Error on failure. */ @@ -453,9 +453,9 @@ protected function get_zip_instance(): ZipArchive { } /** - * Validates an .elp file and returns parsed data. + * Validates an .elpx package and returns parsed data. * - * @param string $file_path Path to the .elp file. + * @param string $file_path Path to the .elpx file. * @return array|WP_Error Array with parsed data or WP_Error on failure. */ public function validate_elp_file( string $file_path ) { diff --git a/includes/class-elp-reprocessor.php b/includes/class-elp-reprocessor.php index c136299..5cc43dc 100644 --- a/includes/class-elp-reprocessor.php +++ b/includes/class-elp-reprocessor.php @@ -420,7 +420,7 @@ public function cleanup_by_hash( $hash ) { $folder = trailingslashit( $upload_dir['basedir'] ) . 'exelearning/' . $hash . '/'; if ( is_dir( $folder ) ) { - ExeLearning_Styles_Service::recursive_delete( $folder ); + ExeLearning_Filesystem::recursive_delete( $folder ); } } diff --git a/includes/class-elp-upload-block.php b/includes/class-elp-upload-block.php index f87e19d..7cde2b6 100644 --- a/includes/class-elp-upload-block.php +++ b/includes/class-elp-upload-block.php @@ -1,6 +1,6 @@ validate_elp_file( $file ); @@ -113,7 +113,7 @@ public function process_elp_upload( $upload ) { if ( is_wp_error( $extract_result ) ) { // Remove any partially extracted files so a rejected upload leaves no // orphaned directory behind. - ExeLearning_Styles_Service::recursive_delete( $destination ); + ExeLearning_Filesystem::recursive_delete( $destination ); wp_delete_file( $file ); return array( 'error' => $extract_result->get_error_message() ); } @@ -152,9 +152,6 @@ public function process_elp_upload( $upload ) { 300 ); - // Optionally, remove the original .elp file. - // Example: unlink( $file ). - return $upload; } @@ -220,7 +217,7 @@ public function exelearning_delete_extracted_folder( $post_id ) { $full_path = trailingslashit( $upload_dir['basedir'] ) . 'exelearning/' . $directory . '/'; if ( is_dir( $full_path ) ) { - ExeLearning_Styles_Service::recursive_delete( $full_path ); + ExeLearning_Filesystem::recursive_delete( $full_path ); } } } diff --git a/includes/class-exelearning-editor.php b/includes/class-exelearning-editor.php index 75d9b28..b52dece 100644 --- a/includes/class-exelearning-editor.php +++ b/includes/class-exelearning-editor.php @@ -2,7 +2,7 @@ /** * Editor integration class for eXeLearning. * - * Handles the fullscreen editor modal for editing .elp files. + * Handles the fullscreen editor modal for editing .elpx packages. * * @package Exelearning */ diff --git a/includes/class-exelearning-rest-api.php b/includes/class-exelearning-rest-api.php index 567253e..b24e032 100644 --- a/includes/class-exelearning-rest-api.php +++ b/includes/class-exelearning-rest-api.php @@ -629,8 +629,8 @@ private function validate_elp_file_path( $attachment_id ) { /** * Clean up extraction directory by hash. * - * Unlike cleanup_old_extraction(), this takes a hash directly instead of - * reading it from meta (which may already be updated to the new hash). + * Takes the hash directly instead of reading it from meta, which may + * already point at the new extraction. * * @param string $hash Extraction hash to clean up. */ diff --git a/includes/class-filesystem.php b/includes/class-filesystem.php new file mode 100644 index 0000000..029bf70 --- /dev/null +++ b/includes/class-filesystem.php @@ -0,0 +1,40 @@ +assertSame( 'elp_too_large', $result->get_error_code() ); $this->assertSame( array(), glob( trailingslashit( $destination ) . '*' ) ); - ExeLearning_Styles_Service::recursive_delete( $destination ); + ExeLearning_Filesystem::recursive_delete( $destination ); } /** @@ -594,7 +594,7 @@ public function test_extract_fails_when_an_entry_cannot_be_written() { $result = $this->service->extract( $archive, trailingslashit( $destination ) ); wp_delete_file( $archive ); - ExeLearning_Styles_Service::recursive_delete( $destination ); + ExeLearning_Filesystem::recursive_delete( $destination ); $this->assertInstanceOf( 'WP_Error', $result ); $this->assertSame( 'elp_mkdir_failed', $result->get_error_code() ); @@ -616,7 +616,7 @@ public function test_extract_entry_creates_directory_entries() { $this->assertTrue( $result ); $this->assertDirectoryExists( $destination . '/media' ); - ExeLearning_Styles_Service::recursive_delete( $destination ); + ExeLearning_Filesystem::recursive_delete( $destination ); } /** diff --git a/tests/unit/ElpUploadHandlerTest.php b/tests/unit/ElpUploadHandlerTest.php index f6e6fde..a0966f1 100644 --- a/tests/unit/ElpUploadHandlerTest.php +++ b/tests/unit/ElpUploadHandlerTest.php @@ -40,7 +40,7 @@ public function set_up() { */ public function tear_down() { foreach ( $this->cleanup_paths as $path ) { - ExeLearning_Styles_Service::recursive_delete( $path ); + ExeLearning_Filesystem::recursive_delete( $path ); } $this->cleanup_paths = array(); parent::tear_down(); diff --git a/tests/unit/FilesystemTest.php b/tests/unit/FilesystemTest.php new file mode 100644 index 0000000..bb18919 --- /dev/null +++ b/tests/unit/FilesystemTest.php @@ -0,0 +1,45 @@ +assertDirectoryDoesNotExist( $missing, 'Precondition: the path must be absent.' ); + + ExeLearning_Filesystem::recursive_delete( $missing ); + + $this->assertDirectoryDoesNotExist( $missing ); + $this->assertFileExists( $bystander . '/keep.txt', 'A sibling directory was deleted.' ); + + ExeLearning_Filesystem::recursive_delete( $bystander ); + } + + public function test_recursive_delete_removes_nested_files() { + $root = sys_get_temp_dir() . '/deltree-' . uniqid(); + mkdir( $root . '/inner/deep', 0755, true ); + file_put_contents( $root . '/a.txt', 'a' ); + file_put_contents( $root . '/inner/b.txt', 'b' ); + file_put_contents( $root . '/inner/deep/c.txt', 'c' ); + $this->assertDirectoryExists( $root ); + ExeLearning_Filesystem::recursive_delete( $root ); + $this->assertDirectoryDoesNotExist( $root ); + } +} diff --git a/tests/unit/RestApiTest.php b/tests/unit/RestApiTest.php index 6388e85..233287a 100644 --- a/tests/unit/RestApiTest.php +++ b/tests/unit/RestApiTest.php @@ -47,7 +47,7 @@ public function tear_down() { global $wp_rest_server; $wp_rest_server = null; foreach ( $this->cleanup_paths as $path ) { - ExeLearning_Styles_Service::recursive_delete( $path ); + ExeLearning_Filesystem::recursive_delete( $path ); } $this->cleanup_paths = array(); parent::tear_down(); diff --git a/tests/unit/ShortcodesTest.php b/tests/unit/ShortcodesTest.php index 41cb8b4..b4a9cc7 100644 --- a/tests/unit/ShortcodesTest.php +++ b/tests/unit/ShortcodesTest.php @@ -40,7 +40,7 @@ public function set_up() { */ public function tear_down() { foreach ( $this->cleanup_paths as $path ) { - ExeLearning_Styles_Service::recursive_delete( $path ); + ExeLearning_Filesystem::recursive_delete( $path ); } $this->cleanup_paths = array(); parent::tear_down(); diff --git a/tests/unit/StylePackageTest.php b/tests/unit/StylePackageTest.php index b8b3e3f..cf3de28 100644 --- a/tests/unit/StylePackageTest.php +++ b/tests/unit/StylePackageTest.php @@ -32,7 +32,7 @@ class StylePackageTest extends WP_UnitTestCase { public function tear_down() { foreach ( $this->temp_dirs as $dir ) { if ( is_dir( $dir ) ) { - ExeLearning_Styles_Service::recursive_delete( $dir ); + ExeLearning_Filesystem::recursive_delete( $dir ); } } parent::tear_down(); diff --git a/tests/unit/StylesServiceTest.php b/tests/unit/StylesServiceTest.php index 5bd1185..c575df0 100644 --- a/tests/unit/StylesServiceTest.php +++ b/tests/unit/StylesServiceTest.php @@ -30,7 +30,7 @@ public function tear_down() { delete_option( ExeLearning_Styles_Service::OPTION_DISABLED_STYLES ); $storage = ExeLearning_Styles_Service::get_storage_dir(); if ( is_dir( $storage ) ) { - ExeLearning_Styles_Service::recursive_delete( $storage ); + ExeLearning_Filesystem::recursive_delete( $storage ); } parent::tear_down(); } @@ -432,37 +432,6 @@ public function test_get_registry_survives_garbage_option_value() { $this->assertSame( array(), $r['disabled_builtins'] ); } - /** - * Deleting a path that is not there returns early and, crucially, does not - * walk up and take a sibling with it. - */ - public function test_recursive_delete_handles_missing_path_gracefully() { - $bystander = sys_get_temp_dir() . '/deltree-keep-' . uniqid(); - mkdir( $bystander, 0755, true ); - file_put_contents( $bystander . '/keep.txt', 'keep' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents - - $missing = sys_get_temp_dir() . '/does-not-exist-' . uniqid(); - $this->assertDirectoryDoesNotExist( $missing, 'Precondition: the path must be absent.' ); - - ExeLearning_Styles_Service::recursive_delete( $missing ); - - $this->assertDirectoryDoesNotExist( $missing ); - $this->assertFileExists( $bystander . '/keep.txt', 'A sibling directory was deleted.' ); - - ExeLearning_Styles_Service::recursive_delete( $bystander ); - } - - public function test_recursive_delete_removes_nested_files() { - $root = sys_get_temp_dir() . '/deltree-' . uniqid(); - mkdir( $root . '/inner/deep', 0755, true ); - file_put_contents( $root . '/a.txt', 'a' ); - file_put_contents( $root . '/inner/b.txt', 'b' ); - file_put_contents( $root . '/inner/deep/c.txt', 'c' ); - $this->assertDirectoryExists( $root ); - ExeLearning_Styles_Service::recursive_delete( $root ); - $this->assertDirectoryDoesNotExist( $root ); - } - public function test_build_theme_registry_override_respects_enabled_flag() { $zip_path = $this->make_zip( array(