de_DE) if ( ! empty( $component ) && 0 === strpos( $pure_name, $component . '-' ) ) { $locale = substr( $pure_name, strlen( $component . '-' ) ); } else { $locale = $pure_name; } // Strict sanitization for locale output $locale = preg_replace( '/[^a-zA-Z0-9_\-]/', '', $locale ); $standardized_filename = ( 'core' === $sub_folder ) ? $filename : $component . '-' . $locale . '.po'; $deletion_targets = array( trailingslashit( $real_sandbox_dir ) . $standardized_filename, trailingslashit( $real_sandbox_dir ) . $filename ); $file_deleted = false; // Direct PO/MO Target Deletion foreach ( array_unique( $deletion_targets ) as $po_path ) { $po_path = wp_normalize_path( $po_path ); $mo_path = str_replace( '.po', '.mo', $po_path ); $l10n_php_path = str_replace( '.po', '.l10n.php', $po_path ); $backup_path = $po_path . '~'; $files_to_check = array( $po_path, $mo_path, $l10n_php_path, $backup_path ); foreach ( $files_to_check as $target_file ) { if ( self::is_safe_deletion_target( $target_file, $real_sandbox_dir ) ) { if ( wp_delete_file( $target_file ) ) { $file_deleted = true; } } } } // ========================================================================= // EXTENDED SIDECAR PURGE: Sweeps ALL JS .json, .l10n.php, and hash sidecars // ========================================================================= $sidecars_deleted = self::purge_all_associated_sidecars( $real_sandbox_dir, $component, $locale, $pure_name ); if ( $sidecars_deleted ) { $file_deleted = true; } if ( $file_deleted ) { // Log activity $log_context = ! empty( $component ) ? $component : $type; $log_message = sprintf( /* translators: %s: Uppercase locale language code string */ __( 'Permanently deleted "%s" language file catalog, compiled binary sidecars, .l10n.php, and JS JSON bundles.', 'af-translate' ), strtoupper( $locale ) ); if ( class_exists( 'AF_Translate_Admin' ) && method_exists( 'AF_Translate_Admin', 'log_activity' ) ) { AF_Translate_Admin::log_activity( $log_message, $log_context ); } wp_send_json_success( esc_html__( 'Target localized catalog assets, .l10n.php, and JS bundles successfully unlinked from storage metrics.', 'af-translate' ) ); } else { wp_send_json_error( esc_html__( 'Disk IO Failure: Target resource locks prevented deletion matrix compliance or files were already absent.', 'af-translate' ) ); } } /** * Strictly validates whether a target file path is safe to delete. * Prevents false rejections on .json and .l10n.php while enforcing strict realpath containment. * * @param string $file_path Absolute normalized file path. * @param string $real_sandbox_dir Realpath of the allowed sandbox directory. * @return bool True if safe, false otherwise. */ private static function is_safe_deletion_target( $file_path, $real_sandbox_dir ) { $file_path = wp_normalize_path( $file_path ); if ( ! file_exists( $file_path ) || is_link( $file_path ) ) { return false; } $filename = basename( $file_path ); $lower_filename = strtolower( $filename ); $ext = strtolower( pathinfo( $filename, PATHINFO_EXTENSION ) ); // 1. Extension Whitelist check $is_allowed = false; if ( in_array( $ext, array( 'po', 'mo', 'json' ), true ) ) { $is_allowed = true; } elseif ( substr( $lower_filename, -9 ) === '.l10n.php' ) { $is_allowed = true; } elseif ( substr( $lower_filename, -4 ) === '.po~' ) { $is_allowed = true; } if ( ! $is_allowed ) { return false; } // 2. Strict Realpath Directory Containment $real_file_path = wp_normalize_path( realpath( $file_path ) ); $real_sandbox = wp_normalize_path( trailingslashit( $real_sandbox_dir ) ); if ( ! $real_file_path || 0 !== strpos( $real_file_path, $real_sandbox ) ) { return false; } // 3. External Validator Integration (Invoked ONLY for .po and .mo files to avoid extension rejection) if ( in_array( $ext, array( 'po', 'mo' ), true ) && class_exists( 'AF_Translate_Path_Validator' ) ) { if ( ! AF_Translate_Path_Validator::validate_containment( $file_path, true ) ) { return false; } } return true; } /** * Scans and purges all JS JSON bundles, .l10n.php files, and hash sidecars matching domain & locale. * * @param string $real_sandbox_dir Realpath of target upload sandbox folder. * @param string $component Text domain or component name. * @param string $locale Language locale code (e.g., de_DE). * @param string $pure_name Base PO filename without extension. * @return bool True if any sidecar file was deleted, false otherwise. */ private static function purge_all_associated_sidecars( $real_sandbox_dir, $component, $locale, $pure_name ) { if ( ! is_dir( $real_sandbox_dir ) ) { return false; } $files = scandir( $real_sandbox_dir ); if ( ! is_array( $files ) ) { return false; } $pure_lower = strtolower( $pure_name ); $comp_loc_lower = strtolower( $component . '-' . $locale ); $locale_lower = strtolower( $locale ); $any_deleted = false; foreach ( $files as $file ) { if ( $file === '.' || $file === '..' || strpos( $file, '.' ) === 0 ) { continue; } $lower_file = strtolower( $file ); // Matches: covernews-de_de.json, covernews-de_de-33503a82.json, covernews-de_de.l10n.php, etc. $is_match = ( 0 === strpos( $lower_file, $pure_lower . '.' ) || 0 === strpos( $lower_file, $pure_lower . '-' ) || 0 === strpos( $lower_file, $comp_loc_lower . '.' ) || 0 === strpos( $lower_file, $comp_loc_lower . '-' ) || ( ! empty( $locale_lower ) && ( 0 === strpos( $lower_file, $locale_lower . '.' ) || 0 === strpos( $lower_file, $locale_lower . '-' ) ) ) ); if ( $is_match ) { $target_path = wp_normalize_path( trailingslashit( $real_sandbox_dir ) . $file ); if ( self::is_safe_deletion_target( $target_path, $real_sandbox_dir ) ) { if ( wp_delete_file( $target_path ) ) { $any_deleted = true; } } } } return $any_deleted; } }