Skip to content

Commit 35f7356

Browse files
authored
Define DB_NAME constant when it's missing (#140)
## Motivation for the change, related issues **This is a WIP draft at the moment. Looking for feedback on the direction I'm taking here.** The new SQLite driver requires the `DB_NAME` constant to be set. This is to be able to store the correct database name in the information schema. However, some WordPress site backups and exports don't include the constant in `wp-config.php`. This happens with hosts that inject the database configuration in a different way. Even though we could probably emulate it dynamically in the SQLite driver, this may be non-trivial, e.g., in cases someone joins on the `information_schema.tables` table, etc. Making sure that the constant is defined seems to be easier, and should also improve the compatibility with any plugins that rely on the `DB_NAME` constant to be set. --- This was discovered when importing a site backup without the `DB_NAME` constant in Studio: ``` Fatal error: Uncaught Error: Undefined constant "DB_NAME" in /var/www/html/wp-content/mu-plugins/sqlite-database-integration/wp-includes/sqlite/db.php:64 Stack trace: #0 /var/www/html/wp-content/db.php(37): require_once() #1 /var/www/html/wp-includes/load.php(683): require_once('/var/www/html/w...') #2 /var/www/html/wp-settings.php(133): require_wp_db() #3 /var/www/html/wp-config.php(85): require_once('/var/www/html/w...') #4 /var/www/html/wp-load.php(50): require_once('/var/www/html/w...') #5 /var/www/html/wp-blog-header.php(13): require_once('/var/www/html/w...') #6 /var/www/html/index.php(17): require('/var/www/html/w...') #7 {main} thrown in /var/www/html/wp-content/mu-plugins/sqlite-database-integration/wp-includes/sqlite/db.php on line 64 ``` Related: - Automattic/sqlite-database-integration#40 - Automattic/sqlite-database-integration#44 ## Implementation details ## Testing Instructions (or ideally a Blueprint) 1. Boot Playground (`npm run dev`). 2. Export the site into a ZIP. 3. Remove the `DB_NAME` constant definition from the ZIP. 4. Import the site without the `DB_NAME` constant set. 5. Verify that a `DB_NAME` constant was injected (TODO: describe how). 6. Export the site ZIP and have the `DB_NAME` constant defined in `wp-config.php` (TODO: is this required?).
1 parent b9dddb7 commit 35f7356

File tree

9 files changed

+415
-226
lines changed

9 files changed

+415
-226
lines changed

packages/php-wasm/node/src/test/php-request-handler.spec.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { RecommendedPHPVersion } from '@wp-playground/common';
22
// eslint-disable-next-line @nx/enforce-module-boundaries -- ignore test-related interdependencies so we can test.
3-
import { getFileNotFoundActionForWordPress } from '@wp-playground/wordpress';
43
import { loadNodeRuntime } from '..';
54
import type {
65
CookieStore,
@@ -15,6 +14,27 @@ import {
1514
} from '@php-wasm/universal';
1615
import { createSpawnHandler, joinPaths } from '@php-wasm/util';
1716

17+
/*
18+
* This is a copy-paste from "@wp-playground/wordpress" in the "boot.ts" file
19+
* to avoid adding a dependency on "@wp-playground/wordpress" and causing
20+
* circular dependency linter errors.
21+
*
22+
* TODO: Remove this when we enable ciruclar deps in test files; after the
23+
* package dependency refactor PR is merged:
24+
* https://github.com/Automattic/wordpress-playground-private/pull/148
25+
*/
26+
export function getFileNotFoundActionForWordPress(
27+
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- maintain consistent FileNotFoundGetActionCallback signature
28+
relativeUri: string
29+
) {
30+
// Delegate unresolved requests to WordPress. This makes WP magic possible,
31+
// like pretty permalinks and dynamically generated sitemaps.
32+
return {
33+
type: 'internal-redirect',
34+
uri: '/index.php',
35+
} as const;
36+
}
37+
1838
interface ConfigForRequestTests {
1939
phpVersion: (typeof SupportedPHPVersions)[number];
2040
docRoot: string;

packages/playground/blueprints/src/lib/steps/define-wp-config-consts.spec.ts

Lines changed: 1 addition & 194 deletions
Original file line numberDiff line numberDiff line change
@@ -1,202 +1,9 @@
11
import { PHP } from '@php-wasm/universal';
2-
import { rewriteDefineCalls, defineBeforeRun } from './define-wp-config-consts';
2+
import { defineBeforeRun } from './define-wp-config-consts';
33
import { RecommendedPHPVersion } from '@wp-playground/common';
44
import { loadNodeRuntime } from '@php-wasm/node';
55
import { setupPlatformLevelMuPlugins } from '@wp-playground/wordpress';
66

7-
describe('rewriteDefineCalls', () => {
8-
let php: PHP;
9-
beforeEach(async () => {
10-
php = new PHP(await loadNodeRuntime(RecommendedPHPVersion));
11-
});
12-
13-
it('should print warnings when a constant name conflicts, just to make sure other tests would fail', async () => {
14-
const phpCode = `<?php
15-
define('SITE_URL','http://initial.value');
16-
define('SITE_URL','http://initial.value');
17-
`;
18-
const response = await php.run({ code: phpCode });
19-
expect(response.errors).toContain('Constant SITE_URL already defined');
20-
expect(response.text).toContain('Constant SITE_URL already defined');
21-
});
22-
23-
it('should prepend constants not already present in the PHP code', async () => {
24-
const phpCode = `<?php
25-
echo json_encode([
26-
"SITE_URL" => SITE_URL,
27-
]);
28-
`;
29-
const rewritten = await rewriteDefineCalls(php, phpCode, {
30-
SITE_URL: 'http://test.url',
31-
});
32-
expect(rewritten).toContain(`define('SITE_URL','http://test.url');`);
33-
34-
const response = await php.run({ code: rewritten });
35-
expect(response.errors).toHaveLength(0);
36-
expect(response.json).toEqual({
37-
SITE_URL: 'http://test.url',
38-
});
39-
});
40-
41-
it('should rewrite the define() calls for the constants that are already defined in the PHP code', async () => {
42-
const phpCode = `<?php
43-
define('SITE_URL','http://initial.value');
44-
echo json_encode([
45-
"SITE_URL" => SITE_URL,
46-
]);
47-
`;
48-
const rewritten = await rewriteDefineCalls(php, phpCode, {
49-
SITE_URL: 'http://new.url',
50-
});
51-
expect(rewritten).not.toContain(
52-
`define('SITE_URL','http://initial.value');`
53-
);
54-
expect(rewritten).toContain(`define('SITE_URL','http://new.url');`);
55-
56-
const response = await php.run({ code: rewritten });
57-
expect(response.errors).toHaveLength(0);
58-
expect(response.json).toEqual({
59-
SITE_URL: 'http://new.url',
60-
});
61-
});
62-
63-
it('should preserve the third argument in existing define() calls', async () => {
64-
const phpCode = `<?php
65-
define('SITE_URL','http://initial.value',true);
66-
echo json_encode([
67-
"SITE_URL" => SITE_URL,
68-
]);
69-
`;
70-
const rewritten = await rewriteDefineCalls(php, phpCode, {
71-
SITE_URL: 'http://new.url',
72-
});
73-
expect(rewritten).not.toContain(
74-
`define('SITE_URL','http://initial.value',true);`
75-
);
76-
expect(rewritten).toContain(
77-
`define('SITE_URL','http://new.url',true);`
78-
);
79-
80-
const response = await php.run({ code: rewritten });
81-
82-
expect(response.errors).toContain(
83-
'case-insensitive constants is no longer supported'
84-
);
85-
expect(response.text).toContain(`{"SITE_URL":"http:\\/\\/new.url"}`);
86-
});
87-
88-
it('should take define() calls where the constant name cannot be statically inferred and wrap them in if(!defined()) checks', async () => {
89-
const phpCode = `<?php
90-
define('SITE'.'_URL','http://initial.value');
91-
echo json_encode([
92-
"SITE_URL" => SITE_URL,
93-
]);
94-
`;
95-
const rewritten = await rewriteDefineCalls(php, phpCode, {});
96-
expect(rewritten).toContain(`if(!defined('SITE'.'_URL'))`);
97-
expect(rewritten).toContain(
98-
`define('SITE'.'_URL','http://initial.value');`
99-
);
100-
101-
const response = await php.run({ code: rewritten });
102-
expect(response.errors).toHaveLength(0);
103-
expect(response.json).toEqual({
104-
SITE_URL: 'http://initial.value',
105-
});
106-
});
107-
108-
it('should not wrap the existing define() calls in if(!defined()) guards twice', async () => {
109-
const phpCode = `<?php
110-
if(!defined('SITE'.'_URL')) {
111-
define('SITE'.'_URL','http://initial.value');
112-
}
113-
echo json_encode([
114-
"SITE_URL" => SITE_URL,
115-
]);
116-
`;
117-
const rewritten = await rewriteDefineCalls(php, phpCode, {});
118-
expect(rewritten).toEqual(phpCode);
119-
});
120-
121-
it('should not wrap the existing define() calls in if(!defined()) guards twice, even if the existing guard is formatted differently than the define() call', async () => {
122-
const phpCode = `<?php
123-
if ( ! defined(
124-
'SITE' .
125-
'_URL'
126-
) ) {
127-
define('SITE'.'_URL','http://initial.value');
128-
}
129-
echo json_encode([
130-
"SITE_URL" => SITE_URL,
131-
]);
132-
`;
133-
const rewritten = await rewriteDefineCalls(php, phpCode, {});
134-
expect(rewritten).toEqual(phpCode);
135-
});
136-
137-
it('should not create conflicts between pre-existing "dynamically" named constants and the newly defined ones', async () => {
138-
const phpCode = `<?php
139-
define('SITE'.'_URL','http://initial.value');
140-
echo json_encode([
141-
"SITE_URL" => SITE_URL,
142-
]);
143-
`;
144-
const rewritten = await rewriteDefineCalls(php, phpCode, {
145-
SITE_URL: 'http://new.url',
146-
});
147-
expect(rewritten).toContain(`if(!defined('SITE'.'_URL'))`);
148-
expect(rewritten).toContain(
149-
`define('SITE'.'_URL','http://initial.value');`
150-
);
151-
expect(rewritten).toContain(`define('SITE_URL','http://new.url');`);
152-
153-
const response = await php.run({ code: rewritten });
154-
expect(response.errors).toHaveLength(0);
155-
expect(response.json).toEqual({
156-
SITE_URL: 'http://new.url',
157-
});
158-
});
159-
160-
it('should handle a complex scenario', async () => {
161-
const phpCode = `<?php
162-
define('WP_DEBUG', true);
163-
164-
// The third define() argument is also supported:
165-
@define('SAVEQUERIES', false, true);
166-
167-
// Expression
168-
define(true ? 'WP_DEBUG_LOG' : 'WP_DEBUG_LOG', 123);
169-
170-
// Guarded expressions shouldn't be wrapped twice
171-
if(!defined(1 ? 'A' : 'B')) {
172-
define(1 ? 'A' : 'B', 0);
173-
}
174-
175-
// More advanced expression
176-
$x = 'abc';
177-
define((function() use($x) {
178-
return $x;
179-
})(), 123);
180-
echo json_encode([
181-
"WP_DEBUG" => WP_DEBUG,
182-
"SAVEQUERIES" => SAVEQUERIES,
183-
"WP_DEBUG_LOG" => WP_DEBUG_LOG,
184-
"NEW_CONSTANT" => NEW_CONSTANT,
185-
]);
186-
`;
187-
const constants = {
188-
WP_DEBUG: false,
189-
WP_DEBUG_LOG: true,
190-
SAVEQUERIES: true,
191-
NEW_CONSTANT: 'new constant',
192-
};
193-
const rewritten = await rewriteDefineCalls(php, phpCode, constants);
194-
const response = await php.run({ code: rewritten });
195-
expect(response.errors).toHaveLength(0);
196-
expect(response.json).toEqual(constants);
197-
});
198-
});
199-
2007
describe('defineBeforeRun', () => {
2018
let php: PHP;
2029
beforeEach(async () => {

packages/playground/blueprints/src/lib/steps/define-wp-config-consts.ts

Lines changed: 6 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
import { joinPaths, phpVars } from '@php-wasm/util';
1+
import { joinPaths } from '@php-wasm/util';
22
import type { StepHandler } from '.';
3-
/** @ts-ignore */
4-
import rewriteWpConfigToDefineConstants from './rewrite-wp-config-to-define-constants.php?raw';
53
import type { UniversalPHP } from '@php-wasm/universal';
4+
import { defineWpConfigConstants } from '@wp-playground/wordpress';
65

76
/**
87
* @inheritDoc defineWpConfigConsts
@@ -63,13 +62,12 @@ export const defineWpConfigConsts: StepHandler<
6362
case 'rewrite-wp-config': {
6463
const documentRoot = await playground.documentRoot;
6564
const wpConfigPath = joinPaths(documentRoot, '/wp-config.php');
66-
const wpConfig = await playground.readFileAsText(wpConfigPath);
67-
const updatedWpConfig = await rewriteDefineCalls(
65+
await defineWpConfigConstants(
6866
playground,
69-
wpConfig,
70-
consts
67+
wpConfigPath,
68+
consts,
69+
'rewrite'
7170
);
72-
await playground.writeFile(wpConfigPath, updatedWpConfig);
7371
break;
7472
}
7573
default:
@@ -85,23 +83,3 @@ export async function defineBeforeRun(
8583
await playground.defineConstant(key, consts[key] as string);
8684
}
8785
}
88-
89-
export async function rewriteDefineCalls(
90-
playground: UniversalPHP,
91-
phpCode: string,
92-
consts: Record<string, unknown>
93-
): Promise<string> {
94-
await playground.writeFile('/tmp/code.php', phpCode);
95-
const js = phpVars({
96-
consts,
97-
});
98-
await playground.run({
99-
code: `${rewriteWpConfigToDefineConstants}
100-
$wp_config_path = '/tmp/code.php';
101-
$wp_config = file_get_contents($wp_config_path);
102-
$new_wp_config = rewrite_wp_config_to_define_constants($wp_config, ${js.consts});
103-
file_put_contents($wp_config_path, $new_wp_config);
104-
`,
105-
});
106-
return await playground.readFileAsText('/tmp/code.php');
107-
}

packages/playground/blueprints/src/lib/steps/import-wordpress-files.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { StepHandler } from '.';
22
import { unzip } from './unzip';
33
import { dirname, joinPaths, phpVar } from '@php-wasm/util';
44
import type { UniversalPHP } from '@php-wasm/universal';
5+
import { ensureRequiredWpConfigConstants } from '@wp-playground/wordpress';
56
import { wpContentFilesExcludedFromExport } from '../utils/wp-content-files-excluded-from-exports';
67
import { defineSiteUrl } from './define-site-url';
78

@@ -109,6 +110,12 @@ export const importWordPressFiles: StepHandler<
109110
// Remove the directory where we unzipped the imported zip file.
110111
await playground.rmdir(importPath);
111112

113+
// Ensure required constants are defined in wp-config.php.
114+
await ensureRequiredWpConfigConstants(
115+
playground,
116+
joinPaths(documentRoot, 'wp-config.php')
117+
);
118+
112119
// Adjust the site URL
113120
await defineSiteUrl(playground, {
114121
siteUrl: await playground.absoluteUrl,

packages/playground/wordpress/src/boot.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
} from '.';
2525
import { joinPaths } from '@php-wasm/util';
2626
import { logger } from '@php-wasm/logger';
27+
import { ensureRequiredWpConfigConstants } from './rewrite-wp-config';
2728

2829
export type PhpIniOptions = Record<string, string>;
2930
export type Hook = (php: PHP) => void | Promise<void>;
@@ -220,6 +221,16 @@ export async function bootWordPress(options: BootOptions) {
220221
php.defineConstant('WP_HOME', options.siteUrl);
221222
php.defineConstant('WP_SITEURL', options.siteUrl);
222223

224+
/*
225+
* Add required constants to "wp-config.php" if they are not already defined.
226+
* This is needed, because some WordPress backups and exports may not include
227+
* definitions for some of the necessary constants.
228+
*/
229+
await ensureRequiredWpConfigConstants(
230+
php,
231+
joinPaths(requestHandler.documentRoot, 'wp-config.php')
232+
);
233+
223234
// Run "before database" hooks to mount/copy more files in
224235
if (options.hooks?.beforeDatabaseSetup) {
225236
await options.hooks.beforeDatabaseSetup(php);

packages/playground/wordpress/src/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ import type { PHP, UniversalPHP } from '@php-wasm/universal';
22
import { joinPaths, phpVar } from '@php-wasm/util';
33
import { unzipFile, createMemoizedFetch } from '@wp-playground/common';
44
export { bootWordPress, getFileNotFoundActionForWordPress } from './boot';
5+
export {
6+
defineWpConfigConstants,
7+
ensureRequiredWpConfigConstants,
8+
} from './rewrite-wp-config';
59
export { getLoadedWordPressVersion } from './version-detect';
610

711
export * from './version-detect';

packages/playground/blueprints/src/lib/steps/rewrite-wp-config-to-define-constants.php renamed to packages/playground/wordpress/src/rewrite-wp-config-to-define-constants.php

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,11 +63,17 @@
6363
* })(), 123);
6464
* }
6565
* ```
66-
*
67-
* @param mixed $content
66+
*
67+
* @param mixed $content A PHP file content.
68+
* @param array $constants An array of constants to define.
69+
* @param bool $when_already_defined Optional. What to do if the constant is already defined.
70+
* Possible values are:
71+
* 'rewrite' - Rewrite the constant, using the new value.
72+
* 'skip' - Skip the definition, keeping the existing value.
73+
* Default: 'rewrite'
6874
* @return string
6975
*/
70-
function rewrite_wp_config_to_define_constants($content, $constants = [])
76+
function rewrite_wp_config_to_define_constants($content, $constants = [], $when_already_defined = 'rewrite')
7177
{
7278
$tokens = array_reverse(token_get_all($content));
7379
$output = [];
@@ -262,6 +268,14 @@ function rewrite_wp_config_to_define_constants($content, $constants = [])
262268
continue;
263269
}
264270

271+
// If "$when_already_defined" is set to 'skip', ignore the definition, and
272+
// remove the constant from the list so it doesn't get added to the output.
273+
if ('skip' === $when_already_defined) {
274+
$output = array_merge($output, $buffer);
275+
unset($constants[$name]);
276+
continue;
277+
}
278+
265279
// We now have a define() call that defines a constant we're looking for.
266280
// Let's rewrite its value to the one
267281
$output = array_merge(

0 commit comments

Comments
 (0)