홀리스 크릭 가족 치과 · 수리 보고서

증거물 B-1 근거: 커밋 141fe03

커밋 시각 · 141fe030db3f18891c7780ad395eb78e118bae60

PHP 경고문이 모든 페이지 위에 찍혔습니다

저장소 자체는 비공개입니다. 이 페이지에 커밋을 기록된 그대로 옮겼습니다.

커밋 메시지

차일드 테마의 PHP 8 폐기 경고 수정, 오류를 페이지에 출력하지 않게 함

Fix B-1: PHP 8 deprecations in the child theme; stop printing errors into pages

Root cause: the child theme carried PHP 7 code that PHP 8 deprecates: an
optional parameter before a required one and ${} string interpolation
(both reported when the file compiles), a dynamic property on a class
that never declared it (8.2), and null passed to trim() and strlen() (8.1).
With WP_DEBUG_DISPLAY left on after the host move, every page printed the
notices above the header, and WP_DEBUG_LOG wrote them to
wp-content/debug.log, a URL anyone can download.

Fix: remove the dead tagline code (the option it read no longer exists)
and the phone helper (the tel: link is a constant), turn error display
off and log to /var/www/logs, outside the web root. WP_DEBUG stays on so
the log proves the repaired site raises nothing.

Proof: tests/phpunit/repair/Php8DeprecationsTest.php,
e2e/tests/b-php8.spec.ts.

Exhibit: B-1

변경 내용

git에 기록된 텍스트 파일만 보여 줍니다. +로 시작하는 줄은 추가, −로 시작하는 줄은 삭제입니다.

바뀐 파일

---
 e2e/tests/b-php8.spec.ts                          | 21 ++++++++++++++++
 site/wp-content/hollis-config.php                 | 15 ++++++++----
 site/wp-content/themes/hollis-child/functions.php | 28 +---------------------
 tests/phpunit/repair/Php8DeprecationsTest.php     | 56 +++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 89 insertions(+), 31 deletions(-)
diff --git a/e2e/tests/b-php8.spec.ts b/e2e/tests/b-php8.spec.ts
new file mode 100644
index 0000000..9971548
--- /dev/null
+++ b/e2e/tests/b-php8.spec.ts
@@ -0,0 +1,21 @@
+import { test, expect } from '@playwright/test';
+import { BEFORE, AFTER } from './helpers';
+
+// Exhibit B: PHP 8 errors as a visitor meets them.
+
+test('B-1 before: PHP deprecation notices are printed above the page and the log is public', async ({ page, request }) => {
+  await page.goto(BEFORE + '/');
+  await expect(page.locator('body')).toContainText('Deprecated:');
+  const log = await request.get(BEFORE + '/wp-content/debug.log');
+  expect(log.status()).toBe(200);
+  expect(await log.text()).toContain('PHP Deprecated');
+});
+
+test('B-1 after: no notice on any page and no public debug log', async ({ page, request }) => {
+  for (const path of ['/', '/our-team/', '/services/', '/request-appointment/', '/news/']) {
+    await page.goto(AFTER + path);
+    await expect(page.locator('body')).not.toContainText(/Deprecated:|Warning:|Notice:/);
+  }
+  const log = await request.get(AFTER + '/wp-content/debug.log');
+  expect(log.status()).toBe(404);
+});
diff --git a/site/wp-content/hollis-config.php b/site/wp-content/hollis-config.php
index a170ccf..014eae1 100644
--- a/site/wp-content/hollis-config.php
+++ b/site/wp-content/hollis-config.php
@@ -1,13 +1,20 @@
 <?php
 /**
- * Site configuration, required from wp-config.php.
+ * Site configuration, required from wp-config.php. Values come from the environment (docker-compose.yml).
  *
  * @package Hollis
  */
 
-// Turned on while debugging the 2024 host move, never turned off.
-define( 'WP_DEBUG_DISPLAY', true );
-define( 'WP_DEBUG_LOG', true );
+// Errors are logged outside the web root and never printed into pages.
+// WP_DEBUG stays on so the log proves the site runs without notices.
+if ( ! defined( 'WP_DEBUG_DISPLAY' ) ) {
+	define( 'WP_DEBUG_DISPLAY', false );
+}
+if ( ! defined( 'WP_DEBUG_LOG' ) ) {
+	define( 'WP_DEBUG_LOG', '/var/www/logs/' . basename( dirname( __DIR__ ) ) . '.log' );
+}
 
+// phpcs:disable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound -- WordPress's own settings.
 define( 'WP_AUTO_UPDATE_CORE', false );
 define( 'AUTOMATIC_UPDATER_DISABLED', true );
+// phpcs:enable
diff --git a/site/wp-content/themes/hollis-child/functions.php b/site/wp-content/themes/hollis-child/functions.php
index 048b342..bc40d13 100644
--- a/site/wp-content/themes/hollis-child/functions.php
+++ b/site/wp-content/themes/hollis-child/functions.php
@@ -27,32 +27,6 @@ function hollis_child_assets() {
 }
 add_action( 'wp_enqueue_scripts', 'hollis_child_assets', 20 );
 
-/**
- * Old settings object for the header tagline.
- */
-class Hollis_Child_Settings {
-	public $phone = '(555) 010-4417';
-}
-
-/**
- * Formats the phone number for the header link.
- */
-function hollis_child_format_phone( $prefix = '+1', $number ) {
-	$digits = preg_replace( '/\D/', '', $number );
-	return "${prefix}${digits}";
-}
-
-/**
- * Header tagline, from the 2019 theme options.
- */
-function hollis_child_tagline() {
-	$settings          = new Hollis_Child_Settings();
-	$settings->tagline = get_option( 'hollis_child_options' );
-	$legacy            = is_array( $settings->tagline ) ? $settings->tagline : array();
-	return trim( $legacy['tagline'] ?? null ) . ' ' . strlen( $legacy['subtitle'] ?? null );
-}
-add_action( 'wp_head', 'hollis_child_tagline' );
-
 /**
  * Practice contact details used by the parent theme's header and footer.
  *
@@ -64,7 +38,7 @@ function hollis_child_contact( $contact ) {
 		$contact,
 		array(
 			'phone'      => '(555) 010-4417',
-			'phone_href' => 'tel:' . hollis_child_format_phone( '+1', '555 010 4417' ),
+			'phone_href' => 'tel:+15550104417',
 			'address'    => hollis_child_is_ko() ? '418 Mill Race Road, Alder Bend (가상의 마을)' : '418 Mill Race Road, Alder Bend',
 			'cta_url'    => home_url( '/request-appointment/' ),
 		)
diff --git a/tests/phpunit/repair/Php8DeprecationsTest.php b/tests/phpunit/repair/Php8DeprecationsTest.php
new file mode 100644
index 0000000..11e255a
--- /dev/null
+++ b/tests/phpunit/repair/Php8DeprecationsTest.php
@@ -0,0 +1,56 @@
+<?php
+/**
+ * Exhibit B-1: the code runs on PHP 8.3 without deprecation notices, and errors never reach the page.
+ *
+ * @package Hollis\Tests
+ */
+
+/**
+ * PHP 8 deprecations.
+ */
+class Php8DeprecationsTest extends WP_UnitTestCase {
+
+	/**
+	 * Every PHP file of the repaired code.
+	 *
+	 * @return string[]
+	 */
+	private function php_files() {
+		$files = array();
+		$it    = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( HOLLIS_CODE, FilesystemIterator::SKIP_DOTS ) );
+		foreach ( $it as $file ) {
+			if ( 'php' === $file->getExtension() ) {
+				$files[] = $file->getPathname();
+			}
+		}
+		return $files;
+	}
+
+	public function test_no_file_has_compile_time_deprecations() {
+		$files = $this->php_files();
+		$this->assertGreaterThan( 10, count( $files ) );
+		foreach ( $files as $file ) {
+			// A fresh PHP process compiles each file, so notices raised at compile time are seen too.
+			$out = shell_exec( escapeshellarg( PHP_BINARY ) . ' -d error_reporting=-1 -d display_errors=1 -l ' . escapeshellarg( $file ) . ' 2>&1' );
+			$this->assertStringNotContainsString( 'Deprecated', (string) $out, $out );
+			$this->assertStringContainsString( 'No syntax errors', (string) $out, $out );
+		}
+	}
+
+	public function test_page_header_renders_without_notices() {
+		// Deprecations, notices and warnings become exceptions in this suite (phpunit.xml.dist).
+		$this->go_to( home_url( '/our-team/' ) );
+		ob_start();
+		get_header();
+		get_footer();
+		$html = ob_get_clean();
+		$this->assertStringContainsString( 'Hollis Creek', $html );
+		$this->assertStringNotContainsString( 'Deprecated:', $html );
+	}
+
+	public function test_errors_are_logged_outside_the_web_root_and_not_displayed() {
+		$config = file_get_contents( HOLLIS_CODE . '/hollis-config.php' );
+		$this->assertMatchesRegularExpression( "/define\(\s*'WP_DEBUG_DISPLAY',\s*false\s*\)/", $config );
+		$this->assertMatchesRegularExpression( "#define\(\s*'WP_DEBUG_LOG',\s*'/var/www/logs/#", $config );
+	}
+}

증명 테스트

tests/phpunit/repair/Php8DeprecationsTest.php

tests/phpunit/repair/Php8DeprecationsTest.php, 커밋된 그대로.

<?php
/**
 * Exhibit B-1: the code runs on PHP 8.3 without deprecation notices, and errors never reach the page.
 *
 * @package Hollis\Tests
 */

/**
 * PHP 8 deprecations.
 */
class Php8DeprecationsTest extends WP_UnitTestCase {

	/**
	 * Every PHP file of the repaired code.
	 *
	 * @return string[]
	 */
	private function php_files() {
		$files = array();
		$it    = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( HOLLIS_CODE, FilesystemIterator::SKIP_DOTS ) );
		foreach ( $it as $file ) {
			if ( 'php' === $file->getExtension() ) {
				$files[] = $file->getPathname();
			}
		}
		return $files;
	}

	public function test_no_file_has_compile_time_deprecations() {
		$files = $this->php_files();
		$this->assertGreaterThan( 10, count( $files ) );
		foreach ( $files as $file ) {
			// A fresh PHP process compiles each file, so notices raised at compile time are seen too.
			$out = shell_exec( escapeshellarg( PHP_BINARY ) . ' -d error_reporting=-1 -d display_errors=1 -l ' . escapeshellarg( $file ) . ' 2>&1' );
			$this->assertStringNotContainsString( 'Deprecated', (string) $out, $out );
			$this->assertStringContainsString( 'No syntax errors', (string) $out, $out );
		}
	}

	public function test_page_header_renders_without_notices() {
		// Deprecations, notices and warnings become exceptions in this suite (phpunit.xml.dist).
		$this->go_to( home_url( '/our-team/' ) );
		ob_start();
		include get_template_directory() . '/header.php';
		include get_template_directory() . '/footer.php';
		$html = ob_get_clean();
		$this->assertStringContainsString( 'Hollis Creek', $html );
		$this->assertStringNotContainsString( 'Deprecated:', $html );
	}

	public function test_migration_deletes_a_leftover_public_debug_log() {
		$file = WP_CONTENT_DIR . '/debug.log';
		file_put_contents( $file, "PHP Deprecated: something in /var/www/after/wp-content/themes/x.php
" );
		$results = hollis_migrations_run();
		$this->assertStringContainsString( 'deleted wp-content/debug.log', $results['remove-public-debug-log'] );
		$this->assertFileDoesNotExist( $file );
		$this->assertSame( 'no public debug.log', hollis_migrations_run()['remove-public-debug-log'] );
	}

	public function test_errors_are_logged_outside_the_web_root_and_not_displayed() {
		$config = file_get_contents( HOLLIS_CODE . '/hollis-config.php' );
		$this->assertMatchesRegularExpression( "/define\(\s*'WP_DEBUG_DISPLAY',\s*false\s*\)/", $config );
		$this->assertMatchesRegularExpression( "#define\(\s*'WP_DEBUG_LOG',\s*'/var/www/logs/#", $config );
	}
}

e2e/tests/b-php8.spec.ts

e2e/tests/b-php8.spec.ts, 커밋된 그대로.

import { test, expect } from '@playwright/test';
import { BEFORE, AFTER } from './helpers';

// Exhibit B: PHP 8 errors as a visitor meets them.

test('B-3 before: the Insurance page is a white screen with a fatal error', async ({ page }) => {
  const res = await page.goto(BEFORE + '/insurance/');
  expect(res!.status()).toBe(500);
  await expect(page.locator('body')).toContainText('There has been a critical error on this website.');
  await expect(page.getByRole('heading', { name: 'Insurance and payment' })).toHaveCount(0);
  await expect(page.getByRole('table')).toHaveCount(0);
});

test('B-3 after: the Insurance page shows the plans, sorted by name', async ({ page }) => {
  const res = await page.goto(AFTER + '/insurance/');
  expect(res!.status()).toBe(200);
  await expect(page.getByRole('heading', { level: 1, name: 'Insurance and payment' })).toBeVisible();
  const table = page.getByRole('table', { name: 'Insurance we work with' });
  const plans = await table.locator('tbody tr td:first-child').allTextContents();
  expect(plans.length).toBe(7);
  expect(plans).toEqual([...plans].sort((a, b) => a.localeCompare(b)));
  expect(plans[0]).toBe('Alder County Employees Plan');
});

test('B-1 before: PHP deprecation notices are printed above the page and the log is public', async ({ page, request }) => {
  await page.goto(BEFORE + '/');
  await expect(page.locator('body')).toContainText('Deprecated:');
  const log = await request.get(BEFORE + '/wp-content/debug.log');
  expect(log.status()).toBe(200);
  expect(await log.text()).toContain('PHP Deprecated');
});

test('B-1 after: no notice on any page and no public debug log', async ({ page, request }) => {
  for (const path of ['/', '/our-team/', '/services/', '/request-appointment/', '/news/']) {
    await page.goto(AFTER + path);
    await expect(page.locator('body')).not.toContainText(/Deprecated:|Warning:|Notice:/);
  }
  const log = await request.get(AFTER + '/wp-content/debug.log');
  expect(log.status()).toBe(404);
});

증거물 B-1로 돌아가기