*/ protected static function protected_paths() { static $paths = null; if ( null !== $paths ) { return $paths; } global $wpdb; $paths = array(); $rows = $wpdb->get_col( // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- custom plugin table. $wpdb->prepare( "SELECT remediated_path FROM %i WHERE status = 'active' AND remediated_path IS NOT NULL", A11yfy_Map::table() ) ); foreach ( $rows as $row_path ) { $paths[ wp_normalize_path( $row_path ) ] = true; } return $paths; } /** * One-time backfill (schema upgrade): flag every already-remediated * attachment, and — when the on-disk bytes are still pristine — save the * credit-protecting copy of the remediated PDF that older versions of the * plugin did not keep. */ public static function backfill() { foreach ( A11yfy_Map::all() as $row ) { if ( 'active' !== $row['status'] ) { continue; } self::protect( (int) $row['attachment_id'] ); if ( empty( $row['remediated_backup_path'] ) && ! empty( $row['remediated_path'] ) && ! empty( $row['remediated_hash'] ) && file_exists( $row['remediated_path'] ) && hash_file( 'sha256', $row['remediated_path'] ) === $row['remediated_hash'] ) { $backup = A11yfy_Replacer::backup_remediated( (int) $row['attachment_id'], $row['remediated_path'] ); if ( $backup ) { A11yfy_Map::upsert( (int) $row['attachment_id'], array( 'remediated_backup_path' => $backup ) ); } } } } // ── Layer 2: detection + visibility ──────────────────────────────────── /** * Active optimizer plugins that will rewrite (and de-tag) PDFs. * * @return array[] { id, name, pdf_enabled, can_disable, per_file_guard } */ public static function optimizer_warnings() { $warnings = array(); if ( function_exists( 'wpSPIO' ) ) { $pdf_on = true; // ShortPixel default is ON. try { $pdf_on = (bool) \wpSPIO()->settings()->optimizePdfs; } catch ( \Throwable $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch -- foreign plugin internals; keep the safe default. } if ( $pdf_on ) { $warnings[] = array( 'id' => 'shortpixel', 'name' => 'ShortPixel Image Optimizer', 'can_disable' => true, 'per_file_guard' => true, // prevent-meta honored. ); } } if ( defined( 'EWWW_IMAGE_OPTIMIZER_VERSION' ) && function_exists( 'ewww_image_optimizer_get_option' ) && (int) ewww_image_optimizer_get_option( 'ewww_image_optimizer_pdf_level' ) > 0 ) { $warnings[] = array( 'id' => 'ewww', 'name' => 'EWWW Image Optimizer', 'can_disable' => false, 'per_file_guard' => true, // bypass filter honored. ); } if ( defined( 'IMAGIFY_VERSION' ) ) { $warnings[] = array( 'id' => 'imagify', 'name' => 'Imagify', 'can_disable' => false, 'per_file_guard' => false, // no reliable per-file exclusion (bulk ignores the helper). ); } return $warnings; } /** * Turn off ShortPixel's PDF optimization (explicit user action from the * dashboard banner — never silent). * * @return bool Success. */ public static function disable_shortpixel_pdf() { // Only through ShortPixel's own settings API — we never write another // plugin's raw option directly (and there is nothing to disable when // ShortPixel is not active anyway). if ( function_exists( 'wpSPIO' ) ) { try { // The settings model persists itself on shutdown. \wpSPIO()->settings()->optimizePdfs = false; return true; } catch ( \Throwable $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch -- no fallback: never touch another plugin's raw option. } } return false; } public static function site_health_tests( $tests ) { $tests['direct']['a11yfy_pdf_optimizers'] = array( 'label' => __( 'PDF optimization conflicts (a11yfy)', 'a11yfy-pdf-accessibility-checker-fixer' ), 'test' => array( __CLASS__, 'site_health_result' ), ); return $tests; } public static function site_health_result() { $result = array( 'label' => __( 'No image optimizer is rewriting PDF files', 'a11yfy-pdf-accessibility-checker-fixer' ), 'status' => 'good', 'badge' => array( 'label' => __( 'Accessibility', 'a11yfy-pdf-accessibility-checker-fixer' ), 'color' => 'blue', ), 'description' => '

' . esc_html__( 'No active plugin was found that compresses PDF files. PDF compression rewrites the file and strips its accessibility tags.', 'a11yfy-pdf-accessibility-checker-fixer' ) . '

', 'test' => 'a11yfy_pdf_optimizers', ); $warnings = self::optimizer_warnings(); if ( ! $warnings ) { return $result; } $names = wp_list_pluck( $warnings, 'name' ); $at_risk = A11yfy_Map::counts()['remediated'] > 0; $result['status'] = $at_risk ? 'critical' : 'recommended'; $result['label'] = sprintf( /* translators: %s: comma-separated plugin names. */ __( 'PDF optimization is enabled in %s — it strips PDF accessibility tags', 'a11yfy-pdf-accessibility-checker-fixer' ), implode( ', ', $names ) ); $result['description'] = '

' . esc_html__( 'Compressing a PDF rewrites the file and removes its accessibility structure (tags, language, PDF/UA metadata) — including PDFs already remediated with a11yfy credits. Disable PDF optimization in the plugin settings; a11yfy also protects remediated files individually where the optimizer supports it.', 'a11yfy-pdf-accessibility-checker-fixer' ) . '

'; $result['actions'] = sprintf( '

%s

', esc_url( admin_url( 'admin.php?page=a11yfy' ) ), esc_html__( 'Review on the a11yfy Dashboard', 'a11yfy-pdf-accessibility-checker-fixer' ) ); return $result; } // ── Layer 3: tamper detection ────────────────────────────────────────── /** * Have the remediated bytes been rewritten by something else (optimizer, * FTP, …)? Request-cached per attachment — hash_file is not free. * * @param array $map_row Active map row. * @return bool */ public static function is_tampered( array $map_row ) { if ( 'active' !== $map_row['status'] || empty( $map_row['remediated_hash'] ) || empty( $map_row['remediated_path'] ) ) { return false; } static $cache = array(); $id = (int) $map_row['attachment_id']; if ( ! array_key_exists( $id, $cache ) ) { $cache[ $id ] = file_exists( $map_row['remediated_path'] ) && hash_file( 'sha256', $map_row['remediated_path'] ) !== $map_row['remediated_hash']; } return $cache[ $id ]; } }