Skip to content

Commit 751f59f

Browse files
committed
Merge branch '4.1' into 'main'
2 parents fb5aa00 + 2989146 commit 751f59f

9 files changed

Lines changed: 166 additions & 31 deletions

File tree

.docker/nginx/default.conf

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,20 @@ server {
7171
# Symfony renders the styled 404 page. Do not intercept upstream PHP errors.
7272
fastcgi_intercept_errors off;
7373

74+
# Service Worker - no caching, proper headers
75+
location = /sw.js {
76+
add_header Content-Type "application/javascript; charset=utf-8";
77+
add_header Service-Worker-Allowed "/";
78+
add_header Cache-Control "no-cache, no-store, must-revalidate";
79+
add_header X-Content-Type-Options "nosniff";
80+
try_files $uri =404;
81+
}
82+
83+
# Update pages - rewrite before try_files can hit the PHP handler
84+
location ^~ /update {
85+
rewrite ^ /index.php last;
86+
}
87+
7488
location / {
7589
index index.php;
7690
try_files $uri $uri/ @rewriteapp;
@@ -207,6 +221,20 @@ server {
207221
# Symfony renders the styled 404 page. Do not intercept upstream PHP errors.
208222
fastcgi_intercept_errors off;
209223

224+
# Service Worker - no caching, proper headers
225+
location = /sw.js {
226+
add_header Content-Type "application/javascript; charset=utf-8";
227+
add_header Service-Worker-Allowed "/";
228+
add_header Cache-Control "no-cache, no-store, must-revalidate";
229+
add_header X-Content-Type-Options "nosniff";
230+
try_files $uri =404;
231+
}
232+
233+
# Update pages - rewrite before try_files can hit the PHP handler
234+
location ^~ /update {
235+
rewrite ^ /index.php last;
236+
}
237+
210238
location / {
211239
index index.php;
212240
try_files $uri $uri/ @rewriteapp;

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ This is a log of major user-visible changes in each phpMyFAQ release.
4242

4343
### phpMyFAQ v4.1.2 – unreleased
4444

45+
- updated third party dependencies (Thorsten)
4546
- fixed bugs (Thorsten)
4647

4748
### phpMyFAQ v4.1.1 - 2026-03-31

docs/installation.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -659,6 +659,8 @@ your-domain.com {
659659
rewrite /setup /setup/index.php
660660
rewrite /admin /admin/index.php
661661
rewrite /api/* /api/index.php
662+
rewrite /update /index.php
663+
rewrite /update/* /index.php
662664
663665
# Security headers
664666
header {

nginx.conf

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,11 @@ server {
8686
try_files $uri =404;
8787
}
8888

89+
# Update pages - rewrite before try_files can hit the PHP handler
90+
location ^~ /update {
91+
rewrite ^ /index.php last;
92+
}
93+
8994
location / {
9095
index index.php;
9196
try_files $uri $uri/ @rewriteapp;
@@ -106,10 +111,6 @@ server {
106111
# Setup pages
107112
rewrite ^/setup/ /setup/index.php last;
108113

109-
# Update page - route directly to index.php (handled by Symfony router)
110-
rewrite ^/update$ /index.php last;
111-
rewrite ^/update/ /index.php last;
112-
113114
# Front controller: route all other requests to index.php (Symfony Router)
114115
rewrite ^ /index.php last;
115116
}

phpmyfaq/assets/src/configuration/update.test.ts

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ describe('handleUpdateInformation', () => {
136136

137137
await handleUpdateInformation();
138138

139-
expect(fetch).toHaveBeenCalledWith('../api/setup/check', {
139+
expect(fetch).toHaveBeenCalledWith('/api/setup/check', {
140140
method: 'POST',
141141
headers: {
142142
Accept: 'application/json, text/plain, */*',
@@ -218,14 +218,15 @@ describe('handleUpdateInformation', () => {
218218

219219
global.fetch = vi.fn().mockResolvedValue({
220220
ok: false,
221+
status: 404,
221222
headers: { get: () => 'text/html' },
222223
text: () => Promise.resolve('Not Found'),
223224
});
224225

225226
await handleUpdateInformation();
226227

227228
const result = document.getElementById('phpmyfaq-update-check-result');
228-
expect(result?.innerText).toContain('The requested resource was not found');
229+
expect(result?.innerText).toContain('The server returned an error (HTTP 404)');
229230
expect(result?.innerText).toContain('RewriteBase');
230231
});
231232

@@ -280,7 +281,7 @@ describe('handleConfigBackup', () => {
280281

281282
it('should do nothing when URL does not match step 2', async () => {
282283
Object.defineProperty(window, 'location', {
283-
value: { href: 'http://localhost/update?step=1' },
284+
value: { href: 'http://localhost/update?step=1', pathname: '/update' },
284285
writable: true,
285286
});
286287

@@ -293,7 +294,7 @@ describe('handleConfigBackup', () => {
293294

294295
it('should do nothing when installed version input is missing', async () => {
295296
Object.defineProperty(window, 'location', {
296-
value: { href: 'http://localhost/update?step=2' },
297+
value: { href: 'http://localhost/update?step=2', pathname: '/update' },
297298
writable: true,
298299
});
299300

@@ -307,7 +308,7 @@ describe('handleConfigBackup', () => {
307308

308309
it('should call backup API on step 2', async () => {
309310
Object.defineProperty(window, 'location', {
310-
value: { href: 'http://localhost/update?step=2' },
311+
value: { href: 'http://localhost/update?step=2', pathname: '/update' },
311312
writable: true,
312313
});
313314

@@ -320,7 +321,7 @@ describe('handleConfigBackup', () => {
320321

321322
await handleConfigBackup();
322323

323-
expect(fetch).toHaveBeenCalledWith('../api/setup/backup', {
324+
expect(fetch).toHaveBeenCalledWith('/api/setup/backup', {
324325
method: 'POST',
325326
headers: {
326327
Accept: 'application/json, text/plain, */*',
@@ -332,7 +333,7 @@ describe('handleConfigBackup', () => {
332333

333334
it('should log error on failed backup response', async () => {
334335
Object.defineProperty(window, 'location', {
335-
value: { href: 'http://localhost/update?step=2' },
336+
value: { href: 'http://localhost/update?step=2', pathname: '/update' },
336337
writable: true,
337338
});
338339

@@ -351,7 +352,7 @@ describe('handleConfigBackup', () => {
351352

352353
it('should log error on network failure', async () => {
353354
Object.defineProperty(window, 'location', {
354-
value: { href: 'http://localhost/update?step=2' },
355+
value: { href: 'http://localhost/update?step=2', pathname: '/update' },
355356
writable: true,
356357
});
357358

@@ -375,7 +376,7 @@ describe('handleDatabaseUpdate', () => {
375376

376377
it('should do nothing when URL does not match step 3', async () => {
377378
Object.defineProperty(window, 'location', {
378-
value: { href: 'http://localhost/update?step=1' },
379+
value: { href: 'http://localhost/update?step=1', pathname: '/update' },
379380
writable: true,
380381
});
381382

@@ -388,7 +389,7 @@ describe('handleDatabaseUpdate', () => {
388389

389390
it('should do nothing when installed version input is missing', async () => {
390391
Object.defineProperty(window, 'location', {
391-
value: { href: 'http://localhost/update?step=3' },
392+
value: { href: 'http://localhost/update?step=3', pathname: '/update' },
392393
writable: true,
393394
});
394395

@@ -402,7 +403,7 @@ describe('handleDatabaseUpdate', () => {
402403

403404
it('should show success on successful database update', async () => {
404405
Object.defineProperty(window, 'location', {
405-
value: { href: 'http://localhost/update?step=3' },
406+
value: { href: 'http://localhost/update?step=3', pathname: '/update' },
406407
writable: true,
407408
});
408409

@@ -419,7 +420,7 @@ describe('handleDatabaseUpdate', () => {
419420

420421
await handleDatabaseUpdate();
421422

422-
expect(fetch).toHaveBeenCalledWith('../api/setup/update-database', {
423+
expect(fetch).toHaveBeenCalledWith('/api/setup/update-database', {
423424
method: 'POST',
424425
headers: {
425426
Accept: 'application/json, text/plain, */*',
@@ -440,7 +441,7 @@ describe('handleDatabaseUpdate', () => {
440441

441442
it('should show error on failed database update response', async () => {
442443
Object.defineProperty(window, 'location', {
443-
value: { href: 'http://localhost/update?step=3' },
444+
value: { href: 'http://localhost/update?step=3', pathname: '/update' },
444445
writable: true,
445446
});
446447

@@ -471,7 +472,7 @@ describe('handleDatabaseUpdate', () => {
471472

472473
it('should show error alert on network failure', async () => {
473474
Object.defineProperty(window, 'location', {
474-
value: { href: 'http://localhost/update?step=3' },
475+
value: { href: 'http://localhost/update?step=3', pathname: '/update' },
475476
writable: true,
476477
});
477478

@@ -494,7 +495,7 @@ describe('handleDatabaseUpdate', () => {
494495

495496
it('should work with URL ending in /update/?step=3', async () => {
496497
Object.defineProperty(window, 'location', {
497-
value: { href: 'http://localhost/update/?step=3' },
498+
value: { href: 'http://localhost/update/?step=3', pathname: '/update/' },
498499
writable: true,
499500
});
500501

phpmyfaq/assets/src/configuration/update.ts

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,26 @@
1313
* @since 2023-10-22
1414
*/
1515

16+
const getBasePath = (): string => {
17+
const path = window.location.pathname;
18+
let basePath: string;
19+
if (path.endsWith('/update/index.php')) {
20+
basePath = path.slice(0, -'/update/index.php'.length);
21+
} else if (path.endsWith('/update/')) {
22+
basePath = path.slice(0, -'/update/'.length);
23+
} else if (path.endsWith('/update')) {
24+
basePath = path.slice(0, -'/update'.length);
25+
} else {
26+
basePath = path;
27+
}
28+
29+
if (!basePath.endsWith('/')) {
30+
basePath += '/';
31+
}
32+
33+
return basePath;
34+
};
35+
1636
export const handleUpdateNextStepButton = (): void => {
1737
const nextStepButton = document.getElementById('phpmyfaq-update-next-step-button') as HTMLButtonElement | null;
1838
const nextStep = document.getElementById('phpmyfaq-update-next-step') as HTMLInputElement | null;
@@ -36,8 +56,10 @@ export const handleUpdateInformation = async (): Promise<void> => {
3656

3757
if (!installedVersion) return;
3858

59+
const basePath = getBasePath();
60+
3961
try {
40-
const response = await fetch('../api/setup/check', {
62+
const response = await fetch(`${basePath}api/setup/check`, {
4163
method: 'POST',
4264
headers: {
4365
Accept: 'application/json, text/plain, */*',
@@ -53,12 +75,15 @@ export const handleUpdateInformation = async (): Promise<void> => {
5375
const errorMessage = await response.json();
5476
errorText = errorMessage.message || errorMessage.error || 'Update check failed';
5577
} else {
56-
errorText = await response.text();
57-
if (!errorText || errorText === 'Not Found') {
78+
const rawText = await response.text();
79+
if (!rawText || rawText === 'Not Found' || rawText.trimStart().startsWith('<')) {
5880
errorText =
59-
'The requested resource was not found on the server. ' +
60-
'Please check your server configuration, if you use Apache, the RewriteBase in your .htaccess ' +
61-
'configuration. If you use nginx, please check your nginx rewrite configuration.';
81+
'The server returned an error (HTTP ' +
82+
response.status +
83+
'). Please check your server configuration: if you use Apache, verify the RewriteBase in your ' +
84+
'.htaccess matches your installation path. If you use nginx, check your rewrite configuration.';
85+
} else {
86+
errorText = rawText;
6287
}
6388
}
6489
const alert = document.getElementById('phpmyfaq-update-check-alert') as HTMLElement | null;
@@ -102,8 +127,10 @@ export const handleConfigBackup = async (): Promise<void> => {
102127

103128
if (!installedVersion) return;
104129

130+
const basePath = getBasePath();
131+
105132
try {
106-
const response = await fetch('../api/setup/backup', {
133+
const response = await fetch(`${basePath}api/setup/backup`, {
107134
method: 'POST',
108135
headers: {
109136
Accept: 'application/json, text/plain, */*',
@@ -131,8 +158,10 @@ export const handleDatabaseUpdate = async (): Promise<void> => {
131158

132159
if (!installedVersion) return;
133160

161+
const basePath = getBasePath();
162+
134163
try {
135-
const response = await fetch('../api/setup/update-database', {
164+
const response = await fetch(`${basePath}api/setup/update-database`, {
136165
method: 'POST',
137166
headers: {
138167
Accept: 'application/json, text/plain, */*',

phpmyfaq/assets/templates/setup/update.twig

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,14 +54,14 @@
5454
<span class="stepIndicator">Database updates</span>
5555
</div>
5656

57-
{{ include('@setup/update/step' ~ currentStep ~ '.twig') }}
58-
5957
{% if checkBasicError %}
60-
<div class="alert alert-danger my-5" role="alert">
58+
<div class="alert alert-danger my-3" role="alert">
6159
{{ checkBasicError | raw }}
6260
</div>
6361
{% endif %}
6462

63+
{{ include('@setup/update/step' ~ currentStep ~ '.twig') }}
64+
6565
</div>
6666
</section>
6767
</main>

phpmyfaq/src/phpMyFAQ/EventListener/RouterListener.php

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,8 @@ public function onKernelRequest(RequestEvent $event): void
5555

5656
$urlMatcher = new UrlMatcher($this->routes, $requestContext);
5757
try {
58-
$parameters = $urlMatcher->match($request->getPathInfo());
58+
$pathInfo = $this->normalizePath($request->getPathInfo());
59+
$parameters = $urlMatcher->match($pathInfo);
5960
} catch (ResourceNotFoundException $exception) {
6061
throw new NotFoundHttpException($exception->getMessage(), $exception);
6162
} catch (MethodNotAllowedException $exception) {
@@ -68,4 +69,22 @@ public function onKernelRequest(RequestEvent $event): void
6869

6970
$request->attributes->add($parameters);
7071
}
72+
73+
/**
74+
* Normalizes the path by removing trailing slashes and /index.php suffix.
75+
*/
76+
private function normalizePath(string $path): string
77+
{
78+
// Strip /index.php suffix (e.g. /update/index.php → /update)
79+
if (str_ends_with($path, '/index.php')) {
80+
$path = substr($path, 0, -10);
81+
}
82+
83+
// Remove trailing slash, but keep root path as /
84+
if ($path !== '/' && str_ends_with($path, '/')) {
85+
$path = rtrim($path, '/');
86+
}
87+
88+
return $path === '' ? '/' : $path;
89+
}
7190
}

0 commit comments

Comments
 (0)