Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
1 change: 1 addition & 0 deletions exelearning.php
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down
12 changes: 6 additions & 6 deletions includes/class-elp-file-service.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand All @@ -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 {

Expand Down Expand Up @@ -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 ) {
Expand Down Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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 ) {
Expand Down
2 changes: 1 addition & 1 deletion includes/class-elp-reprocessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 );
}
}

Expand Down
2 changes: 1 addition & 1 deletion includes/class-elp-upload-block.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?php
/**
* Registers the eXeLearning .elp upload block.
* Registers the eXeLearning .elpx upload block.
*
* @package Exelearning
*/
Expand Down
17 changes: 7 additions & 10 deletions includes/class-elp-upload-handler.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
/**
* File upload handler for eXeLearning files.
*
* This class validates and extracts .elp files upon upload.
* This class validates and extracts .elpx packages upon upload.
*
* @package Exelearning
*/
Expand All @@ -14,7 +14,7 @@
/**
* Class ExeLearning_Elp_Upload_Handler.
*
* Processes the .elp file uploads.
* Processes .elpx package uploads.
*/
class ExeLearning_Elp_Upload_Handler {

Expand Down Expand Up @@ -58,9 +58,9 @@ public function register() {
}

/**
* Processes .elp file uploads.
* Processes .elpx package uploads.
*
* Checks if the uploaded file is a valid .elp (zip) file, verifies its structure,
* Checks if the uploaded file is a valid .elpx (zip) package, verifies its structure,
* and extracts it to a secure folder with a unique hash.
*
* @param array $upload The upload data.
Expand All @@ -82,7 +82,7 @@ public function process_elp_upload( $upload ) {
return $upload;
}

// Validate the .elp file using the ELP File Service.
// Validate the package using the ELP File Service.
$elp_service = new ExeLearning_Elp_File_Service();
$result = $elp_service->validate_elp_file( $file );

Expand Down Expand Up @@ -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() );
}
Expand Down Expand Up @@ -152,9 +152,6 @@ public function process_elp_upload( $upload ) {
300
);

// Optionally, remove the original .elp file.
// Example: unlink( $file ).

return $upload;
}

Expand Down Expand Up @@ -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 );
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion includes/class-exelearning-editor.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down
4 changes: 2 additions & 2 deletions includes/class-exelearning-rest-api.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down
40 changes: 40 additions & 0 deletions includes/class-filesystem.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php
/**
* Filesystem helpers shared by the plugin.
*
* @package Exelearning
*/

if ( ! defined( 'WPINC' ) ) {
die;
}

/**
* Class ExeLearning_Filesystem.
*/
class ExeLearning_Filesystem {

/**
* Recursively delete a directory. Safe to call on a missing path.
*
* @param string $dir Absolute path.
* @return void
*/
public static function recursive_delete( $dir ) {
if ( ! file_exists( $dir ) ) {
return;
}
if ( is_link( $dir ) || is_file( $dir ) ) {
wp_delete_file( $dir );
return;
}
$items = scandir( $dir );
if ( false === $items ) {
return;
}
foreach ( array_diff( $items, array( '.', '..' ) ) as $item ) {
self::recursive_delete( $dir . DIRECTORY_SEPARATOR . $item );
}
@rmdir( $dir ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_rmdir,WordPress.PHP.NoSilencedErrors.Discouraged
}
}
30 changes: 3 additions & 27 deletions includes/class-styles-service.php
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,7 @@ public static function delete_uploaded( $slug ) {
}
$dir = trailingslashit( self::get_storage_dir() ) . $slug;
if ( is_dir( $dir ) ) {
self::recursive_delete( $dir );
ExeLearning_Filesystem::recursive_delete( $dir );
}
unset( $registry['uploaded'][ $slug ] );
self::save_registry( $registry );
Expand Down Expand Up @@ -401,13 +401,13 @@ public static function install_from_zip( $zip_path, $orig_name = '' ) {

$extract_result = ExeLearning_Style_Package::extract_safely( $zip_path, $dest, $validation['prefix'] );
if ( is_wp_error( $extract_result ) ) {
self::recursive_delete( $dest );
ExeLearning_Filesystem::recursive_delete( $dest );
return $extract_result;
}

$css_files = ExeLearning_Style_Package::find_css_files( $dest );
if ( empty( $css_files ) ) {
self::recursive_delete( $dest );
ExeLearning_Filesystem::recursive_delete( $dest );
return new WP_Error(
'style_no_css',
__( 'The uploaded style does not contain any stylesheet.', 'exelearning' )
Expand Down Expand Up @@ -615,28 +615,4 @@ public static function allocate_unique_slug( $requested ) {
}
return $slug;
}

/**
* Recursively delete a directory. Safe to call on a missing path.
*
* @param string $dir Absolute path.
* @return void
*/
public static function recursive_delete( $dir ) {
if ( ! file_exists( $dir ) ) {
return;
}
if ( is_link( $dir ) || is_file( $dir ) ) {
wp_delete_file( $dir );
return;
}
$items = scandir( $dir );
if ( false === $items ) {
return;
}
foreach ( array_diff( $items, array( '.', '..' ) ) as $item ) {
self::recursive_delete( $dir . DIRECTORY_SEPARATOR . $item );
}
@rmdir( $dir ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_rmdir,WordPress.PHP.NoSilencedErrors.Discouraged
}
}
2 changes: 1 addition & 1 deletion includes/integrations/class-media-library.php
Original file line number Diff line number Diff line change
Expand Up @@ -393,7 +393,7 @@ public function render_elp_column( $column_name, $post_id ) {
return;
}

// Check if it is an .elp file using metadata.
// Only extracted eXeLearning packages carry this meta.
$is_elp = get_post_meta( $post_id, '_exelearning_extracted', true );

if ( $is_elp ) {
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/DeveloperHooksTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public function tear_down() {
delete_option( ExeLearning_Styles_Service::OPTION_REGISTRY );
$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();
}
Expand Down
6 changes: 3 additions & 3 deletions tests/unit/ElpFileServiceTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -554,7 +554,7 @@ public function test_extract_refuses_an_archive_over_the_size_budget() {
$this->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 );
}

/**
Expand Down Expand Up @@ -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() );
Expand All @@ -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 );
}

/**
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/ElpUploadHandlerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
45 changes: 45 additions & 0 deletions tests/unit/FilesystemTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php
/**
* Tests for ExeLearning_Filesystem.
*
* @package Exelearning
*/

/**
* Class FilesystemTest.
*
* @covers ExeLearning_Filesystem
*/
class FilesystemTest extends WP_UnitTestCase {

/**
* 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_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 );
}
}
2 changes: 1 addition & 1 deletion tests/unit/RestApiTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/ShortcodesTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/StylePackageTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Loading
Loading