Files
allhaileris afb81b8278
Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s
Close stale issues and PRs / stale (push) Has been cancelled
init
2026-02-16 15:50:16 +03:00

923 lines
29 KiB
Plaintext

// 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]);
} else {
completionHandler(nil);
}
}];
}
- (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"];
}
if (config.safe) {
[configuration.preferences setValue:@NO forKey:@"fraudulentWebsiteWarningEnabled"];
}
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