| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917 |
- // This file is part of Desktop App Toolkit,
- // a set of libraries for developing nice desktop applications.
- //
- // For license and copyright information please follow this link:
- // https://github.com/desktop-app/legal/blob/master/LEGAL
- //
- #include "webview/platform/mac/webview_mac.h"
- #include "webview/webview_data_stream.h"
- #include "webview/webview_data_stream_memory.h"
- #include "base/algorithm.h"
- #include "base/debug_log.h"
- #include "base/unique_qptr.h"
- #include "base/weak_ptr.h"
- #include "base/flat_map.h"
- #include <crl/crl_on_main.h>
- #include <crl/crl_time.h>
- #include <rpl/rpl.h>
- #include <QtCore/QUrl>
- #include <QtGui/QDesktopServices>
- #include <QtGui/QWindow>
- #include <QtWidgets/QWidget>
- #import <Foundation/Foundation.h>
- #import <WebKit/WebKit.h>
- namespace {
- constexpr auto kDataUrlScheme = std::string_view("desktop-app-resource");
- constexpr auto kFullDomain = std::string_view("desktop-app-resource://domain/");
- constexpr auto kPartsCacheLimit = 32 * 1024 * 1024;
- constexpr auto kUuidSize = 16;
- using TaskPointer = id<WKURLSchemeTask>;
- [[nodiscard]] NSString *stdToNS(std::string_view value) {
- return [[NSString alloc]
- initWithBytes:value.data()
- length:value.length()
- encoding:NSUTF8StringEncoding];
- }
- [[nodiscard]] std::unique_ptr<char[]> WrapBytes(const char *data, int64 length) {
- Expects(length > 0);
- auto result = std::unique_ptr<char[]>(new char[length]);
- memcpy(result.get(), data, length);
- return result;
- }
- } // namespace
- @interface Handler : NSObject<WKScriptMessageHandler, WKNavigationDelegate, WKUIDelegate, WKURLSchemeHandler> {
- }
- - (id) initWithMessageHandler:(std::function<void(std::string)>)messageHandler navigationStartHandler:(std::function<bool(std::string,bool)>)navigationStartHandler navigationDoneHandler:(std::function<void(bool)>)navigationDoneHandler dialogHandler:(std::function<Webview::DialogResult(Webview::DialogArgs)>)dialogHandler dataRequested:(std::function<void(id<WKURLSchemeTask>,bool)>)dataRequested updateStates:(std::function<void()>)updateStates dataDomain:(std::string)dataDomain;
- - (void) userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message;
- - (void) webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler;
- - (void) observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context;
- - (void) webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation;
- - (void) webView:(WKWebView *)webView didFailNavigation:(WKNavigation *)navigation withError:(NSError *)error;
- - (nullable WKWebView *)webView:(WKWebView *)webView createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration forNavigationAction:(WKNavigationAction *)navigationAction windowFeatures:(WKWindowFeatures *)windowFeatures;
- - (void) webView:(WKWebView *)webView runOpenPanelWithParameters:(WKOpenPanelParameters *)parameters initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSArray<NSURL *> * _Nullable URLs))completionHandler;
- - (void) webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler;
- - (void) webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL result))completionHandler;
- - (void) webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString *result))completionHandler;
- - (void) webView:(WKWebView *)webView startURLSchemeTask:(id <WKURLSchemeTask>)task;
- - (void) webView:(WKWebView *)webView stopURLSchemeTask:(id <WKURLSchemeTask>)task;
- - (void) dealloc;
- @end // @interface Handler
- @implementation Handler {
- std::function<void(std::string)> _messageHandler;
- std::function<bool(std::string,bool)> _navigationStartHandler;
- std::function<void(bool)> _navigationDoneHandler;
- std::function<Webview::DialogResult(Webview::DialogArgs)> _dialogHandler;
- std::function<void(id<WKURLSchemeTask> task, bool started)> _dataRequested;
- std::function<void()> _updateStates;
- std::string _dataDomain;
- base::flat_map<TaskPointer, NSURLSessionDataTask*> _redirectedTasks;
- base::has_weak_ptr _guard;
- }
- - (id) initWithMessageHandler:(std::function<void(std::string)>)messageHandler navigationStartHandler:(std::function<bool(std::string,bool)>)navigationStartHandler navigationDoneHandler:(std::function<void(bool)>)navigationDoneHandler dialogHandler:(std::function<Webview::DialogResult(Webview::DialogArgs)>)dialogHandler dataRequested:(std::function<void(id<WKURLSchemeTask>,bool)>)dataRequested updateStates:(std::function<void()>)updateStates dataDomain:(std::string)dataDomain {
- if (self = [super init]) {
- _messageHandler = std::move(messageHandler);
- _navigationStartHandler = std::move(navigationStartHandler);
- _navigationDoneHandler = std::move(navigationDoneHandler);
- _dialogHandler = std::move(dialogHandler);
- _dataRequested = std::move(dataRequested);
- _updateStates = std::move(updateStates);
- _dataDomain = std::move(dataDomain);
- }
- return self;
- }
- - (void) userContentController:(WKUserContentController *)userContentController
- didReceiveScriptMessage:(WKScriptMessage *)message {
- id body = [message body];
- if ([body isKindOfClass:[NSString class]]) {
- NSString *string = (NSString*)body;
- _messageHandler([string UTF8String]);
- }
- }
- - (void) webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
- NSString *string = [[[navigationAction request] URL] absoluteString];
- WKFrameInfo *target = [navigationAction targetFrame];
- const auto newWindow = !target;
- const auto url = [string UTF8String];
- if (newWindow) {
- if (_navigationStartHandler && _navigationStartHandler(url, true)) {
- QDesktopServices::openUrl(QString::fromUtf8(url));
- }
- decisionHandler(WKNavigationActionPolicyCancel);
- } else {
- if ([target isMainFrame]
- && !std::string(url).starts_with(_dataDomain)
- && _navigationStartHandler
- && !_navigationStartHandler(url, false)) {
- decisionHandler(WKNavigationActionPolicyCancel);
- } else {
- decisionHandler(WKNavigationActionPolicyAllow);
- }
- }
- }
- - (void) observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
- if ([keyPath isEqualToString:@"URL"] || [keyPath isEqualToString:@"title"]) {
- if (_updateStates) {
- _updateStates();
- }
- }
- }
- - (void) webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation {
- if (_navigationDoneHandler) {
- _navigationDoneHandler(true);
- }
- if (_updateStates) {
- _updateStates();
- }
- }
- - (void) webView:(WKWebView *)webView didFailNavigation:(WKNavigation *)navigation withError:(NSError *)error {
- if (_navigationDoneHandler) {
- _navigationDoneHandler(false);
- }
- if (_updateStates) {
- _updateStates();
- }
- }
- - (nullable WKWebView *) webView:(WKWebView *)webView createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration forNavigationAction:(WKNavigationAction *)navigationAction windowFeatures:(WKWindowFeatures *)windowFeatures {
- NSString *string = [[[navigationAction request] URL] absoluteString];
- const auto url = [string UTF8String];
- if (_navigationStartHandler && _navigationStartHandler(url, true)) {
- QDesktopServices::openUrl(QString::fromUtf8(url));
- }
- return nil;
- }
- - (void) webView:(WKWebView *)webView runOpenPanelWithParameters:(WKOpenPanelParameters *)parameters initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSArray<NSURL *> * _Nullable URLs))completionHandler {
- NSOpenPanel *openPanel = [NSOpenPanel openPanel];
- if (@available(macOS 10.13.4, *)) {
- [openPanel setCanChooseDirectories:parameters.allowsDirectories];
- }
- [openPanel setCanChooseFiles:YES];
- [openPanel setAllowsMultipleSelection:parameters.allowsMultipleSelection];
- [openPanel setResolvesAliases:YES];
- [openPanel beginWithCompletionHandler:^(NSInteger result){
- if (result == NSModalResponseOK) {
- completionHandler([openPanel URLs]);
- }
- }];
- }
- - (void) webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler {
- auto text = [message UTF8String];
- auto uri = [[[frame request] URL] absoluteString];
- auto url = [uri UTF8String];
- const auto result = _dialogHandler(Webview::DialogArgs{
- .type = Webview::DialogType::Alert,
- .text = text,
- .url = url,
- });
- completionHandler();
- }
- - (void) webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL result))completionHandler {
- auto text = [message UTF8String];
- auto uri = [[[frame request] URL] absoluteString];
- auto url = [uri UTF8String];
- const auto result = _dialogHandler(Webview::DialogArgs{
- .type = Webview::DialogType::Confirm,
- .text = text,
- .url = url,
- });
- completionHandler(result.accepted ? YES : NO);
- }
- - (void) webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString *result))completionHandler {
- auto text = [prompt UTF8String];
- auto value = [defaultText UTF8String];
- auto uri = [[[frame request] URL] absoluteString];
- auto url = [uri UTF8String];
- const auto result = _dialogHandler(Webview::DialogArgs{
- .type = Webview::DialogType::Prompt,
- .value = value,
- .text = text,
- .url = url,
- });
- if (result.accepted) {
- completionHandler([NSString stringWithUTF8String:result.text.c_str()]);
- } else {
- completionHandler(nil);
- }
- }
- - (void) webView:(WKWebView *)webView startURLSchemeTask:(id<WKURLSchemeTask>)task {
- if (![self processRedirect:task]) {
- _dataRequested(task, true);
- }
- }
- - (BOOL) processRedirect:(id<WKURLSchemeTask>)task {
- NSString *url = task.request.URL.absoluteString;
- NSString *prefix = stdToNS(_dataDomain);
- NSString *resource = [url substringFromIndex:[prefix length]];
- const auto id = std::string([resource UTF8String]);
- const auto dot = id.find_first_of('.');
- const auto slash = id.find_first_of('/');
- if (dot == std::string::npos
- || slash == std::string::npos
- || dot > slash) {
- return NO;
- }
- NSMutableURLRequest *redirected = [task.request mutableCopy];
- redirected.URL = [NSURL URLWithString:[@"https://" stringByAppendingString:resource]];
- [redirected
- setValue:@"http://desktop-app-resource/page.html"
- forHTTPHeaderField:@"Referer"];
- const auto weak = base::make_weak(&_guard);
- NSURLSessionDataTask *dataTask = [[NSURLSession sharedSession]
- dataTaskWithRequest:redirected
- completionHandler:^(
- NSData * _Nullable data,
- NSURLResponse * _Nullable response,
- NSError * _Nullable error) {
- if (response) [response retain];
- if (error) [error retain];
- if (data) [data retain];
- crl::on_main([=] {
- if (weak) {
- const auto i = _redirectedTasks.find(task);
- if (i == end(_redirectedTasks)) {
- return;
- }
- NSURLSessionDataTask *dataTask = i->second;
- _redirectedTasks.erase(i);
- if (error) {
- [task didFailWithError:error];
- } else {
- [task didReceiveResponse:response];
- [task didReceiveData:data];
- [task didFinish];
- }
- [task release];
- [dataTask release];
- }
- if (response) [response release];
- if (error) [error release];
- if (data) [data release];
- });
- }];
- [task retain];
- [dataTask retain];
- _redirectedTasks.emplace(task, dataTask);
- [dataTask resume];
- return YES;
- }
- - (void) webView:(WKWebView *)webView stopURLSchemeTask:(id <WKURLSchemeTask>)task {
- const auto i = _redirectedTasks.find(task);
- if (i != end(_redirectedTasks)) {
- NSURLSessionDataTask *dataTask = i->second;
- _redirectedTasks.erase(i);
- [task release];
- [dataTask cancel];
- [dataTask release];
- } else {
- _dataRequested(task, false);
- }
- }
- - (void) dealloc {
- for (const auto &[task, dataTask] : base::take(_redirectedTasks)) {
- NSError *error = [NSError
- errorWithDomain:@"org.telegram.desktop"
- code:404
- userInfo:nil];
- [task didFailWithError:error];
- [task release];
- [dataTask cancel];
- [dataTask release];
- }
- [super dealloc];
- }
- @end // @implementation Handler
- namespace Webview {
- namespace {
- class Instance final : public Interface, public base::has_weak_ptr {
- public:
- explicit Instance(Config config);
- ~Instance();
- void navigate(std::string url) override;
- void navigateToData(std::string id) override;
- void reload() override;
- void init(std::string js) override;
- void eval(std::string js) override;
- void focus() override;
- QWidget *widget() override;
- void refreshNavigationHistoryState() override;
- auto navigationHistoryState()
- -> rpl::producer<NavigationHistoryState> override;
- void setOpaqueBg(QColor opaqueBg) override;
- private:
- struct Task {
- int index = 0;
- crl::time started = 0;
- };
- struct PartialResource {
- uint32 index = 0;
- uint32 total = 0;
- std::string mime;
- };
- struct PartData {
- std::unique_ptr<char[]> bytes;
- int64 length = 0;
- };
- struct CachedResult {
- std::string mime;
- NSData *data = nil;
- int64 requestFrom = 0;
- int64 requestLength = 0;
- int64 total = 0;
- explicit operator bool() const {
- return data != nil;
- }
- };
- using CacheKey = uint64;
- static void TaskFail(TaskPointer task);
- void taskFail(TaskPointer task, int indexToCheck);
- void taskDone(
- TaskPointer task,
- int indexToCheck,
- const std::string &mime,
- NSData *data,
- int64 offset,
- int64 total);
- void processDataRequest(TaskPointer task, bool started);
- [[nodiscard]] CachedResult fillFromCache(const DataRequest &request);
- void addToCache(uint32 resourceIndex, int64 offset, PartData data);
- void removeCacheEntry(CacheKey key);
- void pruneCache();
- void updateHistoryStates();
- [[nodiscard]] static CacheKey KeyFromValues(
- uint32 resourceIndex,
- int64 offset);
- [[nodiscard]] static uint32 ResourceIndexFromKey(CacheKey key);
- [[nodiscard]] static int64 OffsetFromKey(CacheKey key);
- WKUserContentController *_manager = nullptr;
- WKWebView *_webview = nullptr;
- Handler *_handler = nullptr;
- base::unique_qptr<QWindow> _window;
- base::unique_qptr<QWidget> _widget;
- std::string _dataProtocol;
- std::string _dataDomain;
- std::function<DataResult(DataRequest)> _dataRequestHandler;
- rpl::variable<NavigationHistoryState> _navigationHistoryState;
- base::flat_map<TaskPointer, Task> _tasks;
- base::flat_map<std::string, PartialResource> _partialResources;
- base::flat_map<CacheKey, PartData> _partsCache;
- std::vector<CacheKey> _partsLRU;
- int64 _cacheTotal = 0;
- int _taskAutoincrement = 0;
- };
- [[nodiscard]] NSUUID *UuidFromToken(const std::string &token) {
- const auto bytes = reinterpret_cast<const unsigned char*>(token.data());
- return (token.size() == kUuidSize)
- ? [[NSUUID alloc] initWithUUIDBytes:bytes]
- : nil;
- }
- [[nodiscard]] std::string UuidToToken(NSUUID *uuid) {
- if (!uuid) {
- return std::string();
- }
- auto result = std::string(kUuidSize, ' ');
- const auto bytes = reinterpret_cast<unsigned char*>(result.data());
- [uuid getUUIDBytes:bytes];
- return result;
- }
- Instance::Instance(Config config) {
- const auto weak = base::make_weak(this);
- const auto handleDataRequest = [=](id<WKURLSchemeTask> task, bool started) {
- if (weak) {
- processDataRequest(task, started);
- } else if (started) {
- TaskFail(task);
- }
- };
- WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init];
- _manager = configuration.userContentController;
- _dataProtocol = kDataUrlScheme;
- _dataDomain = kFullDomain;
- if (!config.dataProtocolOverride.empty()) {
- _dataProtocol = config.dataProtocolOverride;
- _dataDomain = _dataProtocol + "://domain/";
- }
- if (config.debug) {
- [configuration.preferences setValue:@YES forKey:@"developerExtrasEnabled"];
- }
- const auto updateStates = [=] {
- updateHistoryStates();
- };
- _handler = [[Handler alloc] initWithMessageHandler:config.messageHandler navigationStartHandler:config.navigationStartHandler navigationDoneHandler:config.navigationDoneHandler dialogHandler:config.dialogHandler dataRequested:handleDataRequest updateStates:updateStates dataDomain:_dataDomain];
- _dataRequestHandler = std::move(config.dataRequestHandler);
- [configuration setURLSchemeHandler:_handler forURLScheme:stdToNS(_dataProtocol)];
- if (@available(macOS 14, *)) {
- if (config.userDataToken != LegacyStorageIdToken().toStdString()) {
- NSUUID *uuid = UuidFromToken(config.userDataToken);
- [configuration setWebsiteDataStore:[WKWebsiteDataStore dataStoreForIdentifier:uuid]];
- [uuid release];
- }
- }
- _webview = [[WKWebView alloc] initWithFrame:NSZeroRect configuration:configuration];
- if (@available(macOS 13.3, *)) {
- _webview.inspectable = config.debug ? YES : NO;
- }
- [_manager addScriptMessageHandler:_handler name:@"external"];
- [_webview setNavigationDelegate:_handler];
- [_webview setUIDelegate:_handler];
- [_webview addObserver:_handler forKeyPath:@"URL" options:NSKeyValueObservingOptionNew context:nil];
- [_webview addObserver:_handler forKeyPath:@"title" options:NSKeyValueObservingOptionNew context:nil];
- [configuration release];
- _window.reset(QWindow::fromWinId(WId(_webview)));
- _widget.reset();
- _widget.reset(
- QWidget::createWindowContainer(
- _window.get(),
- config.parent,
- Qt::FramelessWindowHint));
- _widget->show();
- setOpaqueBg(config.opaqueBg);
- init(R"(
- window.external = {
- invoke: function(s) {
- window.webkit.messageHandlers.external.postMessage(s);
- }
- };)");
- }
- Instance::~Instance() {
- base::take(_window);
- base::take(_widget);
- [_manager removeScriptMessageHandlerForName:@"external"];
- [_webview setNavigationDelegate:nil];
- [_handler release];
- [_webview release];
- }
- void Instance::TaskFail(TaskPointer task) {
- [task didFailWithError:[NSError errorWithDomain:@"org.telegram.desktop" code:404 userInfo:nil]];
- }
- void Instance::taskFail(TaskPointer task, int indexToCheck) {
- if (indexToCheck) {
- const auto i = _tasks.find(task);
- if (i == end(_tasks) || i->second.index != indexToCheck) {
- return;
- }
- _tasks.erase(i);
- }
- TaskFail(task);
- }
- void Instance::taskDone(
- TaskPointer task,
- int indexToCheck,
- const std::string &mime,
- NSData *data,
- int64 offset,
- int64 total) {
- Expects(data != nil);
- if (indexToCheck) {
- const auto i = _tasks.find(task);
- if (i == end(_tasks) || i->second.index != indexToCheck) {
- return;
- }
- _tasks.erase(i);
- }
- const auto length = int64([data length]);
- const auto partial = (offset > 0) || (total != length);
- NSMutableDictionary *headers = [@{
- @"Content-Type": stdToNS(mime),
- @"Accept-Ranges": @"bytes",
- @"Cache-Control": @"no-store",
- @"Content-Length": stdToNS(std::to_string(length)),
- } mutableCopy];
- if (partial) {
- headers[@"Content-Range"] = stdToNS("bytes "
- + std::to_string(offset)
- + '-'
- + std::to_string(offset + length - 1)
- + '/'
- + std::to_string(total));
- }
- NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc]
- initWithURL:task.request.URL
- statusCode:(partial ? 206 : 200)
- HTTPVersion:@"HTTP/1.1"
- headerFields:headers];
- [task didReceiveResponse:response];
- [task didReceiveData:data];
- [task didFinish];
- }
- Instance::CacheKey Instance::KeyFromValues(uint32 resourceIndex, int64 offset) {
- return (uint64(resourceIndex) << 32) | uint32(offset);
- }
- uint32 Instance::ResourceIndexFromKey(CacheKey key) {
- return uint32(key >> 32);
- }
- int64 Instance::OffsetFromKey(CacheKey key) {
- return int64(key & 0xFFFFFFFFULL);
- }
- void Instance::addToCache(uint32 resourceIndex, int64 offset, PartData data) {
- auto key = KeyFromValues(resourceIndex, offset);
- while (true) { // Remove parts that are already in cache.
- auto i = _partsCache.upper_bound(key);
- if (i != begin(_partsCache)) {
- --i;
- const auto alreadyIndex = ResourceIndexFromKey(i->first);
- if (alreadyIndex == resourceIndex) {
- const auto &already = i->second;
- const auto alreadyOffset = OffsetFromKey(i->first);
- const auto alreadyTill = alreadyOffset + already.length;
- if (alreadyTill >= offset + data.length) {
- return; // Fully in cache.
- } else if (alreadyTill > offset) {
- const auto delta = alreadyTill - offset;
- offset += delta;
- data.length -= delta;
- data.bytes = WrapBytes(data.bytes.get() + delta, data.length);
- key = KeyFromValues(resourceIndex, offset);
- continue;
- }
- }
- ++i;
- }
- if (i != end(_partsCache)) {
- const auto alreadyIndex = ResourceIndexFromKey(i->first);
- if (alreadyIndex == resourceIndex) {
- const auto &already = i->second;
- const auto alreadyOffset = OffsetFromKey(i->first);
- Assert(alreadyOffset > offset);
- const auto alreadyTill = alreadyOffset + already.length;
- if (alreadyTill <= offset + data.length) {
- removeCacheEntry(i->first);
- continue;
- } else if (alreadyOffset < offset + data.length) {
- const auto delta = offset + data.length - alreadyOffset;
- data.length -= delta;
- data.bytes = WrapBytes(data.bytes.get(), data.length);
- continue;
- }
- }
- }
- break;
- }
- _partsLRU.push_back(key);
- _cacheTotal += data.length;
- _partsCache[key] = std::move(data);
- pruneCache();
- }
- void Instance::pruneCache() {
- while (_cacheTotal > kPartsCacheLimit) {
- Assert(!_partsLRU.empty());
- removeCacheEntry(_partsLRU.front());
- }
- }
- void Instance::updateHistoryStates() {
- NSURL *maybeUrl = [_webview URL];
- NSString *maybeTitle = [_webview title];
- const auto url = maybeUrl
- ? std::string([[maybeUrl absoluteString] UTF8String])
- : std::string();
- const auto title = maybeTitle
- ? std::string([maybeTitle UTF8String])
- : std::string();
- _navigationHistoryState = NavigationHistoryState{
- .url = url,
- .title = title,
- .canGoBack = ([_webview canGoBack] == YES),
- .canGoForward = ([_webview canGoForward] == YES),
- };
- }
- void Instance::removeCacheEntry(CacheKey key) {
- auto &part = _partsCache[key];
- Assert(part.length > 0);
- Assert(_cacheTotal >= part.length);
- _cacheTotal -= part.length;
- _partsCache.remove(key);
- _partsLRU.erase(
- std::remove(begin(_partsLRU), end(_partsLRU), key),
- end(_partsLRU));
- }
- Instance::CachedResult Instance::fillFromCache(
- const DataRequest &request) {
- auto &partial = _partialResources[request.id];
- const auto index = partial.index;
- if (!index) {
- partial.index = uint32(_partialResources.size());
- return {};
- }
- auto i = _partsCache.upper_bound(
- KeyFromValues(partial.index, request.offset));
- if (i == begin(_partsCache)) {
- return {};
- }
- --i;
- if (ResourceIndexFromKey(i->first) != index) {
- return {};
- }
- const auto alreadyOffset = OffsetFromKey(i->first);
- const auto alreadyTill = alreadyOffset + i->second.length;
- if (alreadyTill <= request.offset) {
- return {};
- }
- auto till = alreadyTill;
- for (auto j = i + 1; j != end(_partsCache); ++j) {
- const auto offset = OffsetFromKey(j->first);
- if (ResourceIndexFromKey(j->first) != index || offset > till) {
- break;
- }
- till = offset + j->second.length;
- if (request.limit <= 0 || till >= request.offset + request.limit) {
- break;
- }
- }
- const auto length = (request.limit > 0) ? request.limit : (till - request.offset);
- if (till < request.offset + length) {
- return {};
- }
- auto result = [NSMutableData dataWithLength:length];
- auto from = request.offset;
- auto fill = length;
- auto bytes = static_cast<char*>([result mutableBytes]);
- for (auto j = i; j != end(_partsCache); ++j) {
- const auto offset = OffsetFromKey(j->first);
- const auto copy = std::min(fill, offset + j->second.length - from);
- Assert(copy > 0);
- Assert(from >= offset);
- memcpy(bytes, j->second.bytes.get() + (from - offset), copy);
- from += copy;
- fill -= copy;
- bytes += copy;
- const auto lru = std::find(begin(_partsLRU), end(_partsLRU), j->first);
- Assert(lru != end(_partsLRU));
- if (const auto next = lru + 1; next != end(_partsLRU)) {
- std::rotate(lru, next, end(_partsLRU));
- }
- if (!fill) {
- break;
- }
- Assert(fill > 0);
- }
- Assert(fill == 0);
- return { .mime = partial.mime, .data = result, .total = partial.total };
- }
- void Instance::processDataRequest(TaskPointer task, bool started) {
- if (!started) {
- _tasks.remove(task);
- return;
- }
- @autoreleasepool {
- NSString *url = task.request.URL.absoluteString;
- NSString *prefix = stdToNS(_dataDomain);
- if (![url hasPrefix:prefix]) {
- taskFail(task, 0);
- return;
- }
- const auto resourceId = std::string([[url substringFromIndex:[prefix length]] UTF8String]);
- auto prepared = DataRequest{
- .id = resourceId,
- };
- NSString *rangeHeader = [task.request valueForHTTPHeaderField:@"Range"];
- if (rangeHeader) {
- ParseRangeHeaderFor(prepared, std::string([rangeHeader UTF8String]));
- if (const auto cached = fillFromCache(prepared)) {
- taskDone(task, 0, cached.mime, cached.data, prepared.offset, cached.total);
- return;
- }
- }
- const auto index = ++_taskAutoincrement;
- _tasks[task] = Task{ .index = index, .started = crl::now() };
- const auto requestedOffset = prepared.offset;
- const auto requestedLimit = prepared.limit;
- prepared.done = crl::guard(this, [=](DataResponse resolved) {
- auto &stream = resolved.stream;
- if (!stream) {
- return taskFail(task, index);
- }
- const auto length = stream->size();
- Assert(length > 0);
- const auto offset = resolved.streamOffset;
- if (requestedOffset >= offset + length || offset > requestedOffset) {
- return taskFail(task, index);
- }
- auto bytes = std::unique_ptr<char[]>(new char[length]);
- const auto read = stream->read(bytes.get(), length);
- Assert(read == length);
- const auto useLength = (requestedLimit > 0)
- ? std::min(requestedLimit, (offset + length - requestedOffset))
- : (offset + length - requestedOffset);
- const auto useBytes = bytes.get() + (requestedOffset - offset);
- const auto data = [NSData dataWithBytes:useBytes length:useLength];
- const auto mime = stream->mime();
- const auto total = resolved.totalSize ? resolved.totalSize : length;
- const auto i = _partialResources.find(resourceId);
- if (i != end(_partialResources)) {
- auto &partial = i->second;
- if (partial.mime.empty()) {
- partial.mime = mime;
- }
- if (!partial.total) {
- partial.total = total;
- }
- addToCache(partial.index, offset, { std::move(bytes), length });
- }
- taskDone(task, index, mime, data, requestedOffset, total);
- });
- const auto result = _dataRequestHandler
- ? _dataRequestHandler(prepared)
- : DataResult::Failed;
- if (result == DataResult::Failed) {
- return taskFail(task, index);
- }
- }
- }
- void Instance::navigate(std::string url) {
- NSString *string = [NSString stringWithUTF8String:url.c_str()];
- NSURL *native = [NSURL URLWithString:string];
- [_webview loadRequest:[NSURLRequest requestWithURL:native]];
- }
- void Instance::navigateToData(std::string id) {
- auto full = std::string();
- full.reserve(_dataDomain.size() + id.size());
- full.append(_dataDomain);
- full.append(id);
- navigate(full);
- }
- void Instance::reload() {
- [_webview reload];
- }
- void Instance::init(std::string js) {
- NSString *string = [NSString stringWithUTF8String:js.c_str()];
- WKUserScript *script = [[WKUserScript alloc] initWithSource:string injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:YES];
- [_manager addUserScript:script];
- }
- void Instance::eval(std::string js) {
- NSString *string = [NSString stringWithUTF8String:js.c_str()];
- [_webview evaluateJavaScript:string completionHandler:nil];
- }
- void Instance::focus() {
- }
- QWidget *Instance::widget() {
- return _widget.get();
- }
- void Instance::refreshNavigationHistoryState() {
- // Not needed here, there are events.
- }
- auto Instance::navigationHistoryState()
- -> rpl::producer<NavigationHistoryState> {
- return _navigationHistoryState.value();
- }
- void Instance::setOpaqueBg(QColor opaqueBg) {
- if (@available(macOS 12.0, *)) {
- [_webview setValue: @NO forKey: @"drawsBackground"];
- [_webview setUnderPageBackgroundColor:[NSColor clearColor]];
- }
- }
- } // namespace
- Available Availability() {
- return Available{
- .customSchemeRequests = true,
- .customRangeRequests = true,
- .customReferer = true,
- };
- }
- bool SupportsEmbedAfterCreate() {
- return true;
- }
- bool SeparateStorageIdSupported() {
- return true;
- }
- std::unique_ptr<Interface> CreateInstance(Config config) {
- if (!Supported()) {
- return nullptr;
- }
- return std::make_unique<Instance>(std::move(config));
- }
- std::string GenerateStorageToken() {
- return UuidToToken([NSUUID UUID]);
- }
- void ClearStorageDataByToken(const std::string &token) {
- if (@available(macOS 14, *)) {
- if (!token.empty() && token != LegacyStorageIdToken().toStdString()) {
- if (NSUUID *uuid = UuidFromToken(token)) {
- // removeDataStoreForIdentifier crashes without that (if not created first).
- WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init];
- [configuration setWebsiteDataStore:[WKWebsiteDataStore dataStoreForIdentifier:uuid]];
- [configuration release];
- [WKWebsiteDataStore
- removeDataStoreForIdentifier:uuid
- completionHandler:^(NSError *error) {}];
- [uuid release];
- }
- }
- }
- }
- } // namespace Webview
|