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 "SPWebTools.h"
24 : #include "SPWebRoot.h"
25 : #include "SPDbUser.h"
26 : #include "SPDbContinueToken.h"
27 : #include "SPDbQuery.h"
28 : #include "SPSqlHandle.h"
29 :
30 : namespace STAPPLER_VERSIONIZED stappler::web::tools {
31 :
32 25 : static Status makeUnavailablePage(Request &req) {
33 25 : req.runPug("virtual://html/errors_unauthorized.pug", [&] (pug::Context &exec, const pug::Template &) -> bool {
34 25 : exec.set("version", Value(config::getWebserverVersionString()));
35 25 : return true;
36 : });
37 25 : return HTTP_UNAUTHORIZED;
38 : }
39 :
40 75 : Status ErrorsGui::onTranslateName(Request &req) {
41 75 : req.setResponseHeader("WWW-Authenticate", req.host().getHostInfo().hostname);
42 :
43 75 : auto t = req.acquireDbTransaction();
44 75 : if (!t) {
45 0 : return HTTP_INTERNAL_SERVER_ERROR;
46 : }
47 :
48 75 : auto u = req.getAuthorizedUser();
49 75 : if (u && u->isAdmin()) {
50 75 : auto errorScheme = req.host().getErrorScheme();
51 75 : auto &d = req.getInfo().queryData;
52 75 : if (d.hasValue("delete")) {
53 25 : if (auto id = StringView(d.getString("delete")).readInteger().get(0)) {
54 25 : u = req.getAuthorizedUser();
55 25 : if (u && u->isAdmin()) {
56 25 : if (errorScheme) {
57 25 : errorScheme->remove(t, id);
58 : }
59 : }
60 : }
61 :
62 25 : auto c = d.getString("c");
63 25 : auto tag = d.getString("tag");
64 :
65 25 : StringStream url;
66 25 : url << req.getInfo().url.path;
67 :
68 25 : if (!c.empty()) {
69 25 : auto token = db::ContinueToken(d.getString("c"));
70 25 : db::Query q;
71 25 : if (!tag.empty()) {
72 25 : q.select("tags", db::Comparation::Includes, Value(tag));
73 : }
74 25 : token.refresh(*errorScheme, t, q);
75 :
76 25 : url << "?c=" << token.encode();
77 25 : }
78 25 : if (!tag.empty()) {
79 25 : if (c.empty()) {
80 0 : url << "?tag=" << tag;
81 : } else {
82 25 : url << "&tag=" << tag;
83 : }
84 : }
85 25 : return req.redirectTo(url.str());
86 25 : }
87 :
88 50 : String selectedTag;
89 50 : if (d.isString("tag")) {
90 25 : selectedTag = d.getString("tag");
91 : }
92 :
93 50 : Value errorsData;
94 50 : auto token = d.isString("c") ? db::ContinueToken(d.getString("c")) : db::ContinueToken("__oid", 25, true);
95 :
96 50 : if (errorScheme) {
97 50 : db::Query q;
98 50 : if (!selectedTag.empty()) {
99 25 : q.select("tags", db::Comparation::Includes, Value(selectedTag));
100 : }
101 50 : errorsData = token.perform(*errorScheme, t, q);
102 50 : }
103 :
104 50 : req.runPug("virtual://html/errors.pug", [&] (pug::Context &exec, const pug::Template &tpl) -> bool {
105 50 : ServerGui::defineBasics(exec, req, u);
106 :
107 50 : if (!selectedTag.empty()) {
108 25 : exec.set("selectedTag", Value(selectedTag));
109 : }
110 :
111 50 : if (auto iface = dynamic_cast<db::sql::SqlHandle *>(t.getAdapter().getBackendInterface())) {
112 50 : Value ret;
113 :
114 50 : auto driver = iface->getDriver();
115 :
116 50 : auto tagUnwrapQuery = (driver->getDriverName() == "sqlite")
117 50 : ? toString("SELECT __oid, __unwrap_value as tag FROM ", errorScheme->getName(), ", sp_unwrap(tags) as unwrap")
118 100 : : toString("SELECT __oid, unnest(tags) as tag FROM ", errorScheme->getName());
119 :
120 50 : auto query = toString("SELECT tag, COUNT(*) FROM (", tagUnwrapQuery, ") s GROUP BY tag;;");
121 50 : iface->performSimpleSelect(query, [&] (db::Result &res) {
122 150 : for (auto it : res) {
123 700 : ret.addValue(Value({
124 200 : pair("tag", Value(it.toString(0))),
125 200 : pair("count", Value(it.toInteger(1))),
126 200 : pair("selected", Value(it.toString(0) == selectedTag))
127 400 : }));
128 : }
129 50 : });
130 :
131 50 : exec.set("tags", move(ret));
132 50 : }
133 :
134 50 : if (errorsData.size() > 0) {
135 50 : exec.set("errors", true, &errorsData);
136 : }
137 :
138 50 : auto current = token.encode();
139 :
140 : Value cursor({
141 100 : pair("start", Value(token.getStart())),
142 100 : pair("end", Value(token.getEnd())),
143 100 : pair("current", Value(current)),
144 100 : pair("total", Value(token.getTotal())),
145 450 : });
146 :
147 50 : if (token.hasNext()) {
148 0 : cursor.setString(token.encodeNext(), "next");
149 : }
150 :
151 50 : if (token.hasPrev()) {
152 0 : cursor.setString(token.encodePrev(), "prev");
153 : }
154 :
155 50 : exec.set("cursor", move(cursor));
156 50 : return true;
157 50 : });
158 50 : return DONE;
159 50 : } else {
160 0 : return makeUnavailablePage(req);
161 : }
162 : }
163 :
164 50 : Status HandlersGui::onTranslateName(Request &req) {
165 50 : req.setResponseHeader("WWW-Authenticate", req.host().getHostInfo().hostname);
166 :
167 50 : auto u = req.getAuthorizedUser();
168 50 : if (u && u->isAdmin()) {
169 25 : auto &hdl = req.host().getRequestHandlers();
170 :
171 25 : Value servh;
172 25 : Value ret;
173 500 : for (auto &it : hdl) {
174 475 : auto &v = (it.second.component == "root") ? servh.emplace() : ret.emplace();
175 475 : v.setValue(it.first, "name");
176 475 : if (!it.second.data.isNull()) {
177 0 : v.setValue(it.second.data, "data");
178 : }
179 475 : if (it.second.scheme) {
180 250 : v.setString(it.second.scheme->getName(), "scheme");
181 : }
182 475 : if (!it.second.component.empty()) {
183 475 : if (it.second.component == "root") {
184 175 : v.setBool(true, "server");
185 : }
186 475 : v.setString(it.second.component, "component");
187 : }
188 475 : if (it.first.back() == '/') {
189 400 : v.setBool(true, "forSubPaths");
190 : }
191 475 : if (it.second.map) {
192 25 : auto base = StringView(it.first);
193 25 : if (base.ends_with("/")) {
194 25 : base = StringView(base, base.size() - 1);
195 : }
196 25 : auto &m = v.emplace("map");
197 125 : for (auto &iit : it.second.map->getHandlers()) {
198 100 : auto &mVal = m.emplace();
199 100 : mVal.setString(iit.getName(), "name");
200 100 : mVal.setString(toString(base, iit.getPattern()), "pattern");
201 :
202 100 : switch (iit.getMethod()) {
203 25 : case RequestMethod::Get: mVal.setString("GET", "method"); break;
204 0 : case RequestMethod::Put: mVal.setString("PUT", "method"); break;
205 75 : case RequestMethod::Post: mVal.setString("POST", "method"); break;
206 0 : case RequestMethod::Delete: mVal.setString("DELETE", "method"); break;
207 0 : case RequestMethod::Connect: mVal.setString("CONNECT", "method"); break;
208 0 : case RequestMethod::Options: mVal.setString("OPTIONS", "method"); break;
209 0 : case RequestMethod::Trace: mVal.setString("TRACE", "method"); break;
210 0 : case RequestMethod::Patch: mVal.setString("PATCH", "method"); break;
211 0 : default: break;
212 : }
213 :
214 100 : auto &qScheme = iit.getQueryScheme().getFields();
215 100 : if (!qScheme.empty()) {
216 50 : auto &qVal = mVal.emplace("query");
217 150 : for (auto &it : qScheme) {
218 100 : auto &v = qVal.addValue(it.second.getTypeDesc());
219 100 : v.setString(it.first, "name");
220 : }
221 : }
222 :
223 100 : auto &iScheme = iit.getInputScheme().getFields();
224 100 : if (!iScheme.empty()) {
225 50 : auto &qVal = mVal.emplace("input");
226 225 : for (auto &it : iScheme) {
227 175 : auto &v = qVal.addValue(it.second.getTypeDesc());
228 175 : v.setString(it.first, "name");
229 : }
230 : }
231 : }
232 : }
233 : }
234 :
235 200 : for (auto &it : servh.asArray()) {
236 175 : ret.addValue(move(it));
237 : }
238 :
239 25 : req.runPug("virtual://html/handlers.pug", [&] (pug::Context &exec, const pug::Template &tpl) -> bool {
240 25 : ServerGui::defineBasics(exec, req, u);
241 25 : exec.set("handlers", std::move(ret));
242 25 : return true;
243 : });
244 25 : return DONE;
245 25 : } else {
246 25 : return makeUnavailablePage(req);
247 : }
248 : }
249 :
250 250 : Status ReportsGui::onTranslateName(Request &req) {
251 250 : req.setResponseHeader("WWW-Authenticate", req.host().getHostInfo().hostname);
252 :
253 250 : auto u = req.getAuthorizedUser();
254 250 : if (u && u->isAdmin()) {
255 250 : auto reportsAddress = req.host().getDocumentRootPath(".reports");
256 :
257 225 : auto readTime = [&] (StringView name) {
258 225 : if (name.starts_with("crash.")) {
259 152 : name.skipUntil<StringView::CharGroup<CharGroupId::Numbers>>();
260 304 : return Time::microseconds(name.readInteger(10).get());
261 : } else {
262 73 : name.skipUntil<StringView::CharGroup<CharGroupId::Numbers>>();
263 146 : return Time::milliseconds(name.readInteger(10).get());
264 : }
265 : };
266 :
267 250 : Value paths;
268 250 : Value file;
269 250 : if (_subPath.empty() || _subPath == "/") {
270 100 : filesystem::ftw(reportsAddress, [&] (StringView path, bool isFile) {
271 250 : if (isFile) {
272 150 : auto name = filepath::lastComponent(path);
273 150 : if (name.starts_with("crash.") || name.starts_with("update.")) {
274 150 : auto &info = paths.emplace();
275 150 : info.setString(name, "name");
276 150 : if (auto t = readTime(name)) {
277 150 : info.setInteger(t.toMicros(), "time");
278 150 : info.setString(t.toHttp<Interface>(), "date");
279 : }
280 : }
281 : }
282 250 : }, 1);
283 :
284 100 : if (paths) {
285 75 : std::sort(paths.asArray().begin(), paths.asArray().end(), [&] (const Value &l, const Value &r) {
286 129 : return l.getInteger("time") > r.getInteger("time");
287 : });
288 : }
289 150 : } else if (_subPathVec.size() == 1) {
290 150 : auto name = _subPathVec.front();
291 :
292 150 : auto reportsAddress = filepath::merge<Interface>(req.host().getDocumentRootPath(".reports"), name);
293 :
294 150 : filesystem::Stat stat;
295 150 : auto exists = filesystem::stat(reportsAddress, stat);
296 150 : if (exists && !stat.isDir) {
297 150 : if (req.getInfo().queryData.getBool("remove")) {
298 75 : filesystem::remove(reportsAddress);
299 75 : return req.redirectTo(StringView(_originPath, _originPath.size() - _subPath.size()));
300 : }
301 75 : auto data = filesystem::readTextFile<Interface>(reportsAddress);
302 75 : if (!data.empty()) {
303 75 : file.setString(move(data), "data");
304 75 : auto name = filepath::lastComponent(reportsAddress);
305 75 : file.setString(name, "name");
306 75 : if (auto t = readTime(name)) {
307 75 : file.setInteger(t.toMicros(), "time");
308 75 : file.setString(t.toHttp<Interface>(), "date");
309 : }
310 : }
311 75 : }
312 150 : }
313 :
314 175 : req.runPug("virtual://html/reports.pug", [&] (pug::Context &exec, const pug::Template &tpl) -> bool {
315 175 : ServerGui::defineBasics(exec, req, u);
316 175 : if (paths) {
317 75 : exec.set("files", move(paths));
318 100 : } else if (file) {
319 75 : exec.set("file", move(file));
320 : }
321 175 : return true;
322 : });
323 :
324 175 : return DONE;
325 250 : } else {
326 0 : return makeUnavailablePage(req);
327 : }
328 : }
329 :
330 : }
|