From 53049d7064c7ae9c83eb65221782a899fc581a50 Mon Sep 17 00:00:00 2001 From: YvesCesar Date: Fri, 13 Nov 2026 20:19:11 -0400 Subject: [PATCH] fix: keep the site fragment sync within the configured static site and repository Signed-off-by: YvesCesar --- inc/footer-fragment.php | 86 ++++++++++--- inc/github-site-webhook.php | 14 +- src/SiteDeploy.php | 4 + src/SiteFragment.php | 41 +++++- tests/Integration/Inc/FooterFragmentTest.php | 120 +++++++++++++++--- .../Integration/Inc/GithubSiteWebhookTest.php | 27 +++- tests/Unit/SiteDeployTest.php | 16 +++ tests/Unit/SiteFragmentTest.php | 42 +++++- 8 files changed, 299 insertions(+), 51 deletions(-) diff --git a/inc/footer-fragment.php b/inc/footer-fragment.php index c93a3c8..4afa107 100644 --- a/inc/footer-fragment.php +++ b/inc/footer-fragment.php @@ -63,7 +63,10 @@ function libresign_theme_site_fragment_storage_base_url( $fragment_type ) { * @return array|WP_Error */ function libresign_theme_site_fragment_fetch_url( $url ) { - $response = wp_remote_get( + add_filter( 'http_request_host_is_external', 'libresign_theme_site_fragment_allow_origin_host', 10, 2 ); + add_filter( 'http_allowed_safe_ports', 'libresign_theme_site_fragment_allow_origin_port', 10, 2 ); + + $response = wp_safe_remote_get( $url, array( 'timeout' => 15, @@ -74,6 +77,9 @@ function libresign_theme_site_fragment_fetch_url( $url ) { ) ); + remove_filter( 'http_request_host_is_external', 'libresign_theme_site_fragment_allow_origin_host', 10 ); + remove_filter( 'http_allowed_safe_ports', 'libresign_theme_site_fragment_allow_origin_port', 10 ); + if ( is_wp_error( $response ) ) { return $response; } @@ -103,6 +109,22 @@ function libresign_theme_site_fragment_fetch_url( $url ) { ); } +function libresign_theme_site_fragment_allow_origin_host( $is_external, $host ) { + $origin_host = (string) wp_parse_url( libresign_theme_site_origin(), PHP_URL_HOST ); + + return $is_external || ( '' !== $origin_host && strtolower( $origin_host ) === strtolower( (string) $host ) ); +} + +function libresign_theme_site_fragment_allow_origin_port( $allowed_ports, $host ) { + $origin = wp_parse_url( libresign_theme_site_origin() ); + + if ( is_array( $origin ) && isset( $origin['host'], $origin['port'] ) && strtolower( $origin['host'] ) === strtolower( (string) $host ) ) { + $allowed_ports[] = (int) $origin['port']; + } + + return $allowed_ports; +} + /** * Determine whether an HTTP error can be ignored for an optional locale. * @@ -198,7 +220,7 @@ function libresign_theme_site_fragment_collect_artifacts( $site_origin, $fragmen return $fragment_response; } - $asset_urls = SiteFragment::asset_urls( (string) $fragment_response['body'] ); + $asset_urls = SiteFragment::asset_urls( (string) $fragment_response['body'], $site_origin ); if ( null === $asset_urls ) { if ( '' !== $locale ) { continue; @@ -266,37 +288,40 @@ function libresign_theme_site_fragment_collect_artifacts( $site_origin, $fragmen * @return array|WP_Error */ function libresign_theme_site_fragment_persist_artifacts( $collected ) { + $staging_dirs = array(); foreach ( libresign_theme_site_fragment_supported_types() as $fragment_type ) { - $base_dir = libresign_theme_site_fragment_storage_base_dir( $fragment_type ); - if ( is_dir( $base_dir ) ) { - libresign_theme_site_fragment_recursive_delete( $base_dir ); + $staging_dir = libresign_theme_site_fragment_storage_base_dir( $fragment_type ) . '-staging'; + libresign_theme_site_fragment_recursive_delete( $staging_dir ); + + if ( ! wp_mkdir_p( $staging_dir ) ) { + libresign_theme_site_fragment_discard_staging( $staging_dirs ); + + return libresign_theme_site_fragment_storage_error( $fragment_type ); } + + $staging_dirs[ $fragment_type ] = $staging_dir; } foreach ( $collected['artifacts'] as $artifact ) { $fragment_type = (string) $artifact['fragment_type']; - $base_dir = trailingslashit( libresign_theme_site_fragment_storage_base_dir( $fragment_type ) ) . $artifact['locale_key']; + $base_dir = trailingslashit( $staging_dirs[ $fragment_type ] ) . $artifact['locale_key']; $base_url = trailingslashit( libresign_theme_site_fragment_storage_base_url( $fragment_type ) ) . $artifact['locale_key']; $prefix = 'header' === $fragment_type ? 'header' : 'footer'; - if ( ! wp_mkdir_p( $base_dir ) ) { - return new WP_Error( - 'libresign_theme_site_fragment_storage_failed', - sprintf( - /* translators: %s: fragment type. */ - __( 'Unable to create local storage for the %s fragment.', 'libresign' ), - $fragment_type - ) - ); + $resolved_dir = wp_mkdir_p( $base_dir ) ? realpath( $base_dir ) : false; + if ( false === $resolved_dir || dirname( $resolved_dir ) !== realpath( $staging_dirs[ $fragment_type ] ) ) { + libresign_theme_site_fragment_discard_staging( $staging_dirs ); + + return libresign_theme_site_fragment_storage_error( $fragment_type ); } $html_file = $prefix . '.html'; $css_file = $prefix . '.css'; $js_file = $prefix . '.js'; - file_put_contents( trailingslashit( $base_dir ) . $html_file, (string) $artifact['html'] ); - file_put_contents( trailingslashit( $base_dir ) . $css_file, (string) $artifact['css'] ); - file_put_contents( trailingslashit( $base_dir ) . $js_file, (string) $artifact['js'] ); + file_put_contents( trailingslashit( $resolved_dir ) . $html_file, (string) $artifact['html'] ); + file_put_contents( trailingslashit( $resolved_dir ) . $css_file, (string) $artifact['css'] ); + file_put_contents( trailingslashit( $resolved_dir ) . $js_file, (string) $artifact['js'] ); $manifest = array( 'fragment_type' => $fragment_type, @@ -316,14 +341,37 @@ function libresign_theme_site_fragment_persist_artifacts( $collected ) { ); file_put_contents( - trailingslashit( $base_dir ) . 'manifest.json', + trailingslashit( $resolved_dir ) . 'manifest.json', wp_json_encode( $manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ) ); } + foreach ( $staging_dirs as $fragment_type => $staging_dir ) { + $base_dir = libresign_theme_site_fragment_storage_base_dir( $fragment_type ); + libresign_theme_site_fragment_recursive_delete( $base_dir ); + rename( $staging_dir, $base_dir ); + } + return $collected['summary']; } +function libresign_theme_site_fragment_storage_error( $fragment_type ) { + return new WP_Error( + 'libresign_theme_site_fragment_storage_failed', + sprintf( + /* translators: %s: fragment type. */ + __( 'Unable to create local storage for the %s fragment.', 'libresign' ), + $fragment_type + ) + ); +} + +function libresign_theme_site_fragment_discard_staging( $staging_dirs ) { + foreach ( $staging_dirs as $staging_dir ) { + libresign_theme_site_fragment_recursive_delete( $staging_dir ); + } +} + /** * Synchronize site fragments from the configured origin. * diff --git a/inc/github-site-webhook.php b/inc/github-site-webhook.php index b51b89c..dbfcd27 100644 --- a/inc/github-site-webhook.php +++ b/inc/github-site-webhook.php @@ -177,8 +177,8 @@ function libresign_theme_register_github_site_webhook_route() { */ function libresign_theme_github_webhook_secret() { $values = array( - get_theme_mod( 'libresign_github_webhook_secret' ), getenv( 'LIBRESIGN_GITHUB_WEBHOOK_SECRET' ), + get_theme_mod( 'libresign_github_webhook_secret' ), get_theme_mod( 'libresign_footer_webhook_secret' ), getenv( 'LIBRESIGN_FOOTER_WEBHOOK_SECRET' ), ); @@ -531,7 +531,10 @@ function libresign_theme_post_pr_sync_comment( string $site_origin, array $sync_ } $pr_number = (int) $info['pr_number']; - $repository = isset( $info['repository'] ) ? (string) $info['repository'] : libresign_theme_site_deploy_repository_name(); + $repository = libresign_theme_site_deploy_repository_name(); + if ( ! SiteDeploy::is_repository_name( $repository ) ) { + return; + } $token = libresign_theme_github_deploy_token(); if ( '' === $token ) { @@ -540,13 +543,8 @@ function libresign_theme_post_pr_sync_comment( string $site_origin, array $sync_ $body = SiteDeploy::sync_comment( $sync_result['synced'] ?? array(), $site_origin ); - $parts = explode( '/', $repository, 2 ); - if ( count( $parts ) !== 2 || '' === $parts[0] || '' === $parts[1] ) { - return; - } - wp_remote_post( - "https://api.github.com/repos/{$parts[0]}/{$parts[1]}/issues/{$pr_number}/comments", + "https://api.github.com/repos/{$repository}/issues/{$pr_number}/comments", array( 'headers' => array( 'Authorization' => 'Bearer ' . $token, diff --git a/src/SiteDeploy.php b/src/SiteDeploy.php index ba75e35..a758e84 100644 --- a/src/SiteDeploy.php +++ b/src/SiteDeploy.php @@ -57,6 +57,10 @@ public static function is_deploy_starting( array $payload, string $repository ): && self::workflow_name( $payload ) === 'Deploy'; } + public static function is_repository_name( string $repository ): bool { + return 1 === preg_match( '/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/', $repository ); + } + public static function merged_pull_request( mixed $pull_requests ): int { if ( ! is_array( $pull_requests ) ) { return 0; diff --git a/src/SiteFragment.php b/src/SiteFragment.php index d9a4f2a..68d2333 100644 --- a/src/SiteFragment.php +++ b/src/SiteFragment.php @@ -6,6 +6,8 @@ final class SiteFragment { public const DEFAULT_LOCALE_KEY = 'default'; + private const LANGUAGE_TAG_PATTERN = '/^[A-Za-z]{2,3}(-[A-Za-z0-9]{2,8})*$/'; + public static function origin( string $origin ): string { return rtrim( trim( $origin ), '/' ); } @@ -30,7 +32,9 @@ public static function language_tag( string $locale ): string { } } - return implode( '-', $parts ); + $tag = implode( '-', $parts ); + + return 1 === preg_match( self::LANGUAGE_TAG_PATTERN, $tag ) ? $tag : ''; } public static function storage_key( string $locale ): string { @@ -48,7 +52,7 @@ public static function url( string $origin, string $fragment_type, string $local /** * @return array{css: string, js: string}|null */ - public static function asset_urls( string $html ): ?array { + public static function asset_urls( string $html, string $origin ): ?array { if ( ! preg_match( '/\bdata-fragment-css=("|\')([^"\']+)\1/i', $html, $css_matches ) || ! preg_match( '/\bdata-fragment-js=("|\')([^"\']+)\1/i', $html, $js_matches ) @@ -56,12 +60,41 @@ public static function asset_urls( string $html ): ?array { return null; } + $css_url = self::same_origin_url( $origin, $css_matches[2] ); + $js_url = self::same_origin_url( $origin, $js_matches[2] ); + if ( '' === $css_url || '' === $js_url ) { + return null; + } + return array( - 'css' => $css_matches[2], - 'js' => $js_matches[2], + 'css' => $css_url, + 'js' => $js_url, ); } + public static function same_origin_url( string $origin, string $url ): string { + $origin = self::origin( $origin ); + $url = trim( $url ); + + if ( str_starts_with( $url, '/' ) && ! str_starts_with( $url, '//' ) ) { + $url = $origin . $url; + } + + $origin_parts = parse_url( $origin ); + $url_parts = parse_url( $url ); + if ( ! is_array( $origin_parts ) || ! is_array( $url_parts ) || empty( $origin_parts['host'] ) ) { + return ''; + } + + foreach ( array( 'scheme', 'host', 'port' ) as $component ) { + if ( strtolower( (string) ( $origin_parts[ $component ] ?? '' ) ) !== strtolower( (string) ( $url_parts[ $component ] ?? '' ) ) ) { + return ''; + } + } + + return $url; + } + /** * @return string[] */ diff --git a/tests/Integration/Inc/FooterFragmentTest.php b/tests/Integration/Inc/FooterFragmentTest.php index c2b1d41..b4de09f 100644 --- a/tests/Integration/Inc/FooterFragmentTest.php +++ b/tests/Integration/Inc/FooterFragmentTest.php @@ -2,28 +2,43 @@ namespace LibreSign\WordPressTheme\Tests\Integration\Inc; +use donatj\MockWebServer\MockWebServer; +use donatj\MockWebServer\Response; use LibreSign\WordPressTheme\Tests\Support\StaticSite; use WP_Error; use WP_UnitTestCase; final class FooterFragmentTest extends WP_UnitTestCase { + private static $internal_service; + private $site; public function set_up() { parent::set_up(); $this->site = new StaticSite(); + set_theme_mod( 'libresign_site_origin', $this->site->origin() ); } public function tear_down() { foreach ( array( 'header', 'footer' ) as $fragment_type ) { libresign_theme_site_fragment_recursive_delete( libresign_theme_site_fragment_storage_base_dir( $fragment_type ) ); } + libresign_theme_site_fragment_recursive_delete( $this->outside_folder() ); parent::tear_down(); } + public static function tear_down_after_class() { + if ( null !== self::$internal_service ) { + self::$internal_service->stop(); + self::$internal_service = null; + } + + parent::tear_down_after_class(); + } + public function test_stores_each_fragment_type_in_its_own_folder() { $this->assertSame( array( 'header', 'footer' ), libresign_theme_site_fragment_supported_types() ); $this->assertStringEndsWith( '/libresign-header', libresign_theme_site_fragment_storage_base_dir( 'header' ) ); @@ -64,6 +79,11 @@ public function test_a_failing_status_is_an_error_with_the_status() { $this->assertSame( 404, $error->get_error_data()['status'] ); } + public function test_does_not_fetch_a_local_address_other_than_the_static_site() { + $this->assertWPError( libresign_theme_site_fragment_fetch_url( $this->internal_url() ) ); + $this->assertNull( self::$internal_service->getLastRequest() ); + } + public function test_a_transport_failure_is_an_error() { $this->assertWPError( libresign_theme_site_fragment_fetch_url( 'https://libresign.coop/fragments/header' ) ); } @@ -146,6 +166,59 @@ public function test_a_locale_that_fails_for_another_reason_stops_the_sync() { $this->assertSame( 500, $error->get_error_data()['status'] ); } + public function test_does_not_fetch_an_asset_from_another_host() { + $this->publish_site(); + $this->site->serve( '/fragments/header', '
' ); + + $error = libresign_theme_site_fragment_collect_artifacts( $this->site->origin(), array( 'header' ) ); + + $this->assertWPError( $error ); + $this->assertSame( 'libresign_theme_site_fragment_missing_assets', $error->get_error_code() ); + $this->assertNull( self::$internal_service->getLastRequest() ); + } + + public function test_fetches_a_root_relative_asset_from_the_static_site() { + $this->publish_site(); + $this->site->serve( '/fragments/footer', '
' ); + + $collected = libresign_theme_site_fragment_collect_artifacts( $this->site->origin(), array( 'footer' ) ); + + $this->assertSame( $this->site->origin() . '/assets/footer.css', $collected['artifacts'][0]['css_url'] ); + $this->assertSame( 'footer{}', $collected['artifacts'][0]['css'] ); + } + + public function test_a_locale_linked_outside_the_fragments_folder_is_not_stored() { + wp_mkdir_p( $this->outside_folder() ); + $this->publish_site(); + $origin = $this->site->origin(); + $header = '
x
'; + $this->site->serve( '/fragments/header', $header ); + $this->site->serve( '/fragments/..%2Foutside/header', $header ); + + libresign_theme_sync_site_fragments_from_origin( $origin ); + + $this->assertFileDoesNotExist( $this->outside_folder() . '/header.html' ); + $this->assertFileExists( libresign_theme_site_fragment_storage_base_dir( 'header' ) . '/default/header.html' ); + } + + public function test_refuses_to_store_a_fragment_outside_its_folder_and_keeps_the_stored_ones() { + wp_mkdir_p( $this->outside_folder() ); + $this->store( array( array( 'header', 'default', '
stored
' ) ) ); + + $result = libresign_theme_site_fragment_persist_artifacts( + array( + 'artifacts' => array( $this->artifact( 'header', '../outside', '
new
' ) ), + 'summary' => array(), + ) + ); + + $this->assertWPError( $result ); + $this->assertSame( 'libresign_theme_site_fragment_storage_failed', $result->get_error_code() ); + $this->assertFileDoesNotExist( $this->outside_folder() . '/header.html' ); + $this->assertSame( '
stored
', file_get_contents( libresign_theme_site_fragment_storage_base_dir( 'header' ) . '/default/header.html' ) ); + $this->assertDirectoryDoesNotExist( libresign_theme_site_fragment_storage_base_dir( 'header' ) . '-staging' ); + } + public function test_a_missing_asset_stops_the_sync() { $this->publish_site(); $this->site->serve( '/assets/footer.js', 'gone', 404 ); @@ -215,21 +288,7 @@ private function store( $fragments ) { $artifacts = array(); foreach ( $fragments as list( $fragment_type, $locale_key, $html ) ) { - $artifacts[] = array( - 'fragment_type' => $fragment_type, - 'locale' => 'default' === $locale_key ? '' : $locale_key, - 'locale_key' => $locale_key, - 'fragment_url' => '', - 'css_url' => '', - 'js_url' => '', - 'generated_at' => '', - 'source_sha' => '', - 'source_url' => '', - 'html' => $html, - 'css' => '', - 'js' => '', - 'version' => '1', - ); + $artifacts[] = $this->artifact( $fragment_type, $locale_key, $html ); } libresign_theme_site_fragment_persist_artifacts( @@ -240,6 +299,37 @@ private function store( $fragments ) { ); } + private function artifact( $fragment_type, $locale_key, $html ) { + return array( + 'fragment_type' => $fragment_type, + 'locale' => 'default' === $locale_key ? '' : $locale_key, + 'locale_key' => $locale_key, + 'fragment_url' => '', + 'css_url' => '', + 'js_url' => '', + 'generated_at' => '', + 'source_sha' => '', + 'source_url' => '', + 'html' => $html, + 'css' => '', + 'js' => '', + 'version' => '1', + ); + } + + private function outside_folder() { + return wp_upload_dir()['basedir'] . '/outside'; + } + + private function internal_url() { + if ( null === self::$internal_service ) { + self::$internal_service = new MockWebServer(); + self::$internal_service->start(); + } + + return self::$internal_service->setResponseOfPath( '/internal', new Response( 'internal' ) ); + } + private function publish_site() { $origin = $this->site->origin(); $header = '
pt
'; diff --git a/tests/Integration/Inc/GithubSiteWebhookTest.php b/tests/Integration/Inc/GithubSiteWebhookTest.php index f2fdfe4..b42c354 100644 --- a/tests/Integration/Inc/GithubSiteWebhookTest.php +++ b/tests/Integration/Inc/GithubSiteWebhookTest.php @@ -76,10 +76,10 @@ public static function provide_origins() { yield 'a javascript url' => array( 'javascript:alert(1)', 'https://libresign.coop' ); } - public function test_the_secret_comes_from_the_theme_setting_first() { + public function test_the_secret_comes_from_the_environment_first() { putenv( 'LIBRESIGN_GITHUB_WEBHOOK_SECRET=from-environment' ); - $this->assertSame( self::SECRET, libresign_theme_github_webhook_secret() ); + $this->assertSame( 'from-environment', libresign_theme_github_webhook_secret() ); } /** @@ -99,6 +99,7 @@ public function test_falls_back_through_the_secret_sources( $theme_mods, $enviro public static function provide_secret_sources() { yield 'the environment' => array( array(), array( 'LIBRESIGN_GITHUB_WEBHOOK_SECRET' => ' env ' ), 'env' ); + yield 'the theme setting' => array( array( 'libresign_github_webhook_secret' => 'theme' ), array( 'LIBRESIGN_FOOTER_WEBHOOK_SECRET' => 'footer-env' ), 'theme' ); yield 'the old footer setting' => array( array( 'libresign_footer_webhook_secret' => 'footer' ), array(), 'footer' ); yield 'the old footer environment' => array( array(), array( 'LIBRESIGN_FOOTER_WEBHOOK_SECRET' => 'footer-env' ), 'footer-env' ); yield 'nothing configured' => array( array(), array(), '' ); @@ -280,6 +281,27 @@ public function test_comments_on_the_deployed_pull_request_after_the_sync() { ); } + public function test_comments_on_the_configured_repository_whatever_the_deployment_info_says() { + $this->publish_site(); + $this->site->serve( '/_deployment-info.json', '{"pr_number":42,"repository":"another-org/another-repo/commits/abc/comments?x="}' ); + update_option( 'libresign_github_deploy_token', $this->encrypted( 'ghp_token' ) ); + + $this->deliver( 'workflow_run', $this->production_deploy() ); + + $this->assertSame( array( '/repos/LibreSign/site/issues/42/comments' ), $this->github->paths() ); + } + + public function test_does_not_comment_when_the_configured_repository_is_not_an_owner_and_a_name() { + set_theme_mod( 'libresign_site_deploy_repository_name', 'LibreSign/site/commits/abc' ); + $this->publish_site(); + $this->site->serve( '/_deployment-info.json', '{"pr_number":42}' ); + update_option( 'libresign_github_deploy_token', $this->encrypted( 'ghp_token' ) ); + + $this->deliver( 'workflow_run', $this->production_deploy( array( 'repository' => array( 'full_name' => 'LibreSign/site/commits/abc' ) ) ) ); + + $this->assertSame( array(), $this->github->requests() ); + } + /** * @dataProvider provide_deployments_without_comment */ @@ -300,7 +322,6 @@ public function test_does_not_comment_without_a_pull_request_or_a_token( $deploy public static function provide_deployments_without_comment() { yield 'no deployment info' => array( null, 'ghp_token' ); yield 'a deployment without a pr' => array( '{"repository":"LibreSign/site"}', 'ghp_token' ); - yield 'a repository without an owner' => array( '{"pr_number":42,"repository":"site"}', 'ghp_token' ); yield 'no token' => array( '{"pr_number":42}', null ); } diff --git a/tests/Unit/SiteDeployTest.php b/tests/Unit/SiteDeployTest.php index b9221c2..b2c31d6 100644 --- a/tests/Unit/SiteDeployTest.php +++ b/tests/Unit/SiteDeployTest.php @@ -31,6 +31,22 @@ public static function provide_signatures() { yield 'no body' => array( '', 'sha256=' . $signature, self::SECRET, false ); } + /** + * @dataProvider provide_repository_names + */ + public function test_recognizes_a_repository_name( $repository, $expected ) { + $this->assertSame( $expected, SiteDeploy::is_repository_name( $repository ) ); + } + + public static function provide_repository_names() { + yield 'an owner and a name' => array( 'LibreSign/site', true ); + yield 'dots, dashes and underscores' => array( 'Libre-Sign/site_v2.0', true ); + yield 'no owner' => array( 'site', false ); + yield 'more segments' => array( 'LibreSign/site/commits/abc/comments', false ); + yield 'a query' => array( 'LibreSign/site?x=', false ); + yield 'empty' => array( '', false ); + } + /** * @dataProvider provide_user_agents */ diff --git a/tests/Unit/SiteFragmentTest.php b/tests/Unit/SiteFragmentTest.php index 9491f35..1ab74d7 100644 --- a/tests/Unit/SiteFragmentTest.php +++ b/tests/Unit/SiteFragmentTest.php @@ -25,6 +25,9 @@ public static function provide_locales() { yield 'surrounding whitespace' => array( ' nb_NO ', 'nb-NO', 'nb-NO' ); yield 'only separators' => array( '-_', '', 'default' ); yield 'empty' => array( '', '', 'default' ); + yield 'a path' => array( '../../themes/libresign/parts', '', 'default' ); + yield 'a line break' => array( "pt-BR`\n@someone", '', 'default' ); + yield 'a subtag too long' => array( 'en-abcdefghi', '', 'default' ); } /** @@ -46,12 +49,40 @@ public function test_reads_the_asset_urls_of_a_fragment() { 'css' => 'https://libresign.coop/header.css', 'js' => 'https://libresign.coop/header.js', ), - SiteFragment::asset_urls( "
" ) + SiteFragment::asset_urls( "
", 'https://libresign.coop/' ) ); } public function test_a_fragment_without_both_asset_urls_has_none() { - $this->assertNull( SiteFragment::asset_urls( '
' ) ); + $this->assertNull( SiteFragment::asset_urls( '
', 'https://libresign.coop' ) ); + } + + public function test_a_fragment_with_an_asset_from_another_site_has_none() { + $this->assertNull( SiteFragment::asset_urls( '
', 'https://libresign.coop' ) ); + } + + /** + * @dataProvider provide_asset_urls + */ + public function test_accepts_only_urls_of_the_static_site( $url, $expected ) { + $this->assertSame( $expected, SiteFragment::same_origin_url( 'https://libresign.coop', $url ) ); + } + + public static function provide_asset_urls() { + yield 'an absolute url' => array( 'https://libresign.coop/assets/header.css', 'https://libresign.coop/assets/header.css' ); + yield 'a root relative url' => array( '/assets/header.css', 'https://libresign.coop/assets/header.css' ); + yield 'the host in upper case' => array( 'https://LIBRESIGN.coop/a.css', 'https://LIBRESIGN.coop/a.css' ); + yield 'another host' => array( 'https://example.com/a.css', '' ); + yield 'an internal host' => array( 'http://mailpit:8025/api/v1/messages', '' ); + yield 'another scheme' => array( 'http://libresign.coop/a.css', '' ); + yield 'another port' => array( 'https://libresign.coop:8443/a.css', '' ); + yield 'a protocol relative url' => array( '//example.com/a.css', '' ); + yield 'a host after credentials' => array( 'https://libresign.coop@example.com/a.css', '' ); + yield 'a path relative url' => array( 'a.css', '' ); + } + + public function test_an_asset_url_needs_an_origin() { + $this->assertSame( '', SiteFragment::same_origin_url( '', '/a.css' ) ); } public function test_reads_the_locales_the_header_links_to() { @@ -63,6 +94,13 @@ public function test_reads_the_locales_the_header_links_to() { ); } + public function test_a_header_link_that_leaves_the_fragments_folder_is_not_a_locale() { + $this->assertSame( + array( '' ), + SiteFragment::linked_locales( 'x' ) + ); + } + public function test_drops_the_asset_attributes_after_syncing() { $this->assertSame( '
x
',