diff --git a/src/php/UnifiedSnippets/Adapters/DB_Scanner_Adapter.php b/src/php/UnifiedSnippets/Adapters/DB_Scanner_Adapter.php index c429c30e..2d601c87 100644 --- a/src/php/UnifiedSnippets/Adapters/DB_Scanner_Adapter.php +++ b/src/php/UnifiedSnippets/Adapters/DB_Scanner_Adapter.php @@ -13,14 +13,36 @@ */ abstract class DB_Scanner_Adapter extends Scanner_Base { + /** + * The wrapped importer that reads rows from the source plugin's storage. + * + * @var Plugin_Importer + */ protected Plugin_Importer $importer; + /** + * Class constructor. + * + * @param Plugin_Importer|null $importer Optional importer override, primarily for testing. + * When null, the concrete subclass creates one via + * {@see self::create_importer()}. + */ public function __construct( ?Plugin_Importer $importer = null ) { $this->importer = $importer ?? $this->create_importer(); } + /** + * Construct the {@see Plugin_Importer} this adapter wraps. + * + * @return Plugin_Importer + */ abstract protected function create_importer(): Plugin_Importer; + /** + * The source plugin's storage table name (without the WordPress table prefix). + * + * @return string + */ abstract protected function get_table_name(): string; /** @@ -34,10 +56,19 @@ abstract protected function get_table_name(): string; */ abstract protected function map_row( array $row ): ?array; + /** + * {@inheritDoc} + * + * Delegates to the wrapped importer's `is_active()` so the adapter is available exactly + * when the source plugin would be importable. + */ public function is_available(): bool { return (bool) call_user_func( [ get_class( $this->importer ), 'is_active' ] ); } + /** + * {@inheritDoc} + */ public function scan(): array { if ( ! $this->is_available() ) { return []; @@ -59,6 +90,13 @@ public function scan(): array { return $snippets; } + /** + * Merge per-row field overrides on top of adapter-wide defaults. + * + * @param array $fields Row-specific overrides from {@see self::map_row()}. + * + * @return array + */ private function with_defaults( array $fields ): array { return array_merge( [ diff --git a/src/php/UnifiedSnippets/Model/Discovered_Snippet.php b/src/php/UnifiedSnippets/Model/Discovered_Snippet.php index 1d0b9969..0e5cf5e1 100644 --- a/src/php/UnifiedSnippets/Model/Discovered_Snippet.php +++ b/src/php/UnifiedSnippets/Model/Discovered_Snippet.php @@ -34,8 +34,14 @@ class Discovered_Snippet extends Model { private const VALID_TYPES = [ 'php', 'css', 'js', 'html', 'config', 'mixed' ]; private const VALID_SOURCE_TYPES = [ - 'theme', 'child-theme', 'plugin', 'builder', - 'core', 'server', 'customizer', 'mu-plugin', + 'theme', + 'child-theme', + 'plugin', + 'builder', + 'core', + 'server', + 'customizer', + 'mu-plugin', ]; private const VALID_RISK_LEVELS = [ 'low', 'medium', 'high' ]; @@ -146,13 +152,16 @@ protected function prepare_risk_level( string $risk_level ): string { * @return string SHA-256 hash. */ public function generate_hash(): string { - $key = implode( '|', [ - $this->scanner_id, - $this->source_type, - $this->source_path, - $this->line_start, - $this->line_end, - ] ); + $key = implode( + '|', + [ + $this->scanner_id, + $this->source_type, + $this->source_path, + $this->line_start, + $this->line_end, + ] + ); $this->hash = hash( 'sha256', $key ); diff --git a/src/php/UnifiedSnippets/REST/Scan_REST_Controller.php b/src/php/UnifiedSnippets/REST/Scan_REST_Controller.php index df078a1d..5dab5702 100644 --- a/src/php/UnifiedSnippets/REST/Scan_REST_Controller.php +++ b/src/php/UnifiedSnippets/REST/Scan_REST_Controller.php @@ -30,11 +30,15 @@ class Scan_REST_Controller extends REST_Controller { public const BASE_ROUTE = 'scan'; /** + * Registry of available scanners, used to look up and run scanners by ID. + * * @var Scanner_Registry */ private Scanner_Registry $registry; /** + * Persisted scan results store, used to save new scans and read prior results. + * * @var Scan_Results_Store */ private Scan_Results_Store $store; @@ -236,10 +240,12 @@ public function run_single_scanner( WP_REST_Request $request ) { $this->store->merge_scanner_results( $scanner_id, $snippets ); - return rest_ensure_response( [ - 'scanner_id' => $scanner_id, - 'count' => count( $snippets ), - ] ); + return rest_ensure_response( + [ + 'scanner_id' => $scanner_id, + 'count' => count( $snippets ), + ] + ); } /** @@ -263,16 +269,18 @@ public function get_results( WP_REST_Request $request ): WP_REST_Response { $metadata = $this->store->get_metadata(); - return rest_ensure_response( [ - 'scan_date' => $metadata['scan_date'], - 'scanners' => $metadata['scanners'], - 'total_count' => $metadata['total_count'], - 'count' => count( $snippets ), - 'snippets' => array_map( - static fn( $s ) => $s->to_array(), - array_values( $snippets ) - ), - ] ); + return rest_ensure_response( + [ + 'scan_date' => $metadata['scan_date'], + 'scanners' => $metadata['scanners'], + 'total_count' => $metadata['total_count'], + 'count' => count( $snippets ), + 'snippets' => array_map( + static fn( $s ) => $s->to_array(), + array_values( $snippets ) + ), + ] + ); } /** @@ -281,9 +289,11 @@ public function get_results( WP_REST_Request $request ): WP_REST_Response { * @return WP_REST_Response */ public function list_scanners(): WP_REST_Response { - return rest_ensure_response( [ - 'scanners' => $this->registry->get_scanner_info(), - ] ); + return rest_ensure_response( + [ + 'scanners' => $this->registry->get_scanner_info(), + ] + ); } /** @@ -301,11 +311,13 @@ public function get_changes(): WP_REST_Response { $items ); - return rest_ensure_response( [ - 'new' => $serialize( $changes['new'] ), - 'modified' => $serialize( $changes['modified'] ), - 'removed' => $serialize( $changes['removed'] ), - ] ); + return rest_ensure_response( + [ + 'new' => $serialize( $changes['new'] ), + 'modified' => $serialize( $changes['modified'] ), + 'removed' => $serialize( $changes['removed'] ), + ] + ); } /** diff --git a/src/php/UnifiedSnippets/Scanners/Header_Footer_Code_Manager_Scanner.php b/src/php/UnifiedSnippets/Scanners/Header_Footer_Code_Manager_Scanner.php index d42b9998..0382d4b7 100644 --- a/src/php/UnifiedSnippets/Scanners/Header_Footer_Code_Manager_Scanner.php +++ b/src/php/UnifiedSnippets/Scanners/Header_Footer_Code_Manager_Scanner.php @@ -17,22 +17,39 @@ */ class Header_Footer_Code_Manager_Scanner extends DB_Scanner_Adapter { + /** + * {@inheritDoc} + */ public function get_id(): string { return 'hfcm'; } + /** + * {@inheritDoc} + */ public function get_label(): string { return __( 'Header Footer Code Manager', 'code-snippets' ); } + /** + * {@inheritDoc} + */ protected function create_importer(): Plugin_Importer { return new Header_Footer_Code_Manager_Plugin_Importer(); } + /** + * {@inheritDoc} + */ protected function get_table_name(): string { return 'hfcm_scripts'; } + /** + * {@inheritDoc} + * + * @param array $row Raw HFCM row from `{prefix}hfcm_scripts`. + */ protected function map_row( array $row ): ?array { $code = (string) ( $row['snippet'] ?? '' ); diff --git a/src/php/UnifiedSnippets/Scanners/Insert_Headers_And_Footers_Scanner.php b/src/php/UnifiedSnippets/Scanners/Insert_Headers_And_Footers_Scanner.php index 652abf4e..d57f9503 100644 --- a/src/php/UnifiedSnippets/Scanners/Insert_Headers_And_Footers_Scanner.php +++ b/src/php/UnifiedSnippets/Scanners/Insert_Headers_And_Footers_Scanner.php @@ -19,22 +19,39 @@ class Insert_Headers_And_Footers_Scanner extends DB_Scanner_Adapter { private const SUPPORTED_CODE_TYPES = [ 'php', 'css', 'js', 'html', 'universal' ]; + /** + * {@inheritDoc} + */ public function get_id(): string { return 'wpcode'; } + /** + * {@inheritDoc} + */ public function get_label(): string { return __( 'WPCode (Insert Headers and Footers)', 'code-snippets' ); } + /** + * {@inheritDoc} + */ protected function create_importer(): Plugin_Importer { return new Insert_Headers_And_Footers_Plugin_Importer(); } + /** + * {@inheritDoc} + */ protected function get_table_name(): string { return 'wpcode_snippets'; } + /** + * {@inheritDoc} + * + * @param array $row Raw WPCode row from `wpcode` custom posts. + */ protected function map_row( array $row ): ?array { $code_type = (string) ( $row['code_type'] ?? '' ); diff --git a/src/php/UnifiedSnippets/Scanners/Insert_PHP_Code_Snippet_Scanner.php b/src/php/UnifiedSnippets/Scanners/Insert_PHP_Code_Snippet_Scanner.php index f47a2d90..2666068b 100644 --- a/src/php/UnifiedSnippets/Scanners/Insert_PHP_Code_Snippet_Scanner.php +++ b/src/php/UnifiedSnippets/Scanners/Insert_PHP_Code_Snippet_Scanner.php @@ -17,26 +17,48 @@ */ class Insert_PHP_Code_Snippet_Scanner extends DB_Scanner_Adapter { + /** + * {@inheritDoc} + */ public function get_id(): string { return 'insert-php-code-snippet'; } + /** + * {@inheritDoc} + */ public function get_label(): string { return __( 'Insert PHP Code Snippet', 'code-snippets' ); } + /** + * {@inheritDoc} + * + * Discovered snippets execute arbitrary PHP, so the default risk is raised to 'high'. + */ public function get_risk_level(): string { return 'high'; } + /** + * {@inheritDoc} + */ protected function create_importer(): Plugin_Importer { return new Insert_PHP_Code_Snippet_Plugin_Importer(); } + /** + * {@inheritDoc} + */ protected function get_table_name(): string { return 'xyz_ips_short_code'; } + /** + * {@inheritDoc} + * + * @param array $row Raw Insert PHP Code Snippet row from `{prefix}xyz_ips_short_code`. + */ protected function map_row( array $row ): ?array { $code = (string) ( $row['content'] ?? '' ); diff --git a/tests/phpunit/fakes/Fake_Hfcm_Importer.php b/tests/phpunit/fakes/Fake_Hfcm_Importer.php index ce71c374..dd113aef 100644 --- a/tests/phpunit/fakes/Fake_Hfcm_Importer.php +++ b/tests/phpunit/fakes/Fake_Hfcm_Importer.php @@ -4,12 +4,37 @@ use Code_Snippets\REST_API\Import\Plugins\Header_Footer_Code_Manager_Plugin_Importer; +/** + * Test double for {@see Header_Footer_Code_Manager_Plugin_Importer}. + * + * Always reports the source plugin as active and returns whatever rows the test assigns to + * the public {@see self::$rows} property, so HFCM scanner tests do not need a real install. + */ class Fake_Hfcm_Importer extends Header_Footer_Code_Manager_Plugin_Importer { + /** + * Rows the fake importer should return from {@see self::get_data()}. + * + * @var array> + */ + public array $rows = []; + + /** + * Force the source plugin to look active. + * + * @return bool Always true. + */ public static function is_active(): bool { return true; } + /** + * Return the rows assigned to this fake. + * + * @param array $ids_to_import Unused; satisfies the parent signature. + * + * @return array> + */ public function get_data( array $ids_to_import = [] ): array { return $this->rows; } diff --git a/tests/phpunit/fakes/Fake_Ihaf_Importer.php b/tests/phpunit/fakes/Fake_Ihaf_Importer.php index f17e709e..d1d3f55c 100644 --- a/tests/phpunit/fakes/Fake_Ihaf_Importer.php +++ b/tests/phpunit/fakes/Fake_Ihaf_Importer.php @@ -4,14 +4,37 @@ use Code_Snippets\REST_API\Import\Plugins\Insert_Headers_And_Footers_Plugin_Importer; +/** + * Test double for {@see Insert_Headers_And_Footers_Plugin_Importer} (WPCode). + * + * Always reports the source plugin as active and returns whatever rows the test assigns to + * the public {@see self::$rows} property, so WPCode scanner tests do not need a real install. + */ class Fake_Ihaf_Importer extends Insert_Headers_And_Footers_Plugin_Importer { + /** + * Rows the fake importer should return from {@see self::get_data()}. + * + * @var array> + */ public array $rows = []; + /** + * Force the source plugin to look active. + * + * @return bool Always true. + */ public static function is_active(): bool { return true; } + /** + * Return the rows assigned to this fake. + * + * @param array $ids_to_import Unused; satisfies the parent signature. + * + * @return array> + */ public function get_data( array $ids_to_import = [] ): array { return $this->rows; } diff --git a/tests/phpunit/fakes/Fake_Ipcs_Importer.php b/tests/phpunit/fakes/Fake_Ipcs_Importer.php index 7d999c22..233cbe61 100644 --- a/tests/phpunit/fakes/Fake_Ipcs_Importer.php +++ b/tests/phpunit/fakes/Fake_Ipcs_Importer.php @@ -4,14 +4,38 @@ use Code_Snippets\REST_API\Import\Plugins\Insert_PHP_Code_Snippet_Plugin_Importer; +/** + * Test double for {@see Insert_PHP_Code_Snippet_Plugin_Importer}. + * + * Always reports the source plugin as active and returns whatever rows the test assigns to + * the public {@see self::$rows} property, so Insert PHP Code Snippet scanner tests do not + * need a real install. + */ class Fake_Ipcs_Importer extends Insert_PHP_Code_Snippet_Plugin_Importer { + /** + * Rows the fake importer should return from {@see self::get_data()}. + * + * @var array|object> + */ public array $rows = []; + /** + * Force the source plugin to look active. + * + * @return bool Always true. + */ public static function is_active(): bool { return true; } + /** + * Return the rows assigned to this fake. + * + * @param array $ids_to_import Unused; satisfies the parent signature. + * + * @return array|object> + */ public function get_data( array $ids_to_import = [] ): array { return $this->rows; } diff --git a/tests/phpunit/test-db-scanners.php b/tests/phpunit/test-db-scanners.php index 8bee1c43..fde1221b 100644 --- a/tests/phpunit/test-db-scanners.php +++ b/tests/phpunit/test-db-scanners.php @@ -10,8 +10,20 @@ require_once __DIR__ . '/fakes/Fake_Hfcm_Importer.php'; require_once __DIR__ . '/fakes/Fake_Ipcs_Importer.php'; +/** + * Tests for the Tier 2 DB-backed Unified Snippets scanners that adapt existing competitor + * importers (WPCode, HFCM, Insert PHP Code Snippet) through {@see DB_Scanner_Adapter}. + * + * Each test injects a {@see Fake_*_Importer} so the source plugin does not need to be installed. + * + * @group unified-snippets + */ class DB_Scanners_Test extends TestCase { + /** + * WPCode rows whose `code_type` is unsupported, or whose `code` is blank, are filtered + * out so only importable snippets reach the scan results. + */ public function test_wpcode_filters_unsupported_and_empty() { $importer = new Fake_Ihaf_Importer(); $importer->rows = [ @@ -56,6 +68,10 @@ public function test_wpcode_filters_unsupported_and_empty() { $this->assertSame( 'php', $results[1]->type ); } + /** + * HFCM's `status` column maps to `is_active`, and rows with an empty `snippet` are skipped + * so they don't pollute the scan results. + */ public function test_hfcm_active_flag_and_empty_skip() { $importer = new Fake_Hfcm_Importer(); $importer->rows = [ @@ -89,6 +105,11 @@ public function test_hfcm_active_flag_and_empty_skip() { $this->assertFalse( $results[1]->is_active ); } + /** + * Insert PHP Code Snippet rows map `status` (int) to `is_active`, fall back to a generated + * "Insert PHP #N" name when `title` is empty, and inherit the scanner's elevated 'high' risk + * level because the imported code executes PHP. + */ public function test_ipcs_active_flag_name_fallback_and_risk() { $importer = new Fake_Ipcs_Importer(); $importer->rows = [ @@ -115,6 +136,10 @@ public function test_ipcs_active_flag_name_fallback_and_risk() { $this->assertSame( 'Insert PHP #6', $results[1]->name ); } + /** + * When the source plugin is not active, {@see DB_Scanner_Adapter::is_available()} returns + * false and `scan()` short-circuits to an empty array without hitting the database. + */ public function test_adapter_returns_empty_when_unavailable() { if ( ! function_exists( 'is_plugin_active' ) ) { require_once ABSPATH . 'wp-admin/includes/plugin.php';