Hollis Creek Family Dental · Repair report

Exhibit B-1 Evidence: commit fe60ced

Committed · fe60ceda3838463629f575bff5faca3478045833

PHP notices printed above every page

The repository itself is private; this page carries the commit as it is recorded there.

Commit message

Fix B-1 follow-up: delete the public debug.log the old configuration left behind

The browser test for B-1 found that production still served
wp-content/debug.log: changing the configuration stops new lines, but the
file the old configuration wrote (server paths and all) stays public.

Fix: a repair migration step deletes the leftover file; deploy.sh runs it.

Proof: tests/phpunit/repair/Php8DeprecationsTest.php,
e2e/tests/b-php8.spec.ts (404 for /wp-content/debug.log).

Exhibit: B-1

The change

Text files only, as recorded in git. Lines starting with + were added, lines with − removed.

Files changed

---
 site/wp-content/mu-plugins/hollis-migrations.php | 17 +++++++++++++++++
 tests/phpunit/repair/Php8DeprecationsTest.php    | 10 ++++++++++
 2 files changed, 27 insertions(+)
diff --git a/site/wp-content/mu-plugins/hollis-migrations.php b/site/wp-content/mu-plugins/hollis-migrations.php
index 6a79548..772c141 100644
--- a/site/wp-content/mu-plugins/hollis-migrations.php
+++ b/site/wp-content/mu-plugins/hollis-migrations.php
@@ -20,6 +20,7 @@ function hollis_migrations_steps() {
 	return apply_filters(
 		'hollis_migrations_steps',
 		array(
+			'remove-public-debug-log'   => 'hollis_migration_remove_public_debug_log',
 			'remove-social-feed-caches' => 'hollis_migration_remove_social_feed_caches',
 		)
 	);
@@ -39,6 +40,22 @@ function hollis_migrations_run() {
 	return $results;
 }
 
+/**
+ * B-1: the old configuration wrote PHP errors to wp-content/debug.log, a public URL. Logging now goes to
+ * /var/www/logs; the old file, with its server paths, is deleted.
+ *
+ * @return string
+ */
+function hollis_migration_remove_public_debug_log() {
+	$file = WP_CONTENT_DIR . '/debug.log';
+	if ( ! file_exists( $file ) ) {
+		return 'no public debug.log';
+	}
+	$size = (int) filesize( $file );
+	wp_delete_file( $file );
+	return file_exists( $file ) ? 'could not delete wp-content/debug.log' : sprintf( 'deleted wp-content/debug.log (%s)', size_format( $size, 1 ) );
+}
+
 /**
  * C-2: HC Social Feed is removed; its cached responses (autoloaded, ~3.5 MB) go with it.
  *
diff --git a/tests/phpunit/repair/Php8DeprecationsTest.php b/tests/phpunit/repair/Php8DeprecationsTest.php
index 8e2b90f..44e0cb3 100644
--- a/tests/phpunit/repair/Php8DeprecationsTest.php
+++ b/tests/phpunit/repair/Php8DeprecationsTest.php
@@ -48,6 +48,16 @@ class Php8DeprecationsTest extends WP_UnitTestCase {
 		$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 );

The proving test

tests/phpunit/repair/Php8DeprecationsTest.php

tests/phpunit/repair/Php8DeprecationsTest.php, as committed.

<?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, as committed.

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);
});

Back to Exhibit B-1