repository_infos: Enable automatic updates on the main Haiku repostiory.
[haiku.git] / src / apps / haikudepot / server / WebAppInterface.cpp
blobf439b287084d8c084af93ed3ab3221dacfeea3cd
1 /*
2 * Copyright 2014, Stephan Aßmus <superstippi@gmx.de>.
3 * Copyright 2016-2017, Andrew Lindesay <apl@lindesay.co.nz>.
4 * All rights reserved. Distributed under the terms of the MIT License.
5 */
7 #include "WebAppInterface.h"
9 #include <stdio.h>
11 #include <AppFileInfo.h>
12 #include <Application.h>
13 #include <Autolock.h>
14 #include <File.h>
15 #include <HttpHeaders.h>
16 #include <HttpRequest.h>
17 #include <Json.h>
18 #include <Message.h>
19 #include <Roster.h>
20 #include <Url.h>
21 #include <UrlContext.h>
22 #include <UrlProtocolListener.h>
23 #include <UrlProtocolRoster.h>
25 #include "AutoLocker.h"
26 #include "List.h"
27 #include "Logger.h"
28 #include "PackageInfo.h"
29 #include "ServerSettings.h"
32 #define BASEURL_DEFAULT "https://depot.haiku-os.org"
33 #define USERAGENT_FALLBACK_VERSION "0.0.0"
36 class JsonBuilder {
37 public:
38 JsonBuilder()
40 fString("{"),
41 fInList(false)
45 JsonBuilder& AddObject()
47 fString << '{';
48 fInList = false;
49 return *this;
52 JsonBuilder& AddObject(const char* name)
54 _StartName(name);
55 fString << '{';
56 fInList = false;
57 return *this;
60 JsonBuilder& EndObject()
62 fString << '}';
63 fInList = true;
64 return *this;
67 JsonBuilder& AddArray(const char* name)
69 _StartName(name);
70 fString << '[';
71 fInList = false;
72 return *this;
75 JsonBuilder& EndArray()
77 fString << ']';
78 fInList = true;
79 return *this;
82 JsonBuilder& AddStrings(const StringList& strings)
84 for (int i = 0; i < strings.CountItems(); i++)
85 AddItem(strings.ItemAtFast(i));
86 return *this;
89 JsonBuilder& AddItem(const char* item)
91 return AddItem(item, false);
94 JsonBuilder& AddItem(const char* item, bool nullIfEmpty)
96 if (item == NULL || (nullIfEmpty && strlen(item) == 0)) {
97 if (fInList)
98 fString << ",null";
99 else
100 fString << "null";
101 } else {
102 if (fInList)
103 fString << ",\"";
104 else
105 fString << '"';
106 fString << _EscapeString(item);
107 fString << '"';
109 fInList = true;
110 return *this;
113 JsonBuilder& AddValue(const char* name, const char* value)
115 return AddValue(name, value, false);
118 JsonBuilder& AddValue(const char* name, const char* value,
119 bool nullIfEmpty)
121 _StartName(name);
122 if (value == NULL || (nullIfEmpty && strlen(value) == 0)) {
123 fString << "null";
124 } else {
125 fString << '"';
126 fString << _EscapeString(value);
127 fString << '"';
129 fInList = true;
130 return *this;
133 JsonBuilder& AddValue(const char* name, int value)
135 _StartName(name);
136 fString << value;
137 fInList = true;
138 return *this;
141 JsonBuilder& AddValue(const char* name, bool value)
143 _StartName(name);
144 if (value)
145 fString << "true";
146 else
147 fString << "false";
148 fInList = true;
149 return *this;
152 const BString& End()
154 fString << "}\n";
155 return fString;
158 private:
159 void _StartName(const char* name)
161 if (fInList)
162 fString << ",\"";
163 else
164 fString << '"';
165 fString << _EscapeString(name);
166 fString << "\":";
169 BString _EscapeString(const char* original) const
171 BString string(original);
172 string.ReplaceAll("\\", "\\\\");
173 string.ReplaceAll("\"", "\\\"");
174 string.ReplaceAll("/", "\\/");
175 string.ReplaceAll("\b", "\\b");
176 string.ReplaceAll("\f", "\\f");
177 string.ReplaceAll("\n", "\\n");
178 string.ReplaceAll("\r", "\\r");
179 string.ReplaceAll("\t", "\\t");
180 return string;
183 private:
184 BString fString;
185 bool fInList;
189 class ProtocolListener : public BUrlProtocolListener {
190 public:
191 ProtocolListener(bool traceLogging)
193 fDownloadIO(NULL),
194 fTraceLogging(traceLogging)
198 virtual ~ProtocolListener()
202 virtual void ConnectionOpened(BUrlRequest* caller)
206 virtual void HostnameResolved(BUrlRequest* caller, const char* ip)
210 virtual void ResponseStarted(BUrlRequest* caller)
214 virtual void HeadersReceived(BUrlRequest* caller, const BUrlResult& result)
218 virtual void DataReceived(BUrlRequest* caller, const char* data,
219 off_t position, ssize_t size)
221 if (fDownloadIO != NULL)
222 fDownloadIO->Write(data, size);
225 virtual void DownloadProgress(BUrlRequest* caller, ssize_t bytesReceived,
226 ssize_t bytesTotal)
230 virtual void UploadProgress(BUrlRequest* caller, ssize_t bytesSent,
231 ssize_t bytesTotal)
235 virtual void RequestCompleted(BUrlRequest* caller, bool success)
239 virtual void DebugMessage(BUrlRequest* caller,
240 BUrlProtocolDebugMessage type, const char* text)
242 if (fTraceLogging)
243 printf("jrpc: %s\n", text);
246 void SetDownloadIO(BDataIO* downloadIO)
248 fDownloadIO = downloadIO;
251 private:
252 BDataIO* fDownloadIO;
253 bool fTraceLogging;
258 WebAppInterface::fRequestIndex = 0;
261 enum {
262 NEEDS_AUTHORIZATION = 1 << 0,
266 WebAppInterface::WebAppInterface()
268 fLanguage("en")
273 WebAppInterface::WebAppInterface(const WebAppInterface& other)
275 fUsername(other.fUsername),
276 fPassword(other.fPassword),
277 fLanguage(other.fLanguage)
282 WebAppInterface::~WebAppInterface()
287 WebAppInterface&
288 WebAppInterface::operator=(const WebAppInterface& other)
290 if (this == &other)
291 return *this;
293 fUsername = other.fUsername;
294 fPassword = other.fPassword;
295 fLanguage = other.fLanguage;
297 return *this;
301 void
302 WebAppInterface::SetAuthorization(const BString& username,
303 const BString& password)
305 fUsername = username;
306 fPassword = password;
310 void
311 WebAppInterface::SetPreferredLanguage(const BString& language)
313 fLanguage = language;
317 status_t
318 WebAppInterface::RetrieveRepositoriesForSourceBaseURLs(
319 const StringList& repositorySourceBaseURLs,
320 BMessage& message)
322 BString jsonString = JsonBuilder()
323 .AddValue("jsonrpc", "2.0")
324 .AddValue("id", ++fRequestIndex)
325 .AddValue("method", "searchRepositories")
326 .AddArray("params")
327 .AddObject()
328 .AddArray("repositorySourceSearchUrls")
329 .AddStrings(repositorySourceBaseURLs)
330 .EndArray()
331 .AddValue("offset", 0)
332 .AddValue("limit", 1000) // effectively a safety limit
333 .EndObject()
334 .EndArray()
335 .End();
337 return _SendJsonRequest("repository", jsonString, 0, message);
341 status_t
342 WebAppInterface::RetrievePackageInfo(const BString& packageName,
343 const BString& architecture, const BString& repositoryCode,
344 BMessage& message)
346 BString jsonString = JsonBuilder()
347 .AddValue("jsonrpc", "2.0")
348 .AddValue("id", ++fRequestIndex)
349 .AddValue("method", "getPkg")
350 .AddArray("params")
351 .AddObject()
352 .AddValue("name", packageName)
353 .AddValue("architectureCode", architecture)
354 .AddValue("naturalLanguageCode", fLanguage)
355 .AddValue("repositoryCode", repositoryCode)
356 .AddValue("versionType", "NONE")
357 .EndObject()
358 .EndArray()
359 .End();
361 return _SendJsonRequest("pkg", jsonString, 0, message);
365 status_t
366 WebAppInterface::RetrieveBulkPackageInfo(const StringList& packageNames,
367 const StringList& packageArchitectures,
368 const StringList& repositoryCodes, BMessage& message)
370 BString jsonString = JsonBuilder()
371 .AddValue("jsonrpc", "2.0")
372 .AddValue("id", ++fRequestIndex)
373 .AddValue("method", "getBulkPkg")
374 .AddArray("params")
375 .AddObject()
376 .AddArray("pkgNames")
377 .AddStrings(packageNames)
378 .EndArray()
379 .AddArray("architectureCodes")
380 .AddStrings(packageArchitectures)
381 .EndArray()
382 .AddArray("repositoryCodes")
383 .AddStrings(repositoryCodes)
384 .EndArray()
385 .AddValue("naturalLanguageCode", fLanguage)
386 .AddValue("versionType", "LATEST")
387 .AddArray("filter")
388 .AddItem("PKGCATEGORIES")
389 .AddItem("PKGSCREENSHOTS")
390 .AddItem("PKGVERSIONLOCALIZATIONDESCRIPTIONS")
391 .AddItem("PKGCHANGELOG")
392 .EndArray()
393 .EndObject()
394 .EndArray()
395 .End();
397 return _SendJsonRequest("pkg", jsonString, 0, message);
401 status_t
402 WebAppInterface::RetrieveUserRatings(const BString& packageName,
403 const BString& architecture, int resultOffset, int maxResults,
404 BMessage& message)
406 BString jsonString = JsonBuilder()
407 .AddValue("jsonrpc", "2.0")
408 .AddValue("id", ++fRequestIndex)
409 .AddValue("method", "searchUserRatings")
410 .AddArray("params")
411 .AddObject()
412 .AddValue("pkgName", packageName)
413 .AddValue("pkgVersionArchitectureCode", architecture)
414 .AddValue("offset", resultOffset)
415 .AddValue("limit", maxResults)
416 .EndObject()
417 .EndArray()
418 .End();
420 return _SendJsonRequest("userrating", jsonString, 0, message);
424 status_t
425 WebAppInterface::RetrieveUserRating(const BString& packageName,
426 const BPackageVersion& version, const BString& architecture,
427 const BString &repositoryCode, const BString& username,
428 BMessage& message)
430 BString jsonString = JsonBuilder()
431 .AddValue("jsonrpc", "2.0")
432 .AddValue("id", ++fRequestIndex)
433 .AddValue("method", "getUserRatingByUserAndPkgVersion")
434 .AddArray("params")
435 .AddObject()
436 .AddValue("userNickname", username)
437 .AddValue("pkgName", packageName)
438 .AddValue("pkgVersionArchitectureCode", architecture)
439 .AddValue("pkgVersionMajor", version.Major(), true)
440 .AddValue("pkgVersionMinor", version.Minor(), true)
441 .AddValue("pkgVersionMicro", version.Micro(), true)
442 .AddValue("pkgVersionPreRelease", version.PreRelease(), true)
443 .AddValue("pkgVersionRevision", (int)version.Revision())
444 .AddValue("repositoryCode", repositoryCode)
445 .EndObject()
446 .EndArray()
447 .End();
449 return _SendJsonRequest("userrating", jsonString, 0, message);
453 status_t
454 WebAppInterface::CreateUserRating(const BString& packageName,
455 const BString& architecture, const BString& repositoryCode,
456 const BString& languageCode, const BString& comment,
457 const BString& stability, int rating, BMessage& message)
459 BString jsonString = JsonBuilder()
460 .AddValue("jsonrpc", "2.0")
461 .AddValue("id", ++fRequestIndex)
462 .AddValue("method", "createUserRating")
463 .AddArray("params")
464 .AddObject()
465 .AddValue("pkgName", packageName)
466 .AddValue("pkgVersionArchitectureCode", architecture)
467 .AddValue("pkgVersionType", "LATEST")
468 .AddValue("userNickname", fUsername)
469 .AddValue("rating", rating)
470 .AddValue("userRatingStabilityCode", stability, true)
471 .AddValue("comment", comment)
472 .AddValue("repositoryCode", repositoryCode)
473 .AddValue("naturalLanguageCode", languageCode)
474 .EndObject()
475 .EndArray()
476 .End();
478 return _SendJsonRequest("userrating", jsonString, NEEDS_AUTHORIZATION,
479 message);
483 status_t
484 WebAppInterface::UpdateUserRating(const BString& ratingID,
485 const BString& languageCode, const BString& comment,
486 const BString& stability, int rating, bool active, BMessage& message)
488 BString jsonString = JsonBuilder()
489 .AddValue("jsonrpc", "2.0")
490 .AddValue("id", ++fRequestIndex)
491 .AddValue("method", "updateUserRating")
492 .AddArray("params")
493 .AddObject()
494 .AddValue("code", ratingID)
495 .AddValue("rating", rating)
496 .AddValue("userRatingStabilityCode", stability, true)
497 .AddValue("comment", comment)
498 .AddValue("naturalLanguageCode", languageCode)
499 .AddValue("active", active)
500 .AddArray("filter")
501 .AddItem("ACTIVE")
502 .AddItem("NATURALLANGUAGE")
503 .AddItem("USERRATINGSTABILITY")
504 .AddItem("COMMENT")
505 .AddItem("RATING")
506 .EndArray()
507 .EndObject()
508 .EndArray()
509 .End();
511 return _SendJsonRequest("userrating", jsonString, NEEDS_AUTHORIZATION,
512 message);
516 status_t
517 WebAppInterface::RetrieveScreenshot(const BString& code,
518 int32 width, int32 height, BDataIO* stream)
520 BUrl url = ServerSettings::CreateFullUrl(
521 BString("/__pkgscreenshot/") << code << ".png" << "?tw="
522 << width << "&th=" << height);
524 bool isSecure = url.Protocol() == "https";
526 ProtocolListener listener(Logger::IsTraceEnabled());
527 listener.SetDownloadIO(stream);
529 BHttpHeaders headers;
530 ServerSettings::AugmentHeaders(headers);
532 BHttpRequest request(url, isSecure, "HTTP", &listener);
533 request.SetMethod(B_HTTP_GET);
534 request.SetHeaders(headers);
536 thread_id thread = request.Run();
537 wait_for_thread(thread, NULL);
539 const BHttpResult& result = dynamic_cast<const BHttpResult&>(
540 request.Result());
542 int32 statusCode = result.StatusCode();
544 if (statusCode == 200)
545 return B_OK;
547 fprintf(stderr, "failed to get screenshot from '%s': %" B_PRIi32 "\n",
548 url.UrlString().String(), statusCode);
549 return B_ERROR;
553 status_t
554 WebAppInterface::RequestCaptcha(BMessage& message)
556 BString jsonString = JsonBuilder()
557 .AddValue("jsonrpc", "2.0")
558 .AddValue("id", ++fRequestIndex)
559 .AddValue("method", "generateCaptcha")
560 .AddArray("params")
561 .AddObject()
562 .EndObject()
563 .EndArray()
564 .End();
566 return _SendJsonRequest("captcha", jsonString, 0, message);
570 status_t
571 WebAppInterface::CreateUser(const BString& nickName,
572 const BString& passwordClear, const BString& email,
573 const BString& captchaToken, const BString& captchaResponse,
574 const BString& languageCode, BMessage& message)
576 JsonBuilder builder;
577 builder
578 .AddValue("jsonrpc", "2.0")
579 .AddValue("id", ++fRequestIndex)
580 .AddValue("method", "createUser")
581 .AddArray("params")
582 .AddObject()
583 .AddValue("nickname", nickName)
584 .AddValue("passwordClear", passwordClear);
586 if (!email.IsEmpty())
587 builder.AddValue("email", email);
589 builder.AddValue("captchaToken", captchaToken)
590 .AddValue("captchaResponse", captchaResponse)
591 .AddValue("naturalLanguageCode", languageCode)
592 .EndObject()
593 .EndArray()
596 BString jsonString = builder.End();
598 return _SendJsonRequest("user", jsonString, 0, message);
602 status_t
603 WebAppInterface::AuthenticateUser(const BString& nickName,
604 const BString& passwordClear, BMessage& message)
606 BString jsonString = JsonBuilder()
607 .AddValue("jsonrpc", "2.0")
608 .AddValue("id", ++fRequestIndex)
609 .AddValue("method", "authenticateUser")
610 .AddArray("params")
611 .AddObject()
612 .AddValue("nickname", nickName)
613 .AddValue("passwordClear", passwordClear)
614 .EndObject()
615 .EndArray()
616 .End();
618 return _SendJsonRequest("user", jsonString, 0, message);
622 // #pragma mark - private
625 status_t
626 WebAppInterface::_SendJsonRequest(const char* domain, BString jsonString,
627 uint32 flags, BMessage& reply) const
629 if (Logger::IsTraceEnabled())
630 printf("_SendJsonRequest(%s)\n", jsonString.String());
632 BUrl url = ServerSettings::CreateFullUrl(BString("/__api/v1/") << domain);
633 bool isSecure = url.Protocol() == "https";
635 ProtocolListener listener(Logger::IsTraceEnabled());
636 BUrlContext context;
638 BHttpHeaders headers;
639 headers.AddHeader("Content-Type", "application/json");
640 ServerSettings::AugmentHeaders(headers);
642 BHttpRequest request(url, isSecure, "HTTP", &listener, &context);
643 request.SetMethod(B_HTTP_POST);
644 request.SetHeaders(headers);
646 // Authentication via Basic Authentication
647 // The other way would be to obtain a token and then use the Token Bearer
648 // header.
649 if ((flags & NEEDS_AUTHORIZATION) != 0
650 && !fUsername.IsEmpty() && !fPassword.IsEmpty()) {
651 BHttpAuthentication authentication(fUsername, fPassword);
652 authentication.SetMethod(B_HTTP_AUTHENTICATION_BASIC);
653 context.AddAuthentication(url, authentication);
656 BMemoryIO* data = new BMemoryIO(
657 jsonString.String(), jsonString.Length() - 1);
659 request.AdoptInputData(data, jsonString.Length() - 1);
661 BMallocIO replyData;
662 listener.SetDownloadIO(&replyData);
664 thread_id thread = request.Run();
665 wait_for_thread(thread, NULL);
667 const BHttpResult& result = dynamic_cast<const BHttpResult&>(
668 request.Result());
670 int32 statusCode = result.StatusCode();
671 if (statusCode != 200) {
672 printf("Response code: %" B_PRId32 "\n", statusCode);
673 return B_ERROR;
676 jsonString.SetTo(static_cast<const char*>(replyData.Buffer()),
677 replyData.BufferLength());
678 if (jsonString.Length() == 0)
679 return B_ERROR;
681 status_t status = BJson::Parse(jsonString, reply);
682 if (Logger::IsTraceEnabled() && status == B_BAD_DATA) {
683 printf("Parser choked on JSON:\n%s\n", jsonString.String());
685 return status;