Line data Source code
1 : /**
2 : Copyright (c) 2024 Stappler LLC <admin@stappler.dev>
3 :
4 : Permission is hereby granted, free of charge, to any person obtaining a copy
5 : of this software and associated documentation files (the "Software"), to deal
6 : in the Software without restriction, including without limitation the rights
7 : to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 : copies of the Software, and to permit persons to whom the Software is
9 : furnished to do so, subject to the following conditions:
10 :
11 : The above copyright notice and this permission notice shall be included in
12 : all copies or substantial portions of the Software.
13 :
14 : THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 : IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 : FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 : AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 : LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 : OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20 : THE SOFTWARE.
21 : **/
22 :
23 : #include "SPWebOutput.h"
24 : #include "SPWebRequestController.h"
25 : #include "SPWebRoot.h"
26 : #include "SPDbFile.h"
27 :
28 : namespace STAPPLER_VERSIONIZED stappler::web::output {
29 :
30 : constexpr static const char * HTML_LOAD_BEGIN =
31 : R"Html(<!doctype html>
32 : <html><head><title>Serenity Pretty Data Dump</title>
33 : <link rel="stylesheet" href="/__server/virtual/css/style.css" />
34 : <link rel="stylesheet" href="/__server/virtual/css/kawaiJson.css" />
35 : <script src="/__server/virtual/js/kawaiJson.js"></script>
36 : <script>function load(j) { KawaiJson(document.getElementById("content"), j); }
37 : function init() {load()Html";
38 :
39 : constexpr static const char * HTML_LOAD_END =
40 : R"Html()}</script>
41 : </head>)Html";
42 :
43 : constexpr static const char * HTML_PRETTY =
44 : R"Html(<body onload="init();">
45 : <div id="content" class="content"></div>
46 : </body></html>)Html";
47 :
48 : struct HtmlJsonEncoder {
49 0 : HtmlJsonEncoder(const Callback<void(StringView)> &s, bool trackActions = false)
50 0 : : stream(&s), trackActions(trackActions) { }
51 :
52 0 : ~HtmlJsonEncoder() { }
53 :
54 0 : void writeString(const String &str) {
55 0 : (*stream) << "<span class=\"quote\">\"</span>";
56 0 : for (auto i : str) {
57 0 : if (i == '\n') {
58 0 : (*stream) << "\\n";
59 0 : } else if (i == '\r') {
60 0 : (*stream) << "\\r";
61 0 : } else if (i == '\t') {
62 0 : (*stream) << "\\t";
63 0 : } else if (i == '\f') {
64 0 : (*stream) << "\\f";
65 0 : } else if (i == '\b') {
66 0 : (*stream) << "\\b";
67 0 : } else if (i == '\\') {
68 0 : (*stream) << "\\\\";
69 0 : } else if (i == '\"') {
70 0 : (*stream) << "\\\"";
71 0 : } else if (i == ' ') {
72 0 : (*stream) << ' ';
73 0 : } else if (i >= 0 && i <= 0x20) {
74 0 : (*stream) << "\\u00" << base16::charToHex(i, true);
75 : } else {
76 0 : (*stream) << i;
77 : }
78 : }
79 0 : (*stream) << "<span class=\"quote\">\"</span>";
80 0 : offsetted = false;
81 0 : }
82 :
83 0 : void write(nullptr_t) {
84 0 : (*stream) << "<span class=\"null\">null</span>";
85 0 : offsetted = false;
86 0 : }
87 :
88 0 : void write(bool value) {
89 0 : (*stream) << "<span class=\"bool\">" << ((value)?"true":"false") << "</span>";
90 0 : offsetted = false;
91 0 : }
92 :
93 0 : void write(int64_t value) {
94 0 : (*stream) << "<span class=\"integer\">" << value << "</span>";
95 0 : offsetted = false;
96 0 : }
97 :
98 0 : void write(double value) {
99 0 : (*stream) << "<span class=\"float\">" << value << "</span>";
100 0 : offsetted = false;
101 0 : }
102 :
103 0 : void write(const String &str) {
104 0 : if (actionsState == Dict) {
105 0 : auto sep = str.find("|");
106 0 : if (sep == String::npos) {
107 0 : if (action == Delete) {
108 0 : (*stream) << " <a class=\"delete\" href=\"" << str << "\">Remove</a> ";
109 : } else {
110 0 : (*stream) << " <a class=\"edit\" href=\"" << str << "\">Edit</a> ";
111 : }
112 : } else {
113 0 : if (action == Delete) {
114 0 : (*stream) << " <a class=\"delete\" href=\"" << str.substr(sep + 1) << "\">Remove: " << str.substr(0, sep) << "</a> ";
115 : } else {
116 0 : (*stream) << " <a class=\"edit\" href=\"" << str.substr(sep + 1) << "\">Edit: " << str.substr(0, sep) << "</a> ";
117 : }
118 : }
119 0 : } else if (str.size() > 6 && str.compare(0, 2, "~~") == 0) {
120 0 : auto sep = str.find("|");
121 0 : if (sep == String::npos) {
122 0 : (*stream) << "<span class=\"string\">";
123 0 : writeString(str);
124 0 : (*stream) << "</span>";
125 : } else {
126 0 : (*stream) << "<a class=\"file\" target=\"_blank\" href=\"" << str.substr(sep + 1) << "\">file:" << str.substr(2, sep - 2) << "</a>";
127 : }
128 : } else {
129 0 : (*stream) << "<span class=\"string\">";
130 0 : writeString(str);
131 0 : (*stream) << "</span>";
132 : }
133 0 : }
134 :
135 0 : void write(const Bytes &data) {
136 0 : (*stream) << "<span class=\"bytes\">\"" << "BASE64:" << base64::encode<Interface>(data) << "\"</span>";
137 0 : offsetted = false;
138 0 : }
139 :
140 0 : bool isObjectArray(const Array &arr) {
141 0 : for (auto &it : arr) {
142 0 : if (!it.isDictionary()) {
143 0 : return false;
144 : }
145 : }
146 0 : return true;
147 : }
148 :
149 0 : void onBeginArray(const Array &arr) {
150 0 : (*stream) << '[';
151 0 : if (!isObjectArray(arr)) {
152 0 : ++ depth;
153 0 : bstack.push_back(false);
154 0 : offsetted = false;
155 : } else {
156 0 : bstack.push_back(true);
157 : }
158 0 : }
159 :
160 0 : void onEndArray(const Array &arr) {
161 0 : if (!bstack.empty()) {
162 0 : if (!bstack.back()) {
163 0 : -- depth;
164 0 : (*stream) << '\n';
165 0 : for (size_t i = 0; i < depth; i++) {
166 0 : (*stream) << '\t';
167 : }
168 : }
169 0 : bstack.pop_back();
170 : } else {
171 0 : -- depth;
172 0 : (*stream) << '\n';
173 0 : for (size_t i = 0; i < depth; i++) {
174 0 : (*stream) << '\t';
175 : }
176 : }
177 0 : (*stream) << ']';
178 0 : popComplex = true;
179 0 : }
180 :
181 0 : void onBeginDict(const Dictionary &dict) {
182 0 : if (trackActions && actionsState == Key) {
183 0 : actionsState = Dict;
184 0 : (*stream) << "<span class=\"actions\">";
185 : } else {
186 0 : (*stream) << '{';
187 0 : ++ depth;
188 : }
189 0 : }
190 :
191 0 : void onEndDict(const Dictionary &dict) {
192 0 : if (actionsState == Dict) {
193 0 : actionsState = None;
194 0 : (*stream) << "</span>";
195 0 : for (size_t i = 0; i < depth; i++) {
196 0 : (*stream) << '\t';
197 : }
198 : } else {
199 0 : -- depth;
200 0 : (*stream) << '\n';
201 0 : for (size_t i = 0; i < depth; i++) {
202 0 : (*stream) << '\t';
203 : }
204 0 : (*stream) << '}';
205 0 : popComplex = true;
206 : }
207 0 : }
208 :
209 0 : void onKey(const String &str) {
210 0 : if (actionsState == Dict) {
211 0 : if (str == "remove") {
212 0 : action = Delete;
213 0 : } else if (str == "edit") {
214 0 : action = Edit;
215 : }
216 : } else {
217 0 : (*stream) << '\n';
218 0 : for (size_t i = 0; i < depth; i++) {
219 0 : (*stream) << '\t';
220 : }
221 0 : if (trackActions && str == "~ACTIONS~") {
222 0 : actionsState = Key;
223 : //(*stream) << "<span class=\"key\">\"ACTIONS\"</span>";
224 : } else {
225 0 : (*stream) << "<span class=\"key\">";
226 0 : writeString(str);
227 0 : (*stream) << "</span>";
228 0 : (*stream) << ':';
229 0 : (*stream) << ' ';
230 : }
231 0 : offsetted = true;
232 : }
233 :
234 0 : }
235 :
236 0 : void onNextValue() {
237 0 : (*stream) << ',';
238 0 : }
239 :
240 0 : void onValue(const Value &val) {
241 0 : if (depth > 0) {
242 0 : if (popComplex && (val.isArray() || val.isDictionary())) {
243 0 : (*stream) << ' ';
244 : } else {
245 0 : if (!offsetted) {
246 0 : (*stream) << '\n';
247 0 : for (size_t i = 0; i < depth; i++) {
248 0 : (*stream) << '\t';
249 : }
250 0 : offsetted = true;
251 : }
252 : }
253 0 : popComplex = false;
254 : }
255 0 : }
256 :
257 : size_t depth = 0;
258 : bool popComplex = false;
259 : bool offsetted = false;
260 : Vector<bool> bstack;
261 : const Callback<void(StringView)> *stream;
262 : bool trackActions = false;
263 : enum {
264 : None,
265 : Key,
266 : Dict
267 : } actionsState = None;
268 : enum {
269 : Delete,
270 : Edit,
271 : } action = Delete;
272 : };
273 :
274 0 : void formatJsonAsHtml(const Callback<void(StringView)> &stream, const Value &data, bool actionHandling) {
275 0 : HtmlJsonEncoder enc(stream, actionHandling);
276 0 : data.encode(enc);
277 0 : }
278 :
279 0 : static void writeToRequest(Request &rctx, const Callback<void(StringView)> &stream, const Value &data, bool trackActions) {
280 0 : stream << HTML_LOAD_BEGIN;
281 0 : data::write(stream, data, data::EncodeFormat::Json);
282 0 : stream << HTML_LOAD_END;
283 0 : if (!trackActions) {
284 0 : stream << HTML_PRETTY;
285 0 : } else if (trackActions) {
286 0 : auto host = rctx.host();
287 0 : auto &res = host.getResources();
288 0 : stream << "<body class=\"api\" onload=\"init();\">";
289 0 : if (!res.empty()) {
290 0 : stream << "<div class=\"sidebar\"><h3>Resources</h3><ul>";
291 0 : for (auto & it : res) {
292 0 : stream << "<li><a href=\"" << it.second.path << "?pretty=api\">" << it.first->getName() << "</a></li>";
293 : }
294 0 : stream << "</ul></div>";
295 : }
296 :
297 0 : auto &info = rctx.getInfo();
298 0 : stream << "<div class=\"content\"><h3>" << info.url.path;
299 0 : if (info.status >= 400) {
300 0 : stream << " <span class=\"error\">" << info.statusLine << "</span>";
301 : }
302 0 : stream << "</h3><p id=\"content\"></p></div>";
303 :
304 : }
305 0 : }
306 :
307 2150 : void writeData(Request &rctx, const Value &data, bool allowJsonP) {
308 2150 : Request r = rctx;
309 2150 : writeData(rctx, [&] (StringView str) {
310 1072475 : rctx << str;
311 1074625 : }, [&] (const String &ct) {
312 2150 : r.setContentType(String(ct));
313 2150 : }, data, allowJsonP);
314 2150 : }
315 :
316 2150 : void writeData(Request &rctx, const Callback<void(StringView)> &stream, const Function<void(const String &)> &ct,
317 : const Value &data, bool allowJsonP) {
318 :
319 2150 : auto &info = rctx.getInfo();
320 2150 : bool allowCbor = rctx.getController()->isAcceptable("application/cbor") > 0.0f;
321 2150 : auto pretty = info.queryData.getValue("pretty");
322 :
323 2150 : if (allowCbor) {
324 0 : ct("application/cbor"_weak);
325 0 : data::write(stream, data, data::EncodeFormat::Cbor);
326 0 : return;
327 : }
328 :
329 2150 : if (allowJsonP) {
330 2125 : if (!info.queryData.empty()) {
331 850 : String obj;
332 850 : if (info.queryData.isString("callback")) {
333 0 : obj = info.queryData.getString("callback");
334 850 : } else if (info.queryData.isString("jsonp")) {
335 0 : obj = info.queryData.getString("jsonp");
336 : }
337 850 : if (!obj.empty()) {
338 0 : ct("application/javascript;charset=UTF-8");
339 0 : stream << obj << "(";;
340 0 : data::write(stream, data, (pretty?data::EncodeFormat::Pretty:data::EncodeFormat::Json));
341 0 : stream << ");\r\n";
342 0 : return;
343 : }
344 850 : }
345 : }
346 :
347 2150 : if (pretty.isString() && (pretty.getString() == "html" || pretty.getString() == "api")) {
348 0 : ct("text/html;charset=UTF-8");
349 0 : writeToRequest(rctx, stream, data, pretty.getString() == "api");
350 : } else {
351 2150 : if (pretty.isString() && pretty.getString() == "time") {
352 0 : ct("application/json;charset=UTF-8");
353 0 : data::write(stream, data, data::EncodeFormat::PrettyTime);
354 0 : stream << "\r\n";
355 : } else {
356 2150 : ct("application/json;charset=UTF-8");
357 2150 : data::write(stream, data, (pretty.asBool()?data::EncodeFormat::Pretty:data::EncodeFormat::Json));
358 2150 : stream << "\r\n";
359 : }
360 : }
361 2150 : }
362 :
363 25 : Status writeResourceFileData(Request &rctx, Value &&result) {
364 25 : Value file(result.isArray()?move(result.getValue(0)):move(result));
365 25 : auto path = db::File::getFilesystemPath(rctx.host().getRoot(), uint64_t(file.getInteger("__oid")));
366 :
367 25 : auto &info = rctx.getInfo();
368 25 : if (info.queryData.getBool("stat")) {
369 0 : file.setBool(filesystem::exists(path), "exists");
370 0 : return writeResourceData(rctx, move(file), Value());
371 : }
372 :
373 25 : auto &loc = file.getString("location");
374 25 : if (filesystem::exists(path) && loc.empty()) {
375 25 : if (!output::writeFileHeaders(rctx, file)) {
376 0 : return HTTP_NOT_MODIFIED;
377 : }
378 :
379 25 : rctx.setFilename(std::move(path));
380 25 : return OK;
381 : }
382 :
383 0 : if (!loc.empty()) {
384 0 : rctx.setFilename(nullptr);
385 0 : return rctx.redirectTo(std::move(loc));
386 : }
387 :
388 0 : return HTTP_NOT_FOUND;
389 25 : }
390 :
391 1875 : Status writeResourceData(Request &rctx, Value &&result, Value && origin) {
392 1875 : Value data(move(origin));
393 :
394 1875 : data.setInteger(Time::now().toMicros(), "date");
395 : #if DEBUG
396 1875 : auto &debug = rctx.getDebugMessages();
397 1875 : if (!debug.empty()) {
398 0 : data.setArray(debug, "debug");
399 : }
400 : #endif
401 1875 : auto &error = rctx.getErrorMessages();
402 1875 : if (!error.empty()) {
403 0 : data.setArray(error, "errors");
404 : }
405 :
406 1875 : data.setValue(move(result), "result");
407 1875 : data.setBool(true, "OK");
408 1875 : rctx.writeData(data, true);
409 :
410 1875 : return DONE;
411 1875 : }
412 :
413 0 : Status writeResourceFileHeader(Request &rctx, const Value &result) {
414 0 : Value file(result.isArray()?std::move(result.getValue(0)):std::move(result));
415 :
416 0 : if (!file) {
417 0 : return HTTP_NOT_FOUND;
418 : }
419 :
420 0 : auto path = db::File::getFilesystemPath(rctx.host().getRoot(), uint64_t(file.getInteger("__oid")));
421 0 : auto &loc = file.getString("location");
422 :
423 0 : if (!filesystem::exists(path) && loc.empty()) {
424 0 : return HTTP_NOT_FOUND;
425 : }
426 :
427 0 : if (!writeFileHeaders(rctx, file)) {
428 0 : return HTTP_NOT_MODIFIED;
429 : }
430 :
431 0 : return DONE;
432 0 : }
433 :
434 25 : bool writeFileHeaders(Request &rctx, const Value &file, const String &convertType) {
435 25 : auto path = db::File::getFilesystemPath(rctx.host().getRoot(), file.getInteger("__oid"));
436 :
437 25 : rctx.setFilename(path, true, file.getInteger("mtime"));
438 :
439 25 : auto &info = rctx.getInfo();
440 25 : auto mtime = info.stat.mtime;
441 :
442 25 : auto tag = rctx.getResponseHeader("etag");
443 :
444 25 : auto match = rctx.getRequestHeader("if-none-match");
445 25 : auto modified = rctx.getRequestHeader("if-modified-since");
446 25 : if (!match.empty() && !modified.empty()) {
447 0 : if (tag == match && Time::fromHttp(modified).toSeconds() >= mtime.toSeconds()) {
448 0 : return false;
449 : }
450 25 : } else if (!match.empty() && tag == match) {
451 0 : return false;
452 25 : } else if (!modified.empty() && Time::fromHttp(modified).toSeconds() >= mtime.toSeconds()) {
453 0 : return false;
454 : }
455 :
456 25 : rctx.setResponseHeader("X-FileModificationTime", toString(mtime.toMicros()));
457 :
458 25 : if (file.isString("location")) {
459 0 : rctx.setResponseHeader("X-FileLocation", file.getString("location"));
460 : }
461 25 : if (!convertType.empty()) {
462 0 : rctx.setContentType(String(convertType));
463 : } else {
464 25 : rctx.setResponseHeader("X-FileSize", toString(file.getInteger("size")));
465 25 : if (info.headerRequest) {
466 0 : rctx.setResponseHeader("Content-Length", toString(info.stat.size));
467 : }
468 25 : rctx.setContentType(String(file.getString("type")));
469 : }
470 25 : return true;
471 :
472 25 : }
473 :
474 75 : String makeEtag(uint32_t idHash, Time mtime) {
475 75 : auto time = mtime.toMicroseconds();
476 75 : Bytes etagData; etagData.resize(12);
477 75 : memcpy(etagData.data(), (const void *)&idHash, sizeof(uint32_t));
478 75 : memcpy(etagData.data() + 4, (const void *)&time, sizeof(int64_t));
479 :
480 225 : return toString('"', base64::encode<Interface>(etagData), '"');
481 75 : }
482 :
483 75 : bool checkCacheHeaders(Request &rctx, Time t, const StringView &etag) {
484 75 : rctx.setResponseHeader("ETag", etag.str<Interface>());
485 75 : rctx.setResponseHeader("Last-Modified", t.toHttp<Interface>());
486 :
487 75 : auto match = rctx.getRequestHeader("if-none-match");
488 75 : auto modified = rctx.getRequestHeader("if-modified-since");
489 75 : if (!match.empty() && !modified.empty()) {
490 0 : if (etag == match && Time::fromHttp(modified).toSeconds() >= t.toSeconds()) {
491 0 : return true;
492 : }
493 75 : } else if (!match.empty() && etag == match) {
494 0 : return true;
495 75 : } else if (!modified.empty() && Time::fromHttp(modified).toSeconds() >= t.toSeconds()) {
496 0 : return true;
497 : }
498 :
499 75 : return false;
500 : }
501 :
502 75 : bool checkCacheHeaders(Request &rctx, Time t, uint32_t idHash) {
503 75 : return checkCacheHeaders(rctx, t, makeEtag(idHash, t));
504 : }
505 :
506 : }
|