Skip to content

Commit ba7ae14

Browse files
authored
Bug Fixes
1 parent 9ef06c0 commit ba7ae14

File tree

9 files changed

+76
-51
lines changed

9 files changed

+76
-51
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@
4949
- **Type Declarations**: Added PHP 7.4 parameter types and return types to all functions where deterministic. Functions returning union types (`array|WP_Error`, `string|false`, `true|WP_Error`) retain PHPDoc-only annotations since PHP 7.4 does not support union return types.
5050
- **Short Array Syntax**: Standardized all `array()` constructor calls to short `[]` syntax throughout the plugin.
5151
- **Null Coalescing Assignment**: Replaced explicit null check + assignment pattern with PHP 7.4 `??=` operator in `sse_should_exclude_file()` file size cache, and `?:` Elvis operator for the ternary fallback.
52+
- **PHPStan Array Shapes**: Added PHPStan `array{}` shape annotations to all functions accepting or returning associative arrays, resolving 10 level-6 "no value type specified in iterable type array" errors.
53+
- **Trailing Whitespace**: Removed trailing whitespace (tabs on blank lines) across `export.php`, `download.php`, `cleanup.php`, `admin-page.php`, and `security.php`.
54+
- **JS File Header**: Converted `admin.js` file header from JSDoc (`/** @package`, `@since`) to plain block comment to avoid TSDoc linter false positives.
55+
- **Bulk Cleanup Complexity**: Extracted per-file deletion logic from `sse_bulk_cleanup_exports_handler()` into `sse_cleanup_expired_export_file()` helper, reducing cyclomatic complexity from 13 to 8 and NPath complexity from 336 to under 200.
5256

5357
## 2.0.0 - March 1, 2026
5458

includes/admin-page.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ function sse_exporter_page_html(): void {
107107
<form method="post" action="" class="sse-section-spacing">
108108
<?php wp_nonce_field( 'sse_export_action', 'sse_export_nonce' ); ?>
109109
<input type="hidden" name="action" value="sse_export_site">
110-
110+
111111
<table class="form-table sse-form-table">
112112
<tbody>
113113
<tr>
@@ -128,7 +128,7 @@ function sse_exporter_page_html(): void {
128128
</tr>
129129
</tbody>
130130
</table>
131-
131+
132132
<?php submit_button( __( 'Export Site', 'enginescript-site-exporter' ) ); ?>
133133
</form>
134134
<hr>
@@ -175,7 +175,7 @@ function () use ( $message ) {
175175
* Shows a success notice to the user.
176176
*
177177
* @since 1.0.0
178-
* @param array $zip_result The zip file information.
178+
* @param array{filename: string, filepath: string} $zip_result The zip file information.
179179
* @return void
180180
*/
181181
function sse_show_success_notice( array $zip_result ): void {

includes/archive.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@
1313
* Creates a site archive with database and files.
1414
*
1515
* @since 1.0.0
16-
* @param array $export_paths Export directory paths.
17-
* @param array $database_file Database file information.
18-
* @return array|WP_Error Archive info on success, WP_Error on failure.
16+
* @param array{export_dir: string, export_url: string, export_dir_name: string} $export_paths Export directory paths.
17+
* @param array{filename: string, filepath: string} $database_file Database file information.
18+
* @return array{filename: string, filepath: string}|WP_Error Archive info on success, WP_Error on failure.
1919
*/
2020
function sse_create_site_archive( array $export_paths, array $database_file ) {
2121
if ( ! class_exists( 'ZipArchive' ) ) {

includes/cleanup.php

Lines changed: 42 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
* Cleans up temporary files.
1414
*
1515
* @since 1.0.0
16-
* @param array $files Array of file paths to delete.
16+
* @param string[] $files Array of file paths to delete.
1717
* @return void
1818
*/
1919
function sse_cleanup_files( array $files ): void {
@@ -81,15 +81,15 @@ function sse_schedule_bulk_cleanup(): void {
8181
*/
8282
function sse_bulk_cleanup_exports_handler(): void {
8383
sse_log( 'Bulk export cleanup handler triggered', 'info' );
84-
84+
8585
$upload_dir = wp_upload_dir();
8686
$export_dir = trailingslashit( $upload_dir['basedir'] ) . SSE_EXPORT_DIR_NAME;
87-
87+
8888
if ( ! is_dir( $export_dir ) ) {
8989
sse_log( 'Export directory does not exist, nothing to clean up', 'info' );
9090
return;
9191
}
92-
92+
9393
try {
9494
$dir_iterator = new DirectoryIterator( $export_dir );
9595
} catch ( RuntimeException $e ) {
@@ -111,34 +111,51 @@ function sse_bulk_cleanup_exports_handler(): void {
111111
sse_log( 'No export files found in bulk cleanup', 'info' );
112112
return;
113113
}
114-
114+
115115
$cleaned_count = 0;
116116
$cutoff_time = time() - ( 5 * 60 ); // Files older than 5 minutes.
117-
117+
118118
foreach ( $files as $file_path ) {
119-
$file_time = filemtime( $file_path );
120-
121-
if ( $file_time && $file_time < $cutoff_time ) {
122-
// File is older than 5 minutes, validate it's an export file.
123-
$filename = basename( $file_path );
124-
$validation = sse_validate_basic_export_file( $filename );
125-
126-
if ( ! is_wp_error( $validation ) ) {
127-
if ( sse_safely_delete_file( $file_path ) ) {
128-
sse_log( 'Bulk cleanup deleted export file: ' . $file_path, 'info' );
129-
$cleaned_count++;
130-
} else {
131-
sse_log( 'Bulk cleanup failed to delete: ' . $file_path, 'error' );
132-
}
133-
} else {
134-
sse_log( 'Bulk cleanup skipped invalid file: ' . $file_path . ' - ' . $validation->get_error_message(), 'warning' );
135-
}
119+
if ( sse_cleanup_expired_export_file( $file_path, $cutoff_time ) ) {
120+
$cleaned_count++;
136121
}
137122
}
138-
123+
139124
sse_log( "Bulk cleanup completed. Deleted {$cleaned_count} export files.", 'info' );
140125
}
141126

127+
/**
128+
* Attempts to clean up a single expired export file.
129+
*
130+
* @since 2.0.0
131+
* @param string $file_path The file path to check and potentially delete.
132+
* @param int $cutoff_time Unix timestamp; files modified before this are eligible.
133+
* @return bool True if the file was deleted, false otherwise.
134+
*/
135+
function sse_cleanup_expired_export_file( string $file_path, int $cutoff_time ): bool {
136+
$file_time = filemtime( $file_path );
137+
138+
if ( ! $file_time || $file_time >= $cutoff_time ) {
139+
return false;
140+
}
141+
142+
$filename = basename( $file_path );
143+
$validation = sse_validate_basic_export_file( $filename );
144+
145+
if ( is_wp_error( $validation ) ) {
146+
sse_log( 'Bulk cleanup skipped invalid file: ' . $file_path . ' - ' . $validation->get_error_message(), 'warning' );
147+
return false;
148+
}
149+
150+
if ( sse_safely_delete_file( $file_path ) ) {
151+
sse_log( 'Bulk cleanup deleted export file: ' . $file_path, 'info' );
152+
return true;
153+
}
154+
155+
sse_log( 'Bulk cleanup failed to delete: ' . $file_path, 'error' );
156+
return false;
157+
}
158+
142159
/**
143160
* Handles scheduled deletion of export files.
144161
*
@@ -148,7 +165,7 @@ function sse_bulk_cleanup_exports_handler(): void {
148165
*/
149166
function sse_delete_export_file_handler( string $file ): void {
150167
sse_log( 'Scheduled deletion handler triggered for file: ' . $file, 'info' );
151-
168+
152169
// Validate that this is actually an export file before deletion.
153170
$filename = basename( $file );
154171

includes/download.php

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ function sse_set_download_headers( string $filename, int $filesize ): void {
143143
$content_type = 'application/octet-stream';
144144
break;
145145
}
146-
146+
147147
// Security: Set headers to prevent XSS and ensure proper download behavior.
148148
header( 'Content-Type: ' . $content_type );
149149
header( 'Content-Disposition: attachment; filename="' . esc_attr( $filename ) . '"' );
@@ -153,7 +153,7 @@ function sse_set_download_headers( string $filename, int $filesize ): void {
153153
header( 'Expires: 0' );
154154
header( 'X-Content-Type-Options: nosniff' ); // Security: Prevent MIME sniffing.
155155
header( 'X-Frame-Options: DENY' ); // Security: Prevent framing.
156-
156+
157157
// Disable output buffering for large files.
158158
if ( ob_get_level() ) {
159159
ob_end_clean();
@@ -176,18 +176,18 @@ function sse_validate_file_output_security( string $filepath ): string {
176176
sse_log( 'Security: Blocked attempt to serve file with invalid extension: ' . pathinfo( $filepath, PATHINFO_EXTENSION ), 'security' );
177177
wp_die( esc_html__( 'Access denied - invalid file type.', 'enginescript-site-exporter' ) );
178178
}
179-
179+
180180
// Security: Ensure file is within our controlled directory before serving.
181181
$upload_dir = wp_upload_dir();
182182
$export_dir = trailingslashit( $upload_dir['basedir'] ) . SSE_EXPORT_DIR_NAME;
183183
$real_export_dir = realpath( $export_dir );
184184
$real_file_path = realpath( $filepath );
185-
185+
186186
if ( false === $real_export_dir || false === $real_file_path || 0 !== strpos( $real_file_path, $real_export_dir ) ) {
187187
sse_log( 'Security: File not within controlled export directory: ' . $filepath, 'security' );
188188
wp_die( esc_html__( 'Access denied.', 'enginescript-site-exporter' ) );
189189
}
190-
190+
191191
return $real_file_path;
192192
}
193193

@@ -203,14 +203,14 @@ function sse_validate_file_output_security( string $filepath ): string {
203203
function sse_output_file_content( string $filepath, string $filename ): void {
204204
// Security: Validate and resolve to realpath before any filesystem access.
205205
$resolved_path = sse_validate_file_output_security( $filepath );
206-
206+
207207
// Security: Use resolved path (from realpath) for all filesystem operations to prevent SSRF/TOCTOU.
208208
if ( function_exists( 'readfile' ) && is_readable( $resolved_path ) && is_file( $resolved_path ) ) {
209209
readfile( $resolved_path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_readfile -- Security validated export file download.
210210
sse_log( 'Secure file download served via readfile: ' . $filename, 'info' );
211211
exit; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Required to terminate script after file download.
212212
}
213-
213+
214214
sse_log( 'Failed to serve secure file download: ' . $filename, 'error' );
215215
wp_die( esc_html__( 'Unable to serve file download.', 'enginescript-site-exporter' ) );
216216
}
@@ -219,13 +219,13 @@ function sse_output_file_content( string $filepath, string $filename ): void {
219219
* Serves a file download with enhanced security validation.
220220
*
221221
* @since 2.0.0
222-
* @param array $file_data Validated file information array.
222+
* @param array{filename: string, filesize: int, filepath: string} $file_data Validated file information array.
223223
* @return void
224224
*/
225225
function sse_serve_file_download( array $file_data ): void {
226226
// Set download headers.
227227
sse_set_download_headers( $file_data['filename'], $file_data['filesize'] );
228-
228+
229229
// Output file content (includes final security validation before readfile).
230230
sse_output_file_content( $file_data['filepath'], $file_data['filename'] );
231231
}

includes/export.php

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,12 +54,12 @@ function sse_handle_export(): void {
5454
}
5555

5656
sse_cleanup_files( [ $database_file['filepath'] ] );
57-
57+
5858
sse_schedule_export_cleanup( $zip_result['filepath'] );
59-
59+
6060
// Schedule a bulk cleanup sweep in case individual files were missed.
6161
sse_schedule_bulk_cleanup();
62-
62+
6363
sse_show_success_notice( $zip_result );
6464
} finally {
6565
// Always release the lock and clean up user preferences.
@@ -100,7 +100,7 @@ function sse_validate_export_request(): bool { // phpcs:ignore WordPress.Securit
100100
* Sets up export directories and returns path information.
101101
*
102102
* @since 1.0.0
103-
* @return array|WP_Error Array of paths on success, WP_Error on failure.
103+
* @return array{export_dir: string, export_url: string, export_dir_name: string}|WP_Error Array of paths on success, WP_Error on failure.
104104
*/
105105
function sse_setup_export_directories() {
106106
$upload_dir = wp_upload_dir();
@@ -235,7 +235,7 @@ function sse_get_safe_wp_cli_path() {
235235
*
236236
* @since 1.0.0
237237
* @param string $export_dir The directory to save the database dump.
238-
* @return array|WP_Error Array with file info on success, WP_Error on failure.
238+
* @return array{filename: string, filepath: string}|WP_Error Array with file info on success, WP_Error on failure.
239239
*/
240240
function sse_export_database( string $export_dir ) {
241241
$site_name = sanitize_file_name( get_bloginfo( 'name' ) );

includes/security.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ function sse_validate_file_extension( string $file_path ): bool {
112112
sse_log( 'Rejected file access - invalid extension: ' . $file_extension, 'security' );
113113
return false;
114114
}
115-
115+
116116
return true;
117117
}
118118

@@ -180,7 +180,7 @@ function sse_validate_filepath( string $file_path, string $base_dir ): bool {
180180
*
181181
* @since 2.0.0
182182
* @param string $filename The filename to validate.
183-
* @return array|WP_Error Result array with file data or WP_Error on failure.
183+
* @return array{filepath: string, filename: string, filesize: int}|WP_Error Result array with file data or WP_Error on failure.
184184
*/
185185
function sse_validate_export_file_for_download( string $filename ) {
186186
$basic_validation = sse_validate_basic_export_file( $filename );
@@ -211,7 +211,7 @@ function sse_validate_export_file_for_download( string $filename ) {
211211
*
212212
* @since 2.0.0
213213
* @param string $filename The filename to validate.
214-
* @return array|WP_Error Result array with file data or WP_Error on failure.
214+
* @return array{filepath: string, filename: string}|WP_Error Result array with file data or WP_Error on failure.
215215
*/
216216
function sse_validate_basic_export_file( string $filename ) {
217217
$basic_checks = sse_validate_filename_format( $filename );

js/admin.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
/**
1+
/*
22
* EngineScript Site Exporter — Admin Scripts
33
*
44
* Enqueued only on the Site Exporter admin page (tools_page_enginescript-site-exporter).
55
*
6-
* @package EngineScript_Site_Exporter
7-
* @since 2.1.0
6+
* Package: EngineScript_Site_Exporter
7+
* Since: 2.1.0
88
*/
99

1010
( function () {

readme.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,10 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
125125
* **PHP 7.4**: Added type declarations (parameter and return types) to all functions
126126
* **PHP 7.4**: Standardized all `array()` to short `[]` syntax
127127
* **PHP 7.4**: Applied `??=` null coalescing assignment and `?:` Elvis operator
128+
* **PHP 7.4**: Added PHPStan `array{}` shape annotations to all functions with untyped array params/returns
129+
* **Code Quality**: Removed trailing whitespace across 5 include files
130+
* **Code Quality**: Converted `admin.js` file header to plain block comment (avoids TSDoc linter false positives)
131+
* **Code Quality**: Extracted `sse_cleanup_expired_export_file()` from bulk cleanup handler to reduce cyclomatic/NPath complexity
128132

129133
= 2.0.0 =
130134
* **Critical Fix**: Fixed bug where automatic export file cleanup via WordPress cron was completely broken due to referer validation blocking cron-triggered deletions

0 commit comments

Comments
 (0)