Repository navigation
Expand file tree
/
Copy pathindex.php
More file actions
247 lines (216 loc) · 8.26 KB
/
Copy pathindex.php
File metadata and controls
247 lines (216 loc) · 8.26 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
<?php
declare(strict_types=1);
/**
* CSVJSON front controller — static shim.
*
* Serves the built SPA from app/dist and applies the legacy URL map.
* The legacy CodeIgniter app is gone: no server-side conversion, no
* upload endpoint, no save endpoint, no telemetry, no ad views. Legacy
* Legacy permalink data is read directly from S3 by the browser; nothing
* conversion-related is stored server-side. The two sanctioned write paths
* are the vote endpoints: /api/feedback (feedback-api.php) and
* /api/changelog-vote (changelog-vote-api.php) — both rate-limited through
* the shared feedback_write_log table.
*
* On Apache, real files (img/, favicon.ico) are served before PHP runs and
* everything else is rewritten here (.htaccess). Under `php -S` this same
* file acts as the router for every request.
*/
const DIST_DIR = __DIR__ . '/app/dist';
/**
* Legacy tool slugs. The tool root and every sub-route 301s home, with
* two carve-outs: /<tool>/<32-hex-id> permalinks stay live (the SPA
* hydrates them), and the endpoints removed outright below.
*/
const LEGACY_TOOLS = [
'csv2json',
'json2csv',
'json_validator',
'json_beautifier',
'sql2json',
'csvjson2json',
'datajanitor',
'dataclean',
];
/**
* Tools that issued legacy permalinks of the form /<tool>/<32-hex-id>.
* These URLs are external promises (bookmarks, embeds) and stay live;
* dataclean was only ever an alias, so its sub-routes redirect.
*/
const PERMALINK_TOOLS = [
'csv2json', 'json2csv', 'json_validator', 'json_beautifier',
'sql2json', 'csvjson2json', 'datajanitor',
];
/**
* Content-Type for a file served out of the SPA build.
*/
function dist_mime_type(string $file): string
{
static $types = [
'html' => 'text/html; charset=utf-8',
'htm' => 'text/html; charset=utf-8',
'css' => 'text/css; charset=utf-8',
'js' => 'text/javascript; charset=utf-8',
'mjs' => 'text/javascript; charset=utf-8',
'json' => 'application/json',
'map' => 'application/json',
'webmanifest' => 'application/json',
'svg' => 'image/svg+xml',
'png' => 'image/png',
'jpg' => 'image/jpeg',
'jpeg' => 'image/jpeg',
'gif' => 'image/gif',
'ico' => 'image/x-icon',
'txt' => 'text/plain; charset=utf-8',
'xml' => 'application/xml',
'woff2' => 'font/woff2',
'woff' => 'font/woff',
'wasm' => 'application/wasm',
];
$ext = strtolower(pathinfo($file, PATHINFO_EXTENSION));
return $types[$ext] ?? 'application/octet-stream';
}
/**
* Serve one file from the SPA build, refusing anything outside app/dist.
*/
function serve_dist_file(string $path): void
{
$root = realpath(DIST_DIR);
$file = realpath(DIST_DIR . $path);
if ($root === false || $file === false || strpos($file, $root . DIRECTORY_SEPARATOR) !== 0 || !is_file($file)) {
not_found();
}
header('Content-Type: ' . dist_mime_type($file));
// Vite fingerprints its assets, so hashed files cache forever while
// index.html must revalidate on every visit so deploys propagate.
header('Cache-Control: ' . (preg_match('/-[A-Za-z0-9_-]{8,}\.\w+$/', $file)
? 'public, max-age=31536000, immutable'
: 'no-cache'));
readfile($file);
exit;
}
/**
* Serve the SPA shell for "/" and for legacy permalink URLs, which the
* client-side router hydrates (read-only) straight from S3 — no redirect.
*/
function serve_spa(): void
{
if (!is_file(DIST_DIR . '/index.html')) {
// The SPA build ships separately from this shim; without it there
// is nothing to serve.
http_response_code(503);
header('Content-Type: text/plain; charset=utf-8');
exit('The application build is not available.');
}
serve_dist_file('/index.html');
}
function redirect_home(string $target = '/'): void
{
header('Location: ' . $target, true, 301);
exit;
}
function gone(): void
{
http_response_code(410);
header('Content-Type: text/html; charset=utf-8');
exit('<!doctype html><title>410 Gone</title>'
. '<h1>410 Gone</h1><p><a href="/">Go to the converter</a></p>');
}
function not_found(): void
{
http_response_code(404);
header('Content-Type: text/html; charset=utf-8');
exit('<!doctype html><title>404 Not Found</title>'
. '<h1>404 Not Found</h1><p><a href="/">Go to the converter</a></p>');
}
// ---------------------------------------------------------------------------
// Request routing
// ---------------------------------------------------------------------------
$rawPath = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH);
$path = rawurldecode(is_string($rawPath) && $rawPath !== '' ? $rawPath : '/');
// Normalize: a trailing slash changes nothing.
if ($path !== '/') {
$path = rtrim($path, '/');
if ($path === '') {
$path = '/';
}
}
// Refuse anything that tries to escape the document root.
if (strpos($path, "\0") !== false || strpos($path, '..') !== false) {
not_found();
}
// Docroot internals — repo docs, screenshots, dotfiles, build/dep
// metadata — are not public. Under Apache, .htaccess forbids them (403)
// before PHP runs; mirror the same list here so the dev server matches
// production and CI can pin the behavior.
if (preg_match('#^/(?:docs|verification-screenshots)(?:/|$)#', $path)
|| preg_match('#^/\.#', $path)
|| preg_match('#^/(?:composer\.(?:json|lock)|Procfile|ISSUE_TEMPLATE\.md|csvjson\.sublime-\w+)$#', $path)
) {
not_found();
}
// Under the PHP dev server, hand real docroot files (img/) to the built-in
// server. Under Apache these never reach PHP in the first place.
if (PHP_SAPI === 'cli-server' && $path !== '/' && is_file(__DIR__ . $path)) {
return false;
}
// The feedback vote endpoint (the site's one sanctioned write path — spec:
// CSVJSON feedback votes, art_2AdAvo34) and its admin page are routed here
// rather than being standalone entry files so they behave identically under
// Apache (.htaccess rewrite) and the PHP dev server; CI's smoke test
// exercises the real code path either way.
if ($path === '/api/feedback') {
require __DIR__ . '/feedback-api.php';
exit; // The endpoint always answers for itself; never fall through to the 404.
}
if ($path === '/api/changelog-vote') {
require __DIR__ . '/changelog-vote-api.php';
exit; // The endpoint always answers for itself; never fall through to the 404.
}
if ($path === '/feedback-admin') {
require __DIR__ . '/feedback-admin.php';
exit; // The admin page renders inline; never fall through to the 404.
}
// Telemetry write and the upload round-trip are removed outright —
// uploads are read client-side and never hit the server.
if (preg_match('#^/(?:' . implode('|', LEGACY_TOOLS) . ')/upload$#i', $path)
|| strtolower($path) === '/csv2json/instrument'
) {
gone();
}
// Built SPA assets (e.g. /assets/index-abc123.js) live under app/dist, not
// the web root, so this script serves them with the right cache headers.
if (is_file(DIST_DIR . $path)) {
serve_dist_file($path);
}
if ($path === '/') {
serve_spa();
}
// The favicon lives at /img/favicon.ico.
if ($path === '/favicon.ico' && is_file(__DIR__ . '/img/favicon.ico')) {
header('Content-Type: image/x-icon');
readfile(__DIR__ . '/img/favicon.ico');
exit;
}
// Legacy permalinks stay live: serve the SPA shell and let the router
// hydrate the stored object read-only from S3 — no redirect, URL unchanged.
if (preg_match('#^/(?:' . implode('|', PERMALINK_TOOLS) . ')/[0-9a-f]{32}/?$#i', $path)) {
serve_spa();
}
// Bare roots of the converter and JSON-input legacy tools deep-link the
// direction (defect fix: /json2csv, /json_beautifier, and /json_validator
// regulars land in JSON→CSV mode with the JSON editor and Format/Minify).
// Every other legacy root and every sub-route still redirects straight
// home; permalinks above never reach this.
if (preg_match('#^/csv2json/?$#i', $path)) {
redirect_home('/?direction=csv2json');
}
if (preg_match('#^/(?:json2csv|json_beautifier|json_validator)/?$#i', $path)) {
redirect_home('/?direction=json2csv');
}
// Everything left on a retired tool URL — root or any sub-route —
// permanently redirects home.
if (preg_match('#^/(?:' . implode('|', LEGACY_TOOLS) . ')(?:/|$)#i', $path)) {
redirect_home();
}
not_found();