Changeset 3740977
- Timestamp:
- 10/12/2026 09:34:26 AM (6 days ago)
- Location:
- woo-addon-uploads
- Files:
-
- 16 added
- 5 edited
-
tags/1.7.7 (added)
-
tags/1.7.7/assets (added)
-
tags/1.7.7/assets/css (added)
-
tags/1.7.7/assets/css/wau_features.css (added)
-
tags/1.7.7/assets/css/wau_styles.css (added)
-
tags/1.7.7/i18n (added)
-
tags/1.7.7/i18n/languages (added)
-
tags/1.7.7/i18n/languages/woo-addon-uploads.pot (added)
-
tags/1.7.7/includes (added)
-
tags/1.7.7/includes/class-wau-admin-settings.php (added)
-
tags/1.7.7/includes/class-wau-admin.php (added)
-
tags/1.7.7/includes/class-wau-front-end.php (added)
-
tags/1.7.7/includes/class-wau-pro-features.php (added)
-
tags/1.7.7/readme.txt (added)
-
tags/1.7.7/uninstall.php (added)
-
tags/1.7.7/woocommerce-addon-uploads.php (added)
-
trunk/i18n/languages/woo-addon-uploads.pot (modified) (8 diffs)
-
trunk/includes/class-wau-admin.php (modified) (12 diffs)
-
trunk/includes/class-wau-front-end.php (modified) (23 diffs)
-
trunk/readme.txt (modified) (6 diffs)
-
trunk/woocommerce-addon-uploads.php (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
woo-addon-uploads/trunk/i18n/languages/woo-addon-uploads.pot
r3714269 r3740977 3 3 msgid "" 4 4 msgstr "" 5 "Project-Id-Version: File Uploads Addon for WooCommerce 1.7. 6\n"5 "Project-Id-Version: File Uploads Addon for WooCommerce 1.7.7\n" 6 6 "Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/woo-addon-uploads\n" 7 7 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" … … 88 88 89 89 #: includes/class-wau-admin.php:135 90 msgid "Private upload storage isunavailable, so new customer uploads are paused."90 msgid "Private and encrypted storage are unavailable, so new customer uploads are paused." 91 91 msgstr "" 92 92 … … 212 212 msgstr "" 213 213 214 #: includes/class-wau-admin.php:22 6215 msgid "No immediate action is required. New uploads are using private storage and legacy files are either migrated or not present."214 #: includes/class-wau-admin.php:228 215 msgid "No immediate security action is required. New uploads are using private storage or authenticated encrypted storage, and no public plaintext customer files remain." 216 216 msgstr "" 217 217 … … 237 237 238 238 #. translators: %s: PHP constant example. 239 #: includes/class-wau-admin.php:3 31240 msgid "New customer uploads are paused because private storage is unavailable. Ask your host for a writable private directory outside the public web root, then add %s to wp-config.php with that absolute path."239 #: includes/class-wau-admin.php:350 240 msgid "New customer uploads are paused because neither private nor encrypted storage is available. Ask your host for a writable private directory outside the public web root, then add %s to wp-config.php with that absolute path." 241 241 msgstr "" 242 242 … … 259 259 msgstr "" 260 260 261 #: includes/class-wau-admin.php:3 61262 msgid " Legacy files are still being migrated to private storagein small batches. Keep the plugin active and allow normal site traffic or admin page visits to continue the migration."261 #: includes/class-wau-admin.php:380 262 msgid "Public plaintext files are still being migrated in small batches. Keep the plugin active and allow normal site traffic or admin page visits to continue the migration." 263 263 msgstr "" 264 264 … … 283 283 msgstr "" 284 284 285 #: includes/class-wau-admin.php: 451286 msgid " Waiting for private storage"285 #: includes/class-wau-admin.php:520 286 msgid "Blocked: no secure storage backend is available" 287 287 msgstr "" 288 288 … … 331 331 msgstr "" 332 332 333 #: includes/class-wau-front-end.php: 891334 msgid "File upload is temporarily unavailable because private upload storage could not be created. Please contact the store administrator."333 #: includes/class-wau-front-end.php:1433 334 msgid "File upload is temporarily unavailable because secure upload storage could not be created. Please contact the store administrator." 335 335 msgstr "" 336 336 … … 373 373 msgid "File does not exist." 374 374 msgstr "" 375 376 #: includes/class-wau-admin.php:190 377 msgid "Encrypted fallback storage" 378 msgstr "" 379 380 #: includes/class-wau-admin.php:191 381 msgid "Encrypted container count" 382 msgstr "" 383 384 #: includes/class-wau-admin.php:196 385 msgid "Public plaintext file count" 386 msgstr "" 387 388 #: includes/class-wau-admin.php:290 389 msgid "Available with authenticated encryption" 390 msgstr "" 391 392 #: includes/class-wau-admin.php:290 393 msgid "Unavailable: encryption key is missing" 394 msgstr "" 395 396 #: includes/class-wau-admin.php:344 397 msgid "Encrypted upload containers exist, but their encryption key is missing from the WordPress database. Restore the wau_encryption_key option from a matching database backup. Do not delete the encrypted files or generate a replacement key." 398 msgstr "" 399 400 #: includes/class-wau-front-end.php:1457 401 msgid "File upload failed while creating secure encrypted storage. Please try again." 402 msgstr "" 403 404 #: includes/class-wau-front-end.php:2163 405 msgid "The encrypted file could not be authenticated." 406 msgstr "" -
woo-addon-uploads/trunk/includes/class-wau-admin.php
r3714269 r3740977 132 132 <?php esc_html_e( 'Legacy customer files are still present in the public uploads folder. Confirm that direct requests to the path below return 403 or 404 until migration is complete.', 'woo-addon-uploads' ); ?> 133 133 </p> 134 <?php if ( ! $status['private_storage_ok'] ) : ?>135 <p><?php esc_html_e( 'Private upload storage isunavailable, so new customer uploads are paused.', 'woo-addon-uploads' ); ?></p>134 <?php if ( ! $status['private_storage_ok'] && ! $status['encrypted_storage_ok'] ) : ?> 135 <p><?php esc_html_e( 'Private and encrypted storage are unavailable, so new customer uploads are paused.', 'woo-addon-uploads' ); ?></p> 136 136 <?php endif; ?> 137 137 <?php if ( $status['uses_nginx_location_rules'] ) : ?> … … 188 188 <?php $this->render_status_row( __( 'Private upload storage', 'woo-addon-uploads' ), $status['private_storage_label'], $status['private_storage_ok'] ? 'success' : 'warning' ); ?> 189 189 <?php $this->render_status_row( __( 'Private upload path', 'woo-addon-uploads' ), $status['private_path'] ? $status['private_path'] : __( 'Not available', 'woo-addon-uploads' ), $status['private_storage_ok'] ? 'info' : 'warning' ); ?> 190 <?php $this->render_status_row( __( 'Encrypted fallback storage', 'woo-addon-uploads' ), $status['encrypted_storage_label'], $status['encrypted_storage_ok'] ? 'success' : 'warning' ); ?> 191 <?php $this->render_status_row( __( 'Encrypted container count', 'woo-addon-uploads' ), (string) $status['encrypted_file_count'], 'info' ); ?> 190 192 <?php $this->render_status_row( __( 'Manual override', 'woo-addon-uploads' ), "define( 'WAU_PRIVATE_UPLOAD_DIR', '/absolute/private/path/' );", 'info' ); ?> 191 193 <?php $this->render_status_row( __( 'Legacy upload path', 'woo-addon-uploads' ), $status['legacy_path'], 'info' ); ?> 192 194 <?php $this->render_status_row( __( 'Legacy upload URL path', 'woo-addon-uploads' ), $status['legacy_url_path'], 'info' ); ?> 193 195 <?php $this->render_status_row( __( 'Legacy folder exists', 'woo-addon-uploads' ), $status['legacy_exists'] ? __( 'Yes', 'woo-addon-uploads' ) : __( 'No', 'woo-addon-uploads' ), $status['legacy_exists'] ? 'info' : 'success' ); ?> 194 <?php $this->render_status_row( __( ' Legacyfile count', 'woo-addon-uploads' ), (string) $status['legacy_file_count'], $status['legacy_file_count'] > 0 ? 'warning' : 'success' ); ?>196 <?php $this->render_status_row( __( 'Public plaintext file count', 'woo-addon-uploads' ), (string) $status['legacy_file_count'], $status['legacy_file_count'] > 0 ? 'warning' : 'success' ); ?> 195 197 </tbody> 196 198 </table> … … 224 226 </ul> 225 227 <?php else : ?> 226 <p><?php esc_html_e( 'No immediate action is required. New uploads are using private storage and legacy files are either migrated or not present.', 'woo-addon-uploads' ); ?></p>228 <p><?php esc_html_e( 'No immediate security action is required. New uploads are using private storage or authenticated encrypted storage, and no public plaintext customer files remain.', 'woo-addon-uploads' ); ?></p> 227 229 <?php endif; ?> 228 230 </div> … … 254 256 $legacy_url_path = trailingslashit( $baseurl_path ? $baseurl_path : '/wp-content/uploads' ) . 'wau-uploads/'; 255 257 $legacy_exists = $legacy_path && is_dir( $legacy_path ); 258 $encrypted_path = $legacy_path ? trailingslashit( $legacy_path ) . 'encrypted/' . $this->get_site_storage_key() . '/' : ''; 256 259 $private_status = get_option( 'wau_private_uploads_available' ); 257 260 $private_path = get_option( 'wau_private_uploads_path' ); … … 265 268 $server_unknown = ( empty( $server_lower ) || ( ! $uses_nginx_rules && ! $uses_openlitespeed && ! $uses_caddy && ! $uses_apache ) ); 266 269 $legacy_count = $legacy_exists ? $this->count_legacy_upload_files( $legacy_path ) : 0; 270 $encrypted_count = $encrypted_path && is_dir( $encrypted_path ) ? $this->count_encrypted_upload_files( $encrypted_path ) : 0; 271 $encryption_ok = $this->encrypted_fallback_is_available( $legacy_path, $encrypted_path, $upload_dir ); 272 $encoded_key = get_option( 'wau_encryption_key', '' ); 273 $encryption_key = is_string( $encoded_key ) ? base64_decode( $encoded_key, true ) : false; 274 $key_missing = $encrypted_count > 0 && ( ! is_string( $encryption_key ) || 32 !== strlen( $encryption_key ) ); 275 if ( $key_missing ) { 276 $encryption_ok = false; 277 } 267 278 $htaccess_status = $this->get_protection_file_status( $legacy_path . '.htaccess', $this->get_expected_htaccess_content(), $legacy_exists ); 268 279 $web_conf_status = $this->get_protection_file_status( $legacy_path . 'web.config', $this->get_expected_web_config_content(), $legacy_exists ); … … 276 287 'private_storage_label' => $private_ok ? __( 'Available and writable', 'woo-addon-uploads' ) : __( 'Not available or not writable', 'woo-addon-uploads' ), 277 288 'private_path' => $private_path, 289 'encrypted_storage_ok' => $encryption_ok, 290 'encrypted_storage_label' => $encryption_ok ? __( 'Available with authenticated encryption', 'woo-addon-uploads' ) : ( $key_missing ? __( 'Unavailable: encryption key is missing', 'woo-addon-uploads' ) : __( 'Unavailable', 'woo-addon-uploads' ) ), 291 'encrypted_file_count' => $encrypted_count, 292 'encryption_key_missing' => $key_missing, 278 293 'legacy_path' => $legacy_path ? $legacy_path : __( 'Unavailable', 'woo-addon-uploads' ), 279 294 'legacy_url_path' => $legacy_url_path, … … 288 303 'legacy_verification_needed' => $verify_needed, 289 304 'uses_nginx_location_rules' => $uses_nginx_rules, 290 'migration_complete' => ( 0 === $legacy_count && $private_ok),291 'migration_label' => $this->get_migration_label( $private_ok, $ legacy_count ),305 'migration_complete' => ( 0 === $legacy_count ), 306 'migration_label' => $this->get_migration_label( $private_ok, $encryption_ok, $legacy_count ), 292 307 'migration_batch_size' => $batch_size, 293 'has_required_action' => ( ! $private_ok || $server_needed || $verify_needed || ( $legacy_exists && ( ! $htaccess_status['ok'] || ! $web_conf_status['ok'] ) ) || ( $private_ok && $legacy_count > 0 )),308 'has_required_action' => ( ( ! $private_ok && ! $encryption_ok ) || $server_needed || $verify_needed || ( $legacy_exists && ( ! $htaccess_status['ok'] || ! $web_conf_status['ok'] ) ) || $legacy_count > 0 ), 294 309 ); 295 310 } … … 326 341 $items = array(); 327 342 328 if ( ! $status['private_storage_ok'] ) { 343 if ( $status['encryption_key_missing'] ) { 344 $items[] = __( 'Encrypted upload containers exist, but their encryption key is missing from the WordPress database. Restore the wau_encryption_key option from a matching database backup. Do not delete the encrypted files or generate a replacement key.', 'woo-addon-uploads' ); 345 } 346 347 if ( ! $status['private_storage_ok'] && ! $status['encrypted_storage_ok'] ) { 329 348 $items[] = sprintf( 330 349 /* translators: %s: PHP constant example. */ 331 __( 'New customer uploads are paused because private storage is unavailable. Ask your host for a writable private directory outside the public web root, then add %s to wp-config.php with that absolute path.', 'woo-addon-uploads' ),350 __( 'New customer uploads are paused because neither private nor encrypted storage is available. Ask your host for a writable private directory outside the public web root, then add %s to wp-config.php with that absolute path.', 'woo-addon-uploads' ), 332 351 '<code>define( \'WAU_PRIVATE_UPLOAD_DIR\', \'/absolute/private/path/\' );</code>' 333 352 ); … … 358 377 } 359 378 360 if ( $status['private_storage_ok']&& $status['legacy_file_count'] > 0 && ! $status['migration_complete'] ) {361 $items[] = __( ' Legacy files are still being migrated to private storagein small batches. Keep the plugin active and allow normal site traffic or admin page visits to continue the migration.', 'woo-addon-uploads' );379 if ( ( $status['private_storage_ok'] || $status['encrypted_storage_ok'] ) && $status['legacy_file_count'] > 0 && ! $status['migration_complete'] ) { 380 $items[] = __( 'Public plaintext files are still being migrated in small batches. Keep the plugin active and allow normal site traffic or admin page visits to continue the migration.', 'woo-addon-uploads' ); 362 381 } 363 382 … … 385 404 } 386 405 387 if ( in_array( strtolower( $file_info->getFilename() ), array( '.htaccess', 'web.config', 'index.php' ), true ) ) {406 if ( in_array( strtolower( $file_info->getFilename() ), array( '.htaccess', 'web.config', 'index.php' ), true ) || '.wau-pending' === substr( $file_info->getFilename(), -12 ) ) { 388 407 continue; 389 408 } … … 393 412 394 413 return $count; 414 } 415 416 /** 417 * Count authenticated encrypted containers. 418 * 419 * @param string $encrypted_path Encrypted fallback path. 420 * @return int 421 */ 422 private function count_encrypted_upload_files( $encrypted_path ) { 423 $count = 0; 424 425 try { 426 $directory = new DirectoryIterator( $encrypted_path ); 427 } catch ( Exception $exception ) { 428 return 0; 429 } 430 431 foreach ( $directory as $file_info ) { 432 if ( ! $file_info->isDot() && $file_info->isFile() && '.wau' === strtolower( substr( $file_info->getFilename(), -4 ) ) ) { 433 $count++; 434 } 435 } 436 437 return $count; 438 } 439 440 /** 441 * Check whether the host can use authenticated encrypted fallback storage. 442 * 443 * @param string $legacy_path Legacy storage path. 444 * @param string $encrypted_path Encrypted fallback path. 445 * @param array $upload_dir WordPress upload directory data. 446 * @return bool 447 */ 448 private function encrypted_fallback_is_available( $legacy_path, $encrypted_path, $upload_dir ) { 449 $sodium_available = function_exists( 'sodium_crypto_secretstream_xchacha20poly1305_init_push' ) 450 && function_exists( 'sodium_crypto_secretstream_xchacha20poly1305_init_pull' ); 451 $openssl_available = function_exists( 'openssl_encrypt' ) 452 && function_exists( 'openssl_decrypt' ) 453 && in_array( 'aes-256-gcm', openssl_get_cipher_methods(), true ); 454 455 if ( ! $sodium_available && ! $openssl_available ) { 456 return false; 457 } 458 459 if ( $encrypted_path && is_dir( $encrypted_path ) ) { 460 return wp_is_writable( $encrypted_path ); 461 } 462 463 if ( $legacy_path && is_dir( $legacy_path ) ) { 464 return wp_is_writable( $legacy_path ); 465 } 466 467 return empty( $upload_dir['error'] ) && is_dir( $upload_dir['basedir'] ) && wp_is_writable( $upload_dir['basedir'] ); 468 } 469 470 /** 471 * Build the same site-specific storage key used by the upload handler. 472 * 473 * @return string 474 */ 475 private function get_site_storage_key() { 476 $home_url = home_url( '/' ); 477 $host = wp_parse_url( $home_url, PHP_URL_HOST ); 478 $host = $host ? strtolower( $host ) : 'site'; 479 $host = preg_replace( '/[^a-z0-9._-]/', '-', $host ); 480 $host = str_replace( '.', '-', $host ); 481 $blog_id = function_exists( 'get_current_blog_id' ) ? get_current_blog_id() : 1; 482 $hash = substr( hash( 'sha256', $home_url . '|' . ABSPATH . '|' . $blog_id ), 0, 12 ); 483 484 return sanitize_file_name( $host . '-' . $blog_id . '-' . $hash ); 395 485 } 396 486 … … 443 533 * Get migration label. 444 534 * 445 * @param bool $private_ok Whether private storage is available. 446 * @param int $legacy_count Legacy file count. 535 * @param bool $private_ok Whether private storage is available. 536 * @param bool $encryption_ok Whether encrypted fallback is available. 537 * @param int $legacy_count Legacy file count. 447 538 * @return string 448 539 */ 449 private function get_migration_label( $private_ok, $ legacy_count ) {450 if ( ! $private_ok ) {451 return __( ' Waiting for private storage', 'woo-addon-uploads' );540 private function get_migration_label( $private_ok, $encryption_ok, $legacy_count ) { 541 if ( ! $private_ok && ! $encryption_ok ) { 542 return __( 'Blocked: no secure storage backend is available', 'woo-addon-uploads' ); 452 543 } 453 544 -
woo-addon-uploads/trunk/includes/class-wau-front-end.php
r3714269 r3740977 25 25 class wau_front_end_class { 26 26 27 const WAU_ENCRYPTED_FILE_MAGIC = 'WAUENC01'; 28 const WAU_ENCRYPTED_CHUNK_SIZE = 1048576; 29 27 30 /** 28 31 * Resolved private upload directory for the current request. … … 48 51 49 52 add_filter( 'wau_category_checks', array( $this, 'wau_check_category_allowed' ), 10, 2 ); 50 51 53 add_action( 'woocommerce_cart_item_removed', array( $this, 'wau_remove_cart_action' ), 10, 2 ); 52 54 … … 166 168 167 169 /** 168 * Move legacy public uploads into private storage in small batches.170 * Move legacy public uploads into private storage or encrypt them in place. 169 171 * 170 172 * @since 1.7.5 … … 176 178 $legacy_upload_dir = $this->wau_get_legacy_upload_directory(); 177 179 178 if ( empty( $ private_upload_dir['path'] ) || empty( $legacy_upload_dir['path'] ) ) {180 if ( empty( $legacy_upload_dir['path'] ) || ! is_dir( $legacy_upload_dir['path'] ) ) { 179 181 return; 180 182 } 181 183 182 $private_path = $private_upload_dir['path']; 184 if ( get_transient( 'wau_legacy_upload_migration_lock' ) ) { 185 return; 186 } 187 188 set_transient( 'wau_legacy_upload_migration_lock', 1, 5 * MINUTE_IN_SECONDS ); 189 190 $private_path = ! empty( $private_upload_dir['path'] ) ? $private_upload_dir['path'] : ''; 183 191 $legacy_path = $legacy_upload_dir['path']; 184 192 185 if ( $ this->wau_paths_match( $private_path, $legacy_path ) || ! is_dir($legacy_path ) ) {193 if ( $private_path && $this->wau_paths_match( $private_path, $legacy_path ) ) { 186 194 update_option( 'wau_legacy_upload_migration_complete', current_time( 'mysql' ), false ); 195 delete_transient( 'wau_legacy_upload_migration_lock' ); 187 196 return; 188 197 } … … 195 204 WP_Filesystem(); 196 205 197 if ( ! $wp_filesystem || ! $this->wau_prepare_upload_directory( $private_path ) ) { 206 $encrypted_path = ''; 207 if ( empty( $private_path ) ) { 208 $encrypted_dir = $this->wau_get_encrypted_upload_directory( true ); 209 $encrypted_path = $encrypted_dir['path']; 210 } 211 212 if ( ( $private_path && ! $wp_filesystem ) || ( empty( $private_path ) && empty( $encrypted_path ) ) ) { 213 delete_transient( 'wau_legacy_upload_migration_lock' ); 198 214 return; 199 215 } … … 207 223 $directory = new DirectoryIterator( $legacy_path ); 208 224 } catch ( Exception $exception ) { 225 delete_transient( 'wau_legacy_upload_migration_lock' ); 209 226 return; 210 227 } … … 217 234 $file_name = $file_info->getFilename(); 218 235 219 if ( in_array( strtolower( $file_name ), array( '.htaccess', 'web.config', 'index.php' ), true ) ) {236 if ( in_array( strtolower( $file_name ), array( '.htaccess', 'web.config', 'index.php' ), true ) || '.wau-pending' === substr( $file_name, -12 ) ) { 220 237 continue; 221 238 } … … 226 243 } 227 244 228 $source = trailingslashit( $legacy_path ) . $file_name; 245 $source = trailingslashit( $legacy_path ) . $file_name; 246 247 if ( empty( $private_path ) ) { 248 if ( $this->wau_encrypt_and_verify_file( $source, $file_name ) ) { 249 wp_delete_file( $source ); 250 wp_delete_file( $source . '.wau-pending' ); 251 $migrated++; 252 } else { 253 $remaining = true; 254 } 255 continue; 256 } 257 229 258 $destination = trailingslashit( $private_path ) . $file_name; 230 259 … … 251 280 update_option( 'wau_legacy_upload_migration_complete', current_time( 'mysql' ), false ); 252 281 } 282 283 delete_transient( 'wau_legacy_upload_migration_lock' ); 253 284 } 254 285 … … 335 366 336 367 /** 368 * Get the public directory used only for authenticated encrypted containers. 369 * 370 * The files in this directory contain authenticated ciphertext and use opaque 371 * names. Plaintext is never written here. 372 * 373 * @param bool $create Whether to create and protect the directory. 374 * @return array 375 */ 376 private function wau_get_encrypted_upload_directory( $create = false ) { 377 $legacy_dir = $this->wau_get_legacy_upload_directory(); 378 379 if ( empty( $legacy_dir['path'] ) || ! $this->wau_encryption_is_available() ) { 380 return array( 'path' => '', 'url' => '' ); 381 } 382 383 $site_key = $this->wau_get_site_storage_key(); 384 $directory = trailingslashit( $legacy_dir['path'] ) . 'encrypted/' . $site_key . '/'; 385 386 if ( $create ) { 387 $this->wau_ensure_upload_directory_protection( true ); 388 389 if ( ! $this->wau_prepare_upload_directory( $directory ) || ! $this->wau_get_encryption_key( true ) ) { 390 return array( 'path' => '', 'url' => '' ); 391 } 392 393 $this->wau_write_storage_protection_files( $directory ); 394 } 395 396 return array( 397 'path' => $directory, 398 'url' => trailingslashit( $legacy_dir['url'] ) . 'encrypted/' . $site_key . '/', 399 ); 400 } 401 402 /** 403 * Check whether an authenticated encryption implementation is available. 404 * 405 * @return bool 406 */ 407 private function wau_encryption_is_available() { 408 $sodium_available = function_exists( 'sodium_crypto_secretstream_xchacha20poly1305_init_push' ) 409 && function_exists( 'sodium_crypto_secretstream_xchacha20poly1305_init_pull' ); 410 $openssl_available = function_exists( 'openssl_encrypt' ) 411 && function_exists( 'openssl_decrypt' ) 412 && in_array( 'aes-256-gcm', openssl_get_cipher_methods(), true ); 413 414 return $sodium_available || $openssl_available; 415 } 416 417 /** 418 * Get or create this site's encryption key. 419 * 420 * @param bool $create Whether to create a key when no encrypted files exist. 421 * @return string|false Raw 32-byte key, or false. 422 */ 423 private function wau_get_encryption_key( $create = false ) { 424 $encoded_key = get_option( 'wau_encryption_key', '' ); 425 $key = is_string( $encoded_key ) ? base64_decode( $encoded_key, true ) : false; 426 427 if ( is_string( $key ) && 32 === strlen( $key ) ) { 428 return $key; 429 } 430 431 if ( ! $create ) { 432 return false; 433 } 434 435 $legacy_dir = $this->wau_get_legacy_upload_directory(); 436 $encrypted_dir = ! empty( $legacy_dir['path'] ) ? trailingslashit( $legacy_dir['path'] ) . 'encrypted/' . $this->wau_get_site_storage_key() . '/' : ''; 437 if ( $encrypted_dir && is_dir( $encrypted_dir ) ) { 438 try { 439 $directory = new DirectoryIterator( $encrypted_dir ); 440 foreach ( $directory as $file_info ) { 441 if ( ! $file_info->isDot() && $file_info->isFile() && '.wau' === strtolower( substr( $file_info->getFilename(), -4 ) ) ) { 442 return false; 443 } 444 } 445 } catch ( Exception $exception ) { 446 return false; 447 } 448 } 449 450 try { 451 $new_key = random_bytes( 32 ); 452 } catch ( Throwable $exception ) { 453 return false; 454 } 455 456 $encoded_new_key = base64_encode( $new_key ); 457 if ( ! add_option( 'wau_encryption_key', $encoded_new_key, '', false ) ) { 458 $existing_key = get_option( 'wau_encryption_key', '' ); 459 $decoded_key = is_string( $existing_key ) ? base64_decode( $existing_key, true ) : false; 460 461 if ( is_string( $decoded_key ) && 32 === strlen( $decoded_key ) ) { 462 return $decoded_key; 463 } 464 465 update_option( 'wau_encryption_key', $encoded_new_key, false ); 466 } 467 $encoded_key = get_option( 'wau_encryption_key', '' ); 468 $key = is_string( $encoded_key ) ? base64_decode( $encoded_key, true ) : false; 469 470 return is_string( $key ) && 32 === strlen( $key ) ? $key : false; 471 } 472 473 /** 474 * Resolve the opaque encrypted-container path for a logical filename. 475 * 476 * @param string $file_name Logical filename stored in cart/order data. 477 * @param bool $create_directory Whether to create the fallback directory. 478 * @return string|false 479 */ 480 private function wau_get_encrypted_file_path( $file_name, $create_directory = false ) { 481 $safe_filename = sanitize_file_name( basename( $file_name ) ); 482 $directory = $this->wau_get_encrypted_upload_directory( $create_directory ); 483 $key = $this->wau_get_encryption_key( $create_directory ); 484 485 if ( empty( $safe_filename ) || empty( $directory['path'] ) || ! $key ) { 486 return false; 487 } 488 489 $opaque_name = substr( hash_hmac( 'sha256', $safe_filename, $key ), 0, 40 ) . '.wau'; 490 491 return trailingslashit( $directory['path'] ) . $opaque_name; 492 } 493 494 /** 495 * Encrypt a plaintext file and verify it before the caller removes the source. 496 * 497 * @param string $source_path Plaintext source path. 498 * @param string $file_name Logical filename. 499 * @return bool 500 */ 501 private function wau_encrypt_and_verify_file( $source_path, $file_name ) { 502 if ( ! is_file( $source_path ) ) { 503 return false; 504 } 505 506 $destination = $this->wau_get_encrypted_file_path( $file_name, true ); 507 $source_hash = @hash_file( 'sha256', $source_path ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged 508 $source_size = @filesize( $source_path ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged 509 510 if ( ! $destination || false === $source_hash || false === $source_size ) { 511 return false; 512 } 513 514 if ( file_exists( $destination ) && $this->wau_verify_encrypted_file( $destination, $file_name, $source_hash, $source_size ) ) { 515 return true; 516 } 517 518 try { 519 $temp_path = $destination . '.tmp-' . bin2hex( random_bytes( 4 ) ); 520 } catch ( Throwable $exception ) { 521 return false; 522 } 523 524 if ( ! $this->wau_encrypt_file( $source_path, $temp_path, $file_name, $source_size ) 525 || ! $this->wau_verify_encrypted_file( $temp_path, $file_name, $source_hash, $source_size ) ) { 526 wp_delete_file( $temp_path ); 527 return false; 528 } 529 530 if ( file_exists( $destination ) ) { 531 wp_delete_file( $destination ); 532 } 533 534 if ( ! @rename( $temp_path, $destination ) ) { // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged 535 wp_delete_file( $temp_path ); 536 return false; 537 } 538 539 return $this->wau_verify_encrypted_file( $destination, $file_name, $source_hash, $source_size ); 540 } 541 542 /** 543 * Write a versioned authenticated-encryption container. 544 * 545 * @param string $source_path Plaintext source path. 546 * @param string $destination_path Container destination path. 547 * @param string $file_name Logical filename. 548 * @param int $plain_size Plaintext byte size. 549 * @return bool 550 */ 551 private function wau_encrypt_file( $source_path, $destination_path, $file_name, $plain_size ) { 552 $key = $this->wau_get_encryption_key( true ); 553 554 if ( ! $key ) { 555 return false; 556 } 557 558 $use_sodium = function_exists( 'sodium_crypto_secretstream_xchacha20poly1305_init_push' ); 559 $algorithm = $use_sodium ? 'S' : 'O'; 560 $prefix = self::WAU_ENCRYPTED_FILE_MAGIC . $algorithm . str_pad( (string) $plain_size, 20, '0', STR_PAD_LEFT ); 561 $input = @fopen( $source_path, 'rb' ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged 562 $output = @fopen( $destination_path, 'xb' ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged 563 564 if ( ! $input || ! $output || ! $this->wau_write_all( $output, $prefix ) ) { 565 if ( is_resource( $input ) ) { 566 fclose( $input ); 567 } 568 if ( is_resource( $output ) ) { 569 fclose( $output ); 570 } 571 return false; 572 } 573 574 try { 575 $success = $use_sodium 576 ? $this->wau_encrypt_stream_sodium( $input, $output, $key, $prefix, $file_name ) 577 : $this->wau_encrypt_stream_openssl( $input, $output, $key, $prefix, $file_name ); 578 } catch ( Throwable $exception ) { 579 $success = false; 580 } 581 582 fclose( $input ); 583 fclose( $output ); 584 585 return $success; 586 } 587 588 /** 589 * Encrypt a stream with libsodium secretstream. 590 */ 591 private function wau_encrypt_stream_sodium( $input, $output, $key, $prefix, $file_name ) { 592 list( $state, $header ) = sodium_crypto_secretstream_xchacha20poly1305_init_push( $key ); 593 594 if ( ! $this->wau_write_all( $output, $header ) ) { 595 return false; 596 } 597 598 $current = fread( $input, self::WAU_ENCRYPTED_CHUNK_SIZE ); 599 if ( false === $current ) { 600 return false; 601 } 602 603 do { 604 $next = feof( $input ) ? '' : fread( $input, self::WAU_ENCRYPTED_CHUNK_SIZE ); 605 if ( false === $next ) { 606 return false; 607 } 608 609 $is_final = feof( $input ) && '' === $next; 610 $tag = $is_final ? SODIUM_CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_TAG_FINAL : SODIUM_CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_TAG_MESSAGE; 611 $ciphertext = sodium_crypto_secretstream_xchacha20poly1305_push( $state, $current, $prefix . $file_name, $tag ); 612 613 if ( ! $this->wau_write_all( $output, pack( 'N', strlen( $ciphertext ) ) . $ciphertext ) ) { 614 return false; 615 } 616 617 $current = $next; 618 } while ( ! $is_final ); 619 620 return true; 621 } 622 623 /** 624 * Encrypt a stream with independently authenticated AES-256-GCM records. 625 */ 626 private function wau_encrypt_stream_openssl( $input, $output, $key, $prefix, $file_name ) { 627 if ( ! function_exists( 'openssl_encrypt' ) || ! in_array( 'aes-256-gcm', openssl_get_cipher_methods(), true ) ) { 628 return false; 629 } 630 631 $current = fread( $input, self::WAU_ENCRYPTED_CHUNK_SIZE ); 632 $index = 0; 633 if ( false === $current ) { 634 return false; 635 } 636 637 do { 638 $next = feof( $input ) ? '' : fread( $input, self::WAU_ENCRYPTED_CHUNK_SIZE ); 639 if ( false === $next ) { 640 return false; 641 } 642 643 $is_final = feof( $input ) && '' === $next; 644 $flags = $is_final ? "\x01" : "\x00"; 645 $length = pack( 'N', strlen( $current ) ); 646 $iv = random_bytes( 12 ); 647 $tag = ''; 648 $aad = $prefix . $file_name . pack( 'N', $index ) . $flags . $length; 649 $ciphertext = openssl_encrypt( $current, 'aes-256-gcm', $key, OPENSSL_RAW_DATA, $iv, $tag, $aad, 16 ); 650 651 if ( false === $ciphertext || 16 !== strlen( $tag ) || ! $this->wau_write_all( $output, $flags . $length . $iv . $tag . $ciphertext ) ) { 652 return false; 653 } 654 655 $current = $next; 656 $index++; 657 } while ( ! $is_final ); 658 659 return true; 660 } 661 662 /** 663 * Verify an encrypted file against the original content details. 664 */ 665 private function wau_verify_encrypted_file( $file_path, $file_name, $expected_hash, $expected_size ) { 666 $hash_context = hash_init( 'sha256' ); 667 $actual_size = 0; 668 $success = $this->wau_read_encrypted_file( 669 $file_path, 670 $file_name, 671 function ( $chunk ) use ( $hash_context, &$actual_size ) { 672 hash_update( $hash_context, $chunk ); 673 $actual_size += strlen( $chunk ); 674 return true; 675 } 676 ); 677 678 $actual_hash = hash_final( $hash_context ); 679 680 return $success && (int) $expected_size === $actual_size && hash_equals( $expected_hash, $actual_hash ); 681 } 682 683 /** 684 * Read and authenticate an encrypted container chunk by chunk. 685 * 686 * @param string $file_path Container path. 687 * @param string $file_name Logical filename. 688 * @param callable $consumer Receives each plaintext chunk. 689 * @return bool 690 */ 691 private function wau_read_encrypted_file( $file_path, $file_name, $consumer ) { 692 $key = $this->wau_get_encryption_key(); 693 $handle = @fopen( $file_path, 'rb' ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged 694 695 if ( ! $key || ! $handle ) { 696 return false; 697 } 698 699 $prefix = $this->wau_read_exact( $handle, 29 ); 700 if ( false === $prefix || self::WAU_ENCRYPTED_FILE_MAGIC !== substr( $prefix, 0, 8 ) || ! ctype_digit( substr( $prefix, 9, 20 ) ) ) { 701 fclose( $handle ); 702 return false; 703 } 704 705 $algorithm = substr( $prefix, 8, 1 ); 706 $expected_size = (int) ltrim( substr( $prefix, 9, 20 ), '0' ); 707 $total_size = 0; 708 $success = 'S' === $algorithm 709 ? $this->wau_decrypt_stream_sodium( $handle, $key, $prefix, $file_name, $consumer, $total_size ) 710 : ( 'O' === $algorithm ? $this->wau_decrypt_stream_openssl( $handle, $key, $prefix, $file_name, $consumer, $total_size ) : false ); 711 712 fclose( $handle ); 713 714 return $success && $expected_size === $total_size; 715 } 716 717 /** 718 * Decrypt a libsodium container stream. 719 */ 720 private function wau_decrypt_stream_sodium( $handle, $key, $prefix, $file_name, $consumer, &$total_size ) { 721 if ( ! function_exists( 'sodium_crypto_secretstream_xchacha20poly1305_init_pull' ) ) { 722 return false; 723 } 724 725 $header = $this->wau_read_exact( $handle, SODIUM_CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_HEADERBYTES ); 726 if ( false === $header ) { 727 return false; 728 } 729 730 $state = sodium_crypto_secretstream_xchacha20poly1305_init_pull( $header, $key ); 731 while ( true ) { 732 $packed_length = $this->wau_read_exact( $handle, 4 ); 733 if ( false === $packed_length ) { 734 return false; 735 } 736 737 $length = unpack( 'Nlength', $packed_length )['length']; 738 if ( $length > self::WAU_ENCRYPTED_CHUNK_SIZE + SODIUM_CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_ABYTES ) { 739 return false; 740 } 741 742 $ciphertext = $this->wau_read_exact( $handle, $length ); 743 $result = false === $ciphertext ? false : sodium_crypto_secretstream_xchacha20poly1305_pull( $state, $ciphertext, $prefix . $file_name ); 744 if ( false === $result ) { 745 return false; 746 } 747 748 list( $plaintext, $tag ) = $result; 749 $total_size += strlen( $plaintext ); 750 if ( false === call_user_func( $consumer, $plaintext ) ) { 751 return false; 752 } 753 754 if ( SODIUM_CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_TAG_FINAL === $tag ) { 755 return '' === fread( $handle, 1 ); 756 } 757 } 758 } 759 760 /** 761 * Decrypt an AES-256-GCM container stream. 762 */ 763 private function wau_decrypt_stream_openssl( $handle, $key, $prefix, $file_name, $consumer, &$total_size ) { 764 if ( ! function_exists( 'openssl_decrypt' ) ) { 765 return false; 766 } 767 768 $index = 0; 769 while ( true ) { 770 $flags = $this->wau_read_exact( $handle, 1 ); 771 $length = $this->wau_read_exact( $handle, 4 ); 772 if ( false === $flags || false === $length || ( "\x00" !== $flags && "\x01" !== $flags ) ) { 773 return false; 774 } 775 776 $plain_length = unpack( 'Nlength', $length )['length']; 777 if ( $plain_length > self::WAU_ENCRYPTED_CHUNK_SIZE ) { 778 return false; 779 } 780 781 $iv = $this->wau_read_exact( $handle, 12 ); 782 $tag = $this->wau_read_exact( $handle, 16 ); 783 $ciphertext = $this->wau_read_exact( $handle, $plain_length ); 784 if ( false === $iv || false === $tag || false === $ciphertext ) { 785 return false; 786 } 787 788 $aad = $prefix . $file_name . pack( 'N', $index ) . $flags . $length; 789 $plaintext = openssl_decrypt( $ciphertext, 'aes-256-gcm', $key, OPENSSL_RAW_DATA, $iv, $tag, $aad ); 790 if ( false === $plaintext || strlen( $plaintext ) !== $plain_length ) { 791 return false; 792 } 793 794 $total_size += strlen( $plaintext ); 795 if ( false === call_user_func( $consumer, $plaintext ) ) { 796 return false; 797 } 798 799 if ( "\x01" === $flags ) { 800 return '' === fread( $handle, 1 ); 801 } 802 803 $index++; 804 } 805 } 806 807 /** 808 * Read exactly the requested number of bytes from a file handle. 809 */ 810 private function wau_read_exact( $handle, $length ) { 811 $data = ''; 812 813 while ( strlen( $data ) < $length && ! feof( $handle ) ) { 814 $chunk = fread( $handle, $length - strlen( $data ) ); 815 if ( false === $chunk || '' === $chunk ) { 816 break; 817 } 818 $data .= $chunk; 819 } 820 821 return strlen( $data ) === $length ? $data : false; 822 } 823 824 /** 825 * Write all bytes to a file handle. 826 */ 827 private function wau_write_all( $handle, $data ) { 828 $written = 0; 829 $length = strlen( $data ); 830 831 while ( $written < $length ) { 832 $result = fwrite( $handle, substr( $data, $written ) ); 833 if ( false === $result || 0 === $result ) { 834 return false; 835 } 836 $written += $result; 837 } 838 839 return true; 840 } 841 842 /** 843 * Read encrypted-container metadata without decrypting its contents. 844 * 845 * @return array|false 846 */ 847 private function wau_get_encrypted_file_metadata( $file_path ) { 848 if ( '.wau' !== strtolower( substr( $file_path, -4 ) ) ) { 849 return false; 850 } 851 852 $handle = @fopen( $file_path, 'rb' ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged 853 if ( ! $handle ) { 854 return false; 855 } 856 857 $prefix = $this->wau_read_exact( $handle, 29 ); 858 fclose( $handle ); 859 860 if ( false === $prefix || self::WAU_ENCRYPTED_FILE_MAGIC !== substr( $prefix, 0, 8 ) || ! ctype_digit( substr( $prefix, 9, 20 ) ) ) { 861 return false; 862 } 863 864 return array( 865 'algorithm' => substr( $prefix, 8, 1 ), 866 'size' => (int) ltrim( substr( $prefix, 9, 20 ), '0' ), 867 ); 868 } 869 870 /** 337 871 * Get the preferred private upload directory for this site. 338 872 * … … 413 947 'url' => '', 414 948 'storage' => 'private', 949 ); 950 } 951 952 $encrypted_upload_dir = $this->wau_get_encrypted_upload_directory( true ); 953 if ( ! empty( $encrypted_upload_dir['path'] ) ) { 954 return array( 955 'path' => $encrypted_upload_dir['path'], 956 'url' => '', 957 'storage' => 'encrypted', 415 958 ); 416 959 } … … 587 1130 global $wp_filesystem; 588 1131 589 if ( $wp_filesystem && $this->wau_is_allowed_upload_path( $file_path ) ) { 1132 if ( ! $this->wau_is_allowed_upload_path( $file_path ) ) { 1133 return; 1134 } 1135 1136 if ( $wp_filesystem ) { 590 1137 $wp_filesystem->put_contents( $file_path . '.wau-pending', (string) time(), FS_CHMOD_FILE ); 1138 } else { 1139 @file_put_contents( $file_path . '.wau-pending', (string) time(), LOCK_EX ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged, WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents 591 1140 } 592 1141 } … … 616 1165 } 617 1166 618 $private_upload_dir = $this->wau_get_private_upload_directory(); 619 if ( empty( $private_upload_dir['path'] ) || ! is_dir( $private_upload_dir['path'] ) ) { 1167 $private_upload_dir = $this->wau_get_private_upload_directory(); 1168 $encrypted_upload_dir = $this->wau_get_encrypted_upload_directory(); 1169 $cleanup_directories = array_filter( 1170 array( 1171 $private_upload_dir['path'], 1172 $encrypted_upload_dir['path'], 1173 ) 1174 ); 1175 1176 if ( empty( $cleanup_directories ) ) { 620 1177 return; 621 1178 } … … 629 1186 $cutoff = time() - $retention; 630 1187 631 try { 632 $directory = new DirectoryIterator( $private_upload_dir['path'] ); 633 } catch ( Exception $exception ) { 634 return; 635 } 636 637 foreach ( $directory as $file_info ) { 638 $file_name = $file_info->getFilename(); 639 640 if ( $file_info->isDot() || ! $file_info->isFile() || '.wau-pending' !== substr( $file_name, -12 ) ) { 1188 foreach ( $cleanup_directories as $cleanup_directory ) { 1189 if ( ! is_dir( $cleanup_directory ) ) { 641 1190 continue; 642 1191 } 643 1192 644 $marker_path = $file_info->getPathname(); 645 if ( $file_info->getMTime() > $cutoff ) { 1193 try { 1194 $directory = new DirectoryIterator( $cleanup_directory ); 1195 } catch ( Exception $exception ) { 646 1196 continue; 647 1197 } 648 1198 649 $upload_path = substr( $marker_path, 0, -12 ); 650 if ( file_exists( $upload_path ) && $this->wau_is_allowed_upload_path( $upload_path ) ) { 651 wp_delete_file( $upload_path ); 652 } 653 654 wp_delete_file( $marker_path ); 1199 foreach ( $directory as $file_info ) { 1200 $file_name = $file_info->getFilename(); 1201 1202 if ( $file_info->isDot() || ! $file_info->isFile() || '.wau-pending' !== substr( $file_name, -12 ) ) { 1203 continue; 1204 } 1205 1206 $marker_path = $file_info->getPathname(); 1207 if ( $file_info->getMTime() > $cutoff ) { 1208 continue; 1209 } 1210 1211 $upload_path = substr( $marker_path, 0, -12 ); 1212 if ( file_exists( $upload_path ) && $this->wau_is_allowed_upload_path( $upload_path ) ) { 1213 wp_delete_file( $upload_path ); 1214 } 1215 1216 wp_delete_file( $marker_path ); 1217 } 655 1218 } 656 1219 } … … 795 1358 WP_Filesystem(); 796 1359 797 if ( ! $wp_filesystem ) {798 wc_add_notice( __( 'File upload failed. Please try again.', 'woo-addon-uploads' ), 'error' );799 return $cart_item_meta;800 }801 802 1360 // Check if file is uploaded. 803 1361 $post_file = wp_unslash( $_FILES ); … … 889 1447 // Ensure directory exists. 890 1448 if ( empty( $custom_dir ) || ! $this->wau_prepare_upload_directory( $custom_dir ) ) { 891 wc_add_notice( __( 'File upload is temporarily unavailable because private upload storage could not be created. Please contact the store administrator.', 'woo-addon-uploads' ), 'error' );1449 wc_add_notice( __( 'File upload is temporarily unavailable because secure upload storage could not be created. Please contact the store administrator.', 'woo-addon-uploads' ), 'error' ); 892 1450 return $cart_item_meta; 893 1451 } … … 897 1455 if ( false !== $free_space && $free_space - $actual_size < $minimum_free_space ) { 898 1456 wc_add_notice( __( 'File upload is temporarily unavailable because the server is low on storage space.', 'woo-addon-uploads' ), 'error' ); 1457 return $cart_item_meta; 1458 } 1459 1460 if ( 'encrypted' === $storage_dir['storage'] ) { 1461 $original_name = sanitize_file_name( basename( (string) $file['name'] ) ); 1462 $original_name = $original_name ? $original_name : 'upload.' . $file_ext; 1463 1464 try { 1465 $file_name = bin2hex( random_bytes( 16 ) ) . '-' . time() . '-' . $original_name; 1466 } catch ( Throwable $exception ) { 1467 wc_add_notice( __( 'File upload failed. Please try again.', 'woo-addon-uploads' ), 'error' ); 1468 return $cart_item_meta; 1469 } 1470 1471 $encrypted_path = $this->wau_get_encrypted_file_path( $file_name, true ); 1472 if ( ! $encrypted_path || ! $this->wau_encrypt_and_verify_file( $tmp_name, $file_name ) ) { 1473 wc_add_notice( __( 'File upload failed while creating secure encrypted storage. Please try again.', 'woo-addon-uploads' ), 'error' ); 1474 return $cart_item_meta; 1475 } 1476 1477 $access_key = wp_generate_password( 32, false, false ); 1478 $addon_id = array( 1479 'file_path' => $encrypted_path, 1480 'file_url' => '', 1481 'file_name' => $file_name, 1482 'storage' => 'encrypted', 1483 'access_key' => $access_key, 1484 ); 1485 $this->wau_mark_upload_pending( $encrypted_path ); 1486 $cart_item_meta['wau_addon_ids'][] = $addon_id; 1487 1488 return $cart_item_meta; 1489 } 1490 1491 if ( ! $wp_filesystem ) { 1492 wc_add_notice( __( 'File upload failed. Please try again.', 'woo-addon-uploads' ), 'error' ); 899 1493 return $cart_item_meta; 900 1494 } … … 1478 2072 } 1479 2073 2074 $encrypted_path = $this->wau_get_encrypted_file_path( $safe_filename ); 2075 if ( $encrypted_path && file_exists( $encrypted_path ) && is_file( $encrypted_path ) && $this->wau_is_allowed_upload_path( $encrypted_path ) ) { 2076 return $encrypted_path; 2077 } 2078 1480 2079 return false; 1481 2080 } … … 1492 2091 $private_upload_dir = $this->wau_get_private_upload_directory(); 1493 2092 $legacy_upload_dir = $this->wau_get_legacy_upload_directory(); 2093 $encrypted_upload_dir = $this->wau_get_encrypted_upload_directory(); 1494 2094 $directories = array(); 1495 2095 … … 1500 2100 if ( ! empty( $legacy_upload_dir['path'] ) ) { 1501 2101 $directories[] = $legacy_upload_dir['path']; 2102 } 2103 2104 if ( ! empty( $encrypted_upload_dir['path'] ) ) { 2105 $directories[] = $encrypted_upload_dir['path']; 1502 2106 } 1503 2107 … … 1558 2162 } 1559 2163 1560 $file_path = $this->wau_locate_uploaded_file( $safe_filename );2164 $file_path = $this->wau_locate_uploaded_file( $safe_filename ); 1561 2165 1562 2166 // 2. Clear any active output buffers to prevent file corruption/whitespace injections … … 1566 2170 1567 2171 if ( $file_path && file_exists( $file_path ) ) { 1568 $filetype = wp_check_filetype( $ file_path);2172 $filetype = wp_check_filetype( $safe_filename ); 1569 2173 $mime_type = $filetype['type'] ? $filetype['type'] : 'application/octet-stream'; 2174 $encrypted_metadata = $this->wau_get_encrypted_file_metadata( $file_path ); 2175 if ( $encrypted_metadata ) { 2176 $is_valid = $this->wau_read_encrypted_file( 2177 $file_path, 2178 $safe_filename, 2179 function () { 2180 return true; 2181 } 2182 ); 2183 2184 if ( ! $is_valid ) { 2185 wp_die( esc_html__( 'The encrypted file could not be authenticated.', 'woo-addon-uploads' ) ); 2186 } 2187 } 1570 2188 1571 2189 // Only known passive raster formats may render inline. Other formats are downloads. … … 1575 2193 1576 2194 send_nosniff_header(); 2195 nocache_headers(); 1577 2196 header( 'Content-Type: ' . $mime_type ); 1578 header( 'Content-Disposition: ' . $disposition . '; filename="' . basename( $file_path ) . '"' ); 1579 header( 'Content-Length: ' . filesize( $file_path ) ); 1580 readfile( $file_path ); // phpcs:ignore 2197 header( 'Content-Disposition: ' . $disposition . '; filename="' . $safe_filename . '"' ); 2198 2199 if ( $encrypted_metadata ) { 2200 header( 'Content-Length: ' . $encrypted_metadata['size'] ); 2201 $this->wau_read_encrypted_file( 2202 $file_path, 2203 $safe_filename, 2204 function ( $chunk ) { 2205 echo $chunk; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped 2206 return ! connection_aborted(); 2207 } 2208 ); 2209 } else { 2210 header( 'Content-Length: ' . filesize( $file_path ) ); 2211 readfile( $file_path ); // phpcs:ignore 2212 } 1581 2213 exit; 1582 2214 } else { -
woo-addon-uploads/trunk/readme.txt
r3714269 r3740977 5 5 Requires at least: 5.0 6 6 Tested up to: 7.1 7 Stable tag: 1.7. 67 Stable tag: 1.7.7 8 8 Requires PHP: 7.4 9 9 License: GPLv2 or later … … 143 143 == Changelog == 144 144 145 = 1.7.7 (09.10.2026) = 146 * Security fix: Encrypt legacy public uploads with authenticated encryption when private filesystem storage cannot be created, then remove the verified plaintext originals. 147 * Security fix: Store new uploads directly as opaque encrypted containers when private filesystem storage is unavailable, without writing plaintext into the public uploads folder. 148 * Reliability fix: Verify each encrypted migration by byte size and SHA-256 before deleting its public plaintext source. 149 * System Status: Report encrypted fallback availability, encrypted container totals, and the number of public plaintext files still awaiting migration. 150 145 151 = 1.7.6 (26.09.2026) = 146 152 * Security fix: Prevent new uploads from falling back to the public uploads directory when private storage is unavailable. … … 260 266 = What happens if my host does not allow private upload storage? = 261 267 262 Some shared or managed hosts do not allow WordPress to create folders outside the public web root. If private storage is not available, new customer uploads are paused instead of being stored in a public web-accessible folder. 263 264 For files uploaded before this update, the plugin continues to harden the legacy folder's `.htaccess` and `web.config` protection files where supported. On Nginx, Caddy, OpenLiteSpeed, or similar servers, directory-level `.htaccess` files may not be honored, so your host may need to block direct web access to: 268 Some shared or managed hosts do not allow WordPress to create folders outside the public web root. If private storage is not available, the plugin stores new uploads as authenticated encrypted containers with opaque names under: 269 270 ` 271 /wp-content/uploads/wau-uploads/encrypted/<site-specific-folder>/ 272 ` 273 274 The site-specific folder uses the site host, blog ID, and a short hash so subdomain and multisite stores do not share encrypted storage. The original customer filename is not used as the container filename, and plaintext upload contents are never written to that directory. Direct requests can retrieve only authenticated ciphertext, which is decrypted exclusively by the plugin's authorized download handler. 275 276 Files uploaded before this update are moved to private storage when available. Otherwise, they are encrypted and verified in small batches, and each public plaintext original is deleted only after its encrypted copy matches the original byte size and SHA-256 hash. 277 278 While public plaintext files are still awaiting migration, the plugin continues to harden the legacy folder's `.htaccess` and `web.config` protection files where supported. On Nginx, Caddy, OpenLiteSpeed, or similar servers, directory-level `.htaccess` files may not be honored, so your host should temporarily block direct web access to: 265 279 266 280 ` … … 280 294 OpenLiteSpeed and Caddy use different configuration formats; share the displayed URL path with your host instead of copying the Nginx rule. 281 295 282 On Apache or LiteSpeed, the protection file can be present even when the server is configured to ignore it. While legacyfiles remain, test a known legacy file URL and confirm that it returns HTTP 403 or 404. The System Status page keeps this verification visible until migration is complete.296 On Apache or LiteSpeed, the protection file can be present even when the server is configured to ignore it. While public plaintext files remain, test a known legacy file URL and confirm that it returns HTTP 403 or 404. The System Status page keeps this verification visible until migration is complete. 283 297 284 298 You can also review the current storage status and recommended action items from WooCommerce admin under Addon Upload Settings > System Status. … … 298 312 = What happens to files uploaded before this update? = 299 313 300 Existing files in `wp-content/uploads/wau-uploads/` remain accessible to authorized users through the plugin's secure download handler. When private storage is available, the plugin migrates legacy files to the private folder in small batches during normal site activity.314 Existing files in `wp-content/uploads/wau-uploads/` remain accessible to authorized users through the plugin's secure download handler. The plugin migrates them in small batches during normal site activity. It moves them outside the web root when private storage is available; otherwise, it converts them into opaque authenticated encrypted containers and deletes the verified public plaintext originals. 301 315 302 316 Existing order links that already use the plugin's secure download handler continue to work. Older direct links to `/wau-uploads/` are rewritten to secure download links when displayed in order metadata. … … 304 318 Logged-in customers are authorized through order ownership. Guest customers are authorized using the WooCommerce order key included in their generated download link. Each new upload also has its own access key. 305 319 320 The encryption key is stored in the WordPress database and is not embedded in the encrypted files. Back up the WordPress database and uploaded files together; encrypted fallback files cannot be recovered without that database key. 321 306 322 = What file types are supported? = 307 323 -
woo-addon-uploads/trunk/woocommerce-addon-uploads.php
r3714269 r3740977 4 4 * Plugin URI: https://imaginate-solutions.com/downloads/woocommerce-addon-uploads/ 5 5 * Description: WooCommerce addon to upload additional files before adding product to cart 6 * Version: 1.7. 66 * Version: 1.7.7 7 7 * Author: Imaginate Solutions 8 8 * Author URI: https://imaginate-solutions.com … … 45 45 * @var string $version The current version of the plugin. 46 46 */ 47 protected $version = '1.7. 6';47 protected $version = '1.7.7'; 48 48 49 49 /**
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)