diff --git a/src/php/Plugin.php b/src/php/Plugin.php index 35fce33a..b8fd5ac5 100644 --- a/src/php/Plugin.php +++ b/src/php/Plugin.php @@ -23,7 +23,10 @@ use Code_Snippets\UnifiedSnippets\REST\Scan_REST_Controller; use Code_Snippets\UnifiedSnippets\Scanners\Additional_CSS_Scanner; use Code_Snippets\UnifiedSnippets\Scanners\Functions_Php_Scanner; +use Code_Snippets\UnifiedSnippets\Scanners\Header_Footer_Code_Manager_Scanner; use Code_Snippets\UnifiedSnippets\Scanners\Htaccess_Scanner; +use Code_Snippets\UnifiedSnippets\Scanners\Insert_Headers_And_Footers_Scanner; +use Code_Snippets\UnifiedSnippets\Scanners\Insert_PHP_Code_Snippet_Scanner; use Code_Snippets\UnifiedSnippets\Scanners\Mu_Plugins_Scanner; use Code_Snippets\UnifiedSnippets\Scanners\Wp_Config_Scanner; @@ -179,6 +182,9 @@ private function init_unified_snippets(): void { $this->unified_snippets->register( new Htaccess_Scanner() ); $this->unified_snippets->register( new Wp_Config_Scanner() ); $this->unified_snippets->register( new Mu_Plugins_Scanner() ); + $this->unified_snippets->register( new Insert_Headers_And_Footers_Scanner() ); + $this->unified_snippets->register( new Header_Footer_Code_Manager_Scanner() ); + $this->unified_snippets->register( new Insert_PHP_Code_Snippet_Scanner() ); new Scan_REST_Controller( $this->unified_snippets, $this->unified_snippets_store ); } diff --git a/src/php/UnifiedSnippets/Adapters/DB_Scanner_Adapter.php b/src/php/UnifiedSnippets/Adapters/DB_Scanner_Adapter.php new file mode 100644 index 00000000..c429c30e --- /dev/null +++ b/src/php/UnifiedSnippets/Adapters/DB_Scanner_Adapter.php @@ -0,0 +1,109 @@ +importer = $importer ?? $this->create_importer(); + } + + abstract protected function create_importer(): Plugin_Importer; + + abstract protected function get_table_name(): string; + + /** + * Map a single raw row from the importer into Discovered_Snippet field overrides. + * + * Return null to skip the row (unsupported type, missing code, etc.). + * + * @param array $row Raw row cast to associative array. + * + * @return array|null + */ + abstract protected function map_row( array $row ): ?array; + + public function is_available(): bool { + return (bool) call_user_func( [ get_class( $this->importer ), 'is_active' ] ); + } + + public function scan(): array { + if ( ! $this->is_available() ) { + return []; + } + + $snippets = []; + + foreach ( $this->importer->get_data() as $row ) { + $row_array = is_array( $row ) ? $row : (array) $row; + $fields = $this->map_row( $row_array ); + + if ( null === $fields ) { + continue; + } + + $snippets[] = $this->build_snippet( $this->with_defaults( $fields ) ); + } + + return $snippets; + } + + private function with_defaults( array $fields ): array { + return array_merge( + [ + 'source_type' => 'plugin', + 'source_name' => $this->get_label(), + 'line_start' => 0, + 'line_end' => 0, + ], + $fields + ); + } + + /** + * Build a synthetic URI identifying a row in the source plugin's table. + * + * @param int|string $id Row identifier. + * + * @return string e.g. 'db://hfcm_scripts/42'. + */ + protected function build_source_path( $id ): string { + return 'db://' . $this->get_table_name() . '/' . $id; + } + + /** + * Derive a {@see Discovered_Snippet} `type` from the source plugin's code-type value. + * + * @param string $code_type Source-plugin code type, e.g. 'html', 'universal'. + * + * @return string + */ + protected function derive_type( string $code_type ): string { + switch ( strtolower( $code_type ) ) { + case 'css': + return 'css'; + case 'js': + case 'javascript': + return 'js'; + case 'html': + case 'universal': + return 'html'; + case 'php': + case '': + return 'php'; + default: + return 'mixed'; + } + } +} diff --git a/src/php/UnifiedSnippets/Scanners/Header_Footer_Code_Manager_Scanner.php b/src/php/UnifiedSnippets/Scanners/Header_Footer_Code_Manager_Scanner.php new file mode 100644 index 00000000..d42b9998 --- /dev/null +++ b/src/php/UnifiedSnippets/Scanners/Header_Footer_Code_Manager_Scanner.php @@ -0,0 +1,54 @@ + '' !== $title ? $title : sprintf( 'HFCM #%d', $id ), + 'code' => $code, + 'type' => $this->derive_type( (string) ( $row['snippet_type'] ?? '' ) ), + 'source_path' => $this->build_source_path( $id ), + 'is_active' => 'active' === ( $row['status'] ?? '' ), + ]; + } +} diff --git a/src/php/UnifiedSnippets/Scanners/Insert_Headers_And_Footers_Scanner.php b/src/php/UnifiedSnippets/Scanners/Insert_Headers_And_Footers_Scanner.php new file mode 100644 index 00000000..652abf4e --- /dev/null +++ b/src/php/UnifiedSnippets/Scanners/Insert_Headers_And_Footers_Scanner.php @@ -0,0 +1,62 @@ + '' !== $title ? $title : sprintf( 'WPCode #%d', (int) $id ), + 'code' => $code, + 'type' => $this->derive_type( $code_type ), + 'source_path' => $this->build_source_path( (int) $id ), + 'is_active' => true, + ]; + } +} diff --git a/src/php/UnifiedSnippets/Scanners/Insert_PHP_Code_Snippet_Scanner.php b/src/php/UnifiedSnippets/Scanners/Insert_PHP_Code_Snippet_Scanner.php new file mode 100644 index 00000000..f47a2d90 --- /dev/null +++ b/src/php/UnifiedSnippets/Scanners/Insert_PHP_Code_Snippet_Scanner.php @@ -0,0 +1,58 @@ + '' !== $title ? $title : sprintf( 'Insert PHP #%d', $id ), + 'code' => $code, + 'type' => 'php', + 'source_path' => $this->build_source_path( $id ), + 'is_active' => 1 === (int) ( $row['status'] ?? 0 ), + ]; + } +} diff --git a/tests/phpunit/fakes/Fake_Hfcm_Importer.php b/tests/phpunit/fakes/Fake_Hfcm_Importer.php new file mode 100644 index 00000000..ce71c374 --- /dev/null +++ b/tests/phpunit/fakes/Fake_Hfcm_Importer.php @@ -0,0 +1,16 @@ +rows; + } +} diff --git a/tests/phpunit/fakes/Fake_Ihaf_Importer.php b/tests/phpunit/fakes/Fake_Ihaf_Importer.php new file mode 100644 index 00000000..f17e709e --- /dev/null +++ b/tests/phpunit/fakes/Fake_Ihaf_Importer.php @@ -0,0 +1,18 @@ +rows; + } +} diff --git a/tests/phpunit/fakes/Fake_Ipcs_Importer.php b/tests/phpunit/fakes/Fake_Ipcs_Importer.php new file mode 100644 index 00000000..7d999c22 --- /dev/null +++ b/tests/phpunit/fakes/Fake_Ipcs_Importer.php @@ -0,0 +1,18 @@ +rows; + } +} diff --git a/tests/phpunit/test-db-scanners.php b/tests/phpunit/test-db-scanners.php new file mode 100644 index 00000000..8bee1c43 --- /dev/null +++ b/tests/phpunit/test-db-scanners.php @@ -0,0 +1,128 @@ +rows = [ + [ + 'code' => "console.log('hi');", + 'code_type' => 'js', + 'table_data' => [ + 'id' => 1, + 'title' => 'JS', + ], + ], + [ + 'code' => '// php', + 'code_type' => 'php', + 'table_data' => [ + 'id' => 2, + 'title' => 'PHP', + ], + ], + [ + 'code' => '', + 'code_type' => 'text', + 'table_data' => [ + 'id' => 3, + 'title' => 'Unsupported', + ], + ], + [ + 'code' => ' ', + 'code_type' => 'php', + 'table_data' => [ + 'id' => 4, + 'title' => 'Blank', + ], + ], + ]; + + $results = ( new Insert_Headers_And_Footers_Scanner( $importer ) )->scan(); + + $this->assertCount( 2, $results ); + $this->assertSame( 'js', $results[0]->type ); + $this->assertSame( 'php', $results[1]->type ); + } + + public function test_hfcm_active_flag_and_empty_skip() { + $importer = new Fake_Hfcm_Importer(); + $importer->rows = [ + [ + 'script_id' => 1, + 'name' => 'On', + 'snippet' => '1', + 'snippet_type' => 'html', + 'status' => 'active', + ], + [ + 'script_id' => 2, + 'name' => 'Off', + 'snippet' => '2', + 'snippet_type' => 'html', + 'status' => 'inactive', + ], + [ + 'script_id' => 3, + 'name' => 'Blank', + 'snippet' => '', + 'snippet_type' => 'html', + 'status' => 'active', + ], + ]; + + $results = ( new Header_Footer_Code_Manager_Scanner( $importer ) )->scan(); + + $this->assertCount( 2, $results ); + $this->assertTrue( $results[0]->is_active ); + $this->assertFalse( $results[1]->is_active ); + } + + public function test_ipcs_active_flag_name_fallback_and_risk() { + $importer = new Fake_Ipcs_Importer(); + $importer->rows = [ + (object) [ + 'id' => 5, + 'title' => 'Custom Hook', + 'content' => ' 1, + ], + (object) [ + 'id' => 6, + 'title' => '', + 'content' => ' 0, + ], + ]; + + $scanner = new Insert_PHP_Code_Snippet_Scanner( $importer ); + $results = $scanner->scan(); + + $this->assertSame( 'high', $scanner->get_risk_level() ); + $this->assertTrue( $results[0]->is_active ); + $this->assertFalse( $results[1]->is_active ); + $this->assertSame( 'Insert PHP #6', $results[1]->name ); + } + + public function test_adapter_returns_empty_when_unavailable() { + if ( ! function_exists( 'is_plugin_active' ) ) { + require_once ABSPATH . 'wp-admin/includes/plugin.php'; + } + + $hfcm = new Header_Footer_Code_Manager_Scanner(); + + $this->assertFalse( $hfcm->is_available() ); + $this->assertSame( [], $hfcm->scan() ); + } +}