diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 3a9ffb27a..08c2aaf31 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -105,6 +105,9 @@ endif() include(${CMAKE_CURRENT_LIST_DIR}/cmake/3rdparty.cmake) include(${CMAKE_CURRENT_LIST_DIR}/cmake/sources.cmake) +# Add webview module +add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/core/webview) + include_directories( ${CMAKE_CURRENT_LIST_DIR}/../ipc ${CMAKE_CURRENT_LIST_DIR}/../common/logger @@ -194,7 +197,7 @@ elseif(APPLE) include(cmake/macos.cmake) endif() -target_link_libraries(${PROJECT} PRIVATE ${LIBS}) +target_link_libraries(${PROJECT} PRIVATE ${LIBS} webview) target_compile_definitions(${PROJECT} PRIVATE "MZ_$") # deploy artifacts required to run the application to the debug build folder diff --git a/client/amnezia_application.cpp b/client/amnezia_application.cpp index b9e372958..7e0f68179 100644 --- a/client/amnezia_application.cpp +++ b/client/amnezia_application.cpp @@ -15,6 +15,11 @@ #include #include #include +#include +#include +#include "core/webview/plugin.h" + +Q_IMPORT_PLUGIN(WebViewPlugin) #include "logger.h" #include "ui/controllers/pageController.h" @@ -99,6 +104,14 @@ void AmneziaApplication::init() { m_engine = new QQmlApplicationEngine; + // Register AmneziaWebView plugin explicitly + QObject *pluginInstance = qt_static_plugin_WebViewPlugin().instance(); + QQmlExtensionPlugin *p = qobject_cast(pluginInstance); + if (p) { + p->registerTypes("AmneziaWebView"); + p->initializeEngine(m_engine, "AmneziaWebView"); + } + const QUrl url(QStringLiteral("qrc:/ui/qml/main2.qml")); QObject::connect( m_engine, &QQmlApplicationEngine::objectCreated, this, @@ -130,6 +143,7 @@ void AmneziaApplication::init() m_coreController.reset(new CoreController(m_vpnConnection, m_settings, m_engine)); m_engine->addImportPath("qrc:/ui/qml/Modules/"); + m_engine->addImportPath("qrc:/"); if (m_parser.isSet(m_optImport)) { const QString data = m_parser.value(m_optImport); diff --git a/client/android/src/org/amnezia/vpn/WebViewController.java b/client/android/src/org/amnezia/vpn/WebViewController.java new file mode 100644 index 000000000..a49bf5e11 --- /dev/null +++ b/client/android/src/org/amnezia/vpn/WebViewController.java @@ -0,0 +1,609 @@ +package org.amnezia.vpn; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.os.Handler; +import android.os.Looper; +import android.os.SystemClock; +import android.util.Log; +import android.util.DisplayMetrics; +import android.view.ViewGroup; +import android.view.MotionEvent; +import android.webkit.*; +import android.net.http.SslError; +import android.os.Message; +import org.qtproject.qt.android.WebViewControllerEx; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.net.URLDecoder; +import java.io.ByteArrayInputStream; +import java.io.UnsupportedEncodingException; +import java.util.concurrent.Semaphore; +import android.app.Activity; +import android.view.View; +import android.widget.FrameLayout; + +public class WebViewController +{ + private interface RequestFinished { + void onRequestCompleted(); + } + + private String baseUrl = ""; + private static final String INTERNAL_BASE_URL = "file:///"; + private static final long GEOMETRY_STABLE_INTERVAL = 150; //ms wait geometry settle + + private final Activity m_activity; + private final long m_id; + private WebView m_webView = null; + private ViewGroup m_layout = null; + private boolean m_loading = true; + private long mLastGeometryChange = 0L; + + private float m_displayDensity = (float) 1.0; + + public native void urlChanged(long viewId, String url); + public native byte[] dataForUrl(long viewId, String url, StringBuilder mimeType, StringBuilder encoding); + public native boolean canHandleUrl(long viewId, String url); + private final Handler m_handler; + + private native void pageFinished(long id, String url); + private native void pageStarted(long id, String url); + private static final String TAG = WebViewController.class.getSimpleName(); + + private class AndroidWebChromeClient extends WebChromeClient { + @Override + public boolean onCreateWindow(WebView view, boolean dialog, boolean userGesture, Message resultMsg) + { + // Prevent opening new windows/tabs - load URLs in the same WebView instead + // This handles links with target="_blank" or window.open() + // Return false to prevent creating new windows - URLs will be handled by shouldOverrideUrlLoading + return false; + } + } + + public WebViewController(final Activity activity, final long id) { + m_activity = activity; + m_id = id; + + ViewGroup root = (ViewGroup)(((ViewGroup)(m_activity.findViewById(android.R.id.content))).getChildAt(0)); + if (root != null) { + m_layout = root; + } + + m_displayDensity = m_activity.getResources().getDisplayMetrics().density; + + m_handler = new Handler(Looper.getMainLooper()); + m_handler.post(new Runnable() { + @SuppressLint("SetJavaScriptEnabled") + @Override + public void run() { + m_webView = new WebView(m_activity); + m_webView.setFocusable(true); + + m_webView.setFocusableInTouchMode(true); + m_webView.getSettings().setJavaScriptEnabled(true); + m_webView.getSettings().setAllowFileAccess(true); + m_webView.getSettings().setAllowFileAccessFromFileURLs(true); + m_webView.getSettings().setAllowUniversalAccessFromFileURLs(true); + m_webView.getSettings().setAllowContentAccess(true); + m_webView.getSettings().setBuiltInZoomControls(true); + m_webView.getSettings().setDisplayZoomControls(false); + m_webView.getSettings().setLoadWithOverviewMode(true); + + m_webView.getSettings().setSupportMultipleWindows(false); // Prevent opening new windows + m_webView.getSettings().setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW); + + m_webView.getSettings().setUseWideViewPort(true); + m_webView.getSettings().setLoadWithOverviewMode(true); + m_webView.getSettings().setLayoutAlgorithm(WebSettings.LayoutAlgorithm.NORMAL); + + m_webView.getSettings().setSupportZoom(true); + m_webView.getSettings().setBuiltInZoomControls(true); + m_webView.getSettings().setDisplayZoomControls(false); + + m_webView.setInitialScale(0); + m_webView.setVisibility(android.view.View.INVISIBLE); + + // Ensure WebView can receive and handle touch events for link clicks + m_webView.setClickable(true); + m_webView.setLongClickable(true); + m_webView.setHapticFeedbackEnabled(false); + + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) { + m_webView.setElevation(0f); + } + + m_webView.setWebViewClient(buildWebViewClient()); + m_webView.setWebChromeClient(buildWebChromeClient()); + + // Ensure IME appears on tap when focusing editable content + m_webView.setOnTouchListener(new android.view.View.OnTouchListener() { + @Override + public boolean onTouch(android.view.View v, MotionEvent event) { + // Let WebView handle touch events normally for link clicks + // Only request focus on ACTION_UP to allow IME to appear for input fields + if (event.getAction() == MotionEvent.ACTION_UP) { + v.requestFocus(); + // Do not show IME if app temporarily suppresses it + try { + android.view.inputmethod.InputMethodManager imm = (android.view.inputmethod.InputMethodManager) v.getContext().getSystemService(android.content.Context.INPUT_METHOD_SERVICE); + if (imm != null) { + imm.showSoftInput(v, 0); + } + } catch (Throwable ignore) {} + } + // Return false to let WebView handle the touch event (for link clicks, etc.) + return false; + } + }); + } + }); + } + + private WebViewClient buildWebViewClient() { + return new WebViewClient() { + @Override + public void onReceivedSslError (WebView view, SslErrorHandler handler, SslError error) { + handler.proceed(); + Log.e(TAG, "SSL certificate error"); + } + + @Override + public void onPageStarted (WebView view, String url, Bitmap favicon) { + m_loading = true; + + String dataUrl = updateUrl(url); + urlChanged(m_id, dataUrl); + pageStarted(m_id, dataUrl); + } + + @Override + public void onPageFinished(WebView view, String url) { + m_loading = false; + + String dataUrl = updateUrl(url); + pageFinished(m_id, dataUrl); + urlChanged(m_id, dataUrl); + } + + @Override + public boolean shouldOverrideUrlLoading(WebView view, String url) { + Log.d(TAG, "shouldOverrideUrlLoading (deprecated): " + url); + if (url == null || url.isEmpty()) { + return false; + } + + urlChanged(m_id, url); + + // Always load URLs within WebView, don't open in external browser + // Explicitly load the URL in the WebView and return true to indicate we handled it + view.loadUrl(url); + return true; + } + + @Override + public boolean shouldOverrideUrlLoading(WebView view, android.webkit.WebResourceRequest request) { + String url = request.getUrl().toString(); + Log.d(TAG, "shouldOverrideUrlLoading (new): " + url + ", isMainFrame: " + request.isForMainFrame() + ", method: " + request.getMethod()); + + if (url == null || url.isEmpty()) { + return false; + } + + urlChanged(m_id, url); + + // Always load URLs within WebView, don't open in external browser + // Handle main frame navigation (link clicks, form submissions, etc.) + // For sub-resources (images, CSS, JS), let WebView handle normally by returning false + if (request.isForMainFrame()) { + view.loadUrl(url); + return true; + } + // For sub-resources, let WebView handle normally + return false; + } + + @Override + public WebResourceResponse shouldInterceptRequest(WebView view, String url) { + + if (url.startsWith("data:") || !canHandleUrl(m_id, url)) { + return super.shouldInterceptRequest(view, url); + } + + StringBuilder mimeType = new StringBuilder(); + StringBuilder encoding = new StringBuilder(); + byte[] data = dataForUrl(m_id, url, mimeType, encoding); + + boolean isDataInvalid = (data == null) || (data.length == 0); + if (isDataInvalid) { + Log.w(TAG, String.format("Invalid data received for url: %s", url)); + return null; + } + if ((mimeType.length() == 0) || mimeType.toString().isEmpty()) { + Log.w(TAG, String.format("Invalid mimeType received for url: %s", url)); + } + if ((encoding.length() == 0) || encoding.toString().isEmpty()) { + Log.w(TAG, String.format("Invalid encoding received for url: %s", url)); + } + + ByteArrayInputStream dataStream = new ByteArrayInputStream(data); + return new WebResourceResponse(mimeType.toString(), encoding.toString(), dataStream); + } + }; + } + + private WebChromeClient buildWebChromeClient() { + return new AndroidWebChromeClient() { + @Override + public void onProgressChanged(WebView view, int newProgress) { + super.onProgressChanged(view, newProgress); + if (newProgress == 100) { + m_loading = false; + } + } + + @Override + public void onGeolocationPermissionsShowPrompt(String origin, GeolocationPermissions.Callback callback) { + callback.invoke(origin, true, false); + } + }; + } + + private String updateUrl(String url) { + if (!url.startsWith(INTERNAL_BASE_URL)) { + return url; + } + + String dataUrl = url; + + try { + dataUrl = URLDecoder.decode(url.substring(INTERNAL_BASE_URL.length()), "UTF-8"); + if ((dataUrl.length() == 1) && dataUrl.endsWith("#")) { + dataUrl = baseUrl; + } else { + dataUrl = baseUrl + dataUrl; + } + } catch (UnsupportedEncodingException e) { + Log.e(TAG, e.toString()); + } + + return dataUrl; + } + + public void release() { + if (m_handler == null) { + return; + } + + m_handler.post(() -> { + if (m_webView == null) { + return; + } + m_webView.setVisibility(android.view.View.INVISIBLE); + m_layout.removeView(m_webView); + m_webView.stopLoading(); + m_webView.setWebViewClient(new WebViewClient()); + m_webView.setWebChromeClient(null); + m_webView = null; + }); + } + + public void setGeometry(final int x, final int y, final int width, final int height) { + + + Log.d(TAG, String.format( + "setGeometry called: x=%d, y=%d, width=%d, height=%d", + x, y, width, height)); + + + if (m_handler == null) + return; + + m_handler.post(() -> { + if (m_webView == null || m_layout == null) + return; + + if (m_layout.indexOfChild(m_webView) < 0) { + m_layout.addView(m_webView); + } + + float scale = m_activity.getResources().getDisplayMetrics().density; + + int pxX = Math.round(x * scale); + int pxY = Math.round(y * scale); + int pxW = Math.round(width * scale); + int pxH = Math.round(height * scale); + + Log.d(TAG, String.format( + "density=%.2f qml: x=%d y=%d w=%d h=%d -> px: x=%d y=%d w=%d h=%d", + scale, x, y, width, height, pxX, pxY, pxW, pxH)); + + ViewGroup.LayoutParams params = + WebViewControllerEx.createQtLayoutParams( + pxW, + pxH, + pxX, + pxY + ); + + m_webView.setLayoutParams(params); + m_webView.setInitialScale(0); + + Log.d(TAG, String.format( + "WebView positioned (QtLayout) at px: x=%d, y=%d, w=%d, h=%d", + pxX, pxY, pxW, pxH)); + + m_layout.requestLayout(); + m_webView.requestLayout(); + + mLastGeometryChange = SystemClock.uptimeMillis(); + }); +} + + + public void show() { + if (m_handler == null) { + return; + } + + m_handler.post(new Runnable() { + @Override + public void run() { + if (m_webView == null) { + return; + } + if (m_webView.getVisibility() != android.view.View.VISIBLE) { + m_webView.setVisibility(android.view.View.VISIBLE); + } + // Don't bring WebView to front - let QML elements render on top + // Set low elevation so QML elements can appear above WebView + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) { + m_webView.setElevation(0f); + } + m_webView.requestLayout(); + m_layout.requestLayout(); + m_layout.postInvalidate(); + } + }); + } + + public void hide() { + hideWebView(); + long now = SystemClock.uptimeMillis(); + } + + private void hideWebView() { + if (m_handler == null) { + return; + } + m_handler.post(() -> { + if (m_webView == null) { + return; + } + if (m_webView.getVisibility() == android.view.View.VISIBLE) { + m_webView.setVisibility(android.view.View.INVISIBLE); + } + }); + } + + public void loadUrl(String url) { + final String newUrl; + if ((baseUrl.length() > 0) && url.startsWith(baseUrl)) { + newUrl = INTERNAL_BASE_URL + url.substring(baseUrl.length()); + } else { + newUrl = url; + } + + if (m_handler == null) { + return; + } + + m_handler.post(new Runnable() { + @Override + public void run() { + if (m_webView == null) { + return; + } + m_webView.loadUrl(newUrl); + } + }); + } + + public void loadDataWithBaseURL(final String url, final String html, final String mime, final String encoding) { + baseUrl = url.trim(); + if (m_handler == null) { + return; + } + + m_handler.postDelayed(new Runnable() { + @Override + public void run() { + if (m_webView == null) { + return; + } + + m_webView.loadUrl("about:blank"); + m_webView.loadDataWithBaseURL(INTERNAL_BASE_URL, html, mime, encoding, null); + } + }, 100); + } + + public void evaluateJavaScript(final String script) { + if (m_handler == null || script == null) { + return; + } + + m_handler.post(new Runnable() { + @Override + public void run() { + if (m_webView == null) { + return; + } + m_webView.evaluateJavascript(script, null); + } + }); + } + + public boolean canGoBack() { + final WebView view = m_webView; + if (view == null) { + return false; + } + + final boolean[] ret = new boolean[1]; + ret[0] = false; + boolean can = false; + + final Semaphore semaphore = new Semaphore(0); + if (m_activity != null) { + m_activity.runOnUiThread(new Runnable() { + @Override + public void run() { + ret[0] = view.canGoBack(); + semaphore.release(); + } + }); + } + + try { + semaphore.acquire(1); + can = ret[0]; + } catch (InterruptedException e) { + Log.e(TAG, e.toString()); + Thread.currentThread().interrupt(); + } + return can; + } + + public void goBack() { + if (m_handler == null) { + return; + } + + m_handler.post(new Runnable() { + @Override + public void run() { + if (m_webView == null) { + return; + } + m_webView.goBack(); + } + }); + } + + public boolean canGoForward() { + final WebView view = m_webView; + if (view == null) { + return false; + } + + final boolean[] ret = new boolean[1]; + ret[0] = false; + boolean can = false; + + final Semaphore semaphore = new Semaphore(0); + if (m_activity != null) { + m_activity.runOnUiThread(new Runnable() { + @Override + public void run() { + ret[0] = view.canGoForward(); + semaphore.release(); + } + }); + } + + try { + semaphore.acquire(1); + can = ret[0]; + } catch (InterruptedException e) { + Log.e(TAG, e.toString()); + Thread.currentThread().interrupt(); + } + return can; + } + + public void goForward() { + if (m_handler == null) { + return; + } + + m_handler.post(new Runnable() { + @Override + public void run() { + if (m_webView == null) { + return; + } + m_webView.goForward(); + } + }); + } + + public void setBackgroundColor(final int color) { + if (m_handler == null) { + return; + } + + m_handler.post(new Runnable() { + @Override + public void run() { + if (m_webView == null) { + return; + } + m_webView.setBackgroundColor(color); + } + }); + } + + public void setTextZoom(final int percent) { + if (m_handler == null) { + return; + } + final int clamped = Math.max(25, Math.min(500, percent)); + m_handler.post(new Runnable() { + @Override + public void run() { + if (m_webView == null) { + return; + } + m_webView.getSettings().setTextZoom(clamped); + } + }); + } + + private int convertToDp(int input) { + return (int)(input / m_displayDensity + 0.5f); + } + + public void setDefaultFontSize(int size) { + if (m_handler == null) { + return; + } + final int fontSize = size; + m_handler.post(new Runnable() { + @Override + public void run() { + m_webView.getSettings().setDefaultFontSize(convertToDp(fontSize)); + } + }); + } + + void setStandardFontFamily(String family) { + if (m_handler == null) { + return; + } + + final String fontFamily = family; + m_handler.post(new Runnable() { + @Override + public void run() { + m_webView.getSettings().setStandardFontFamily(fontFamily); + } + }); + } + +} diff --git a/client/android/src/org/qtproject/qt/android/WebViewControllerEx.java b/client/android/src/org/qtproject/qt/android/WebViewControllerEx.java new file mode 100644 index 000000000..f24c4b893 --- /dev/null +++ b/client/android/src/org/qtproject/qt/android/WebViewControllerEx.java @@ -0,0 +1,10 @@ +package org.qtproject.qt.android; + +import android.view.ViewGroup; + +public class WebViewControllerEx { + + public static ViewGroup.LayoutParams createQtLayoutParams(int width, int height, int x, int y) { + return new QtLayout.LayoutParams(width, height, x, y); + } +} diff --git a/client/core/webview/CMakeLists.txt b/client/core/webview/CMakeLists.txt new file mode 100644 index 000000000..867a0a9ff --- /dev/null +++ b/client/core/webview/CMakeLists.txt @@ -0,0 +1,124 @@ +get_filename_component(DIR_NAME ${CMAKE_CURRENT_SOURCE_DIR} NAME) +message("Configuring " ${DIR_NAME}) + +set(webview_URI AmneziaWebView) + +find_package(QT NAMES Qt6 REQUIRED COMPONENTS Quick) +find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Quick) + +# Widgets and WebEngineWidgets are only available on desktop platforms +if(WIN32 OR (APPLE AND NOT IOS) OR (LINUX AND NOT ANDROID)) + find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Widgets) + find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS WebEngineWidgets) +endif() + +set(CMAKE_AUTOMOC ON) +set(CMAKE_INCLUDE_CURRENT_DIR ON) + +set(CMAKE_CXX_STANDARD 20) + +set(PLUGIN_CLASS_NAME WebViewPlugin) +add_definitions(-DURI=${webview_URI}) + +include_directories(${CMAKE_CURRENT_SOURCE_DIR}) + +set(webview_HEADERS + amneziawebview.h + amneziawebview_p.h + websettings.h + mimecache.h + filehandler.h + qrchandler.h + jshandler.h + plugin.h + amneziawebhistory.h + amneziawebhistory_p.h +) + +set(webview_SOURCES + amneziawebview.cpp + amneziawebview_p.cpp + websettings.cpp + mimecache.cpp + qrchandler.cpp + jshandler.cpp + filehandler.cpp + plugin.cpp + amneziawebhistory.cpp +) + +if (CMAKE_CROSSCOMPILING AND ANDROID) + + list(APPEND webview_SOURCES + "${CMAKE_CURRENT_LIST_DIR}/jshandler_android.cpp" + "${CMAKE_CURRENT_LIST_DIR}/qrchandler_android.cpp" + "${CMAKE_CURRENT_LIST_DIR}/filehandler_android.cpp" + "${CMAKE_CURRENT_LIST_DIR}/amneziawebview_android.cpp" + ) + +endif () + +if (CMAKE_CROSSCOMPILING AND APPLE) + + add_definitions(-DENABLE_WKWEBVIEW) + list(APPEND webview_SOURCES + "${CMAKE_CURRENT_LIST_DIR}/amneziawebview_ios.mm" + "${CMAKE_CURRENT_LIST_DIR}/qrchandler_ios.mm" + "${CMAKE_CURRENT_LIST_DIR}/jshandler_ios.mm" + "${CMAKE_CURRENT_LIST_DIR}/filehandler_ios.mm" + ) + +endif () + +if (NOT CMAKE_CROSSCOMPILING) + # Require WebEngineWidgets for desktop platforms (QtWebKit is not available in Qt 6) + if (Qt6WebEngineWidgets_FOUND) + message(STATUS "Using Qt WebEngineWidgets for desktop webview") + list(APPEND webview_HEADERS + amneziawebview_webengine_p.h + ) + list(APPEND webview_SOURCES + amneziawebview_webengine.cpp + ) + else () + message(FATAL_ERROR "Qt WebEngineWidgets is required for desktop builds. QtWebKit is not available in Qt 6. Please install Qt WebEngineWidgets module.") + endif () +endif () + +add_library(webview STATIC ${webview_SOURCES} ${webview_HEADERS}) + +target_compile_definitions(webview PRIVATE + QT_PLUGIN + QT_STATICPLUGIN +) + +target_link_libraries(webview PUBLIC + Qt${QT_VERSION_MAJOR}::Core + Qt${QT_VERSION_MAJOR}::Quick + ) + +# Widgets and WebEngineWidgets are only available on desktop platforms +if(WIN32 OR (APPLE AND NOT IOS) OR (LINUX AND NOT ANDROID)) + if(TARGET Qt${QT_VERSION_MAJOR}::Widgets) + target_link_libraries(webview PUBLIC Qt${QT_VERSION_MAJOR}::Widgets) + endif() + + if (Qt6WebEngineWidgets_FOUND) + target_link_libraries(webview PRIVATE + Qt${QT_VERSION_MAJOR}::WebEngineWidgets + ) + endif () +endif() + +# Link WebKit framework for iOS +if (CMAKE_CROSSCOMPILING AND APPLE) + find_library(FW_WEBKIT WebKit) + if(FW_WEBKIT) + target_link_libraries(webview PRIVATE ${FW_WEBKIT}) + endif() +endif() + +set_target_properties(webview PROPERTIES AUTOMOC_MOC_OPTIONS "-Muri=${webview_URI}") + +#include(precompiled.headers) +#add_precompiled_header(webview pch.h FORCEINCLUDE) diff --git a/client/core/webview/amneziawebhistory.cpp b/client/core/webview/amneziawebhistory.cpp new file mode 100644 index 000000000..892865655 --- /dev/null +++ b/client/core/webview/amneziawebhistory.cpp @@ -0,0 +1,434 @@ +#include "amneziawebhistory.h" +#include "amneziawebhistory_p.h" +#include "amneziawebview.h" +#include "amneziawebview_p.h" + +#include +#include + + +/*! + Constructs a history item from \a other. The new item and \a other + will share their data, and modifying either this item or \a other will + modify both instances. +*/ +AmneziaWebHistoryItem::AmneziaWebHistoryItem(const AmneziaWebHistoryItem &other) + : d_ptr(other.d_ptr) +{ +} + +/*! + Assigns the \a other history item to this. This item and \a other + will share their data, and modifying either this item or \a other will + modify both instances. +*/ +AmneziaWebHistoryItem &AmneziaWebHistoryItem::operator=(const AmneziaWebHistoryItem &other) +{ + d_ptr = other.d_ptr; + return *this; +} + +/*! + Destroys the history item. +*/ +AmneziaWebHistoryItem::~AmneziaWebHistoryItem() +{ +} + +/*! + Returns the URL associated with the history item. + + \sa originalUrl(), title(), lastVisited(), data(), mimeType() +*/ +QUrl AmneziaWebHistoryItem::url() const +{ + Q_D(const AmneziaWebHistoryItem); + if (d) + return d->url(); + return QUrl(); +} + + +/*! + Returns the title of the page associated with the history item. + + \sa icon(), url(), lastVisited(), data(), mimeType() +*/ +QString AmneziaWebHistoryItem::title() const +{ + Q_D(const AmneziaWebHistoryItem); + if (d) + return d->title(); + return QString(); +} + +/*! + Returns the icon associated with the history item. + + \sa title(), url(), lastVisited(), data(), mimeType() +*/ +QIcon AmneziaWebHistoryItem::icon() const +{ + Q_D(const AmneziaWebHistoryItem); + if (d) + return d->icon(); + + return QIcon(); +} + +/*! + Returns the data associated with the history item. + + \sa icon(), title(), url(), lastVisited(), mimeType() +*/ +QByteArray AmneziaWebHistoryItem::data() const +{ + Q_D(const AmneziaWebHistoryItem); + if(d) return d->data(); + return QByteArray(); +} + +/*! + Returns the mimeType associated with the history item. + + \sa icon(), title(), url(), lastVisited(), data() +*/ +QString AmneziaWebHistoryItem::mimeType() const +{ + Q_D(const AmneziaWebHistoryItem); + if(d) return d->mimeType(); + return QString("text/html"); +} + +/*!* + \internal +*/ +AmneziaWebHistoryItem::AmneziaWebHistoryItem(AmneziaWebHistoryItemPrivate *priv) : d_ptr(priv) +{ +} + +/*! + \since 4.5 + Returns whether this is a valid history item. +*/ +bool AmneziaWebHistoryItem::isValid() const +{ + Q_D(const AmneziaWebHistoryItem); + bool valid = (d); + return valid; +} + +AmneziaWebHistory::AmneziaWebHistory(AmneziaWebView *parent) : QObject(parent) + , d_ptr(new AmneziaWebHistoryPrivate()) +{ + Q_D(AmneziaWebHistory); + d->q_ptr = this; +} + +AmneziaWebHistory::~AmneziaWebHistory() +{ + clear(); +} + +/*! + Clears the history. + + \sa count(), items() +*/ +void AmneziaWebHistory::clear() +{ + Q_D(AmneziaWebHistory); + while (d->items.count()) { + d->items.removeFirst(); + } +} + +/*! + Returns a list of all items currently in the history. + + \sa count(), clear() +*/ +QList AmneziaWebHistory::items() const +{ + Q_D(const AmneziaWebHistory); + QList ret; + + for (int i = 0; i < d->items.size(); ++i) { + AmneziaWebHistoryItem item(d->items[i]); + ret.append(item); + } + return ret; +} + +/*! + Returns the list of items in the backwards history list. + At most \a maxItems entries are returned. + + \sa forwardItems() +*/ +QList AmneziaWebHistory::backItems(int maxItems) const +{ + Q_D(const AmneziaWebHistory); + + int count = d->currentIndex; + if (maxItems >= 0) { + count = qMin(count, maxItems); + } + + QList ret; + for (int i = (d->currentIndex - count); i < d->currentIndex; i++) { + ret.append(d->items[i]); + } + return ret; +} + +/*! + Returns the list of items in the forward history list. + At most \a maxItems entries are returned. + + \sa backItems() +*/ +QList AmneziaWebHistory::forwardItems(int maxItems) const +{ + Q_D(const AmneziaWebHistory); + + int count = d->items.count() - d->currentIndex - 1; + if (maxItems >= 0) { + count = qMin(count, maxItems); + } + + QList ret; + for (int i = (d->currentIndex + 1); i <= d->currentIndex + count; i++) { + ret.append(d->items[i]); + } + return ret; +} + + +/*! + Returns true if there is an item preceding the current item in the history; + otherwise returns false. + + \sa canGoForward() +*/ +bool AmneziaWebHistory::canGoBack() const +{ + const AmneziaWebHistoryItem current = currentItem(); + bool can = (current.isValid() && current.d_ptr->backItem() != nullptr); + return can; +} + +/*! + Returns true if we have an item to go forward to; otherwise returns false. + + \sa canGoBack() +*/ +bool AmneziaWebHistory::canGoForward() const +{ + const AmneziaWebHistoryItem current = currentItem(); + bool can = (current.isValid() && current.d_ptr->forwardItem() != nullptr); + return can; +} + +/*! + Set the current item to be the previous item in the history and goes to the + corresponding page; i.e., goes back one history item. + + \sa forward(), goToItem() +*/ +void AmneziaWebHistory::back() +{ + Q_D(AmneziaWebHistory); + if(!canGoBack()) return; + AmneziaWebView *view = qobject_cast(parent()); + AmneziaWebHistoryItem item = backItem(); + + d->currentIndex--; + if (view) { + if (item.data().length() > 0) { + view->setContent(item.data(), item.mimeType(), item.url()); + } + else { + + view->setUrl(item.url()); + } + } +} + +/*! + Sets the current item to be the next item in the history and goes to the + corresponding page; i.e., goes forward one history item. + + \sa back(), goToItem() +*/ +void AmneziaWebHistory::forward() +{ + Q_D(AmneziaWebHistory); + if(!canGoForward()) return; + AmneziaWebView *view = qobject_cast(parent()); + AmneziaWebHistoryItem item = backItem(); + + d->currentIndex++; + if (view) { + if (item.data().length() > 0) { + view->setContent(item.data(), item.mimeType(), item.url()); + } + else { + view->setUrl(item.url()); + } + } +} + +/*! + Sets the current item to be the specified \a item in the history and goes to the page. + + \sa back(), forward() +*/ +void AmneziaWebHistory::goToItem(const AmneziaWebHistoryItem &item) +{ + Q_D(AmneziaWebHistory); + if(!item.isValid()) return; + AmneziaWebView *view = qobject_cast(parent()); + if (!view) return; //There is no view to go. + if (item.url().isEmpty()) return; // + + int index = -1; + for(int i= 0; i < d->items.count(); ++i) { + if(d->items[i].d_ptr.data() == item.d_ptr.data()) { + index = i; + break; + } + } + + if (index >= 0) { + + d->currentIndex = index; + if (item.data().length() > 0) { + view->setContent(item.data(), item.mimeType(), item.url()); + } + else { + view->setUrl(item.url()); + } + } +} + +/*! + Returns the current item in the history. + */ +AmneziaWebHistoryItem AmneziaWebHistory::currentItem() const +{ + Q_D(const AmneziaWebHistory); + + if ((d->currentIndex >= 0) && (d->currentIndex < d->items.count())) { + return AmneziaWebHistoryItem(d->items.at(d->currentIndex)); + } + return AmneziaWebHistoryItem(nullptr); +} + +void AmneziaWebHistory::append(const QUrl& url, const QByteArray& data, const QString& mimeType) +{ + Q_D(AmneziaWebHistory); + + const AmneziaWebHistoryItem current = currentItem(); + // Check if url is same as current, and do not add it second time. + if (current.url() == url) return; + + AmneziaWebHistoryItemPrivate *priv = new AmneziaWebHistoryItemPrivate(); + if(current.isValid()) { + current.d_ptr->_forwardItem = priv; + priv->_backItem = current.d_ptr.data(); + } + priv->_data = data; + priv->_url = url; + priv->_mimeType = mimeType; + + //Remove last items till current + while (d->items.count() > (d->currentIndex + 1)) { + d->items.removeLast(); + } + + //No more then maximum + while (d->items.count() >= d->maximumCount) { + d->items.removeFirst(); + } + + d->items.append(AmneziaWebHistoryItem(priv)); + d->currentIndex = (d->items.count() - 1); + +} + +/*! + Returns the item before the current item in the history. +*/ +AmneziaWebHistoryItem AmneziaWebHistory::backItem() const +{ + AmneziaWebHistoryItem current = currentItem(); + return AmneziaWebHistoryItem(current.d_ptr->backItem()); +} + + +/*! + Returns the item after the current item in the history. +*/ +AmneziaWebHistoryItem AmneziaWebHistory::forwardItem() const +{ + AmneziaWebHistoryItem current = currentItem(); + return AmneziaWebHistoryItem(current.d_ptr->forwardItem()); +} + +/*! + \since 4.5 + Returns the index of the current item in history. +*/ +int AmneziaWebHistory::currentItemIndex() const +{ + Q_D(const AmneziaWebHistory); + return d->currentIndex; +} + +/*! + Returns the item at index \a i in the history. +*/ +AmneziaWebHistoryItem AmneziaWebHistory::itemAt(int i) const +{ + Q_D(const AmneziaWebHistory); + int index = (i < 0) ? 0 : i; + index = (index >= count()) ? (count() -1) : index; + if (index >= 0) { + return AmneziaWebHistoryItem(d->items.at(index)); + } + return AmneziaWebHistoryItem(nullptr); +} + +/*! + Returns the total number of items in the history. +*/ +int AmneziaWebHistory::count() const +{ + Q_D(const AmneziaWebHistory); + return d->items.count(); +} + +/*! + \since 4.5 + Returns the maximum number of items in the history. + + \sa setMaximumItemCount() +*/ +int AmneziaWebHistory::maximumItemCount() const +{ + Q_D(const AmneziaWebHistory); + return d->maximumCount; +} + +/*! + \since 4.5 + Sets the maximum number of items in the history to \a count. + + \sa maximumItemCount() +*/ +void AmneziaWebHistory::setMaximumItemCount(int count) +{ + Q_D(AmneziaWebHistory); + d->maximumCount = count; +} diff --git a/client/core/webview/amneziawebhistory.h b/client/core/webview/amneziawebhistory.h new file mode 100644 index 000000000..227507501 --- /dev/null +++ b/client/core/webview/amneziawebhistory.h @@ -0,0 +1,78 @@ +#ifndef WEBHISTORY_H +#define WEBHISTORY_H + +#include +#include + +class AmneziaWebViewPrivate; +class AmneziaWebView; + +class AmneziaWebHistory; +class AmneziaWebHistoryItemPrivate; +class AmneziaWebHistoryItem +{ + Q_DECLARE_PRIVATE(AmneziaWebHistoryItem) +public: + + AmneziaWebHistoryItem(const AmneziaWebHistoryItem &other); + AmneziaWebHistoryItem &operator=(const AmneziaWebHistoryItem &other); + ~AmneziaWebHistoryItem(); + + QUrl url() const; + QString title() const; + QIcon icon() const; + + QByteArray data() const; + QString mimeType() const; + + bool isValid() const; + +private: + explicit AmneziaWebHistoryItem(AmneziaWebHistoryItemPrivate *priv); + friend class AmneziaWebHistory; + friend class AmneziaWebViewPrivate; + QExplicitlySharedDataPointer d_ptr; +}; + +class AmneziaWebHistoryPrivate; +class AmneziaWebHistory : public QObject +{ + Q_OBJECT + Q_DECLARE_PRIVATE(AmneziaWebHistory) + +public: + virtual ~AmneziaWebHistory(); + + void append(const QUrl& url, const QByteArray& data = QByteArray(), const QString& mimeType = QString("text/html")); + void clear(); + + QList items() const; + QList backItems(int maxItems) const; + QList forwardItems(int maxItems) const; + + bool canGoBack() const; + bool canGoForward() const; + + void back(); + void forward(); + void goToItem(const AmneziaWebHistoryItem &item); + + AmneziaWebHistoryItem backItem() const; + AmneziaWebHistoryItem currentItem() const; + AmneziaWebHistoryItem forwardItem() const; + AmneziaWebHistoryItem itemAt(int i) const; + int currentItemIndex() const; + + int count() const; + int maximumItemCount() const; + void setMaximumItemCount(int count); + +private: + + friend class AmneziaWebViewPrivate; + explicit AmneziaWebHistory(AmneziaWebView *parent); + Q_DISABLE_COPY(AmneziaWebHistory) + QScopedPointer d_ptr; +}; + +#endif diff --git a/client/core/webview/amneziawebhistory_p.h b/client/core/webview/amneziawebhistory_p.h new file mode 100644 index 000000000..1faeab8bd --- /dev/null +++ b/client/core/webview/amneziawebhistory_p.h @@ -0,0 +1,72 @@ +#ifndef WEBHISTORY_P_H +#define WEBHISTORY_P_H + +#include + +#include "amneziawebhistory.h" + +class AmneziaWebHistoryItemPrivate; +class AmneziaWebHistoryItem; + +class AmneziaWebHistoryPrivate +{ + Q_DECLARE_PUBLIC(AmneziaWebHistory) +public: + + static AmneziaWebHistoryPrivate *get(AmneziaWebHistory *q) + { + if (!q) { return nullptr; } + return q->d_func(); + } + + AmneziaWebHistoryPrivate(): currentIndex(-1), maximumCount(10), q_ptr(nullptr) { } + ~AmneziaWebHistoryPrivate() = default; + + +private: + friend class AmneziaWebHistoryItemPrivate; + int currentIndex; + int maximumCount; + QList items; + AmneziaWebHistory *q_ptr; +}; + +class AmneziaWebHistoryItemPrivate : public QSharedData +{ +public: + + static QExplicitlySharedDataPointer get(AmneziaWebHistoryItem *q) + { + return q->d_ptr; + } + + ~AmneziaWebHistoryItemPrivate() + { + } + + QUrl url() const { return _url; } + QString title() const { return _title; } + QIcon icon() const {return _icon;} + QByteArray data() const {return _data;} + QString mimeType() const {return _mimeType;} + + // Every item knows its back and forward items + AmneziaWebHistoryItemPrivate *backItem() {return _backItem; } + AmneziaWebHistoryItemPrivate *forwardItem() {return _forwardItem; } + +private: + friend class AmneziaWebHistory; + AmneziaWebHistoryItemPrivate() = default; + + AmneziaWebHistoryItemPrivate *_backItem = nullptr; + AmneziaWebHistoryItemPrivate *_forwardItem = nullptr; + QIcon _icon; + QString _title; + QUrl _url; + QString _html; + QByteArray _data; + QString _mimeType; +}; + + +#endif diff --git a/client/core/webview/amneziawebview.cpp b/client/core/webview/amneziawebview.cpp new file mode 100644 index 000000000..94c247f9c --- /dev/null +++ b/client/core/webview/amneziawebview.cpp @@ -0,0 +1,751 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS) + #include + #define qApp qGuiApp +#else + #include +#endif + +#include "amneziawebview.h" +#include "amneziawebview_p.h" + +QUrl defaultBaseUrl() +{ +#if defined(Q_OS_MACOS) || defined(Q_OS_WINDOWS) + return QUrl(QLatin1String("local:///")); +#else + return QUrl(QLatin1String("file:///")); +#endif +} + +QT_BEGIN_NAMESPACE + +AmneziaWebView::AmneziaWebView(QQuickItem *parent) : QQuickPaintedItem(parent), + d_ptr(AmneziaWebViewPrivate::create(this)) +{ + Q_D(AmneziaWebView); + d->q_ptr = this; + d->init(); + init(); +} + +AmneziaWebView::~AmneziaWebView() +{ + disconnect(this); +} + +void AmneziaWebView::init() +{ + Q_D(AmneziaWebView); + setAcceptedMouseButtons(Qt::LeftButton); + setFlag(QQuickItem::ItemHasContents, true); + setOpaquePainting(true); + + setClip(true); + + connect(this, SIGNAL(windowChanged(QQuickWindow*)), this, SLOT(windowWasChanged(QQuickWindow*))); + connect(this, SIGNAL(parentChanged(QQuickItem*)), this, SLOT(parentWasChanged())); + connect(this, SIGNAL(fillColorChanged()), this,SLOT(fillColorWasChanged())); + + connect(d, SIGNAL(titleChanged(QString)), this, SIGNAL(titleChanged(QString))); + connect(d, SIGNAL(loadStarted()), this, SLOT(doLoadStarted())); + connect(d, SIGNAL(loadFinished(bool)), this, SLOT(doLoadFinished(bool))); +} + +void AmneziaWebView::componentComplete() +{ + Q_D(AmneziaWebView); + QQuickItem::componentComplete(); + + // Update geometry after component is complete + if (window()) { + QMetaObject::invokeMethod(this, "updateGeometry", Qt::QueuedConnection); + } + + switch (d->pending) { + case AmneziaWebViewPrivate::PendingUrl: + // Make WebView visible before loading + if (isVisible() && !d->visible) { + d->show(); + } + setUrl(d->pendingUrl); + break; + case AmneziaWebViewPrivate::PendingHtml: + if (isVisible() && !d->visible) { + d->show(); + } + setHtml(d->pendingString, d->pendingUrl); + break; + case AmneziaWebViewPrivate::PendingContent: + if (isVisible() && !d->visible) { + d->show(); + } + setContent(d->pendingData, d->pendingString, d->pendingUrl); + break; + default: + break; + } +} + +AmneziaWebView::Status AmneziaWebView::status() const +{ + Q_D(const AmneziaWebView); + return d->status; +} + +/*! + \qmlproperty real WebView::progress + This property holds the progress of loading the current URL, from 0 to 1. + + If you just want to know when progress gets to 1, use + WebView::onLoadFinished() or WebView::onLoadFailed() instead. +*/ +qreal AmneziaWebView::progress() const +{ + Q_D(const AmneziaWebView); + return d->progress; +} + +void AmneziaWebView::doLoadStarted() +{ + Q_D(AmneziaWebView); + + if (!d->url.isEmpty()) { + d->status = Loading; + emit statusChanged(d->status); + } + emit loadStarted(); +} + +void AmneziaWebView::doLoadProgress(int p) +{ + Q_D(AmneziaWebView); + + if (d->progress == p / 100.0) + return; + d->progress = p / 100.0; + emit progressChanged(); +} + + +void AmneziaWebView::doLoadFinished(bool ok) +{ + Q_D(AmneziaWebView); + + if (ok) { + d->status = d->url.isEmpty() ? Null : Ready; + emit loadFinished(); + } else { + d->status = Error; + emit loadFailed(); + } + emit statusChanged(d->status); +} + +/*! + \qmlproperty url AmneziaWebView::url + This property holds the URL to the page displayed in this item. It can be set, + but also can change spontaneously (eg. because of network redirection). + + If the url is empty, the page is blank. + + The url is always absolute (QML will resolve relative URL strings in the context + of the containing QML document). +*/ +QUrl AmneziaWebView::url() const +{ + Q_D(const AmneziaWebView); + return d->url; +} + +void AmneziaWebView::setUrl(const QUrl& url) +{ + Q_D(AmneziaWebView); + + QString urlString = url.toString(); + while ( urlString.endsWith('#')) urlString.chop(1); + QUrl newUrl(urlString); + + if (newUrl == QUrl(QLatin1String("about:blank")) ) { + newUrl = QUrl(""); + } + + if ((url == d->url) || (newUrl == d->url)) + return; + + if (isComponentComplete()) { + // Make WebView visible before loading + if (isVisible() && !d->visible) { + d->show(); + } + d->load(url); + + } else { + + d->pending = d->PendingUrl; + d->pendingUrl = url; + } +} + +qreal AmneziaWebView::preferredWidth() const +{ + Q_D(const AmneziaWebView); + return d->preferredwidth; +} + +void AmneziaWebView::setPreferredWidth(qreal width) +{ + Q_D(AmneziaWebView); + + if (d->preferredwidth == width) + return; + + d->preferredwidth = width; + updateContentsSize(); + setImplicitWidth(width); + emit preferredWidthChanged(); +} + +qreal AmneziaWebView::preferredHeight() const +{ + Q_D(const AmneziaWebView); + return d->preferredheight; +} + +void AmneziaWebView::setPreferredHeight(qreal height) +{ + Q_D(AmneziaWebView); + + if (d->preferredheight == height) + return; + + d->preferredheight = height; + updateContentsSize(); + setImplicitHeight(height); + emit preferredHeightChanged(); +} + + +/*! + \qmlmethod bool AmneziaWebView::evaluateJavaScript(string scriptSource) + + Evaluates the \a scriptSource JavaScript inside the context of the + main web frame, and returns the result of the last executed statement. + + Note that this JavaScript does \e not have any access to QML objects + except as made available as windowObjects. +*/ +void AmneziaWebView::evaluateJavaScript(const QString& scriptSource) +{ + Q_D(AmneziaWebView); + if (qApp->thread() == QThread::currentThread()) { + d->evaluateJavaScript(scriptSource); + } + else { + QMetaObject::invokeMethod(d, "evaluateJavaScript", Qt::BlockingQueuedConnection, + Q_ARG(const QString, scriptSource)); + } +} + +void AmneziaWebView::windowWasChanged(QQuickWindow* window) +{ + Q_D(AmneziaWebView); + d->setWindowParent(window); +} + +void AmneziaWebView::updateGeometry() +{ + Q_D(AmneziaWebView); + QRectF geometry = QRectF(QPointF(x(), y()), QSizeF(width(), height())); + if (!geometry.isEmpty() && window()) { + QRectF sceneGeometry = mapRectToScene(geometry); + QRect rect = sceneGeometry.toRect(); + qDebug() << "AmneziaWebView::updateGeometry() - local:" << geometry + << "scene:" << sceneGeometry << "rect:" << rect + << "width:" << width() << "height:" << height(); + d->setGeometry(rect); + } +} + + +void AmneziaWebView::parentWasChanged() +{ + if (parentItem()) { + updateGeometry(); + } +} + +QList recurseChildren(QQuickItem * parentItem) +{ + QListchilds = parentItem->childItems(); + QList items; + int count = childs.count(); + for(int i = count - 1; i >= 0; --i) { + QQuickItem *next = childs.at(i); + items.append(recurseChildren(next)); + } + items.append(childs); + return items; +} + + +void AmneziaWebView::paint(QPainter *painter) +{ + Q_D(AmneziaWebView); + if (!painter || !window() ) { + return; + } + + QRectF contentRect = contentsBoundingRect(); + if ((contentRect.height() <= 0) || (contentRect.width() <= 0)) return; + + painter->setOpacity(1.0); + QMutexLocker lock(&d->renderMutex); + + QColor color = d->backgroundColor; + painter->fillRect(contentRect, color); + +} + +void AmneziaWebView::afterRendering() +{ + Q_D(AmneziaWebView); + + Qt::ApplicationState state = qApp->applicationState(); + if ( state != Qt::ApplicationActive) { + if (d->visible && isVisible()) { + QMetaObject::invokeMethod(d, "requestHide", Qt::QueuedConnection); + } + return; + } + + if (!window()) return; + + if (isVisible()) { + QMetaObject::invokeMethod(this, "updateGeometry", Qt::QueuedConnection); + } + + QMetaObject::invokeMethod(d, "requestShow", Qt::QueuedConnection); + +} + +void AmneziaWebView::geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry) +{ + Q_D(AmneziaWebView); + + QQuickPaintedItem::geometryChange(newGeometry, oldGeometry); + + // Update WebView geometry when QML item size changes + if (window() && !newGeometry.isEmpty() && newGeometry != oldGeometry) { + QMetaObject::invokeMethod(this, "updateGeometry", Qt::QueuedConnection); + } +} + +void AmneziaWebView::itemChange(ItemChange change, const ItemChangeData & value) +{ + Q_D(AmneziaWebView); + switch (change) { + case ItemSceneChange: { + QQuickWindow *sc = value.window; + if (sc) { + connect(sc, SIGNAL(afterRendering()), this, SLOT(afterRendering()), Qt::QueuedConnection); + } + else { + disconnect(this, SLOT(afterRendering())); + } + } + break; + case ItemVisibleHasChanged: { + + if (!window()) break; + + if (value.boolValue) { + // Component became visible - show WebView + if (!d->visible) { + d->show(); + } + } else { + QMetaObject::invokeMethod(d, "requestHide", Qt::QueuedConnection); + } + if (value.boolValue && !d->overlapped) { + QMetaObject::invokeMethod(d, "requestShow", Qt::QueuedConnection); + } + } + break; + default: + break; + } + QQuickPaintedItem::itemChange(change, value); +} + +/*! + \qmlproperty list WebView::javaScriptWindowObjects + + A list of QML objects to expose to the web page. + + Each object will be added as a property of the web frame's window object. The + property name is controlled by the value of \c WebView.windowObjectName + attached property. + + Exposing QML objects to a web page allows JavaScript executing in the web + page itself to communicate with QML, by reading and writing properties and + by calling methods of the exposed QML objects. + + This example shows how to call into a QML method using a window object. + + \qml + WebView { + javaScriptWindowObjects: QtObject { + WebView.windowObjectName: "qml" + + function qmlCall() { + console.log("This call is in QML!"); + } + } + + html: "" + } + \endqml + + The output of the example will be: + \code + This call is in QML! + \endcode + + If Javascript is not enabled for the page, then this property does nothing. +*/ +QQmlListProperty AmneziaWebView::javaScriptWindowObjects() +{ + Q_D(AmneziaWebView); + return QQmlListProperty(this, d, &AmneziaWebViewPrivate::windowObjectsAppend, + &AmneziaWebViewPrivate::windowObjectsCount, + &AmneziaWebViewPrivate::windowObjectsAt, + &AmneziaWebViewPrivate::windowObjectsClear ); +} + +AmneziaWebViewSettings* AmneziaWebView::settingsObject() const +{ + Q_D(const AmneziaWebView); + return d->m_settings.data(); +} + + +AmneziaWebViewAttached* AmneziaWebView::qmlAttachedProperties(QObject* o) +{ + return new AmneziaWebViewAttached(o); +} + +void AmneziaWebViewPrivate::updateWindowObjects() +{ + + if (!q_ptr->isComponentCompletePublic()) + return; + + for (int i = 0; i < windowObjects.count(); ++i) { + QObject* object = windowObjects.at(i); + AmneziaWebViewAttached* attached = static_cast(qmlAttachedPropertiesObject(object)); + if (attached && !attached->windowObjectName().isEmpty()) + addToJavaScriptWindowObject(attached->windowObjectName(), object); + } +} + +int AmneziaWebView::pressGrabTime() const +{ + return 0; +} + +void AmneziaWebView::setPressGrabTime(int millis) +{ + Q_UNUSED(millis) + + emit pressGrabTimeChanged(); +} + +#ifndef QT_NO_ACTION +/*! + \qmlproperty action WebView::back + This property holds the action for causing the previous URL in the history to be displayed. +*/ +QAction* AmneziaWebView::backAction() const +{ + return action(AmneziaWebView::Back); +} + +/*! + \qmlproperty action WebView::forward + This property holds the action for causing the next URL in the history to be displayed. +*/ +QAction* AmneziaWebView::forwardAction() const +{ + return action(AmneziaWebView::Forward); +} + +/*! + \qmlproperty action WebView::reload + This property holds the action for reloading with the current URL +*/ +QAction* AmneziaWebView::reloadAction() const +{ + return action(AmneziaWebView::Reload); +} + +/*! + \qmlproperty action WebView::stop + This property holds the action for stopping loading with the current URL +*/ +QAction* AmneziaWebView::stopAction() const +{ + return action(AmneziaWebView::Stop); +} +#endif // QT_NO_ACTION + +/*! + \qmlproperty string WebView::title + This property holds the title of the web page currently viewed + + By default, this property contains an empty string. +*/ +QString AmneziaWebView::title() const +{ + Q_D(const AmneziaWebView); + return d->title; +} + +/*! + \qmlproperty pixmap WebView::icon + This property holds the icon associated with the web page currently viewed +*/ +QPixmap AmneziaWebView::icon() const +{ + Q_D(const AmneziaWebView); + return d->icon().pixmap(QSize(256, 256)); +} + +/*! + \qmlproperty string WebView::statusText + + This property is the current status suggested by the current web page. In a web browser, + such status is often shown in some kind of status bar. +*/ +void AmneziaWebView::setStatusText(const QString& text) +{ + Q_D(AmneziaWebView); + d->statusText = text; + emit statusTextChanged(); +} + +void AmneziaWebView::windowObjectCleared() +{ + Q_D(AmneziaWebView); + d->updateWindowObjects(); +} + +QString AmneziaWebView::statusText() const +{ + Q_D(const AmneziaWebView); + return d->statusText; +} + + +void AmneziaWebView::load(const QNetworkRequest& request, QNetworkAccessManager::Operation operation, const QByteArray& body) +{ + Q_D(AmneziaWebView); + d->load(request, operation, body); +} + +QString AmneziaWebView::html() const +{ + Q_D(const AmneziaWebView); + return d->toHtml(); +} + +void AmneziaWebView::setHtml(const QString& html, const QUrl& baseUrl) +{ + Q_D(AmneziaWebView); + auto originUrl = baseUrl.isValid() ? baseUrl : defaultBaseUrl(); + + updateContentsSize(); + if (isComponentComplete()) { + d->setHtml(html, originUrl); + } + else { + d->pending = d->PendingHtml; + d->pendingUrl = originUrl; + d->pendingString = html; + } + emit htmlChanged(); +} + +void AmneziaWebView::setContent(const QByteArray& data, const QString& mimeType, const QUrl& baseUrl) +{ + Q_D(AmneziaWebView); + + updateContentsSize(); + auto originUrl = baseUrl.isValid() ? baseUrl : defaultBaseUrl(); + + if (isComponentComplete()) + d->setContent(data, mimeType, qmlContext(this)->resolvedUrl(baseUrl)); + else { + d->pending = d->PendingContent; + d->pendingUrl = originUrl; + d->pendingString = mimeType; + d->pendingData = data; + } +} + +AmneziaWebHistory* AmneziaWebView::history() const +{ + Q_D(const AmneziaWebView); + return d->history(); +} + +#ifndef QT_NO_ACTION +QAction* AmneziaWebView::action(AmneziaWebView::WebAction action) const +{ + Q_D(const AmneziaWebView); + return d->action(action); +} +#endif + +/*! + \qmlproperty component WebView::newWindowComponent + + This property holds the component to use for new windows. + The component must have a WebView somewhere in its structure. + + When the web engine requests a new window, it will be an instance of + this component. + + The parent of the new window is set by newWindowParent. It must be set. +*/ +QQmlComponent* AmneziaWebView::newWindowComponent() const +{ + Q_D(const AmneziaWebView); + return d->newWindowComponent; +} + +void AmneziaWebView::setNewWindowComponent(QQmlComponent* newWindow) +{ + Q_D(AmneziaWebView); + if (newWindow == d->newWindowComponent) + return; + d->newWindowComponent = newWindow; + emit newWindowComponentChanged(); +} + + +/*! + \qmlproperty item WebView::newWindowParent + + The parent item for new windows. + + \sa newWindowComponent +*/ +QQuickItem* AmneziaWebView::newWindowParent() const +{ + Q_D(const AmneziaWebView); + return d->newWindowParent; +} + +void AmneziaWebView::setNewWindowParent(QQuickItem *parent) +{ + Q_D(AmneziaWebView); + if (parent == d->newWindowParent) + return; + if (d->newWindowParent && parent) { + QList children = d->newWindowParent->childItems(); + for (int i = 0; i < children.count(); ++i) + children.at(i)->setParentItem(parent); + } + d->newWindowParent = parent; + emit newWindowParentChanged(); +} + +QSize AmneziaWebView::contentsSize() const +{ + Q_D(const AmneziaWebView); + return d->contentsSize() * contentsScale(); +} + +qreal AmneziaWebView::contentsScale() const +{ + Q_D(const AmneziaWebView); + return d->scale(); +} + +void AmneziaWebView::setContentsScale(qreal scale) +{ + Q_D(AmneziaWebView); + if (scale == d->scale()) + return; + d->setScale(scale); + + //updateGeometry(); + emit contentsScaleChanged(); +} + +void AmneziaWebView::setDefaultFontSize(int size) +{ + Q_D(AmneziaWebView); + d->setDefaultFontSize(size); +} + +void AmneziaWebView::setStandardFontFamily(const QString &family) +{ + Q_D(AmneziaWebView); + d->setStandardFontFamily(family); +} + +void AmneziaWebView::setTextZoom(int percent) +{ + Q_D(AmneziaWebView); + d->setTextZoom(percent); +} + + +#ifdef Q_REVISION +/*! + \qmlproperty color WebView::backgroundColor + \since QtWebKit 1.1 + This property holds the background color of the view. +*/ + +QColor AmneziaWebView::backgroundColor() const +{ + Q_D(const AmneziaWebView); + return d->backgroundColor; +} + +void AmneziaWebView::setBackgroundColor(const QColor& color) +{ + setFillColor(color); +} + +void AmneziaWebView::fillColorWasChanged() +{ + Q_D(AmneziaWebView); + QColor color = fillColor(); + d->setBackgroundColor(color); + emit backgroundColorChanged(); +} + +#endif + + +QT_END_NAMESPACE + diff --git a/client/core/webview/amneziawebview.h b/client/core/webview/amneziawebview.h new file mode 100644 index 000000000..02507bda8 --- /dev/null +++ b/client/core/webview/amneziawebview.h @@ -0,0 +1,309 @@ +#ifndef DECLARATIVEWEBVIEW_H +#define DECLARATIVEWEBVIEW_H + +#include +#include +#include +#include +#include + +#include +#include + +#include "websettings.h" + +QT_BEGIN_NAMESPACE + +class AmneziaWebViewSettings; +class AmneziaWebViewPrivate; +class AmneziaWebViewAttached; +class AmneziaWebHistory; + +class AmneziaWebView : public QQuickPaintedItem +{ + Q_OBJECT + + Q_ENUMS(Status SelectionMode) + + Q_PROPERTY(QString title READ title NOTIFY titleChanged) + Q_PROPERTY(QPixmap icon READ icon NOTIFY iconChanged) + Q_PROPERTY(QString statusText READ statusText NOTIFY statusTextChanged) + Q_PROPERTY(QString html READ html WRITE setHtml NOTIFY htmlChanged) + Q_PROPERTY(int pressGrabTime READ pressGrabTime WRITE setPressGrabTime NOTIFY pressGrabTimeChanged) + Q_PROPERTY(qreal preferredWidth READ preferredWidth WRITE setPreferredWidth NOTIFY preferredWidthChanged) + Q_PROPERTY(qreal preferredHeight READ preferredHeight WRITE setPreferredHeight NOTIFY preferredHeightChanged) + Q_PROPERTY(QUrl url READ url WRITE setUrl NOTIFY urlChanged) + Q_PROPERTY(qreal progress READ progress NOTIFY progressChanged) + Q_PROPERTY(Status status READ status NOTIFY statusChanged) + + +#ifndef QT_NO_ACTION + Q_PROPERTY(QAction* reload READ reloadAction CONSTANT) + Q_PROPERTY(QAction* back READ backAction CONSTANT) + Q_PROPERTY(QAction* forward READ forwardAction CONSTANT) + Q_PROPERTY(QAction* stop READ stopAction CONSTANT) +#endif + + Q_PROPERTY(AmneziaWebViewSettings* settings READ settingsObject CONSTANT) + Q_PROPERTY(QQmlListProperty javaScriptWindowObjects READ javaScriptWindowObjects CONSTANT) + Q_PROPERTY(QQmlComponent* newWindowComponent READ newWindowComponent WRITE setNewWindowComponent NOTIFY newWindowComponentChanged) + Q_PROPERTY(QQuickItem* newWindowParent READ newWindowParent WRITE setNewWindowParent NOTIFY newWindowParentChanged) + Q_PROPERTY(QSize contentsSize READ contentsSize NOTIFY contentsSizeChanged) + Q_PROPERTY(qreal contentsScale READ contentsScale WRITE setContentsScale NOTIFY contentsScaleChanged) + Q_PROPERTY(QColor backgroundColor READ backgroundColor WRITE setBackgroundColor NOTIFY backgroundColorChanged) + +public: + + enum WebAction { + NoWebAction = - 1, + + OpenLink, + + OpenLinkInNewWindow, + OpenFrameInNewWindow, + + DownloadLinkToDisk, + CopyLinkToClipboard, + + OpenImageInNewWindow, + DownloadImageToDisk, + CopyImageToClipboard, + + Back, + Forward, + Stop, + Reload, + + Cut, + Copy, + Paste, + + Undo, + Redo, + MoveToNextChar, + MoveToPreviousChar, + MoveToNextWord, + MoveToPreviousWord, + MoveToNextLine, + MoveToPreviousLine, + MoveToStartOfLine, + MoveToEndOfLine, + MoveToStartOfBlock, + MoveToEndOfBlock, + MoveToStartOfDocument, + MoveToEndOfDocument, + SelectNextChar, + SelectPreviousChar, + SelectNextWord, + SelectPreviousWord, + SelectNextLine, + SelectPreviousLine, + SelectStartOfLine, + SelectEndOfLine, + SelectStartOfBlock, + SelectEndOfBlock, + SelectStartOfDocument, + SelectEndOfDocument, + DeleteStartOfWord, + DeleteEndOfWord, + + SetTextDirectionDefault, + SetTextDirectionLeftToRight, + SetTextDirectionRightToLeft, + + ToggleBold, + ToggleItalic, + ToggleUnderline, + + InspectElement, + + InsertParagraphSeparator, + InsertLineSeparator, + + SelectAll, + ReloadAndBypassCache, + + PasteAndMatchStyle, + RemoveFormat, + + ToggleStrikethrough, + ToggleSubscript, + ToggleSuperscript, + InsertUnorderedList, + InsertOrderedList, + Indent, + Outdent, + + AlignCenter, + AlignJustified, + AlignLeft, + AlignRight, + + StopScheduledPageRefresh, + + CopyImageUrlToClipboard, + + WebActionCount + }; + + + explicit AmneziaWebView(QQuickItem *parent = nullptr); + virtual ~AmneziaWebView(); + + QUrl url() const; + void setUrl(const QUrl &); + + QString title() const; + + QPixmap icon() const; + + int pressGrabTime() const; + void setPressGrabTime(int); + + qreal preferredWidth() const; + void setPreferredWidth(qreal); + qreal preferredHeight() const; + void setPreferredHeight(qreal); + + enum Status { Null, Ready, Loading, Error }; + Status status() const; + qreal progress() const; + QString statusText() const; + +#ifndef QT_NO_ACTION + QAction *reloadAction() const; + QAction *backAction() const; + QAction *forwardAction() const; + QAction *stopAction() const; + QAction* action(AmneziaWebView::WebAction) const; +#endif + + void load(const QNetworkRequest &request, QNetworkAccessManager::Operation operation = QNetworkAccessManager::GetOperation, + const QByteArray &body = QByteArray()); + + QString html() const; + + void setHtml(const QString &html, const QUrl &baseUrl = QUrl()); + void setContent(const QByteArray &data, const QString &mimeType = QString(), const QUrl &baseUrl = QUrl()); + + AmneziaWebHistory* history() const; + + QQmlListProperty javaScriptWindowObjects(); + AmneziaWebViewSettings* settingsObject() const; + static AmneziaWebViewAttached* qmlAttachedProperties(QObject*); + + QQmlComponent *newWindowComponent() const; + void setNewWindowComponent(QQmlComponent *newWindow); + QQuickItem* newWindowParent() const; + void setNewWindowParent(QQuickItem* newWindow); + + bool isComponentCompletePublic() const { return isComponentComplete(); } + + QSize contentsSize() const; + + void setContentsScale(qreal scale); + qreal contentsScale() const; + + QColor backgroundColor() const; + void setBackgroundColor(const QColor&); + + void paint(QPainter *painter) override; + + void setDefaultFontSize(int size); + void setStandardFontFamily(const QString &family); + Q_INVOKABLE void setTextZoom(int percent); + +Q_SIGNALS: + + void preferredWidthChanged(); + void preferredHeightChanged(); + + void urlChanged(); + void progressChanged(); + void statusChanged(Status); + void titleChanged(const QString&); + void iconChanged(); + void statusTextChanged(); + void htmlChanged(); + void pressGrabTimeChanged(); + void newWindowComponentChanged(); + void newWindowParentChanged(); + void renderingEnabledChanged(); + void contentsSizeChanged(const QSize&); + void contentsScaleChanged(); + void backgroundColorChanged(); + + void loadStarted(); + void loadFinished(); + void loadFinished(bool ok); + void loadFailed(); + + void doubleClick(int clickX, int clickY); + void zoomTo(qreal zoom, int centerX, int centerY); + void alert(const QString& message); + +public Q_SLOTS: + void evaluateJavaScript(const QString&); + +private Q_SLOTS: + void afterRendering(); + void updateGeometry(); + void windowWasChanged(QQuickWindow* window); + void parentWasChanged(); + void fillColorWasChanged(); + + void doLoadStarted(); + void doLoadProgress(int p); + void doLoadFinished(bool ok); + void setStatusText(const QString&); + void windowObjectCleared(); + +protected: + + void itemChange(ItemChange, const ItemChangeData &) override; + void geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry) override; + QScopedPointer d_ptr; + +private: + void updateContentsSize() {} + void init(); + void componentComplete() override; + QTimer upadeTimer; + + + Q_DISABLE_COPY(AmneziaWebView) + Q_DECLARE_PRIVATE(AmneziaWebView) + + friend class QDeclarativeWebPage; +}; + +class AmneziaWebViewAttached : public QObject +{ + Q_OBJECT + Q_PROPERTY(QString windowObjectName READ windowObjectName WRITE setWindowObjectName) +public: + explicit AmneziaWebViewAttached(QObject* parent) + : QObject(parent) + { + } + + QString windowObjectName() const + { + return m_windowObjectName; + } + + void setWindowObjectName(const QString &n) + { + m_windowObjectName = n; + } + +private: + QString m_windowObjectName; +}; + +QT_END_NAMESPACE + +QML_DECLARE_TYPE(AmneziaWebView) +QML_DECLARE_TYPEINFO(AmneziaWebView, QML_HAS_ATTACHED_PROPERTIES) + +#endif diff --git a/client/core/webview/amneziawebview_android.cpp b/client/core/webview/amneziawebview_android.cpp new file mode 100644 index 000000000..7739d512d --- /dev/null +++ b/client/core/webview/amneziawebview_android.cpp @@ -0,0 +1,369 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "amneziawebview_p.h" +#include "qrchandler.h" +#include "filehandler.h" + +#include +#include + +namespace Jni +{ + +using Object = QJniObject; +} +static const char qtAndroidWebViewControllerClass[] = "org/amnezia/vpn/WebViewController"; + +class AndroidWebViewPrivate; + +typedef QMap WebViews; +Q_GLOBAL_STATIC(WebViews, g_webViews) +Q_GLOBAL_STATIC(QMutex, g_webMutex) + +class AndroidWebViewPrivate : public AmneziaWebViewPrivate +{ + Q_DECLARE_PUBLIC(AmneziaWebView) + +public: + + explicit AndroidWebViewPrivate(AmneziaWebView* q); + virtual ~AndroidWebViewPrivate(); + + static AndroidWebViewPrivate *get(AmneziaWebView *q) + { + return static_cast(AmneziaWebViewPrivate::get(q)); + } + + virtual void setWindowParent(QWindow *parent); + virtual void setBackgroundColor(const QColor backgroundColor); + virtual void show(); + virtual void hide(); + virtual void setGeometry(const QRect &); + virtual QString innerHTML() const; + virtual void load(const QUrl& url); + virtual void setHtml(const QString& html, const QUrl& baseUrl = QUrl()); + virtual void setContent(const QByteArray& data, const QString& mimeType, const QUrl& baseUrl); + virtual void evaluateJavaScript(const QString& scriptSource); + virtual bool isLoading() const; + virtual bool canGoBack() const; + virtual bool canGoForward() const; + virtual void back(); + virtual void forward(); + virtual void reload(); + virtual void stop(); + virtual void setUrl(const QUrl &url); + virtual QIcon icon() const; + virtual void setScale(qreal scale); + virtual qreal scale() const; + virtual QSize contentsSize() const; + virtual void setDefaultFontSize(int size); + virtual void setStandardFontFamily(const QString &family); + virtual void setTextZoom(int percent); +private: + + quintptr viewId; + Jni::Object m_viewController; +}; + +AndroidWebViewPrivate::AndroidWebViewPrivate(AmneziaWebView* q): AmneziaWebViewPrivate(q), + viewId(reinterpret_cast(this)) +{ + m_viewController = Jni::Object(qtAndroidWebViewControllerClass, + "(Landroid/app/Activity;J)V", + QNativeInterface::QAndroidApplication::context().object(), + viewId); + QMutexLocker lock(g_webMutex()); + g_webViews->insert(viewId, this); + setBackgroundColor(backgroundColor); +} + +AndroidWebViewPrivate::~AndroidWebViewPrivate() +{ + QMutexLocker lock(g_webMutex()); + m_viewController.callMethod("release", "()V"); + g_webViews->take(viewId); +} + +AmneziaWebViewPrivate *AmneziaWebViewPrivate::create(AmneziaWebView *q) +{ + return new AndroidWebViewPrivate(q); +} + +void AndroidWebViewPrivate::setWindowParent(QWindow *parent) +{ + Q_UNUSED(parent); +} + + +void AndroidWebViewPrivate::setBackgroundColor(const QColor backgroundColor) +{ + m_viewController.callMethod("setBackgroundColor", "(I)V", + jint(backgroundColor.rgb())); + emit backgroundColorChanged(); +} + + +bool AndroidWebViewPrivate::isLoading() const +{ + return false; +} + +/// Deprecated +void AndroidWebViewPrivate::setScale(qreal scale) +{ + Q_UNUSED(scale); +} + +/// Deprecated +qreal AndroidWebViewPrivate::scale() const +{ + return 1; +} + +QIcon AndroidWebViewPrivate::icon() const +{ + return QIcon(); +} + +QSize AndroidWebViewPrivate::contentsSize() const +{ + return QSize(); +} + +void AndroidWebViewPrivate::setGeometry(const QRect &geometry) +{ + if (this->geometry != geometry) { + this->geometry = geometry; + + m_viewController.callMethod("setGeometry", "(IIII)V", + jint(geometry.x()), jint(geometry.y()), + jint(geometry.width()), jint(geometry.height()) ); + } +} + +void AndroidWebViewPrivate::setTextZoom(int percent) +{ + m_viewController.callMethod("setTextZoom", "(I)V", jint(percent)); +} + +void AndroidWebViewPrivate::hide() +{ + Q_Q(AmneziaWebView); + if (visible) { + m_viewController.callMethod("hide", "()V"); + visible = false; + q->update(); + } +} + +void AndroidWebViewPrivate::show() +{ + if (!visible) { + m_viewController.callMethod("show", "()V"); + visible = true; + } +} + + +void AndroidWebViewPrivate::setContent(const QByteArray& data, const QString& mimeType, const QUrl& baseUrl) +{ + Q_UNUSED(data); + Q_UNUSED(mimeType); + Q_UNUSED(baseUrl); +} + +void AndroidWebViewPrivate::setUrl(const QUrl &url) +{ + AmneziaWebViewPrivate::setUrl(url); +} + +void AndroidWebViewPrivate::load(const QUrl &url) +{ + // Make WebView visible before loading + if (!visible) { + show(); + } + Jni::Object jurl = Jni::Object::fromString(url.isValid() ? url.toString() : QString("about:blank")); + m_viewController.callMethod("loadUrl", "(Ljava/lang/String;)V", jurl.object()); +} + +void AndroidWebViewPrivate::setHtml(const QString &html, const QUrl &baseUrl) +{ + if (html.isNull()) { + return; + } + + Jni::Object url = Jni::Object::fromString(baseUrl.isValid() ? baseUrl.toString() : QString("about:blank")); + Jni::Object data = Jni::Object::fromString(html); + Jni::Object mime = Jni::Object::fromString(QString("text/html")); + Jni::Object encoding = Jni::Object::fromString(QString("utf-8")); + + m_viewController.callMethod("loadDataWithBaseURL", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V", + url.object(), data.object(), mime.object(), encoding.object()); +} + +void AndroidWebViewPrivate::evaluateJavaScript(const QString &scriptSource) +{ + Jni::Object script = Jni::Object::fromString(scriptSource); + m_viewController.callMethod("evaluateJavaScript", "(Ljava/lang/String;)V", script.object()); +} + +bool AndroidWebViewPrivate::canGoBack() const +{ + jboolean can = m_viewController.callMethod("canGoBack", "()Z"); + return can; +} + +bool AndroidWebViewPrivate::canGoForward() const +{ + jboolean can = m_viewController.callMethod("canGoForward", "()Z"); + return can; +} + +void AndroidWebViewPrivate::back() +{ + m_viewController.callMethod("goBack", "()V"); +} + +void AndroidWebViewPrivate::forward() +{ + m_viewController.callMethod("goForward", "()V"); +} + +void AndroidWebViewPrivate::reload() +{ +} + +void AndroidWebViewPrivate::stop() +{ +} + +QString AndroidWebViewPrivate::innerHTML() const +{ + return QString(); +} + +void AndroidWebViewPrivate::setDefaultFontSize(int size) +{ + m_viewController.callMethod("setDefaultFontSize", "(I)V", jint(size)); +} + +void AndroidWebViewPrivate::setStandardFontFamily(const QString &family) +{ + Jni::Object fontFamily = Jni::Object::fromString(family); + m_viewController.callMethod("setStandardFontFamily", "(Ljava/lang/String;)V", fontFamily.object()); +} + +extern "C" { + +JNIEXPORT void Java_org_amnezia_vpn_WebViewController_pageStarted(JNIEnv *env, jobject obj, jlong viewId, jstring url) +{ + Q_UNUSED(env); + Q_UNUSED(obj); + + QMutexLocker lock(g_webMutex()); + AndroidWebViewPrivate *view = g_webViews()->value(viewId); + if (view) { + const char *urlChars = env->GetStringUTFChars(url, 0); + const QUrl url = QUrl(QString(urlChars)); + QMetaObject::invokeMethod(view, "onPageStarted", Qt::QueuedConnection); + } +} + +JNIEXPORT void Java_org_amnezia_vpn_WebViewController_pageFinished(JNIEnv *env, jobject obj, jlong viewId, jstring url) +{ + Q_UNUSED(env); + Q_UNUSED(obj); + QMutexLocker lock(g_webMutex()); + AndroidWebViewPrivate *view = g_webViews()->value(viewId); + if (view) { + const char *urlChars = env->GetStringUTFChars(url, 0); + const QUrl url = QUrl(QString(urlChars)); + QMetaObject::invokeMethod(view, "onPageFinished", Qt::QueuedConnection); + } +} + +JNIEXPORT void Java_org_amnezia_vpn_WebViewController_urlChanged(JNIEnv *env, jobject obj, jlong viewId, jstring url) +{ + Q_UNUSED(env); + Q_UNUSED(obj); + QMutexLocker lock(g_webMutex()); + AndroidWebViewPrivate *view = g_webViews()->value(viewId); + if (view) { + + const char *urlChars = env->GetStringUTFChars(url, 0); + const QUrl url = QUrl(QString(urlChars)); + QMetaObject::invokeMethod(view, "onUrlChanged", Qt::QueuedConnection, Q_ARG( const QUrl, url)); + } +} + +JNIEXPORT jbyteArray Java_org_amnezia_vpn_WebViewController_dataForUrl(JNIEnv *env, jobject obj, jlong viewId, jstring url, jobject mimeType, jobject encoding) +{ + Q_UNUSED(env) + Q_UNUSED(obj) + + QMutexLocker lock(g_webMutex()); + AndroidWebViewPrivate *view = g_webViews()->value(viewId); + if (view) { + + const char *urlChars = env->GetStringUTFChars(url, 0); + const QUrl url = QUrl(QString(urlChars)); + + QByteArray buffer = view->dataForUrl(url); + QString mime = view->mimeTypeForUrl(url); + QString enc("utf-8"); + + jstring jMimeType = env->NewStringUTF(mime.toUtf8().constData()); + Jni::Object jMimeTypeObject(mimeType); + if (jMimeTypeObject.isValid()) { + jMimeTypeObject.callObjectMethod("insert", "(ILjava/lang/String;)Ljava/lang/StringBuilder;", 0, jMimeType); + } + + jstring jEncoding = env->NewStringUTF(enc.toUtf8().constData()); + Jni::Object jEncodingObject(encoding); + if (jEncodingObject.isValid()) { + + jEncodingObject.callObjectMethod("insert", "(ILjava/lang/String;)Ljava/lang/StringBuilder;", 0, jEncoding); + } + + env->DeleteLocalRef(jEncoding); + env->DeleteLocalRef(jMimeType); + + jbyteArray data = env->NewByteArray(buffer.size()); + env->SetByteArrayRegion(data, 0, buffer.size(), (const jbyte*) buffer.data()); + return data; + } + + return NULL; +} + +JNIEXPORT jboolean Java_org_amnezia_vpn_WebViewController_canHandleUrl(JNIEnv *env, jobject obj, jlong viewId, jstring url) + +{ + Q_UNUSED(env); + Q_UNUSED(obj); + QMutexLocker lock(g_webMutex()); + AndroidWebViewPrivate *view = g_webViews()->value(viewId); + if (view) { + const char *urlChars = env->GetStringUTFChars(url, 0); + const QUrl url = QUrl(QString(urlChars)); + return view->canHandleUrl(url); + } + + return jboolean(false); +} + + +} diff --git a/client/core/webview/amneziawebview_desktop.cpp b/client/core/webview/amneziawebview_desktop.cpp new file mode 100644 index 000000000..bda4acc58 --- /dev/null +++ b/client/core/webview/amneziawebview_desktop.cpp @@ -0,0 +1,433 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "amneziawebview_desktop_p.h" +#include "qrchandler.h" +#include "filehandler.h" + +typedef QMap WebViews; +Q_GLOBAL_STATIC(WebViews, g_webViews) + + +QrcHandler::QrcHandler() +{} + + +FileHandler::FileHandler() +{ +} + +JsHandler::JsHandler(AmneziaWebView *host): _host(host), scriptObjectsInjected(false) +{ + init(); +} + +JsHandler::~JsHandler() {} + +WebPage::WebPage(QObject *parent) + : QWebPage(parent) +{ + connect(this, SIGNAL(unsupportedContent(QNetworkReply*)), + this, SLOT(handleUnsupportedContent(QNetworkReply*))); +} + +void WebPage::javaScriptAlert(QWebFrame *frame, const QString& msg) +{ + Q_UNUSED(frame) + Q_UNUSED(msg) +} + +WebPage::~WebPage() +{ + disconnect(this); +} + +bool WebPage::acceptNavigationRequest(QWebFrame *frame, const QNetworkRequest &request, NavigationType type) +{ + return QWebPage::acceptNavigationRequest(frame, request, type); +} + +void WebPage::handleUnsupportedContent(QNetworkReply *reply) +{ + QString errorString = reply->errorString(); + + if (m_loadingUrl != reply->url()) { + // sub resource of this page + qWarning() << "Resource" << reply->url().toEncoded() << "has unknown Content-Type, will be ignored."; + reply->deleteLater(); + return; + } + + if (reply->error() == QNetworkReply::NoError && !reply->header(QNetworkRequest::ContentTypeHeader).isValid()) { + errorString = "Unknown Content-Type"; + } + + QFile file(QLatin1String(":/notfound.html")); + bool isOpened = file.open(QIODevice::ReadOnly); + Q_ASSERT(isOpened); + Q_UNUSED(isOpened) + + QString title = QCoreApplication::translate("webview", "Error loading page: %1").arg(reply->url().toString()); + QString html = QString(QLatin1String(file.readAll())) + .arg(title) + .arg(errorString) + .arg(reply->url().toString()); + + QBuffer imageBuffer; + imageBuffer.open(QBuffer::ReadWrite); + QIcon icon = view()->style()->standardIcon(QStyle::SP_MessageBoxWarning, nullptr, view()); + QPixmap pixmap = icon.pixmap(QSize(32,32)); + if (pixmap.save(&imageBuffer, "PNG")) { + html.replace(QLatin1String("IMAGE_BINARY_DATA_HERE"), + QString(QLatin1String(imageBuffer.buffer().toBase64()))); + } + + QList frames; + frames.append(mainFrame()); + while (!frames.isEmpty()) { + QWebFrame *frame = frames.takeFirst(); + if (frame->url() == reply->url()) { + frame->setHtml(html, reply->url()); + return; + } + QList children = frame->childFrames(); + foreach(QWebFrame *frame, children) + frames.append(frame); + } + if (m_loadingUrl == reply->url()) { + mainFrame()->setHtml(html, reply->url()); + } +} + +DesktopWebViewPrivate::DesktopWebViewPrivate(AmneziaWebView* q): AmneziaWebViewPrivate(q) + , viewId(reinterpret_cast(this)) + , containerWindow(nullptr) + , window(nullptr) +{ + container = new QWidget(0, Qt::FramelessWindowHint | Qt::NoDropShadowWindowHint | Qt::Tool); + container->setAttribute(Qt::WA_NativeWindow, true); + container->setAttribute(Qt::WA_DontCreateNativeAncestors, true); + + // Do not remove next line -> prevent some sort of spontaneous crashes + QWebSettings::setObjectCacheCapacities(0, 0, 0); + + view = new QWebView(container); + WebPage *page = new WebPage(view); + page->setForwardUnsupportedContent(true); + page->setNetworkAccessManager(networkAccessManager()); + page->settings()->setAttribute(QWebSettings::DeveloperExtrasEnabled, true); + view->setPage(page); + + container->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + container->setLayout(new QHBoxLayout(container)); + container->layout()->setSpacing(0); + container->layout()->setMargin(0); + container->layout()->addWidget(view); + + setBackgroundColor(backgroundColor); + g_webViews->insert(viewId, this); + + connect(view, SIGNAL(loadFinished(bool)), this, SIGNAL(loadFinished(bool))); + + connect(page, SIGNAL(loadFinished(bool)), this, SLOT(onLoadFinished(bool)), Qt::QueuedConnection); + connect(page, SIGNAL(loadStarted()), this, SLOT(onPageStarted()), Qt::QueuedConnection); + connect(view, SIGNAL(urlChanged(const QUrl &)), this, SLOT(onUrlChanged(const QUrl &)), Qt::QueuedConnection); + container->createWinId(); +} + +DesktopWebViewPrivate::~DesktopWebViewPrivate() +{ + disconnect(this, SLOT(loadFinished(bool))); + disconnect(this, SLOT(applicationStateChanged(Qt::ApplicationState))); + disconnect(this, SLOT(onUrlChanged(const QUrl &))); + disconnect(this, SLOT(onPageStarted())); + disconnect(this, SLOT(onLoadFinished(bool))); + + g_webViews->take(viewId); + view->stop(); + view->setPage(nullptr); + + container->deleteLater(); +} + +AmneziaWebViewPrivate *AmneziaWebViewPrivate::create(AmneziaWebView *q) +{ + return new DesktopWebViewPrivate(q); +} + +void DesktopWebViewPrivate::setWindowParent(QWindow *parent) +{ + if (window) { + window->removeEventFilter(this); + } + + if (parent) { + + containerWindow = qobject_cast(container->windowHandle()); + containerWindow->setTransientParent(parent); + parent->installEventFilter(this); + + } + window = parent; +} + +void DesktopWebViewPrivate::setBackgroundColor(const QColor backgroundColor) +{ + this->backgroundColor = backgroundColor; + QPalette p = container->palette(); + p.setColor(QPalette::Background, backgroundColor); + container->setPalette(p); + p = view->palette(); + p.setColor(QPalette::Background, backgroundColor); + view->setPalette(p); + emit backgroundColorChanged(); +} + +/// Deprecated +void DesktopWebViewPrivate::setScale(qreal scale) +{ + Q_UNUSED(scale) + //qreal s = view->geometry().width() / view->page()->preferredContentsSize().width(); + //view->setZoomFactor(s); +} + +/// Deprecated +qreal DesktopWebViewPrivate::scale() const +{ + return 1; +} + +QIcon DesktopWebViewPrivate::icon() const +{ + return QIcon(); +} + +QSize DesktopWebViewPrivate::contentsSize() const +{ + return QSize(); +} + +void DesktopWebViewPrivate::takeSnapshot() +{ + if (geometry.isEmpty() || !containerWindow) { + return; + } + else { + + container->updateGeometry(); + QPixmap pixmap = container->grab(); + snapshot = pixmap.toImage(); + emit snapshotChanged(); + } +} + +void DesktopWebViewPrivate::setGeometry(const QRect &geometry) +{ + Q_Q(AmneziaWebView); + QQuickWindow *window = q->window(); + if (!window) return; + QRect newGeometry = QRect(window->mapToGlobal(geometry.topLeft()), QSize(geometry.width(), geometry.height())); + if (newGeometry.isValid() && container->geometry() != newGeometry ) { + + this->geometry = geometry; + container->setGeometry(newGeometry); + container->updateGeometry(); + } +} + +bool DesktopWebViewPrivate::eventFilter(QObject *obj, QEvent *event) +{ + Q_UNUSED(obj) + Q_Q(AmneziaWebView); + + switch (event->type()) { + case QEvent::Move: { + QMoveEvent *moveEvent = static_cast(event); + QPoint p = q->mapToScene(QPointF(moveEvent->pos())).toPoint(); + container->move(p); + //container->updateGeometry(); + + if (visible) { + show(); + } + return false; + } + case QEvent::Resize: { + + //QRect newGeometry = QRect(window->mapToGlobal(geometry.topLeft()), QSize(geometry.width(), geometry.height())); + //container->setGeometry(newGeometry); + //container->updateGeometry(); + + if (visible) { + show(); + } + return false; + } + case QEvent::WindowStateChange: { + + Qt::WindowState state = window->windowState(); + if ((state == Qt::WindowMaximized) || (state == Qt::WindowFullScreen) || (state == Qt::WindowActive)) { + show(); + } + else { + hide(); + } + return false; + } + default: { + return false; + } + } +} + +void DesktopWebViewPrivate::hide() +{ + Q_Q(AmneziaWebView); + if (!q->window()) return; + + if (visible) { + + QMetaObject::invokeMethod(this, "requestSnapshot", Qt::QueuedConnection); + QMetaObject::invokeMethod(container, "hide", Qt::QueuedConnection); + visible = false; + + //if (q->isVisible()) + // q->update(); + } +} + +void DesktopWebViewPrivate::show() +{ + Q_Q(AmneziaWebView); + if (!q->window()) return; + if (!visible) { + + QMetaObject::invokeMethod(container, "show", Qt::QueuedConnection); + QMetaObject::invokeMethod(container, "update", Qt::QueuedConnection); + visible = true; + } + + if ((qApp->topLevelWindows().at(0) != containerWindow) || !containerWindow->isVisible()) { + + containerWindow->raise(); + } +} + +void DesktopWebViewPrivate::load(const QUrl& baseUrl) +{ + QUrl url = baseUrl; + if (!url.isValid()) { + url = QUrl(QLatin1String("about:blank")); + } + view->load(url); + history()->append(baseUrl); +} + +void DesktopWebViewPrivate::setContent(const QByteArray& data, const QString& mimeType, const QUrl& baseUrl) +{ + QUrl url = baseUrl; + if (!url.isValid()) { + url = QUrl(QLatin1String("about:blank")); + } + view->setContent(data, mimeType, url); + history()->append(url, data, mimeType); +} + +void DesktopWebViewPrivate::setHtml(const QString& html, const QUrl& baseUrl) +{ + if(html.isNull()) return; + QUrl url = baseUrl; + if (!baseUrl.isValid()) + url = QUrl(QLatin1String("about:blank")); + + view->setHtml(html, url); + history()->append(url, html.toUtf8(), "text/html"); +} + +void DesktopWebViewPrivate::evaluateJavaScript(const QString& scriptSource) +{ + view->page()->mainFrame()->evaluateJavaScript(scriptSource); +} + +bool DesktopWebViewPrivate::canGoBack() const +{ + QAction *pageAction = view->pageAction(QWebPage::Back); + bool can = (pageAction && pageAction->isEnabled()); + return can; +} + +bool DesktopWebViewPrivate::canGoForward() const +{ + QAction *pageAction = view->pageAction(QWebPage::Forward); + bool can = (pageAction && pageAction->isEnabled()); + return can; +} + +void DesktopWebViewPrivate::back() +{ + QAction *pageAction = view->pageAction(QWebPage::Back); + if (pageAction) + emit pageAction->trigger(); +} + +void DesktopWebViewPrivate::forward() +{ + QAction *pageAction = view->pageAction(QWebPage::Forward); + if (pageAction) + emit pageAction->trigger(); +} + +void DesktopWebViewPrivate::reload() +{ + QAction *pageAction = view->pageAction(QWebPage::Reload); + if (pageAction) + emit pageAction->trigger(); +} + +void DesktopWebViewPrivate::stop() +{ + QAction *pageAction = view->pageAction(QWebPage::Stop); + if (pageAction) + emit pageAction->trigger(); +} + +bool DesktopWebViewPrivate::isLoading() const +{ + return false; +} + +QString DesktopWebViewPrivate::innerHTML() const +{ + QVariant result = view->page()->mainFrame()->evaluateJavaScript("document.body.innerHTML"); + return result.toString(); +} + +void DesktopWebViewPrivate::onLoadFinished(bool success) +{ + if (success) { + QMetaObject::invokeMethod(this, "onPageFinished", Qt::QueuedConnection); + } + else { + QMetaObject::invokeMethod(this, "onPageError", Qt::QueuedConnection); + } +} + +void DesktopWebViewPrivate::setDefaultFontSize(int size) +{ + view->settings()->setFontSize(QWebSettings::DefaultFontSize, size); +} + +void DesktopWebViewPrivate::setStandardFontFamily(const QString &family) +{ + view->settings()->setFontFamily(QWebSettings::StandardFont, family); +} + +void DesktopWebViewPrivate::setTextZoom(int percent) +{ + Q_UNUSED(percent) +} \ No newline at end of file diff --git a/client/core/webview/amneziawebview_desktop_p.h b/client/core/webview/amneziawebview_desktop_p.h new file mode 100644 index 000000000..a260069c6 --- /dev/null +++ b/client/core/webview/amneziawebview_desktop_p.h @@ -0,0 +1,96 @@ +#ifndef AMNEZIAWEBVIEW_DESKTOP_P_H +#define AMNEZIAWEBVIEW_DESKTOP_P_H + +// QtWebKit is deprecated and not available in Qt 6 +// This file should only be used with Qt 5 when WebEngineWidgets is not available +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) +#error "amneziawebview_desktop_p.h uses QtWebKit which is not available in Qt 6. Use amneziawebview_webengine_p.h instead." +#endif + +#include +#include +#include +#include + +#include "amneziawebview.h" +#include "amneziawebview_p.h" + +class DesktopWebViewPrivate; + +class WebPage : public QWebPage +{ + Q_OBJECT + +signals: + void loadingUrl(const QUrl &url); + +public: + explicit WebPage(QObject *parent = nullptr); + virtual ~WebPage(); + +protected: + bool acceptNavigationRequest(QWebFrame *frame, const QNetworkRequest &request, NavigationType type); + virtual void javaScriptAlert(QWebFrame *frame, const QString& msg); + +private slots: + void handleUnsupportedContent(QNetworkReply *reply); + +private: + + friend class DesktopWebViewPrivate; + QUrl m_loadingUrl; +}; + +class DesktopWebViewPrivate : public AmneziaWebViewPrivate +{ + Q_OBJECT + Q_DECLARE_PUBLIC(AmneziaWebView) +public: + + explicit DesktopWebViewPrivate(AmneziaWebView* q); + virtual ~DesktopWebViewPrivate(); + + virtual void setWindowParent(QWindow *parent); + + virtual void setBackgroundColor(const QColor backgroundColor); + virtual void show(); + virtual void hide(); + virtual void takeSnapshot(); + virtual void setGeometry(const QRect &); + virtual QString innerHTML() const; + virtual void load(const QUrl& url); + virtual void setHtml(const QString& html, const QUrl& baseUrl = QUrl()); + virtual void setContent(const QByteArray& data, const QString& mimeType, const QUrl& baseUrl); + virtual void evaluateJavaScript(const QString& scriptSource); + virtual bool isLoading() const; + virtual bool canGoBack() const; + virtual bool canGoForward() const; + virtual void back(); + virtual void forward(); + virtual void reload(); + virtual void stop(); + + virtual QIcon icon() const; + virtual void setScale(qreal scale); + virtual qreal scale() const; + virtual QSize contentsSize() const; + virtual void setDefaultFontSize(int size); + virtual void setTextZoom(int percent) { Q_UNUSED(percent); } + virtual void setStandardFontFamily(const QString &family); + +protected: + bool eventFilter(QObject *obj, QEvent *event); + +private slots: + void onLoadFinished(bool); + +private: + quintptr viewId; + QWebView *view; + QWidget *container; + QWindow *containerWindow; + QWindow *window; +}; + + +#endif // AMNEZIAWEBVIEW_DESKTOP_P_H diff --git a/client/core/webview/amneziawebview_ios.mm b/client/core/webview/amneziawebview_ios.mm new file mode 100644 index 000000000..513437eb1 --- /dev/null +++ b/client/core/webview/amneziawebview_ios.mm @@ -0,0 +1,1045 @@ +#import +#import +#import +#import + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "amneziawebview.h" +#include "amneziawebview_p.h" +#include "mimecache.h" +#include "qrchandler.h" +#include "filehandler.h" + +static inline CGRect toCGRect(const QRectF &rect) +{ + return CGRectMake(rect.x(), rect.y(), rect.width(), rect.height()); +} + +#if defined(ENABLE_WKWEBVIEW) + +@interface WebSchemeHandler : NSObject +@property AmneziaWebView *view; +@end + +@interface WebDelegate: NSObject +@property AmneziaWebView *view; +@end + +#else + +@interface WebDelegate: NSObject +@property AmneziaWebView *view; +@end + +#endif + +class IosWebViewPrivate : public AmneziaWebViewPrivate +{ + Q_DECLARE_PUBLIC(AmneziaWebView) +public: + + IosWebViewPrivate(AmneziaWebView* q); + virtual ~IosWebViewPrivate(); + + virtual void setWindowParent(QWindow *parent); + virtual void setBackgroundColor(const QColor backgroundColor); + virtual void show(); + virtual void hide(); + virtual void setGeometry(const QRect &); + virtual QString innerHTML() const; + virtual void load(const QUrl& url); + virtual void setHtml(const QString& html, const QUrl& baseUrl = QUrl()); + virtual void setContent(const QByteArray& data, const QString& mimeType, const QUrl& baseUrl); + virtual void evaluateJavaScript(const QString& scriptSource); + virtual bool isLoading() const; + virtual bool canGoBack() const; + virtual bool canGoForward() const; + virtual void back(); + virtual void forward(); + virtual void reload(); + virtual void stop(); + virtual void setUrl(const QUrl &url); + virtual QIcon icon() const; + virtual void setScale(qreal scale); + virtual qreal scale() const; + virtual QSize contentsSize() const; + virtual void setDefaultFontSize(int size); + virtual void setStandardFontFamily(const QString &family); + virtual void setTextZoom(int percent); + void getSnapshot(); + + WebDelegate *webDelegate; + +#if defined(ENABLE_WKWEBVIEW) + WebSchemeHandler *schemeHandler; + WKWebViewConfiguration *configuration; + WKWebView *webView; + QString _innerHTML; +#else + UIWebView *webView; +#endif + NSString *_defaultUrl; +}; + +#if defined(ENABLE_WKWEBVIEW) + +@implementation WebSchemeHandler ++ (NSString *)charsetFromHtml:(NSString *)html +{ + if (!html) return nil; + + NSString *charset = nil; + NSString *charsetPattern = @"((?<=charset=)\\s*[a-zA-Z0-9-]*)"; + NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:charsetPattern options: NSRegularExpressionCaseInsensitive error:nil]; + NSTextCheckingResult *charsetResult = [regex firstMatchInString:html options:kNilOptions range:NSMakeRange(0, [html length])]; + if (charsetResult && charsetResult.range.location != NSNotFound) { + charset = [[html substringWithRange:[charsetResult range]] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; + charset = [charset lowercaseString]; + } + return charset; +} + ++ (NSString*)headFromHtml:(NSString*)html +{ + if (!html) return nil; + + NSString *head = nil; + NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"(?<=)[\\w\\W.]*(?=)" options:NSRegularExpressionCaseInsensitive error:nil]; + NSTextCheckingResult *headResult = [regex firstMatchInString:html options:0 range:NSMakeRange(0, html.length)]; + + if (headResult && headResult.range.location != NSNotFound) { + head = [html substringWithRange:[headResult range]]; + } + return head; +} + +- (void)webView:(WKWebView *)webView startURLSchemeTask:(id)urlSchemeTask +{ + NSURLRequest *request = urlSchemeTask.request; + AmneziaWebViewPrivate *d = AmneziaWebViewPrivate::get(self.view); + if (!d) return; + + NSString *url = [[request URL] path]; + NSString *mimeType = mimeTypeForExtension(QString::fromNSString(url.pathExtension)).toNSString(); + + QByteArray buffer; + QUrl requestUrl = QUrl::fromNSURL(request.URL); + + if (d->qrcHandler.canHandleUrl(requestUrl)) { + buffer = d->qrcHandler.dataForUrl(requestUrl); + } + else if (requestUrl.scheme().compare(d->jsHandler.scheme()) == 0) { + buffer = d->jsHandler.dataForUrl(requestUrl); + } + else if (d->fileHandler.canHandleUrl(requestUrl)) { + buffer = d->fileHandler.dataForUrl(requestUrl); + } + + if (!buffer.isEmpty()) { + + NSData *pageData = [NSData dataWithBytes:buffer.constData() length:buffer.size()]; + NSString *encoding = @"utf-8"; + if ([mimeType isEqualToString:@"text/html"]) { + + NSString *content = [[NSString alloc] initWithBytesNoCopy: (char* )[pageData bytes] length:pageData.length encoding:NSISOLatin1StringEncoding freeWhenDone:NO]; + + NSString *charset = [WebSchemeHandler charsetFromHtml:[WebSchemeHandler headFromHtml:content]]; + if (charset) { + encoding = charset; + } + [content release]; + } + + NSURLResponse *response = [[NSURLResponse alloc]initWithURL:request.URL + MIMEType:mimeType + expectedContentLength:[pageData length] + textEncodingName:encoding]; + + [urlSchemeTask didReceiveResponse:response]; + [urlSchemeTask didReceiveData:pageData]; + [urlSchemeTask didFinish]; + [response release]; + } + else { + [urlSchemeTask didFailWithError:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:nil]]; + } +} + +- (void)webView:(WKWebView *)webView stopURLSchemeTask:(id)urlSchemeTask +{ + Q_UNUSED(webView); +} + +@end + +@implementation WebDelegate + +- (void)webView:(WKWebView *)webView didFailNavigation:(WKNavigation *)navigation withError:(NSError *)error +{ + Q_UNUSED(webView); + + [UIApplication sharedApplication].networkActivityIndicatorVisible = NO; + if([error code] == NSURLErrorCancelled) return; + + AmneziaWebViewPrivate *d = AmneziaWebViewPrivate::get(self.view); + if (d) { + d->lastError = QString::fromNSString(error.localizedDescription); + QMetaObject::invokeMethod(d, "onPageFinished", Qt::QueuedConnection); + } +} + +- (void)webView:(WKWebView *)webView didCommitNavigation:(WKNavigation *)navigation +{ + [UIApplication sharedApplication].networkActivityIndicatorVisible = YES; + NSString *path = [webView.URL.path stringByTrimmingCharactersInSet: [NSCharacterSet characterSetWithCharactersInString: @"/"]]; + path = [path stringByTrimmingCharactersInSet: [NSCharacterSet whitespaceCharacterSet]]; + QMetaObject::invokeMethod(AmneziaWebViewPrivate::get(self.view), "onPageStarted", Qt::QueuedConnection); +} + +- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation +{ + [UIApplication sharedApplication].networkActivityIndicatorVisible = NO; + NSString *urlString = [[webView.URL absoluteString] stringByRemovingPercentEncoding]; + NSString *path = [webView.URL.path stringByTrimmingCharactersInSet: [NSCharacterSet characterSetWithCharactersInString: @"/"]]; + path = [path stringByTrimmingCharactersInSet: [NSCharacterSet whitespaceCharacterSet]]; + + __block __typeof(self) strongSelf = self; + [strongSelf retain]; + [webView evaluateJavaScript:@"document.body.innerHTML" completionHandler:^(id _Nullable result, NSError * _Nullable error) { + + AmneziaWebViewPrivate *d = AmneziaWebViewPrivate::get(strongSelf.view); + if ((d != nullptr) && !error && result && [result isKindOfClass:[NSString class]]) { + NSString *html = (NSString *)result; + IosWebViewPrivate *iosView = static_cast(d); + iosView->_innerHTML = QString::fromNSString(html); + } + [strongSelf release]; + }]; + + QUrl url = QUrl(QString::fromNSString(urlString)); + QMetaObject::invokeMethod(AmneziaWebViewPrivate::get(self.view), "onPageFinished", Qt::QueuedConnection); + + //Scale WkWebView contents to view width + [webView evaluateJavaScript: + [NSString stringWithFormat:@"var viewport = null;" + "var document_head = document.getElementsByTagName( 'head' )[0];" + "var child = document_head.firstChild;" + "while ( child ) {" + " if ( null == viewport && child.nodeType == 1 && child.nodeName == 'META' && child.getAttribute( 'name' ) == 'viewport' ) {" + " viewport = child;" + " var content = child.getAttribute( 'content' );" + " if (content != null) {" + " var regex = new RegExp('width');" //or maybe "var regex = new RegExp('width\\s*=\\s*device-width');" + " var found = regex.test(content);" + " if(!found) {" + " content = content + ',width=device-width';" + " viewport.setAttribute( 'content' , content );" + " }" + " }" + " else {" + " viewport.setAttribute( 'content' , 'width=device-width' );" + " }" + " }" + " child = child.nextSibling;" + "}" + "if (null == viewport) {" + " var meta = document.createElement( 'meta' );" + " meta.setAttribute( 'name' , 'viewport' );" + " meta.setAttribute( 'content' , 'width=device-width' );" + " document_head.appendChild( meta );" + "}" + ] completionHandler: nil]; +} + +-(void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler +{ + NSURLRequest *request = navigationAction.request; + NSURL *url = request.URL; + NSLog(@"url: %@", url); + + if ([url.scheme isEqualToString:@"tel"]) { + decisionHandler(WKNavigationActionPolicyAllow); + return; + } + + AmneziaWebViewPrivate *q = AmneziaWebViewPrivate::get(self.view); + NSString *baseUrlString = [((IosWebViewPrivate *)q)->_defaultUrl stringByRemovingPercentEncoding]; + NSString *urlString = [[request.URL absoluteString] stringByRemovingPercentEncoding]; + urlString = [urlString stringByReplacingOccurrencesOfString:baseUrlString withString:@""]; + + q->setUrl(QString::fromNSString(urlString)); + decisionHandler(WKNavigationActionPolicyAllow); +} + +@end + +#else + +@implementation WebDelegate + +- (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error +{ + Q_UNUSED(webView); + + [UIApplication sharedApplication].networkActivityIndicatorVisible = NO; + if([error code] == NSURLErrorCancelled) return; + + AmneziaWebViewPrivate *d = AmneziaWebViewPrivate::get(self.view); + if (d) { + d->lastError = QString::fromNSString(error.localizedDescription); + QMetaObject::invokeMethod(d, "onPageFinished", Qt::QueuedConnection); + } +} + +- (void)webViewDidStartLoad:(UIWebView *)webView +{ + [UIApplication sharedApplication].networkActivityIndicatorVisible = YES; + NSString *path = [webView.request.URL.path stringByTrimmingCharactersInSet: [NSCharacterSet characterSetWithCharactersInString: @"/"]]; + path = [path stringByTrimmingCharactersInSet: [NSCharacterSet whitespaceCharacterSet]]; + QMetaObject::invokeMethod(AmneziaWebViewPrivate::get(self.view), "onPageStarted", Qt::QueuedConnection); +} + +- (void)webViewDidFinishLoad:(UIWebView *)webView +{ + [UIApplication sharedApplication].networkActivityIndicatorVisible = NO; + NSString *urlString = [[webView.request.URL absoluteString] stringByRemovingPercentEncoding]; + + NSString *path = [webView.request.URL.path stringByTrimmingCharactersInSet: [NSCharacterSet characterSetWithCharactersInString: @"/"]]; + path = [path stringByTrimmingCharactersInSet: [NSCharacterSet whitespaceCharacterSet]]; + + NSString *title=[[webView stringByEvaluatingJavaScriptFromString:@"document.title"] stringByRemovingPercentEncoding]; + + AmneziaWebViewPrivate::get(self.view)->setTitle(QString::fromNSString(title)); + + QUrl url = QUrl(QString::fromNSString(urlString)); + QMetaObject::invokeMethod(AmneziaWebViewPrivate::get(self.view), "onPageFinished", Qt::QueuedConnection); + + //Scale UIWebView contents to view width + [webView stringByEvaluatingJavaScriptFromString: + [NSString stringWithFormat:@"var viewport = null;" + "var document_head = document.getElementsByTagName( 'head' )[0];" + "var child = document_head.firstChild;" + "while ( child ) {" + " if ( null == viewport && child.nodeType == 1 && child.nodeName == 'META' && child.getAttribute( 'name' ) == 'viewport' ) {" + " viewport = child;" + " var content = child.getAttribute( 'content' );" + " if (content != null) {" + " var regex = new RegExp('width');" //or maybe "var regex = new RegExp('width\\s*=\\s*device-width');" + " var found = regex.test(content);" + " if(!found) {" + " content = content + ',width=device-width';" + " viewport.setAttribute( 'content' , content );" + " }" + " }" + " else {" + " viewport.setAttribute( 'content' , 'width=device-width' );" + " }" + " }" + " child = child.nextSibling;" + "}" + "if (null == viewport) {" + " var meta = document.createElement( 'meta' );" + " meta.setAttribute( 'name' , 'viewport' );" + " meta.setAttribute( 'content' , 'width=device-width' );" + " document_head.appendChild( meta );" + "}" + ] + ]; +} + +- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType +{ + (void)navigationType; + (void)webView; + + NSURL *url = request.URL; + if ([url.scheme isEqualToString:@"tel"]) { + return YES; + } + + AmneziaWebViewPrivate *q = AmneziaWebViewPrivate::get(self.view); + NSString *baseUrlString = [((IosWebViewPrivate *)q)->_defaultUrl stringByRemovingPercentEncoding]; + NSString *urlString = [[request.URL absoluteString] stringByRemovingPercentEncoding]; + urlString = [urlString stringByReplacingOccurrencesOfString:baseUrlString withString:@""]; + + //QUrl url = QUrl(QString::fromNSString(urlString)); + q->setUrl(QString::fromNSString(urlString)); + return YES; +} + + +/* +bridge.js + var sendObjectMessage = function(parameters) { + var iframe = document.createElement('iframe'); + iframe.setAttribute('src', 'js:' + JSON.stringify(parameters)); + + document.documentElement.appendChild(iframe); + iframe.parentNode.removeChild(iframe); + iframe = null; + }; + + + sendObjectMessage({name: 'Bryan', company: 'Tumblr'}); + +*/ + +- (NSString *) onloadJavaScript +{ + return @"" +" window.onload = function() { " +" alert(\"Hello!\"); " +" } " +" window.onload = myFunction;"; + +} + +-(void)addScript:(NSString *)script ToWebView:(UIWebView *)webView{ + + NSString *execStr = [NSString stringWithFormat:@"var script = document.createElement('script');" + "script.type = 'text/javascript';" + "script.text = \"%@\";" + "document.getElementsByTagName('head')[0].appendChild(script);",script]; + + NSString * ret = [webView stringByEvaluatingJavaScriptFromString:execStr]; + + NSString *html = [webView stringByEvaluatingJavaScriptFromString: + @"document.getElementsByTagName('head')[0].innerHTML"]; + + NSLog(@"html:\n%@",html); +} + +-(void)addScriptSRC:(NSString *)scriptPath ToWebView:(UIWebView *)webView{ + + NSString *execStr = [NSString stringWithFormat:@"var script = document.createElement('script');" + "script.type = 'text/javascript';" + "script.src = \"%@\";" + "document.getElementsByTagName('head')[0].appendChild(script);",scriptPath]; + + [webView stringByEvaluatingJavaScriptFromString:execStr]; +} + +@end + +#endif + +/* +UIViewController *AmneziaWebViewPrivate::getViewController(UIView * view, UIViewController * controller) +{ + UIViewController *rootController = controller; + if(!rootController) { + rootController = [[view window] rootViewController]; + } + + if(view) { + NSArray *list = [rootController childViewControllers]; + for ( NSUInteger i = 0; i < [list count]; i++) { + UIViewController *c = [list objectAtIndex:i]; + if (c.view == view) { + return c; + } + if (c) { + c = getViewController(view, c); + if (c && c.view == view) { + return c; + } + } + } + } +} +*/ + +AmneziaWebViewPrivate *AmneziaWebViewPrivate::create(AmneziaWebView *q) +{ + return new IosWebViewPrivate(q); +} + + +//AmneziaWebView::AmneziaWebView(QObject *parent): QObject(parent), d_ptr(new IosWebViewPrivate(this)) +//{ +// setBackgroundColor(QColor::fromRgbF(0.5, 0.5, 0.5)); +//} +// +//QColor AmneziaWebView::backgroundColor() const +//{ +// Q_D(const AmneziaWebView); +// return d->backgroundColor; +//} + +//void *AmneziaWebView::nativeWebView() const +//{ +// Q_D(const AmneziaWebView); +// return d->nativeWebView(); +//} + + +void IosWebViewPrivate::setWindowParent(QWindow *parent) +{ + if (parent) { + + UIView *parentView = reinterpret_cast(parent->winId()); + [parentView addSubview: webView]; + } + else { + [webView removeFromSuperview]; + } +} + +void IosWebViewPrivate::setBackgroundColor(const QColor value) +{ + if (backgroundColor != value) { + + backgroundColor = value; + +#if defined(ENABLE_WKWEBVIEW) + WKWebView *webView = static_cast(this)->webView; +#else + UIWebView *webView = static_cast(this)->webView; +#endif + if (webView) { + [webView setBackgroundColor:[UIColor colorWithRed:backgroundColor.redF() green:backgroundColor.greenF() blue:backgroundColor. blueF() alpha:backgroundColor.alphaF()]]; + } + emit backgroundColorChanged(); + } +} + +IosWebViewPrivate::~IosWebViewPrivate() +{ + if (webView) { + +#if defined(ENABLE_WKWEBVIEW) + webView.navigationDelegate = nil; + webView.UIDelegate = nil; +#else + webView.delegate = nil; +#endif + } + if (webDelegate) { + webDelegate.view = NULL; + [webDelegate release]; + webDelegate = nil; + } + if (webView) { + [webView release]; + webView = nil; + } + if (_defaultUrl) { + [_defaultUrl release]; + _defaultUrl = nil; + } + +#if defined(ENABLE_WKWEBVIEW) + if (schemeHandler) { + schemeHandler.view = NULL; + [schemeHandler release]; + schemeHandler = nil; + } + + if (configuration) { + [configuration release]; + configuration = nil; + } +#endif +} + +IosWebViewPrivate::IosWebViewPrivate(AmneziaWebView* q): AmneziaWebViewPrivate(q) + , webDelegate(nil) +#if defined(ENABLE_WKWEBVIEW) + , schemeHandler(nil) + , configuration(nil) +#endif + , webView(nil) + , _defaultUrl(nil) +{ + NSArray *arr = [[NSFileManager defaultManager] URLsForDirectory: NSDocumentDirectory inDomains: NSUserDomainMask]; + webDelegate = [[WebDelegate alloc] init]; + CGRect frame = CGRectMake(0.0, 0.0, 400, 400); +#if defined(ENABLE_WKWEBVIEW) + + _defaultUrl = [[NSString alloc] initWithString:[[arr firstObject] absoluteString]]; + if ([_defaultUrl hasPrefix:@"file"]) { + NSString *local = [[NSString alloc] initWithFormat:@"local%@", [_defaultUrl substringFromIndex:4]]; + [_defaultUrl release]; + _defaultUrl = local; + } + + schemeHandler = [[WebSchemeHandler alloc] init]; + schemeHandler.view = q; + configuration = [[WKWebViewConfiguration alloc] init]; + + NSString *qrcSheme = qrcHandler.scheme().toNSString(); + if (![WKWebView handlesURLScheme:qrcSheme]) { + [configuration setURLSchemeHandler:schemeHandler forURLScheme:qrcSheme]; + } + + NSString *jsSheme = jsHandler.scheme().toNSString(); + if (![WKWebView handlesURLScheme:jsSheme]) { + [configuration setURLSchemeHandler:schemeHandler forURLScheme:jsSheme]; + } + + if (![WKWebView handlesURLScheme:@"local"]) { + //sinonimous fo file scheme + [configuration setURLSchemeHandler:schemeHandler forURLScheme:@"local"]; + } + + webView = [[WKWebView alloc] initWithFrame: frame configuration:configuration]; +#ifdef DEBUG + if (@available(iOS 16.4, *)) { + [webView setInspectable: YES]; + } +#endif + [webView setOpaque:NO]; + [webView.configuration setDataDetectorTypes: WKDataDetectorTypeLink | WKDataDetectorTypeAddress | WKDataDetectorTypeCalendarEvent]; + + [webView setNavigationDelegate:webDelegate]; + +#else + _defaultUrl = [[NSString alloc] initWithString:[[arr firstObject] absoluteString]]; + webView = [[UIWebView alloc] initWithFrame: frame]; + [webView setOpaque:NO]; + [webView setDataDetectorTypes: UIDataDetectorTypeLink | UIDataDetectorTypeAddress | UIDataDetectorTypeCalendarEvent]; + + [webView setDelegate:webDelegate]; +#endif + + webDelegate.view = q; + + [webView setHidden:TRUE]; +#if !defined(ENABLE_WKWEBVIEW) + [webView setScalesPageToFit:TRUE]; +#endif + setBackgroundColor(backgroundColor); +} + +void IosWebViewPrivate::show() +{ + if ( !visible) { + + if (webView && [webView superview]) { + + if ([webView isHidden]) + [webView setHidden:FALSE]; + + [[webView superview] setNeedsLayout]; + [[webView superview] setNeedsDisplay]; + [webView setNeedsLayout]; + [webView setNeedsDisplay]; + + [[[webView superview] layer] setNeedsDisplay]; + [[webView layer] setNeedsDisplay]; + } + visible = true; + } +} + +void IosWebViewPrivate::hide() +{ + Q_Q(AmneziaWebView); + if (visible) { + + if (webView && [webView superview]) { + + if (![webView isHidden]) { + [webView setHidden:TRUE]; + } + } + q->update(); + visible = false; + } +} + +void IosWebViewPrivate::setGeometry(const QRect &geometry) +{ + if (this->geometry != geometry) { + this->geometry = geometry; + CGRect frame = toCGRect(geometry); + + if (webView) { + + CGRect bounds = [webView bounds]; + bounds.size = frame.size; + [webView setFrame: frame]; + [webView setBounds:bounds]; + [webView setNeedsDisplay]; + } + } +} + +QString IosWebViewPrivate::innerHTML() const +{ +#if defined(ENABLE_WKWEBVIEW) + return _innerHTML; +#else + //NSString *htmlSource = [d->webView stringByEvaluatingJavaScriptFromString:@"document.body.innerHTML"]; + //NSString *htmlSource = [d->webView stringByEvaluatingJavaScriptFromString:@"document.documentElement.outerHTML"]; + + NSString *htmlSource = [webView stringByEvaluatingJavaScriptFromString: @"document.body.innerHTML"]; + QString innerHtml = QString::fromNSString(htmlSource); + return innerHtml; +#endif +} + +static inline QUrl ensureAbsoluteUrl(const QUrl &url) +{ + if (!url.isValid() || !url.isRelative()) + return url; + + // This contains the URL with absolute path but without. + // the query and the fragment part. + QUrl baseUrl = QUrl::fromLocalFile(QFileInfo(url.toLocalFile()).absoluteFilePath()); + + // The path is removed so the query and the fragment parts are there. + QString pathRemoved = url.toString(QUrl::RemovePath); + QUrl toResolve(pathRemoved); + return baseUrl.resolved(toResolve); +} + +void IosWebViewPrivate::load(const QUrl& url) +{ + QUrl effectiveUrl = url; + QString effectivePath = effectiveUrl.path(); + QString defaultPath = QUrl(QString::fromNSString(_defaultUrl)).path(); + if ((effectiveUrl.scheme() == "file") && (effectivePath.compare(defaultPath, Qt::CaseInsensitive) >= 0)) { + effectiveUrl.setScheme("local"); + } + NSURL *requestUrl = ensureAbsoluteUrl(effectiveUrl).toNSURL(); + [webView loadRequest:[NSURLRequest requestWithURL:requestUrl]]; +} + +void IosWebViewPrivate::setHtml(const QString& html, const QUrl& baseUrl) +{ + this->baseUrl = baseUrl; + + QRegularExpression comments("", QRegularExpression::MultilineOption | QRegularExpression::DotMatchesEverythingOption); + QString compressedHtml(html); + compressedHtml.remove(comments); + + NSURL *baseDataUrl = nil; + if (!baseUrl.isEmpty()) { + baseDataUrl = [NSURL URLWithString:[NSString stringWithUTF8String: baseUrl.toString().toUtf8()]]; + } + else { + baseDataUrl = [NSURL URLWithString: _defaultUrl]; + } + + NSData *htmlData = [compressedHtml.toNSString() dataUsingEncoding:NSUTF8StringEncoding]; +#if defined(ENABLE_WKWEBVIEW) + [webView loadData: htmlData MIMEType:@"text/html" characterEncodingName:@"utf-8" baseURL: baseDataUrl]; +#else + [webView loadData: htmlData MIMEType:@"text/html" textEncodingName:@"utf-8" baseURL: baseDataUrl]; +#endif +} + +void IosWebViewPrivate::setContent(const QByteArray& data, const QString& mimeType, const QUrl& baseUrl) +{ + this->baseUrl = baseUrl; + + NSData *contentData = [NSData dataWithBytes:data.data() length:data.length()]; + NSString *contentMimeType = mimeType.toNSString(); + + NSURL *baseContentUrl = nil; + if (!baseUrl.isEmpty()) { + baseContentUrl = [NSURL URLWithString:[NSString stringWithUTF8String: baseUrl.toString().toUtf8()]]; + } + else { + baseContentUrl = [NSURL URLWithString: _defaultUrl]; + } + + +#if defined(ENABLE_WKWEBVIEW) + [webView loadData: contentData MIMEType:contentMimeType characterEncodingName:@"utf-8" baseURL: baseContentUrl]; +#else + [webView loadData: contentData MIMEType:contentMimeType textEncodingName:@"utf-8" baseURL: baseContentUrl]; +#endif +} + +QSize IosWebViewPrivate::contentsSize() const +{ + return QSize(); +} + + +//QString AmneziaWebView::title() const +//{ +// Q_D(const WebView); +// return d->title; +//} + +//QUrl AmneziaWebView::url() const +//{ +// Q_D(const WebView); +// return d->url; +//} + +//WebSettings* AmneziaWebView::settings() const +//{ +// Q_D(const WebView); +// return d->settings; +//} + +void IosWebViewPrivate::evaluateJavaScript(const QString& scriptSource) +{ + NSString *script = scriptSource.toNSString(); +#if defined(ENABLE_WKWEBVIEW) + [webView evaluateJavaScript:script completionHandler:nil]; +#else + [webView stringByEvaluatingJavaScriptFromString:script]; +#endif +} + +bool IosWebViewPrivate::isLoading() const +{ + if( [webView isLoading] ) + return true; + return false; +} + +bool IosWebViewPrivate::canGoBack() const +{ + bool can = false; + if (webView) { + can = [webView canGoBack]; + } + return can; +} + +bool IosWebViewPrivate::canGoForward() const +{ + bool can = false; + if (webView) { + can = [webView canGoForward]; + } + return can; +} + +void IosWebViewPrivate::back() +{ + if(webView) { + [webView goBack]; + } +} + +void IosWebViewPrivate::forward() +{ + if (webView) { + [webView goForward]; + } +} + +void IosWebViewPrivate::reload() +{ + if (webView) { + [webView reload]; + } +} + +void IosWebViewPrivate::stop() +{ + if (webView) { + [webView stopLoading]; + } +} + + +void IosWebViewPrivate::setScale(qreal scale) +{ + [[webView scrollView] setZoomScale:scale]; +} + +void IosWebViewPrivate::setTextZoom(int percent) +{ + Q_UNUSED(percent) +} + +qreal IosWebViewPrivate::scale() const +{ + CGFloat scale = [[webView scrollView] zoomScale]; + return scale; +} + +void IosWebViewPrivate::setUrl(const QUrl &url) +{ + AmneziaWebViewPrivate::setUrl(url); +} + +QIcon IosWebViewPrivate::icon() const +{ + return QIcon(); +} + + +void IosWebViewPrivate::setDefaultFontSize(int size) +{ + NSString *fontSize = [[NSNumber numberWithInt: (int)size] stringValue]; +#if defined(ENABLE_WKWEBVIEW) + [webView evaluateJavaScript:[NSString stringWithFormat: @"document.body.style.fontSize = '%@px';", fontSize] completionHandler:nil]; +#else + [webView stringByEvaluatingJavaScriptFromString: [NSString stringWithFormat: @"document.body.style.fontSize = '%@px';", fontSize]]; +#endif +} +void IosWebViewPrivate::setStandardFontFamily(const QString &family) +{ + NSString *fontFamily = family.toNSString(); +#if defined(ENABLE_WKWEBVIEW) + [webView evaluateJavaScript:[NSString stringWithFormat: @"document.body.style.fontFamily = '%@';", fontFamily] completionHandler: nil]; +#else + [webView stringByEvaluatingJavaScriptFromString: [NSString stringWithFormat: @"document.body.style.fontFamily = '%@';", fontFamily]]; +#endif +} + +//void AmneziaWebView::setScalesPageToFit(bool enable) { } +//bool AmneziaWebView::scalesPageToFit() const { } + + + +//void AmneziaWebView::setPreferredContentsSize(const QSize size) +//{ +// //setGeometry(0, 0, size.width(), size.height()); +// +// /* +// if ((size.height() > 0 ) && (size.width() > 0 )) { +// id winId = (id)((void *) effectiveWinId()); +// if ([winId isKindOfClass: [UIView class]]) { +// UIView *view = (UIView*)winId; +// +// [view setFrame:CGRectMake(0,0,size.width(), size.height())]; +// //[d->webView setFrame:CGRectMake(0,0,size.width(), size.height())]; +// } +// } +// */ +//} +// +//void WebView::setResizesToContents(bool resizeToContent) { } +// +//int WebView::preferredWidth() +//{ +// +//} +// +//int WebView::preferredHeight() +//{ +// +//} + +//void WebView::setRenderingEnabled(bool enabled) { } + + +//QRect WebView::elementAreaAt(int x, int y, int maxWidth, int maxHeight) const { } + +//QSize WebView::preferredContentsSize() const { } + + + + + + +// Clear history +/* +[webView stringByEvaluatingJavaScriptFromString:[NSString stringWithFormat:@"if( window.history.length > 1 ) { window.history.go( -( window.history.length - 1 ) ) }; window.setTimeout( \"window.location.replace( '%@' )\", 300 );", targetLocation]]; +*/ + +// Font size change +/* +-(void)webViewDidFinishLoad:(UIWebView *)webView1{ + + int fontSize = 20; + NSString *jsString = [[NSString alloc] initWithFormat:@"document.getElementsByTagName('body')[0].style.webkitTextSizeAdjust= '%d%%'", fontSize]; + [webView1 stringByEvaluatingJavaScriptFromString:jsString]; + [jsString release]; + +} +*/ + + +/* + +extern UIViewController *UnityGetGLViewController(); // Root view controller of Unity screen. + +static UIWebView *webView; + +extern "C" void _WebViewPluginInstall() { + // Add the web view onto the root view (but don't show). + UIViewController *rootViewController = UnityGetGLViewController(); + webView = [[UIWebView alloc] initWithFrame:rootViewController.view.frame]; + webView.hidden = YES; + [rootViewController.view addSubview:webView]; +} + +extern "C" void _WebViewPluginMakeTransparentBackground() { + [webView setBackgroundColor:[UIColor clearColor]]; + [webView setOpaque:NO]; +} + +extern "C" void _WebViewPluginLoadUrl(const char* url, boolean isClearCache) { + if (isClearCache) { + [[NSURLCache sharedURLCache] removeAllCachedResponses]; + } + [webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:[NSString stringWithUTF8String:url]]]]; +} + +extern "C" void _WebViewPluginSetVisibility(bool visibility) { + webView.hidden = visibility ? NO : YES; +} + +extern "C" void _WebViewPluginSetMargins(int left, int top, int right, int bottom) { + UIViewController *rootViewController = UnityGetGLViewController(); + + CGRect frame = rootViewController.view.frame; + CGFloat scale = rootViewController.view.contentScaleFactor; + + CGRect screenBound = [[UIScreen mainScreen] bounds]; + CGSize screenSize = screenBound.size; + // Obtaining the current device orientation + UIDeviceOrientation orientation = [[UIDevice currentDevice] orientation]; + + // landscape + if (orientation) { + frame.size.width = screenSize.height - (left + right) / scale; + frame.size.height = screenSize.width - (top + bottom) / scale; + } else { // portrait + frame.size.width = screenSize.width - (left + right) / scale; + frame.size.height = screenSize.height - (top + bottom) / scale; + } + + frame.origin.x += left / scale; + frame.origin.y += top / scale; + + webView.frame = frame; +} + +extern "C" char *_WebViewPluginPollMessage() { + // Try to retrieve a message from the message queue in JavaScript context. + NSString *message = [webView stringByEvaluatingJavaScriptFromString:@"unityWebMediatorInstance.pollMessage()"]; + if (message && message.length > 0) { + NSLog(@"UnityWebViewPlugin: %@", message); + char* memory = static_cast(malloc(strlen(message.UTF8String) + 1)); + if (memory) strcpy(memory, message.UTF8String); + return memory; + } else { + return NULL; + } +} + +*/ + diff --git a/client/core/webview/amneziawebview_p.cpp b/client/core/webview/amneziawebview_p.cpp new file mode 100644 index 000000000..32dba0ece --- /dev/null +++ b/client/core/webview/amneziawebview_p.cpp @@ -0,0 +1,423 @@ +#include "amneziawebview_p.h" +#include "amneziawebhistory.h" +#include "websettings.h" + +NetworkAccessManager::NetworkAccessManager(QNetworkAccessManager *manager, QObject *parent) + : QNetworkAccessManager(parent) +{ + this->manager = manager; + setCache(manager->cache()); + setCookieJar(manager->cookieJar()); + setProxy(manager->proxy()); + setProxyFactory(manager->proxyFactory()); + + init(); +} + +void NetworkAccessManager::init() +{ + connect(this, SIGNAL(sslErrors(QNetworkReply*, const QList & )), this, + SLOT(handleSslErrors(QNetworkReply*, const QList & ))); +} + +QNetworkReply *NetworkAccessManager::createRequest(QNetworkAccessManager::Operation operation, const QNetworkRequest &request, QIODevice *device) +{ + AmneziaWebView *view = qobject_cast(parent()); + AmneziaWebViewPrivate *d = AmneziaWebViewPrivate::get(view); + + if (!d || !d->canHandleUrl(request.url())) { + return QNetworkAccessManager::createRequest(operation, request, device); + } + + if (operation == GetOperation) { + return new DataReply(this, operation, request, view); + } + else + + return QNetworkAccessManager::createRequest(operation, request, device); +} + +void NetworkAccessManager::handleSslErrors(QNetworkReply* reply, const QList &errors) +{ + Q_UNUSED(errors) + reply->ignoreSslErrors(); +} + +DataReply::DataReply(QObject *parent, const QNetworkAccessManager::Operation operation, const QNetworkRequest &request, AmneziaWebView *view): QNetworkReply(parent) +{ + setRequest(request); + setUrl(request.url()); + setOperation(operation); + setFinished(true); + this->view = view; + offset = 0; + + //QUrl url = request.url(); + //url.setHost(QString()); + //if (url.path().isEmpty()) + // url.setPath(QLatin1String("/")); + //setUrl(url); + + QMetaObject::invokeMethod(this, "setContent", Qt::QueuedConnection ); +} + +void DataReply::setContent() +{ + if (!view || view.isNull()) return; + + AmneziaWebViewPrivate *q = AmneziaWebViewPrivate::get(view); + content = q->dataForUrl(url()); + QString mimeType = q->mimeTypeForUrl(url()).toLower(); + //open(ReadOnly | Unbuffered); + QNetworkReply::open(QIODevice::ReadOnly); + + int size = content.size(); + if (size <= 0 ) { + + QString msg = QString("Error opening %1").arg(url().toString()); + qCritical() << msg; + setError(QNetworkReply::ContentNotFoundError, msg); + QMetaObject::invokeMethod(this, "error", Qt::QueuedConnection, + Q_ARG(QNetworkReply::NetworkError, QNetworkReply::ContentNotFoundError)); + + QMetaObject::invokeMethod(this, "finished", Qt::QueuedConnection); + return; + } + + setHeader(QNetworkRequest::ContentTypeHeader, QVariant(QString("%1; charset=%2").arg(mimeType, "utf-8"))); + + setHeader(QNetworkRequest::ContentLengthHeader, size); + QMetaObject::invokeMethod(this, "metaDataChanged", Qt::QueuedConnection); + QMetaObject::invokeMethod(this, "downloadProgress", Qt::QueuedConnection, + Q_ARG(qint64, size), Q_ARG(qint64, size)); + QMetaObject::invokeMethod(this, "readyRead", Qt::QueuedConnection); + QMetaObject::invokeMethod(this, "finished", Qt::QueuedConnection); +} + +void DataReply::abort() +{ + QNetworkReply::close(); +} + +void DataReply::close() +{ + QNetworkReply::close(); +} + +qint64 DataReply::size() const +{ + return content.size(); +} + +bool DataReply::isSequential() const +{ + return true; +} + +qint64 DataReply::bytesAvailable() const +{ + return content.size() - offset + QIODevice::bytesAvailable(); +} +qint64 DataReply::readData(char *data, qint64 maxSize) +{ + if (offset < content.size()) { + qint64 number = qMin(maxSize, content.size() - offset); + memcpy(data, content.constData() + offset, number); + offset += number; + return number; + } else { + return -1; + } +} + +AmneziaWebViewPrivate::AmneziaWebViewPrivate(AmneziaWebView *q) : QObject(q) + , pending(PendingNone) + , status(AmneziaWebView::Null) + , preferredwidth(0) + , preferredheight(0) + , progress(1.0) + , newWindowComponent(nullptr) + , newWindowParent(nullptr) + , jsHandler(q) + , rendering(true) + , overlapped(false) + , backgroundColor(QColor::fromRgb(28, 29, 33)) + , visible(false) + , networkManager(nullptr) + , q_ptr(q) + , m_history(new AmneziaWebHistory(q)) + , m_settings(new AmneziaWebViewSettings(q)) +{ +} + +void AmneziaWebViewPrivate::init() +{ + Q_Q(AmneziaWebView); + m_settings->apply(); + + actions.setMapping(new QAction(q), (int)AmneziaWebView::Back); + actions.setMapping(new QAction(q), (int)AmneziaWebView::Forward); + actions.setMapping(new QAction(q), (int)AmneziaWebView::Stop); + actions.setMapping(new QAction(q), (int)AmneziaWebView::Reload); + connect(action(AmneziaWebView::Back), SIGNAL(triggered()), &actions, SLOT(map())); + connect(action(AmneziaWebView::Forward), SIGNAL(triggered()), &actions, SLOT(map())); + connect(action(AmneziaWebView::Forward), SIGNAL(triggered()), &actions, SLOT(map())); + connect(action(AmneziaWebView::Forward), SIGNAL(triggered()), &actions, SLOT(map())); + connect(&actions, SIGNAL(mappedInt(int)), this, SLOT(onAction(int))); + connect(qApp, SIGNAL(applicationStateChanged(Qt::ApplicationState)), this, SLOT(applicationStateChanged(Qt::ApplicationState))); +} + +AmneziaWebViewPrivate::~AmneziaWebViewPrivate() +{ + Q_Q(AmneziaWebView); + disconnect(this, SLOT(onAction(int))); + disconnect(&actions, SLOT(map())); + if (networkManager && networkManager->parent() == q) + delete networkManager; +} + +AmneziaWebViewPrivate *AmneziaWebViewPrivate::get(AmneziaWebView *q) +{ + if (!q) { return nullptr; } + return q->d_func(); +} + +void AmneziaWebViewPrivate::applicationStateChanged(Qt::ApplicationState state) +{ + Q_Q(AmneziaWebView); + if ((state == Qt::ApplicationActive) && q->isVisible()) { + emit q->update(); + } + else { + hide(); + } +} + +void AmneziaWebViewPrivate::requestShow() +{ + show(); +} + +void AmneziaWebViewPrivate::requestHide() +{ + hide(); +} + +void AmneziaWebViewPrivate::move(const QPoint &point) +{ + QRect newGeomentry = geometry; + newGeomentry.moveTo(point); + setGeometry(newGeomentry); +} + +void AmneziaWebViewPrivate::windowObjectsClear(QQmlListProperty* prop) +{ + static_cast(prop->data)->windowObjects.clear(); +} + +QObject *AmneziaWebViewPrivate::windowObjectsAt(QQmlListProperty *prop, qsizetype index) +{ + return static_cast(prop->data)->windowObjects.at(index); +} + +qsizetype AmneziaWebViewPrivate::windowObjectsCount(QQmlListProperty *prop) +{ + return static_cast(prop->data)->windowObjects.count(); +} + +void AmneziaWebViewPrivate::windowObjectsAppend(QQmlListProperty* prop, QObject* o) +{ + static_cast(prop->data)->windowObjects.append(o); + static_cast(prop->data)->updateWindowObjects(); +} + +void AmneziaWebViewPrivate::setTitle(const QString &title) +{ + Q_Q(AmneziaWebView); + if (this->title != title) { + this->title = title; + emit q->titleChanged(this->title); + } +} + +QByteArray AmneziaWebViewPrivate::dataForUrl(const QUrl &url) const +{ + QByteArray data; + if (qrcHandler.canHandleUrl(url)) { + data = qrcHandler.dataForUrl(url); + } + else if (jsHandler.canHandleUrl(url)) { + + data = jsHandler.dataForUrl(url); + } + else if (fileHandler.canHandleUrl(url)) { + + data = fileHandler.dataForUrl(url); + } + return data; +} + +QString AmneziaWebViewPrivate::mimeTypeForUrl(const QUrl &url) const +{ + QString mimeType("application/octet-stream"); + if (qrcHandler.canHandleUrl(url)) { + mimeType = qrcHandler.mimeTypeForUrl(url); + } + else if (jsHandler.canHandleUrl(url)) { + mimeType = jsHandler.mimeTypeForUrl(url); + } + else if (fileHandler.canHandleUrl(url)) { + mimeType = fileHandler.mimeTypeForUrl(url); + } + return mimeType; +} + +void AmneziaWebViewPrivate::onPageStarted() +{ + Q_Q(AmneziaWebView); + emit q->loadStarted(); +} + +void AmneziaWebViewPrivate::onPageFinished() +{ + Q_Q(AmneziaWebView); + jsHandler.updateWebView(); + + if (lastError.length() > 0) { + emit q->loadFinished(false); + } + else { + emit q->loadFinished(true); + } +} + +void AmneziaWebViewPrivate::onPageError() +{ + +} + +void AmneziaWebViewPrivate::onUrlChanged(const QUrl &url) +{ + history()->append(url); + setUrl(url); +} + +void AmneziaWebViewPrivate::setUrl(const QUrl &url) +{ + Q_Q(AmneziaWebView); + + QString urlString = url.toString(); + while ( urlString.endsWith('#')) urlString.chop(1); + QUrl newUrl(urlString); + + if (newUrl == QUrl(QLatin1String("about:blank")) ) { + newUrl = QUrl(""); + } + + if (this->url != newUrl) { + + this->url = newUrl; + emit q->urlChanged(); + } +} + +void AmneziaWebViewPrivate::addToJavaScriptWindowObject(const QString& name, QObject* object) +{ + jsHandler.addToJavaScriptWindowObject(name, object); +} + + +bool AmneziaWebViewPrivate::canHandleUrl(const QUrl &url) const +{ + bool can = (jsHandler.canHandleUrl(url) || qrcHandler.canHandleUrl(url) || fileHandler.canHandleUrl(url)); + return can; +} + +QString AmneziaWebViewPrivate::toHtml() const +{ + return QString(); +} + +void AmneziaWebViewPrivate::load(const QNetworkRequest& request, QNetworkAccessManager::Operation operation, const QByteArray& body) +{ + Q_UNUSED(request) + Q_UNUSED(operation) + Q_UNUSED(body) +} + +QNetworkAccessManager* AmneziaWebViewPrivate::networkAccessManager() +{ + Q_Q(AmneziaWebView); + + if (!networkManager) + networkManager = new NetworkAccessManager(q); + return networkManager; +} + +void AmneziaWebViewPrivate::setNetworkAccessManager(QNetworkAccessManager* manager) +{ + Q_Q(AmneziaWebView); + if (manager == networkManager) + return; + if (networkManager && networkManager->parent() == q) + delete networkManager; + + NetworkAccessManager *newManager = qobject_cast(manager); + if (!newManager && manager) { + newManager = new NetworkAccessManager(manager, q); + } + networkManager = newManager; +} + +QAction *AmneziaWebViewPrivate::action(AmneziaWebView::WebAction action) const +{ + QAction *ret = qobject_cast(actions.mapping((int)action)); + if (ret) { + switch (action) { + case AmneziaWebView::Back: { + bool can = canGoBack() || history()->canGoBack(); + ret->setEnabled(can); + } + break; + case AmneziaWebView::Forward: { + bool can = canGoForward() || history()->canGoForward(); + ret->setEnabled(can); + } + break; + default: + break; + } + } + return ret; +} + +void AmneziaWebViewPrivate::onAction(int action) +{ + switch (action) { + case AmneziaWebView::Back: { + if (canGoBack()) { + back(); + } + //else { + // history()->back(); + //} + } + break; + case AmneziaWebView::Forward: + if (canGoForward()) forward(); + break; + case AmneziaWebView::Stop: + stop(); + break; + case AmneziaWebView::Reload: + reload(); + break; + default: + break; + } +} + +AmneziaWebHistory* AmneziaWebViewPrivate::history() const +{ + return m_history.data(); +} diff --git a/client/core/webview/amneziawebview_p.h b/client/core/webview/amneziawebview_p.h new file mode 100644 index 000000000..f63da131c --- /dev/null +++ b/client/core/webview/amneziawebview_p.h @@ -0,0 +1,187 @@ +#ifndef WEBVIEW_P_H +#define WEBVIEW_P_H + +#include "amneziawebview.h" +#include "qrchandler.h" +#include "jshandler.h" +#include "filehandler.h" + +#include "amneziawebhistory.h" + +class WebSettings; +class QIcon; +class QSize; + + +class WebViewSettings; +class WebSettings; + +class AmneziaWebViewPrivate : public QObject +{ + Q_OBJECT + Q_DECLARE_PUBLIC(AmneziaWebView) + +public: + explicit AmneziaWebViewPrivate(AmneziaWebView *q); + virtual ~AmneziaWebViewPrivate(); + static AmneziaWebViewPrivate *create(AmneziaWebView *q); + void init(); + + virtual void setWindowParent(QWindow *parent) = 0; + virtual void setBackgroundColor(const QColor backgroundColor) = 0; + + virtual void setGeometry(const QRect &) = 0; + virtual QString innerHTML() const = 0; + virtual void load(const QUrl& url) = 0; + virtual void setHtml(const QString& html, const QUrl& baseUrl = QUrl()) = 0; + virtual void setContent(const QByteArray& data, const QString& mimeType, const QUrl& baseUrl) = 0; + virtual void evaluateJavaScript(const QString& scriptSource) = 0; + virtual bool isLoading() const = 0; + virtual bool canGoBack() const = 0; + virtual bool canGoForward() const = 0; + virtual void back() = 0; + virtual void forward() = 0; + virtual void reload() = 0; + virtual void stop() = 0; + virtual QIcon icon() const = 0; + virtual void setScale(qreal scale) = 0; + virtual qreal scale() const = 0; + virtual QSize contentsSize() const = 0; + virtual void show() = 0; + virtual void hide() = 0; + virtual void setDefaultFontSize(int size) = 0; + virtual void setStandardFontFamily(const QString &family) = 0; + virtual void setTextZoom(int percent) = 0; + + virtual void setUrl(const QUrl &url); + + static AmneziaWebViewPrivate *get(AmneziaWebView *q); + + enum { PendingNone, PendingUrl, PendingHtml, PendingContent } pending; + + QString toHtml() const; + AmneziaWebHistory* history() const; + + QByteArray dataForUrl(const QUrl &url) const; + QString mimeTypeForUrl(const QUrl &url) const; + bool canHandleUrl(const QUrl &url) const; + + static void windowObjectsClear(QQmlListProperty *prop); + static QObject *windowObjectsAt(QQmlListProperty *prop, qsizetype index); + static qsizetype windowObjectsCount(QQmlListProperty *prop); + static void windowObjectsAppend(QQmlListProperty *prop, QObject *o); + + void updateWindowObjects(); + QObjectList windowObjects; + + QNetworkAccessManager* networkAccessManager(); + void setNetworkAccessManager(QNetworkAccessManager* manager); + void load(const QNetworkRequest& request, QNetworkAccessManager::Operation operation, const QByteArray& body); + QAction *action(AmneziaWebView::WebAction) const; + +public Q_SLOTS: + void setTitle(const QString &title); + void move(const QPoint &); + void requestHide(); + void requestShow(); + +Q_SIGNALS: + void loadStarted(); + void loadFinished(bool ok); + void loadProgress(int progress); + void titleChanged(const QString& title); + void urlChanged(const QUrl& url); + void backgroundColorChanged(); +public: + + QUrl url; + AmneziaWebView::Status status; + qreal preferredwidth, preferredheight; + qreal progress; + QString statusText; + QUrl pendingUrl; + QString pendingString; + QByteArray pendingData; + + QQmlComponent* newWindowComponent; + QQuickItem* newWindowParent; + + QrcHandler qrcHandler; + JsHandler jsHandler; + FileHandler fileHandler; + + bool rendering; + bool overlapped; + + QColor backgroundColor; + QUrl baseUrl; + QString title; + QRect geometry; + QString lastError; + mutable bool visible; + +protected Q_SLOTS: + void applicationStateChanged(Qt::ApplicationState state); + void onPageStarted(); + void onPageFinished(); + void onPageError(); + void onUrlChanged(const QUrl &url); + void onAction(int); + +protected: + + void addToJavaScriptWindowObject(const QString& name, QObject* object); + + QMutex renderMutex; + QNetworkAccessManager *networkManager; + AmneziaWebView *q_ptr; + +private: + QSignalMapper actions; + QScopedPointer m_history; + QScopedPointer m_settings; +}; + +//!internal +class NetworkAccessManager : public QNetworkAccessManager +{ + Q_OBJECT +public: + explicit NetworkAccessManager(QNetworkAccessManager *manager, QObject *parent); + explicit NetworkAccessManager(QObject *parent): QNetworkAccessManager(parent) { init(); } + +public Q_SLOTS: + void handleSslErrors(QNetworkReply* reply, const QList &errors); + +protected: + virtual QNetworkReply *createRequest(Operation op, const QNetworkRequest &request, QIODevice *outgoingData = nullptr); +private: + void init(); + QNetworkAccessManager *manager; +}; + +class DataReply : public QNetworkReply +{ + Q_OBJECT + +public: + explicit DataReply(QObject *parent, const QNetworkAccessManager::Operation operation, const QNetworkRequest &request, AmneziaWebView *view = nullptr); + virtual ~DataReply() = default; + + virtual void abort(); + virtual void close(); + virtual qint64 size() const; + + virtual qint64 bytesAvailable() const; + virtual bool isSequential() const; +protected: + virtual qint64 readData(char *data, qint64 maxSize); + Q_INVOKABLE void setContent(); + +private: + QByteArray content; + qint64 offset; + QPointer view; +}; + +#endif diff --git a/client/core/webview/amneziawebview_webengine.cpp b/client/core/webview/amneziawebview_webengine.cpp new file mode 100644 index 000000000..78cafc45b --- /dev/null +++ b/client/core/webview/amneziawebview_webengine.cpp @@ -0,0 +1,494 @@ +#include +#include + +#include "amneziawebview_webengine_p.h" +#include "qrchandler.h" +#include "filehandler.h" +#include "amneziawebhistory.h" + +typedef QMap WebViews; +Q_GLOBAL_STATIC_WITH_ARGS(WebViews, g_webViews, ()) + +QrcHandler::QrcHandler() +{} + +FileHandler::FileHandler() +{ +} + +JsHandler::JsHandler(AmneziaWebView *host): _host(host), scriptObjectsInjected(false) +{ + init(); +} + +JsHandler::~JsHandler() {} + +WebPage::WebPage(QObject *parent) + : QWebEnginePage(parent) +{ + +} + +WebPage::WebPage(QWebEngineProfile *profile, QObject *parent) + : QWebEnginePage(profile, parent) +{ + +} + +WebPage::~WebPage() +{ + disconnect(this); +} + +bool WebPage::acceptNavigationRequest(const QUrl &url, QWebEnginePage::NavigationType type, bool isMainFrame) +{ + Q_UNUSED(type); + // Always accept navigation requests to open links within WebView + // This prevents opening links in external browser + if (isMainFrame) { + m_loadingUrl = url; + emit loadingUrl(url); + } + return true; +} + +QWebEnginePage *WebPage::createWindow(QWebEnginePage::WebWindowType type) +{ + Q_UNUSED(type); + // Return this page instead of creating a new window + // This prevents opening new browser windows/tabs + return this; +} + +void WebPage::handleUnsupportedContent(QNetworkReply *reply) +{ + QString errorString = reply->errorString(); + + if (m_loadingUrl != reply->url()) { + // sub resource of this page + qWarning() << "Resource" << reply->url().toEncoded() << "has unknown Content-Type, will be ignored."; + reply->deleteLater(); + return; + } + + if (reply->error() == QNetworkReply::NoError && !reply->header(QNetworkRequest::ContentTypeHeader).isValid()) { + errorString = "Unknown Content-Type"; + } + + QFile file(QLatin1String(":/notfound.html")); + bool isOpened = file.open(QIODevice::ReadOnly); + Q_ASSERT(isOpened); + Q_UNUSED(isOpened) + + QString title = QCoreApplication::translate("webview", "Error loading page: %1").arg(reply->url().toString()); + QString html = QString(QLatin1String(file.readAll())) + .arg(title) + .arg(errorString) + .arg(reply->url().toString()); + + QBuffer imageBuffer; + imageBuffer.open(QBuffer::ReadWrite); + QIcon icon = qApp->style()->standardIcon(QStyle::SP_MessageBoxWarning, 0); + QPixmap pixmap = icon.pixmap(QSize(32,32)); + if (pixmap.save(&imageBuffer, "PNG")) { + html.replace(QLatin1String("IMAGE_BINARY_DATA_HERE"), + QString(QLatin1String(imageBuffer.buffer().toBase64()))); + } + + if (m_loadingUrl == reply->url()) { + setHtml(html, reply->url()); + } +} + +const QString &LocalSchemeHandler::scheme() +{ + static const QString localScheme("local"); + return localScheme; +} + +const QMimeDatabase &LocalSchemeHandler::mimeDatabase() +{ + static const QMimeDatabase mimeDatabase; + return mimeDatabase; +} + +void LocalSchemeHandler::requestStarted(QWebEngineUrlRequestJob *job) +{ + const QByteArray requestMethod = job->requestMethod(); + const QUrl requestUrl = job->requestUrl(); + QString requestScheme = requestUrl.scheme(); + DesktopWebViewPrivate *d = qobject_cast(parent()); + + if (!d) { + job->fail(QWebEngineUrlRequestJob::RequestFailed); + return; + } + + if (requestScheme != LocalSchemeHandler::scheme()) { + job->fail(QWebEngineUrlRequestJob::UrlInvalid); + return; + } + + QBuffer *buffer = nullptr; + QMimeType mimeType = mimeDatabase().mimeTypeForFile(requestUrl.fileName(), QMimeDatabase::MatchExtension); + + if (d->fileHandler.canHandleUrl(requestUrl)) { + const QByteArray content = d->fileHandler.dataForUrl(requestUrl); + if (!content.isNull()) { + buffer = new QBuffer(); + buffer->setData(content); + } + } + if (!buffer) { + + auto historyItems = d->history()->items(); + const auto it = std::find_if(historyItems.begin(), historyItems.end(), + [requestUrl](const auto &item) { + bool urlsMatch = item.url().matches(requestUrl, QUrl::RemoveQuery | QUrl::RemoveFragment); + bool hasData = (item.data().length() > 0); + return urlsMatch && hasData; }); + + if (it != historyItems.end()) { + buffer = new QBuffer(); + buffer->setData(it->data()); + mimeType = mimeDatabase().mimeTypeForName(it->mimeType()); + } + } + + if (!buffer) { + job->fail(QWebEngineUrlRequestJob::UrlNotFound); + return; + } + + connect(job, &QObject::destroyed, buffer, &QObject::deleteLater); + job->reply(mimeType.name().toLocal8Bit(), buffer); +} + +DesktopWebViewPrivate::DesktopWebViewPrivate(AmneziaWebView* q): AmneziaWebViewPrivate(q) + , viewId(reinterpret_cast(this)) + , containerWindow(0) + , window(0) +{ + m_localHandler = new LocalSchemeHandler(this); + + container = new QWidget(0, Qt::FramelessWindowHint | Qt::NoDropShadowWindowHint | Qt::Tool); + container->setAttribute(Qt::WA_NativeWindow, true); + container->setAttribute(Qt::WA_DontCreateNativeAncestors, true); + + QWebEngineUrlScheme localScheme(LocalSchemeHandler::scheme().toUtf8()); + localScheme.setFlags(QWebEngineUrlScheme::LocalAccessAllowed | + QWebEngineUrlScheme::SecureScheme | + QWebEngineUrlScheme::ViewSourceAllowed | + QWebEngineUrlScheme::ContentSecurityPolicyIgnored | + QWebEngineUrlScheme::CorsEnabled | + QWebEngineUrlScheme::FetchApiAllowed); + QWebEngineUrlScheme::registerScheme(localScheme); + + QWebEngineUrlScheme qrcScheme("qrc"); + qrcScheme.setFlags(QWebEngineUrlScheme::LocalScheme | + QWebEngineUrlScheme::LocalAccessAllowed | + QWebEngineUrlScheme::SecureScheme | + QWebEngineUrlScheme::ContentSecurityPolicyIgnored | + QWebEngineUrlScheme::CorsEnabled | + QWebEngineUrlScheme::FetchApiAllowed); + QWebEngineUrlScheme::registerScheme(qrcScheme); + + view = new QWebEngineView(container); + QWebEngineProfile *profile = new QWebEngineProfile(view); + profile->installUrlSchemeHandler(LocalSchemeHandler::scheme().toUtf8(), m_localHandler); + + m_page = new WebPage(profile, profile); + m_page->settings()->setAttribute(QWebEngineSettings::PluginsEnabled, true); + connect(m_page, &QWebEnginePage::fileSystemAccessRequested, this, [](QWebEngineFileSystemAccessRequest request) { + request.accept(); + }); + + view->settings()->setUnknownUrlSchemePolicy(QWebEngineSettings::AllowAllUnknownUrlSchemes); + view->settings()->setAttribute(QWebEngineSettings::LocalContentCanAccessRemoteUrls, true); + view->settings()->setAttribute(QWebEngineSettings::LocalContentCanAccessFileUrls, true); + view->settings()->setAttribute(QWebEngineSettings::AllowRunningInsecureContent, true); + view->settings()->setAttribute(QWebEngineSettings::AllowGeolocationOnInsecureOrigins, true); + + view->setPage(m_page); + container->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + container->setLayout(new QHBoxLayout(container)); + container->layout()->setSpacing(0); + container->layout()->setContentsMargins(0, 0, 0, 0); + container->layout()->addWidget(view); + + setBackgroundColor(backgroundColor); + g_webViews->insert(viewId, this); + + connect(view, SIGNAL(loadFinished(bool)), this, SIGNAL(loadFinished(bool))); + connect(m_page, SIGNAL(loadFinished(bool)), this, SLOT(onLoadFinished(bool)), Qt::QueuedConnection); + connect(m_page, SIGNAL(loadStarted()), this, SLOT(onPageStarted()), Qt::QueuedConnection); + connect(view, SIGNAL(urlChanged(const QUrl &)), this, SLOT(onUrlChanged(const QUrl &)), Qt::QueuedConnection); + container->createWinId(); +} + +DesktopWebViewPrivate::~DesktopWebViewPrivate() +{ + g_webViews->take(viewId); + disconnect(this, SLOT(loadFinished(bool))); + disconnect(this, SLOT(applicationStateChanged(Qt::ApplicationState))); + disconnect(this, SLOT(onUrlChanged(const QUrl &))); + disconnect(this, SLOT(onPageStarted())); + disconnect(this, SLOT(onLoadFinished(bool))); + + delete container; +} + +AmneziaWebViewPrivate *AmneziaWebViewPrivate::create(AmneziaWebView *q) +{ + return new DesktopWebViewPrivate(q); +} + +void DesktopWebViewPrivate::setWindowParent(QWindow *parent) +{ + if (window) { + window->removeEventFilter(this); + } + + if (parent) { + + containerWindow = qobject_cast(container->windowHandle()); + containerWindow->setTransientParent(parent); + parent->installEventFilter(this); + + } + window = parent; +} + +void DesktopWebViewPrivate::setBackgroundColor(const QColor backgroundColor) +{ + this->backgroundColor = backgroundColor; + QPalette p = container->palette(); + p.setColor(QPalette::Window, backgroundColor); + container->setPalette(p); + p = view->palette(); + p.setColor(QPalette::Window, backgroundColor); + view->setPalette(p); + emit backgroundColorChanged(); +} + +/// Deprecated +void DesktopWebViewPrivate::setScale(qreal scale) +{ + Q_UNUSED(scale); +} + +/// Deprecated +qreal DesktopWebViewPrivate::scale() const +{ + return 1; +} + +QIcon DesktopWebViewPrivate::icon() const +{ + return QIcon(); +} + +QSize DesktopWebViewPrivate::contentsSize() const +{ + return QSize(); +} + +void DesktopWebViewPrivate::setGeometry(const QRect &geometry) +{ + Q_Q(AmneziaWebView); + QQuickWindow *window = q->window(); + if (!window) return; + QRect newGeometry = QRect(window->mapToGlobal(geometry.topLeft()), QSize(geometry.width(), geometry.height())); + if (newGeometry.isValid() && container->geometry() != newGeometry ) { + + this->geometry = geometry; + container->setGeometry(newGeometry); + container->updateGeometry(); + } +} + +bool DesktopWebViewPrivate::eventFilter(QObject *obj, QEvent *event) +{ + Q_UNUSED(obj); + Q_Q(AmneziaWebView); + + switch (event->type()) { + case QEvent::Move: { + QMoveEvent *moveEvent = static_cast(event); + QPoint p = q->mapToScene(QPointF(moveEvent->pos())).toPoint(); + container->move(p); + + if (visible) { + show(); + } + return false; + } + break; + case QEvent::Resize: { + if (visible) { + show(); + } + return false; + } + break; + case QEvent::WindowStateChange: { + + Qt::WindowState state = window->windowState(); + if ((state == Qt::WindowMaximized) || (state == Qt::WindowFullScreen) || (state == Qt::WindowActive)) { + show(); + } + else { + hide(); + } + return false; + } + default: { + return false; + } + } + return false; +} + +void DesktopWebViewPrivate::hide() +{ + Q_Q(AmneziaWebView); + if (!q->window()) return; + + if (visible) { + QMetaObject::invokeMethod(container, "hide", Qt::QueuedConnection); + visible = false; + } +} + +void DesktopWebViewPrivate::show() +{ + Q_Q(AmneziaWebView); + if (!q->window()) return; + if (!visible) { + + QMetaObject::invokeMethod(container, "show", Qt::QueuedConnection); + QMetaObject::invokeMethod(container, "update", Qt::QueuedConnection); + visible = true; + } + + containerWindow = qobject_cast(container->windowHandle()); + + if ((containerWindow != nullptr) && + ((qApp->topLevelWindows().at(0) != containerWindow) || !containerWindow->isVisible())) { + + containerWindow->raise(); + } +} + +void DesktopWebViewPrivate::load(const QUrl& baseUrl) +{ + QUrl url = baseUrl; + if (!url.isValid()) { + url = QUrl(QLatin1String("about:blank")); + } + view->load(url); + history()->append(baseUrl); +} + +void DesktopWebViewPrivate::setContent(const QByteArray& data, const QString& mimeType, const QUrl& baseUrl) +{ + QUrl url = baseUrl; + if (!url.isValid()) { + url = QUrl(QLatin1String("about:blank")); + } + view->setContent(data, mimeType, url); + history()->append(url, data, mimeType); +} + +void DesktopWebViewPrivate::setHtml(const QString& html, const QUrl& baseUrl) +{ + if(html.isNull()) return; + QUrl url = baseUrl; + if (!baseUrl.isValid()) + url = QUrl(QLatin1String("about:blank")); + + view->setHtml(html, url); + history()->append(url, html.toUtf8(), "text/html"); +} + +void DesktopWebViewPrivate::evaluateJavaScript(const QString& scriptSource) +{ + view->page()->runJavaScript(scriptSource); +} + +bool DesktopWebViewPrivate::canGoBack() const +{ + QAction *pageAction = view->pageAction(QWebEnginePage::Back); + bool can = (pageAction && pageAction->isEnabled()); + return can; +} + +bool DesktopWebViewPrivate::canGoForward() const +{ + QAction *pageAction = view->pageAction(QWebEnginePage::Forward); + bool can = (pageAction && pageAction->isEnabled()); + return can; +} + +void DesktopWebViewPrivate::back() +{ + QAction *pageAction = view->pageAction(QWebEnginePage::Back); + if (pageAction) + emit pageAction->trigger(); +} + +void DesktopWebViewPrivate::forward() +{ + QAction *pageAction = view->pageAction(QWebEnginePage::Forward); + if (pageAction) + emit pageAction->trigger(); +} + +void DesktopWebViewPrivate::reload() +{ + QAction *pageAction = view->pageAction(QWebEnginePage::Reload); + if (pageAction) + emit pageAction->trigger(); +} + +void DesktopWebViewPrivate::stop() +{ + QAction *pageAction = view->pageAction(QWebEnginePage::Stop); + if (pageAction) + emit pageAction->trigger(); +} + +bool DesktopWebViewPrivate::isLoading() const +{ + return false; +} + +QString DesktopWebViewPrivate::innerHTML() const +{ + return QString(); +} + +void DesktopWebViewPrivate::onLoadFinished(bool success) +{ + if (success) { + QMetaObject::invokeMethod(this, "onPageFinished", Qt::QueuedConnection); + } + else { + QMetaObject::invokeMethod(this, "onPageError", Qt::QueuedConnection); + } +} + +void DesktopWebViewPrivate::setDefaultFontSize(int size) +{ + view->settings()->setFontSize(QWebEngineSettings::DefaultFontSize, size); +} + +void DesktopWebViewPrivate::setStandardFontFamily(const QString &family) +{ + view->settings()->setFontFamily(QWebEngineSettings::StandardFont, family); +} + +void DesktopWebViewPrivate::setTextZoom(int percent) +{ + Q_UNUSED(percent); +} diff --git a/client/core/webview/amneziawebview_webengine_p.h b/client/core/webview/amneziawebview_webengine_p.h new file mode 100644 index 000000000..37922cbda --- /dev/null +++ b/client/core/webview/amneziawebview_webengine_p.h @@ -0,0 +1,109 @@ +#ifndef AMNEZIAWEBVIEW_WEBENGINE_P_H +#define AMNEZIAWEBVIEW_WEBENGINE_P_H + +#include + +#include +#include +#include +#include +#include + +#include "amneziawebview.h" +#include "amneziawebview_p.h" + +class DesktopWebViewPrivate; +class LocalSchemeHandler; + +class WebPage : public QWebEnginePage +{ + Q_OBJECT + +signals: + void loadingUrl(const QUrl &url); + +public: + explicit WebPage(QObject *parent = 0); + explicit WebPage(QWebEngineProfile *profile, QObject *parent = 0); + virtual ~WebPage(); + +protected: + bool acceptNavigationRequest(const QUrl &url, QWebEnginePage::NavigationType type, bool isMainFrame) override; + QWebEnginePage *createWindow(QWebEnginePage::WebWindowType type) override; + +private slots: + void handleUnsupportedContent(QNetworkReply *reply); + +private: + + friend class DesktopWebViewPrivate; + QUrl m_loadingUrl; +}; + +class DesktopWebViewPrivate : public AmneziaWebViewPrivate +{ + Q_OBJECT + Q_DECLARE_PUBLIC(AmneziaWebView) +public: + + explicit DesktopWebViewPrivate(AmneziaWebView* q); + virtual ~DesktopWebViewPrivate(); + + virtual void setWindowParent(QWindow *parent); + + virtual void setBackgroundColor(const QColor backgroundColor); + virtual void show(); + virtual void hide(); + virtual void setGeometry(const QRect &); + virtual QString innerHTML() const; + virtual void load(const QUrl& url); + virtual void setHtml(const QString& html, const QUrl& baseUrl = QUrl()); + virtual void setContent(const QByteArray& data, const QString& mimeType, const QUrl& baseUrl); + virtual void evaluateJavaScript(const QString& scriptSource); + virtual bool isLoading() const; + virtual bool canGoBack() const; + virtual bool canGoForward() const; + virtual void back(); + virtual void forward(); + virtual void reload(); + virtual void stop(); + + virtual QIcon icon() const; + virtual void setScale(qreal scale); + virtual qreal scale() const; + virtual QSize contentsSize() const; + virtual void setDefaultFontSize(int size); + virtual void setTextZoom(int percent); + virtual void setStandardFontFamily(const QString &family); + +protected: + bool eventFilter(QObject *obj, QEvent *event); + +private slots: + void onLoadFinished(bool); + +private: + quintptr viewId; + QWebEngineView *view; + QWidget *container; + QWindow *containerWindow; + QWindow *window; + WebPage *m_page; + LocalSchemeHandler *m_localHandler; +}; + +class LocalSchemeHandler : public QWebEngineUrlSchemeHandler +{ + Q_OBJECT +public: + LocalSchemeHandler(QObject *parent) : QWebEngineUrlSchemeHandler(parent) + { + Q_UNUSED(parent); + } + + static const QString &scheme(); + static const QMimeDatabase &mimeDatabase(); + void requestStarted(QWebEngineUrlRequestJob *job) override; +}; + +#endif // AMNEZIAWEBVIEW_WEBENGINE_P_H diff --git a/client/core/webview/filehandler.cpp b/client/core/webview/filehandler.cpp new file mode 100644 index 000000000..09f39cb92 --- /dev/null +++ b/client/core/webview/filehandler.cpp @@ -0,0 +1,43 @@ +#include +#include +#include +#include + +#include "filehandler.h" +#include "mimecache.h" + +QList FileHandler::schemes() +{ + static QList list = QList() << "file" << "local"; + return list; +} + +QByteArray FileHandler::dataForUrl(const QUrl &url) const +{ + QUrl fileUrl = url; + if (fileUrl.scheme() != "file") { + fileUrl.setScheme("file"); + } + QString requestUrl(fileUrl.toLocalFile()); + QFile resource(requestUrl); + QByteArray buffer; + if (resource.exists() && resource.open(QIODevice::ReadOnly)) { + + buffer = resource.readAll(); + resource.close(); + } + return buffer; +} + +bool FileHandler::canHandleUrl(const QUrl &url) const +{ + if (schemes().contains(url.scheme().toLower())) + return true; + return false; +} + + +QString FileHandler::mimeTypeForUrl(const QUrl &url) const +{ + return mimeTypeForExtension(url.path().section('.', -1)); +} diff --git a/client/core/webview/filehandler.h b/client/core/webview/filehandler.h new file mode 100644 index 000000000..a9878c6de --- /dev/null +++ b/client/core/webview/filehandler.h @@ -0,0 +1,18 @@ +#ifndef FILEHANDLER_H +#define FILEHANDLER_H + +class QString; +class QByteArray; +class QUrl; + +class FileHandler +{ +public: + explicit FileHandler(); + static QList schemes(); + bool canHandleUrl(const QUrl &url) const; + QByteArray dataForUrl(const QUrl &url) const; + QString mimeTypeForUrl(const QUrl &url) const; +}; + +#endif diff --git a/client/core/webview/filehandler_android.cpp b/client/core/webview/filehandler_android.cpp new file mode 100644 index 000000000..79ddcbcbc --- /dev/null +++ b/client/core/webview/filehandler_android.cpp @@ -0,0 +1,10 @@ +#include +#include +#include +#include + +#include "filehandler.h" + +FileHandler::FileHandler() +{ +} diff --git a/client/core/webview/filehandler_ios.mm b/client/core/webview/filehandler_ios.mm new file mode 100644 index 000000000..a1c056003 --- /dev/null +++ b/client/core/webview/filehandler_ios.mm @@ -0,0 +1,135 @@ +#import +#import +#import + +#include +#include +#include +#include "mimecache.h" +#include "filehandler.h" +#define kProtocolFileScheme @"file" + +#if !defined(ENABLE_WKWEBVIEW) + +@interface FileProtocol : NSURLProtocol +@end + +#endif + +FileHandler::FileHandler() +{ +#if !defined(ENABLE_WKWEBVIEW) + static bool protocolRegistered = false; + if (!protocolRegistered) { + [NSURLProtocol registerClass:[FileProtocol class]]; + protocolRegistered = true; + } +#endif +} + +#if !defined(ENABLE_WKWEBVIEW) + +@implementation FileProtocol + ++ (BOOL)canInitWithRequest:(NSURLRequest *)request +{ + NSString *scheme = request.URL.scheme; + if ([kProtocolFileScheme caseInsensitiveCompare:scheme] == NSOrderedSame) { + return YES; + } + + return NO; +} + ++ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request +{ + return request; +} + ++ (NSString*)headFromHtml:(NSString*)html +{ + if (!html) return nil; + + NSString *head = nil; + NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"(?<=)[\\w\\W.]*(?=)" options:NSRegularExpressionCaseInsensitive error:nil]; + NSTextCheckingResult *headResult = [regex firstMatchInString:html options:0 range:NSMakeRange(0, html.length)]; + + if (headResult && headResult.range.location != NSNotFound) { + head = [html substringWithRange:[headResult range]]; + } + return head; +} + ++ (NSString *)charsetFromHtml:(NSString *)html +{ + if (!html) return nil; + + NSString *charset = nil; + NSString *charsetPattern = @"((?<=charset=)\\s*[a-zA-Z0-9-]*)"; + NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:charsetPattern options: NSRegularExpressionCaseInsensitive error:nil]; + NSTextCheckingResult *charsetResult = [regex firstMatchInString:html options:kNilOptions range:NSMakeRange(0, [html length])]; + if (charsetResult && charsetResult.range.location != NSNotFound) { + charset = [[html substringWithRange:[charsetResult range]] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; + charset = [charset lowercaseString]; + } + return charset; +} + +- (void)startLoading +{ + /* retrieve the current request. */ + NSURLRequest *request = [self request]; + NSString *url = [[request URL] path]; + NSString *mimeType = mimeTypeForExtension(QString::fromNSString(url.pathExtension)).toNSString(); + + QString requestUrl(QString("%1").arg(QString::fromNSString(url))); + QFile resource(requestUrl); + + NSData *pageData = nil; + if (resource.exists() && resource.open(QIODevice::ReadOnly)) { + + QByteArray buffer = resource.readAll(); + + //pageData = [[NSData alloc] initWithBytes:buffer.constData() length:buffer.size()]; + pageData = [NSData dataWithBytes:buffer.constData() length:buffer.size()]; + resource.close(); + } + + if (pageData) { + + NSString *encoding = @"utf-8"; + if ([mimeType isEqualToString:@"text/html"]) { + + NSString *content = [[NSString alloc] initWithBytesNoCopy: (char* )[pageData bytes] length:pageData.length encoding:NSISOLatin1StringEncoding freeWhenDone:NO]; + + NSString *charset = [FileProtocol charsetFromHtml:[FileProtocol headFromHtml:content]]; + if (charset) { + encoding = charset; + } + [content autorelease]; + } + + NSURLResponse *response =[[NSURLResponse alloc]initWithURL:self.request.URL + MIMEType:mimeType + expectedContentLength:[pageData length] + textEncodingName:encoding]; + + + [[self client] URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed]; + [[self client] URLProtocol:self didLoadData:pageData]; + [[self client] URLProtocolDidFinishLoading:self]; + [response autorelease]; + } + else { + [[self client] URLProtocol:self didFailWithError:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:nil]]; + } +} + +- (void)stopLoading +{ + +} + +@end + +#endif diff --git a/client/core/webview/jshandler.cpp b/client/core/webview/jshandler.cpp new file mode 100644 index 000000000..3244207f0 --- /dev/null +++ b/client/core/webview/jshandler.cpp @@ -0,0 +1,432 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "amneziawebview_p.h" +#include "mimecache.h" +#include "jshandler.h" + +QString JsHandler::scheme() const +{ + return QString("js"); +} + +QString JsHandler::host() const +{ + qulonglong h = (qulonglong)(void*)_host; + return QString("js%1").arg(h); +} + +QString JsHandler::scriptObjectsUrl() const +{ + return QString("%1://%2/%3.js").arg(scheme()).arg(host()).arg(scriptObjectsId()); +} + +QString JsHandler::scriptObjectsId() const +{ + return QString("__scriptObjects__"); +} + +QString JsHandler::scriptObjects() const +{ + QString script; + QList scripts = scriptParts.values(); + + for ( int i = 0; i < scripts.count(); i++) { + script = QString("%1 %2").arg(script).arg(scripts.at(i)).trimmed(); + } + return script; +} + +void JsHandler::updateWebView() +{ + if (!scriptObjectsInjected) { + + QString injector = QString( + " var script = document.getElementById(\"%1\"); " + " if(script === null) { " + " script = document.createElement('script'); " + " script.type = \"text/javascript\"; " + " script.id = \"%2\"; " + " document.getElementsByTagName('head')[0].appendChild(script); " + " } " + " script.text = \"%3\"; " + ).arg(scriptObjectsId()).arg(scriptObjectsId()).arg(scriptObjects()); + + + + /* + QString injector = QString( + " var script = document.getElementById(\"%1\"); " + " if(script === null) { " + " script = document.createElement('script'); " + " script.type = \"text/javascript\"; " + " script.id = \"%2\"; " + " document.getElementsByTagName('head')[0].appendChild(script); " + " } " + " script.src = \"%3\"; " + ).arg(scriptObjectsId()).arg(scriptObjectsId()).arg(scriptObjectsUrl()); + + */ + + //убрали, так как теперь есть рабочий вебинспектор + //_host->evaluateJavaScript(injector); + scriptObjectsInjected = true; + } +} + +void JsHandler::addToJavaScriptWindowObject(const QString& name, QObject* object) +{ + windowObjects[name] = object; + QString script = windowScriptObject(name, object); + scriptParts[name] = script; +} + +bool JsHandler::canHandleUrl(const QUrl &url) const +{ + if (scheme() == url.scheme() && url.host() == host()) + return true; + + if (this->host() == url.host() && (url.scheme() == QString("http"))) + return true; + + return false; +} + +QString JsHandler::mimeTypeForUrl(const QUrl &url) const +{ + if (scheme() == url.scheme() && url.host() == host()) + return mimeTypeForUrl(url); + + if (this->host() == url.host() && (url.scheme() == QString("http"))) + return mimeTypeForExtension("json"); + + return QString("application/octet-stream"); +} + +QString JsHandler::windowScriptObject(const QString& name, QObject* object) const +{ + QString script = QString(" var %1 = {}; " + " %2.responseText = null; " + ).arg(name).arg(name); + if (object) { + const QMetaObject *meta = object->metaObject(); + const QString prefix = QString("%1.").arg(name); + + int methodCount = meta->methodCount(); + for (int i = 0; i < methodCount; i++) { + QMetaMethod metaMethod = meta->method(i); + + if ((metaMethod.access() == QMetaMethod::Public) && ((metaMethod.methodType() == QMetaMethod::Slot) || + (metaMethod.methodType() == QMetaMethod::Method)) ) { + const QString methodName = QString("%1%2").arg(prefix).arg(QString(metaMethod.name())); + + //Parameters + QList parametersNames = metaMethod.parameterNames(); + QString methodParameters = QString(""); + QString stringifyParameters = QString(""); + + for (int j = 0; j < parametersNames.count(); j++) { + methodParameters += QString(parametersNames.at(j)); + + stringifyParameters += QString("'%1=' + encodeURIComponent(%2)").arg(QString(parametersNames.at(j))) + .arg(QString(parametersNames.at(j))); + + if(j < (parametersNames.count() -1)) { + methodParameters += QString(", "); + stringifyParameters += QString(" + '&' + "); + } + } + if (stringifyParameters.length() > 0) + stringifyParameters = QString(" + '?'+ %1").arg(stringifyParameters); + + const QString methodUrl = QString("http://%1/%2").arg(host()).arg(methodName); + //Body + const QString bodyTemplate = QString("" + " var obj = this; " + " if (obj.responseText !== null) { " + " var ret = eval( obj.responseText ); " + " obj.responseText = null; " + " return ret;" + " }; " + " var caller = arguments.callee.caller; " + " var callerArgs = caller.arguments; " + " var xhr = new XMLHttpRequest; " + " xhr.onload=function(){ " + " if (xhr.status == 200) { " + " obj.responseText = xhr.responseText; " + " if (obj.responseText == null) { " + " obj.responceText = ''; " + " } " + " caller(callerArgs); " + " }; " + " }; " + " xhr.open('GET', '%1'%2, true); " + //" xhr.setRequestHeader('Access-Control-Allow-Origin', '*'); " + //" xhr.setRequestHeader('Access-Control-Allow-Headers', 'Content-Type'); " + " xhr.send(null);" + ).arg(methodUrl).arg(stringifyParameters); + + + QString methodBody = QString("%1").arg(bodyTemplate); + + script = script + QString("%1=function(%2){ %3 }; " ) + .arg(methodName) + .arg(methodParameters) + .arg(methodBody); + + } + } + } + return script; +} + + +const QString createPopup = QString( "" + " window.createPopup=function(){ " + " var popup=document.createElement('iframe'), " + " isShown=false, popupClicked=false; " + " popup.src='about:blank'; " + " popup.style.position='absolute'; " + " popup.style.border='0px'; " + " popup.style.display='none'; " + " popup.addEventListener('load', function(e){ " + " popup.document=(popup.contentWindow || popup.contentDocument); " + " if(popup.document.document) popup.document=popup.document.document; " + " }); " + " document.body.appendChild(popup); " + " var hidepopup=function(event){ " + " if(isShown) " + " setTimeout(function(){ " + //" if(!popupClicked){ " + " popup.hide(); " + //" } " + " popupClicked=false; " + " }, 150); " + " }; " + " popup.show=function(x, y, w, h, pElement){ " + " if(typeof(x) !== 'undefined'){ " + " var elPos=[0, 0]; " + " if(pElement) elPos=findPos(pElement); " + " elPos[0]+=y, elPos[1]+=x; " + " if(isNaN(w)) w=popup.document.scrollWidth; " + " if(isNaN(h)) h=popup.document.scrollHeight; " + " if(elPos[0] + w > document.body.clientWidth) elPos[0]=document.body.clientWidth - w - 5; " + " if(elPos[1] + h > document.body.clientHeight) elPos[1]=document.body.clientHeight - h - 5; " + " popup.style.left=elPos[0] + 'px'; " + " popup.style.top=elPos[1] + 'px'; " + " popup.style.width=(w + 'px'); " + " popup.style.height=(h + 'px'); " + " } " + " popup.style.display='block'; " + " isShown=true; " + " }; " + " popup.hide=function(){ " + " isShown=false; " + " popup.style.display='none'; " + " }; " + " window.addEventListener('click', hidepopup, true); " + " window.addEventListener('blur', hidepopup, true); " + " return popup; " + " }; " + " function findPos(obj, foundScrollLeft, foundScrollTop) { " + " var curleft = 0; " + " var curtop = 0; " + " if(obj.offsetLeft) curleft += parseInt(obj.offsetLeft); " + " if(obj.offsetTop) curtop += parseInt(obj.offsetTop); " + " if(obj.scrollTop && obj.scrollTop > 0) { " + " curtop -= parseInt(obj.scrollTop); " + " foundScrollTop = true; " + " } " + " if(obj.scrollLeft && obj.scrollLeft > 0) { " + " curleft -= parseInt(obj.scrollLeft); " + " foundScrollLeft = true; " + " } " + " if(obj.offsetParent) { " + " var pos = findPos(obj.offsetParent, foundScrollLeft, foundScrollTop); " + " curleft += pos[0]; " + " curtop += pos[1]; " + " } else if(obj.ownerDocument) { " + " var thewindow = obj.ownerDocument.defaultView; " + " if(!thewindow && obj.ownerDocument.parentWindow) " + " thewindow = obj.ownerDocument.parentWindow; " + " if(thewindow) { " + " if (!foundScrollTop && thewindow.scrollY && thewindow.scrollY > 0) curtop -= parseInt(thewindow.scrollY); " + " if (!foundScrollLeft && thewindow.scrollX && thewindow.scrollX > 0) curleft -= parseInt(thewindow.scrollX); " + " if(thewindow.frameElement) { " + " var pos = findPos(thewindow.frameElement); " + " curleft += pos[0]; " + " curtop += pos[1]; " + " } " + " }" + " }" + " return [curleft,curtop]; " + " } " ); + + +void JsHandler::init() +{ + /* + QString consoleObject = createPopup + QString ( + "function popup(msg){ " + " var p = window.createPopup(); " + " var pbody = p.document.body; " + " pbody.style.backgroundColor='lime'; " + " pbody.style.border='solid black 1px'; " + " pbody.innerHTML=msg; " + " p.show(NaN,NaN,NaN,NaN, document.body); }" + " window.console={ " + " log=function(msg){ popup(msg); }" + " warning=function(msg){ popup(msg); }" + " error=function(msg){ popup(msg); }" + " info=function(msg){ popup(msg); }" + " }"); + */ + + QString consoleObject = QString ( "" + " webviewPopup=function(msg){ " + " var p = window.createPopup(); " + " var pbody = p.document.body; " + " pbody.style.backgroundColor='white'; " + " pbody.style.border='solid black 1px'; " + " pbody.innerHTML=msg + ' ' + document.body.clientWidth; " + " p.show(0, 0, document.body.clientWidth, NaN, document.body); }; " + " window.console.log=function(msg){ webviewPopup(msg); };" + " window.console.warning=function(msg){ webviewPopup(msg); };" + " window.console.error=function(msg){ webviewPopup(msg); };" + " window.console.info=function(msg){ webviewPopup(msg); };" + ) + createPopup; + /* + QString object = QString( " var script = document.getElementById(\"consoleScriptObject\"); " + " if(script === null) { " + " script = document.createElement('script'); " + " script.type = \"text/javascript\";" + " script.text = \"%1\";" + " script.id = \"consoleScriptObject\"; " + " document.getElementsByTagName('head')[0].appendChild(script); " + " } " + ).arg(consoleObject); + */ + + scriptParts["__console__"] = consoleObject; +} + +QByteArray JsHandler::dataForUrl(const QUrl &url) const +{ + QByteArray buffer; + QVariant ret = callMethodForUrl(url); + if (ret.isValid()) { + + QJsonValue value = QJsonValue::fromVariant(ret); + QJsonArray jsonArray; + jsonArray.append(value); + QJsonDocument jsonDoc(jsonArray); + buffer = jsonDoc.toJson(); + } + return buffer; +} + + +// Method call specified by url of type: http://scripthost/object.methodname?paramName1=paramValue1¶mName2=paramValue2&... +QVariant JsHandler::callMethodForUrl(const QUrl &url) const +{ + qDebug() << url.toString(); + qDebug() << "Path: " << url.path(); + qDebug() << "Query: " << url.query(); + + QVariant ret; + QStringList objectPath = url.path().trimmed().remove('/').split("."); + QStringList query; + if (url.query().trimmed().length() > 0) + query = url.query().trimmed().split("&"); + QList arguments; + QList parametersNames; + QList parametersTypes; + QString methodName; + QString signature; + + AmneziaWebViewPrivate *d = AmneziaWebViewPrivate::get(_host); + + if (d && objectPath.count() == 2) { + qDebug() << "Called method: " << objectPath[0] << "."<< objectPath[1]; + QObject *object = d->jsHandler.windowObjects.value(objectPath[0]); + if (object) { + int methodCount = object->metaObject()->methodCount(); + int methodIndex = -1; + for(int i = 0; i < methodCount; i++) { + const QMetaMethod method = object->metaObject()->method(i); + parametersNames = method.parameterNames(); + methodName = method.name(); + if ((query.count() == parametersNames.count()) && (methodName == objectPath[1])) { + methodIndex = i; + parametersTypes = method.parameterTypes(); + break; + } + } + + if (methodIndex >= 0) { + const QMetaMethod method = object->metaObject()->method(methodIndex); + QVariantList params; + for (int i = 0; i < query.count(); i++) { + QStringList param = query[i].split('='); + + params.append(QVariant(QString(param[1].toLocal8Bit()))); + QGenericArgument arg(param[0].toLocal8Bit().constData(), ¶ms[i]); + arguments.append(arg); + } + switch (arguments.count()) { + case 1: + method.invoke(object, Qt::DirectConnection, Q_RETURN_ARG(QVariant, ret), arguments[0]); + break; + + case 2: + method.invoke(object, Qt::DirectConnection, Q_RETURN_ARG(QVariant, ret), arguments[0], arguments[1]); + break; + + case 3: + method.invoke(object, Qt::DirectConnection, Q_RETURN_ARG(QVariant, ret), arguments[0], arguments[1], arguments[2]); + break; + + case 4: + method.invoke(object, Qt::DirectConnection, Q_RETURN_ARG(QVariant, ret), arguments[0], arguments[1], arguments[2], + arguments[3]); + break; + + case 5: + method.invoke(object, Qt::DirectConnection, Q_RETURN_ARG(QVariant, ret), arguments[0], arguments[1], arguments[2], + arguments[3], arguments[4]); + break; + case 6: + method.invoke(object, Qt::DirectConnection, Q_RETURN_ARG(QVariant, ret), arguments[0], arguments[1], arguments[2], + arguments[3], arguments[4], arguments[5]); + break; + + case 7: + method.invoke(object, Qt::DirectConnection, Q_RETURN_ARG(QVariant, ret), arguments[0], arguments[1], arguments[2], + arguments[3], arguments[4], arguments[5], arguments[6]); + break; + + case 8: + method.invoke(object, Qt::DirectConnection, Q_RETURN_ARG(QVariant, ret), arguments[0], arguments[1], arguments[2], + arguments[3], arguments[4], arguments[5], arguments[6], arguments[7]); + break; + + default: + method.invoke(object, Qt::DirectConnection, Q_RETURN_ARG(QVariant, ret)); + } + } + } + } + + return ret; +} + diff --git a/client/core/webview/jshandler.h b/client/core/webview/jshandler.h new file mode 100644 index 000000000..1bbb769e0 --- /dev/null +++ b/client/core/webview/jshandler.h @@ -0,0 +1,41 @@ +#ifndef JSHANDLER_H +#define JSHANDLER_H + +class QUrl; +class QVariant; +class AmneziaWebView; + +class JsHandler +{ + friend class AmneziaWebView; + +public: + explicit JsHandler(AmneziaWebView *host); + virtual ~JsHandler(); + + bool canHandleUrl(const QUrl &url) const; + QByteArray dataForUrl(const QUrl &url) const; + void addToJavaScriptWindowObject(const QString& name, QObject* object); + QString mimeTypeForUrl(const QUrl &url) const; + + void updateWebView(); + QString host() const; + QString scheme() const; + QString scriptObjectsUrl() const; + QString scriptObjectsId() const; + QString scriptObjects() const; + +private: + + QVariant callMethodForUrl(const QUrl &url) const; + QString windowScriptObject(const QString& name, QObject* object) const; + + void init(); + AmneziaWebView *_host; + bool scriptObjectsInjected; + + QHash windowObjects; + QHash scriptParts; +}; + +#endif diff --git a/client/core/webview/jshandler_android.cpp b/client/core/webview/jshandler_android.cpp new file mode 100644 index 000000000..13783f5b8 --- /dev/null +++ b/client/core/webview/jshandler_android.cpp @@ -0,0 +1,24 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "amneziawebview_p.h" +#include "jshandler.h" +#include "mimecache.h" + +JsHandler::JsHandler(AmneziaWebView *host): _host(host) +{ + init(); +} + +JsHandler::~JsHandler() {} + diff --git a/client/core/webview/jshandler_ios.mm b/client/core/webview/jshandler_ios.mm new file mode 100644 index 000000000..fe745793b --- /dev/null +++ b/client/core/webview/jshandler_ios.mm @@ -0,0 +1,185 @@ +#import +#import +#import + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "amneziawebview_p.h" +#include "mimecache.h" +#import "jshandler.h" + +typedef QHash JavaScriptHosts; +Q_GLOBAL_STATIC(JavaScriptHosts, hosts); + +#if !defined(ENABLE_WKWEBVIEW) + +@interface JsProtocol : NSURLProtocol ++ (NSString*) requestVarsKey; +@end + +@interface NSURLRequest (JsProtocol) +- (NSDictionary *)requestVars; +@end + +@interface NSMutableURLRequest (JsProtocol) +- (void)setRequestVars:(NSDictionary *)vars; +@end + +#endif + +JsHandler::JsHandler(AmneziaWebView *h): _host(h) + , scriptObjectsInjected(false) +{ +#if !defined(ENABLE_WKWEBVIEW) + static bool protocolRegistered = false; + if (!protocolRegistered) { + [NSURLProtocol registerClass:[JsProtocol class]]; + protocolRegistered = true; + } +#endif + QString key = host(); + if (!hosts()->keys().contains(key)) { + hosts()->insert(key, h); + } + init(); +} + +JsHandler::~JsHandler() +{ + QString key = host(); + if (hosts()->keys().contains(key)) { + hosts()->remove(key); + } +} + +#if !defined(ENABLE_WKWEBVIEW) + +@implementation NSURLRequest (JsProtocol) + +- (NSDictionary *)requestVars { + NSLog(@"%@ received %@", self, NSStringFromSelector(_cmd)); + return [NSURLProtocol propertyForKey:[JsProtocol requestVarsKey] inRequest:self]; +} + +@end + + +@implementation NSMutableURLRequest (JsProtocol) + +- (void)setRequestVars:(NSDictionary *)requestVars { + + NSLog(@"%@ received %@", self, NSStringFromSelector(_cmd)); + + NSDictionary *specialVarsCopy = [requestVars copy]; + [NSURLProtocol setProperty:specialVarsCopy forKey:[JsProtocol requestVarsKey] inRequest:self]; + [specialVarsCopy release]; +} + +@end + + +@implementation JsProtocol + ++ (BOOL)canInitWithRequest:(NSURLRequest *)request +{ + //NSString *url = request.URL.absoluteString; + QString host = QString::fromNSString(request.URL.host).toLower(); + if (hosts()->keys().contains(host)) + return YES; + + //NSLog(@"Requested Url: %@", url); + return NO; +} + ++ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request +{ + return request; +} + ++ (NSString*) requestVarsKey +{ + return @"requestVars"; +} + +- (void)startLoading +{ + QString host = QString::fromNSString(self.request.URL.host); + AmneziaWebViewPrivate *d = AmneziaWebViewPrivate::get(hosts()->value(host)); + NSURLRequest *request = [self request]; + + if ([request.URL.absoluteString isEqualToString: d->jsHandler.scriptObjectsUrl().toNSString()]) { + + //NSString *mimeType = mimeTypeForExtension(QString::fromNSString(request.URL.path.pathExtension)).toNSString(); + + NSString *mimeType = d->jsHandler.mimeTypeForUrl(QUrl::fromNSURL(request.URL)).toNSString(); + QByteArray buffer = d->jsHandler.scriptObjects().toLocal8Bit(); + + //NSData *data = [[NSData alloc] initWithBytes:buffer.constData() length:buffer.size()]; + NSData *data = [NSData dataWithBytes:buffer.constData() length:buffer.size()]; + + NSURLResponse *response =[[NSURLResponse alloc]initWithURL:self.request.URL + MIMEType:mimeType + expectedContentLength:[data length] + textEncodingName:@"utf-8"]; + + + [[self client] URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed]; + [[self client] URLProtocol:self didLoadData:data]; + [[self client] URLProtocolDidFinishLoading:self]; + [response autorelease]; + + return; + } + + if ( d && [request.URL.scheme isEqualToString:@"http"] + && [request.URL.host isEqualToString:host.toNSString()]) { + + const QByteArray buffer = d->dataForUrl(QUrl::fromNSURL(request.URL)); + //NSData *data = data = [[NSData alloc] initWithBytes:buffer.constData() length:buffer.size()]; + NSData *data = data = [NSData dataWithBytes:buffer.constData() length:buffer.size()]; + + //NSString *mimeType = mimeTypeForExtension(QString("json")).toNSString(); + NSString *mimeType = d->jsHandler.mimeTypeForUrl(QUrl::fromNSURL(request.URL)).toNSString(); + + NSDictionary *headers = @{@"Access-Control-Allow-Origin" : @"*", + @"Access-Control-Allow-Headers" : @"Content-Type", + @"Cache-Control" : @"no-cache", + @"Content-Type" : [NSString stringWithFormat:@"%@; %@", mimeType, @"charset=UTF-8"] }; + + NSHTTPURLResponse* response = [[NSHTTPURLResponse alloc] initWithURL:request.URL + statusCode:200 + HTTPVersion:@"HTTP/1.1" + headerFields:headers]; + + [self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed]; + [self.client URLProtocol:self didLoadData:data]; + [self.client URLProtocolDidFinishLoading:self]; + [response autorelease]; + return; + } + else { + [[self client] URLProtocol:self didFailWithError:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:nil]]; + } +} + +- (void)stopLoading +{ +} + +@end + +#endif diff --git a/client/core/webview/mimecache.cpp b/client/core/webview/mimecache.cpp new file mode 100644 index 000000000..c2e6eff2c --- /dev/null +++ b/client/core/webview/mimecache.cpp @@ -0,0 +1,274 @@ +#include "mimecache.h" + +#include +#include +#include +#include +#include +#include +#include + +typedef QHash MimeTypes; +Q_GLOBAL_STATIC(MimeTypes, cache) +Q_GLOBAL_STATIC(QMimeDatabase, mimeDatabase) + + +void initCache() +{ + if (cache()->isEmpty()) { + + cache()->insert("323", "text/h323"); + cache()->insert("*", "application/octet-stream"); + cache()->insert("acx", "application/internet-property-stream"); + cache()->insert("ai", "application/postscript"); + cache()->insert("aif", "audio/x-aiff"); + cache()->insert("aifc", "audio/x-aiff"); + cache()->insert("aiff", "audio/x-aiff"); + cache()->insert("asf", "video/x-ms-asf"); + cache()->insert("asr", "video/x-ms-asf"); + cache()->insert("asx", "video/x-ms-asf"); + cache()->insert("au", "audio/basic"); + cache()->insert("avi", "video/x-msvideo"); + cache()->insert("axs", "application/olescript"); + cache()->insert("bas", "text/plain"); + cache()->insert("bcpio", "application/x-bcpio"); + cache()->insert("bin", "application/octet-stream"); + cache()->insert("bmp", "image/bmp"); + cache()->insert("c", "text/plain"); + cache()->insert("cat", "application/vnd.ms-pkiseccat"); + cache()->insert("cdf", "application/x-cdf"); + cache()->insert("cdf", "application/x-netcdf"); + cache()->insert("cer", "application/x-x509-ca-cert""cer"); + cache()->insert("class", "application/octet-stream"); + cache()->insert("clp", "application/x-msclip"); + cache()->insert("cmx", "image/x-cmx"); + cache()->insert("cod", "image/cis-cod"); + cache()->insert("cpio", "application/x-cpio"); + cache()->insert("crd", "application/x-mscardfile"); + cache()->insert("crl", "application/pkix-crl"); + cache()->insert("crt", "application/x-x509-ca-cert"); + cache()->insert("csh", "application/x-csh"); + cache()->insert("css", "text/css"); + cache()->insert("dcr", "application/x-director"); + cache()->insert("der", "application/x-x509-ca-cert"); + cache()->insert("dir", "application/x-director"); + cache()->insert("dll", "application/x-msdownload"); + cache()->insert("dms", "application/octet-stream"); + cache()->insert("doc", "application/msword"); + cache()->insert("dot", "application/msword"); + cache()->insert("dvi", "application/x-dvi"); + cache()->insert("dxr", "application/x-director"); + cache()->insert("eot", "application/vnd.ms-fontobject"); + cache()->insert("eps", "application/postscript"); + cache()->insert("etx", "text/x-setext"); + cache()->insert("evy", "application/envoy"); + cache()->insert("exe", "application/octet-stream"); + cache()->insert("fif", "application/fractals"); + cache()->insert("flr", "x-world/x-vrml"); + cache()->insert("gif", "image/gif"); + cache()->insert("gtar", "application/x-gtar"); + cache()->insert("gz", "application/x-gzip"); + cache()->insert("h", "text/plain"); + cache()->insert("hdf", "application/x-hdf"); + cache()->insert("hlp", "application/winhlp"); + cache()->insert("hqx", "application/mac-binhex40"); + cache()->insert("hta", "application/hta"); + cache()->insert("htc", "text/x-component"); + cache()->insert("htm", "text/html"); + cache()->insert("html", "text/html"); + cache()->insert("htt", "text/webviewhtml"); + cache()->insert("ico", "image/x-icon"); + cache()->insert("ief", "image/ief"); + cache()->insert("iii", "application/x-iphone"); + cache()->insert("ins", "application/x-internet-signup"); + cache()->insert("isp", "application/x-internet-signup"); + cache()->insert("jfif", "image/pipeg"); + cache()->insert("jpe", "image/jpeg"); + cache()->insert("jpeg", "image/jpeg"); + cache()->insert("jpg", "image/jpeg"); + cache()->insert("js", "application/x-javascript"); + cache()->insert("json", "application/json"); + cache()->insert("latex", "application/x-latex"); + cache()->insert("lha", "application/octet-stream"); + cache()->insert("lsf", "video/x-la-asf"); + cache()->insert("lsx", "video/x-la-asf"); + cache()->insert("lzh", "application/octet-stream"); + cache()->insert("m13", "application/x-msmediaview"); + cache()->insert("m14", "application/x-msmediaview"); + cache()->insert("m3u", "audio/x-mpegurl"); + cache()->insert("m4v", "video/x-m4v"); + cache()->insert("man", "application/x-troff-man"); + cache()->insert("mdb", "application/x-msaccess"); + cache()->insert("me", "application/x-troff-me"); + cache()->insert("mht", "message/rfc822"); + cache()->insert("mhtml", "message/rfc822"); + cache()->insert("mid", "audio/mid"); + cache()->insert("mny", "application/x-msmoney"); + cache()->insert("mov", "video/quicktime"); + cache()->insert("movie", "video/x-sgi-movie""movie"); + cache()->insert("mp2", "video/mpeg"); + cache()->insert("mp3", "audio/mpeg"); + cache()->insert("mpa", "video/mpeg"); + cache()->insert("mpe", "video/mpeg"); + cache()->insert("mpeg", "video/mpeg"); + cache()->insert("mpg", "video/mpeg"); + cache()->insert("mpp", "application/vnd.ms-project"); + cache()->insert("mpv2", "video/mpeg"); + cache()->insert("ms", "application/x-troff-ms"); + cache()->insert("msg", "application/vnd.ms-outlook"); + cache()->insert("mvb", "application/x-msmediaview"); + cache()->insert("nc", "application/x-netcdf"); + cache()->insert("nws", "message/rfc822"); + cache()->insert("oda", "application/oda"); + cache()->insert("otf", "application/font-sfnt"); + cache()->insert("p10", "application/pkcs10"); + cache()->insert("p12", "application/x-pkcs12"); + cache()->insert("p7b", "application/x-pkcs7-certificates"); + cache()->insert("p7c", "application/x-pkcs7-mime"); + cache()->insert("p7m", "application/x-pkcs7-mime"); + cache()->insert("p7r", "application/x-pkcs7-certreqresp"); + cache()->insert("p7s", "application/x-pkcs7-signature"); + cache()->insert("pbm", "image/x-portable-bitmap"); + cache()->insert("pdf", "application/pdf"); + cache()->insert("pfx", "application/x-pkcs12"); + cache()->insert("pgm", "image/x-portable-graymap"); + cache()->insert("pko", "application/ynd.ms-pkipko"); + cache()->insert("pma", "application/x-perfmon"); + cache()->insert("pmc", "application/x-perfmon"); + cache()->insert("pml", "application/x-perfmon"); + cache()->insert("pmr", "application/x-perfmon"); + cache()->insert("pmw", "application/x-perfmon"); + cache()->insert("pnm", "image/x-portable-anymap"); + cache()->insert("pot", "application/vnd.ms-powerpoint"); + cache()->insert("ppm", "image/x-portable-pixmap"); + cache()->insert("pps", "application/vnd.ms-powerpoint"); + cache()->insert("ppt", "application/vnd.ms-powerpoint"); + cache()->insert("prf", "application/pics-rules"); + cache()->insert("ps", "application/postscript"); + cache()->insert("pub", "application/x-mspublisher"); + cache()->insert("qt", "video/quicktime"); + cache()->insert("ra", "audio/x-pn-realaudio"); + cache()->insert("ram", "audio/x-pn-realaudio"); + cache()->insert("ras", "image/x-cmu-raster"); + cache()->insert("rgb", "image/x-rgb"); + cache()->insert("rmi", "audio/mid"); + cache()->insert("roff", "application/x-troff"); + cache()->insert("rtf", "application/rtf""rtf"); + cache()->insert("rtx", "text/richtext""rtx"); + cache()->insert("scd", "application/x-msschedule"); + cache()->insert("sct", "text/scriptlet"); + cache()->insert("setpay", "application/set-payment-initiation"); + cache()->insert("setreg", "application/set-registration-initiation"); + cache()->insert("sh", "application/x-sh"); + cache()->insert("shar", "application/x-shar"); + cache()->insert("sit", "application/x-stuffit"); + cache()->insert("snd", "audio/basic"); + cache()->insert("spc", "application/x-pkcs7-certificates"); + cache()->insert("spl", "application/futuresplash"); + cache()->insert("src", "application/x-wais-source"); + cache()->insert("sst", "application/vnd.ms-pkicertstore"); + cache()->insert("stl", "application/vnd.ms-pkistl"); + cache()->insert("stm", "text/html"); + cache()->insert("sv4cpio", "application/x-sv4cpio"); + cache()->insert("sv4crc", "application/x-sv4crc"); + cache()->insert("svg", "image/svg+xml"); + cache()->insert("swf", "application/x-shockwave-flash"); + cache()->insert("t", "application/x-troff"); + cache()->insert("tar", "application/x-tar"); + cache()->insert("tcl", "application/x-tcl"); + cache()->insert("tex", "application/x-tex"); + cache()->insert("texi", "application/x-texinfo"); + cache()->insert("texinfo", "application/x-texinfo"); + cache()->insert("tgz", "application/x-compressed"); + cache()->insert("tif", "image/tiff"); + cache()->insert("tiff", "image/tiff"); + cache()->insert("tr", "application/x-troff"); + cache()->insert("trm", "application/x-msterminal"); + cache()->insert("tsv", "text/tab-separated-values"); + cache()->insert("txt", "text/plain"); + cache()->insert("ttf", "application/font-sfnt"); + cache()->insert("uls", "text/iuls"); + cache()->insert("ustar", "application/x-ustar"); + cache()->insert("vcf", "text/x-vcard"); + cache()->insert("vrml", "x-world/x-vrml"); + cache()->insert("wav", "audio/x-wav"); + cache()->insert("wcm", "application/vnd.ms-works"); + cache()->insert("wdb", "application/vnd.ms-works"); + cache()->insert("wks", "application/vnd.ms-works"); + cache()->insert("wmf", "application/x-msmetafile"); + cache()->insert("woff", "application/font-woff"); + cache()->insert("wps", "application/vnd.ms-works"); + cache()->insert("wri", "application/x-mswrite"); + cache()->insert("wrl", "x-world/x-vrml"); + cache()->insert("wrz", "x-world/x-vrml"); + cache()->insert("xaf", "x-world/x-vrml"); + cache()->insert("xbm", "image/x-xbitmap"); + cache()->insert("xla", "application/vnd.ms-excel"); + cache()->insert("xlc", "application/vnd.ms-excel"); + cache()->insert("xlm", "application/vnd.ms-excel"); + cache()->insert("xls", "application/vnd.ms-excel"); + cache()->insert("xlt", "application/vnd.ms-excel"); + cache()->insert("xlw", "application/vnd.ms-excel"); + cache()->insert("xof", "x-world/x-vrml"); + cache()->insert("xpm", "image/x-xpixmap"); + cache()->insert("xwd", "image/x-xwindowdump"); + cache()->insert("z", "application/x-compress"); + cache()->insert("zip", "application/zip"); + } +} + +QString mimeTypeForExtension(const QString &extension) +{ + initCache(); + QString ext = extension; + + const int lastDot = ext.lastIndexOf(QLatin1Char('.')); + if (lastDot != -1) { + const int extLength = ext.length() - lastDot - 1; + ext = ext.right(extLength).toLower(); + } + + QString mimeType = cache()->value(ext); + + if (mimeType.isNull()) { + mimeType = QString("application/octet-stream"); + } + + return mimeType; +} + +QString mimeTypeForUrl(const QUrl &url) +{ + initCache(); + QString path = url.path(); + + QString extension; + QString mimeType; + + const int lastDot = path.lastIndexOf(QLatin1Char('.')); + if (lastDot != -1) { + const int extLength = path.length() - lastDot - 1; + extension = path.right(extLength).toLower(); + } + + if (!extension.isNull()) { + mimeType = cache()->value(extension); + } + if (mimeType.isNull()) { + QMimeType mime = mimeDatabase()->mimeTypeForUrl(url); + if (mime.isValid()) { + mimeType = mime.name(); + if(!extension.isNull()) { + cache()->insert(extension, mimeType); + } + } + } + + if (mimeType.isNull()) { + mimeType = QString("application/octet-stream"); + } + + qDebug() << mimeType; + return mimeType; +} + diff --git a/client/core/webview/mimecache.h b/client/core/webview/mimecache.h new file mode 100644 index 000000000..11d4100bb --- /dev/null +++ b/client/core/webview/mimecache.h @@ -0,0 +1,10 @@ +#ifndef MIMECACHE_H +#define MIMECACHE_H + +class QString; +class QUrl; + +QString mimeTypeForExtension(const QString &extension); +QString mimeTypeForUrl(const QUrl &url); + +#endif diff --git a/client/core/webview/pch.h b/client/core/webview/pch.h new file mode 100644 index 000000000..ed8409e1f --- /dev/null +++ b/client/core/webview/pch.h @@ -0,0 +1,66 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtCore module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +/* + * This is a precompiled header file for use in Xcode / Mac GCC / + * GCC >= 3.4 / VC to greatly speed the building of Qt. It may also be + * of use to people developing their own project, but it is probably + * better to define your own header. Use of this header is currently + * UNSUPPORTED. + */ + + +#if defined __cplusplus +// for rand_s, _CRT_RAND_S must be #defined before #including stdlib.h. +// put it at the beginning so some indirect inclusion doesn't break it +#ifndef _CRT_RAND_S +#define _CRT_RAND_S +#endif +#include +#include +#ifdef Q_OS_WIN +# define _POSIX_ +# include +# undef _POSIX_ +#endif +#include +#ifndef Q_OS_WIN +#include +#endif +#endif diff --git a/client/core/webview/plugin.cpp b/client/core/webview/plugin.cpp new file mode 100644 index 000000000..ca42247f5 --- /dev/null +++ b/client/core/webview/plugin.cpp @@ -0,0 +1,17 @@ +#include "plugin.h" + +#include "amneziawebview.h" + +QT_BEGIN_NAMESPACE + +void WebViewPlugin::registerTypes(const char* uri) +{ +#ifndef QT_NO_ACTION + qmlRegisterAnonymousType(uri, 1); +#endif + qmlRegisterAnonymousType(uri, 1); + qmlRegisterType(uri, 1, 0, "AmneziaWebView"); + qmlRegisterRevision("AmneziaWebView", 1, 0); +} + +QT_END_NAMESPACE diff --git a/client/core/webview/plugin.h b/client/core/webview/plugin.h new file mode 100644 index 000000000..4f6ac4d5a --- /dev/null +++ b/client/core/webview/plugin.h @@ -0,0 +1,15 @@ +#include + +QT_BEGIN_NAMESPACE + +class WebViewPlugin : public QQmlExtensionPlugin +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QQmlExtensionInterface" FILE "webview.json") + Q_INTERFACES(QQmlExtensionInterface) + +public: + void registerTypes(const char* uri) override; +}; + +QT_END_NAMESPACE diff --git a/client/core/webview/qmldir b/client/core/webview/qmldir new file mode 100644 index 000000000..a6c1fdcd6 --- /dev/null +++ b/client/core/webview/qmldir @@ -0,0 +1,2 @@ +module AmneziaWebView +plugin webview diff --git a/client/core/webview/qrchandler.cpp b/client/core/webview/qrchandler.cpp new file mode 100644 index 000000000..337e0dcba --- /dev/null +++ b/client/core/webview/qrchandler.cpp @@ -0,0 +1,37 @@ +#include +#include +#include +#include + +#include "qrchandler.h" +#include "mimecache.h" + +QString QrcHandler::scheme() +{ + return QString("qrc"); +} + +QByteArray QrcHandler::dataForUrl(const QUrl &url) const +{ + QString requestUrl(QString(":/%1").arg(url.path())); + QFile resource(requestUrl); + QByteArray buffer; + if (resource.exists() && resource.open(QIODevice::ReadOnly)) { + + buffer = resource.readAll(); + resource.close(); + } + return buffer; +} + +bool QrcHandler::canHandleUrl(const QUrl &url) const +{ + if (scheme() == url.scheme()) + return true; + return false; +} + +QString QrcHandler::mimeTypeForUrl(const QUrl &url) const +{ + return mimeTypeForExtension(url.path().section('.', -1)); +} diff --git a/client/core/webview/qrchandler.h b/client/core/webview/qrchandler.h new file mode 100644 index 000000000..ea7323b1b --- /dev/null +++ b/client/core/webview/qrchandler.h @@ -0,0 +1,18 @@ +#ifndef QRCHANDLER_H +#define QRCHANDLER_H + +class QString; +class QByteArray; +class QUrl; + +class QrcHandler +{ +public: + explicit QrcHandler(); + static QString scheme(); + bool canHandleUrl(const QUrl &url) const; + QByteArray dataForUrl(const QUrl &url) const; + QString mimeTypeForUrl(const QUrl &url) const; +}; + +#endif diff --git a/client/core/webview/qrchandler_android.cpp b/client/core/webview/qrchandler_android.cpp new file mode 100644 index 000000000..98e809187 --- /dev/null +++ b/client/core/webview/qrchandler_android.cpp @@ -0,0 +1,10 @@ +#include +#include +#include +#include + +#include "qrchandler.h" +#include "mimecache.h" + +QrcHandler::QrcHandler() +{} diff --git a/client/core/webview/qrchandler_ios.mm b/client/core/webview/qrchandler_ios.mm new file mode 100644 index 000000000..40c1b9e8e --- /dev/null +++ b/client/core/webview/qrchandler_ios.mm @@ -0,0 +1,189 @@ +#import +#import +#import + +#include +#include +#include +#include +#include "mimecache.h" + +#include "qrchandler.h" +#define kProtocolQrcScheme @"qrc" + +#if !defined(ENABLE_WKWEBVIEW) +@interface QrcProtocol : NSURLProtocol + ++ (NSString*)protocolScheme; ++ (NSString*)requestVarsKey; ++ (void)registerProtocol; + +@end + +@interface NSURLRequest (QrcProtocol) +- (NSDictionary *)requestVars; +@end + +@interface NSMutableURLRequest (QrcProtocol) +- (void)setRequestVars:(NSDictionary *)vars; +@end +#endif + +QrcHandler::QrcHandler() +{ +#if !defined(ENABLE_WKWEBVIEW) + [QrcProtocol registerProtocol]; +#endif +} + +#if !defined(ENABLE_WKWEBVIEW) + +@implementation NSURLRequest (QrcProtocol) + +- (NSDictionary *)requestVars { + NSLog(@"%@ received %@", self, NSStringFromSelector(_cmd)); + return [NSURLProtocol propertyForKey:[QrcProtocol requestVarsKey] inRequest:self]; +} + +@end + +@implementation NSMutableURLRequest (QrcProtocol) + +- (void)setRequestVars:(NSDictionary *)requestVars { + + NSLog(@"%@ received %@", self, NSStringFromSelector(_cmd)); + + NSDictionary *specialVarsCopy = [requestVars copy]; + [NSURLProtocol setProperty:specialVarsCopy forKey:[QrcProtocol requestVarsKey] inRequest:self]; + [specialVarsCopy release]; +} + +@end + +@implementation QrcProtocol + ++ (BOOL)canInitWithRequest:(NSURLRequest *)request +{ + NSString *scheme = request.URL.scheme; + if ([kProtocolQrcScheme caseInsensitiveCompare:scheme] == NSOrderedSame) { + return YES; + } + return NO; +} + ++ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request +{ + return request; +} + ++ (NSString*) protocolScheme +{ + return kProtocolQrcScheme; +} + ++ (NSString*) requestVarsKey { + return @"requestVars"; +} + ++ (void)registerProtocol +{ + static bool qrcProtocolRegistered = false; + if (!qrcProtocolRegistered) { + [NSURLProtocol registerClass:[QrcProtocol class]]; + qrcProtocolRegistered = true; + } +} + ++ (NSString*)headFromHtml:(NSString*)html +{ + if (!html) return nil; + + NSString *head = nil; + NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"(?<=)[\\w\\W.]*(?=)" options:NSRegularExpressionCaseInsensitive error:nil]; + NSTextCheckingResult *headResult = [regex firstMatchInString:html options:0 range:NSMakeRange(0, html.length)]; + + if (headResult && headResult.range.location != NSNotFound) { + head = [html substringWithRange:[headResult range]]; + } + return head; +} + ++ (NSString *)charsetFromHtml:(NSString *)html +{ + if (!html) return nil; + + NSString *charset = nil; + NSString *charsetPattern = @"((?<=charset=)\\s*[a-zA-Z0-9-]*)"; + NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:charsetPattern options: NSRegularExpressionCaseInsensitive error:nil]; + NSTextCheckingResult *charsetResult = [regex firstMatchInString:html options:kNilOptions range:NSMakeRange(0, [html length])]; + if (charsetResult && charsetResult.range.location != NSNotFound) { + charset = [[html substringWithRange:[charsetResult range]] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; + charset = [charset lowercaseString]; + } + return charset; +} + + +- (void)startLoading +{ + /* retrieve the current request. */ + NSURLRequest *request = [self request]; + NSString *url = [[request URL] path]; + //NSString *absoluteString = request.URL.absoluteString; + + //NSString *mimeType = [self mimeTypeForExtension: [url pathExtension]]; + NSString *mimeType = mimeTypeForExtension(QString::fromNSString(url.pathExtension)).toNSString(); + + /* extract our special variables from the request. */ + //NSDictionary *requestVars = [request requestVars]; + NSData *pageData = nil; + + QString requestUrl(QString(":/%1").arg(QString::fromNSString(url))); + QFile resource(requestUrl); + if (resource.exists() && resource.open(QIODevice::ReadOnly)) { + + QByteArray buffer = resource.readAll(); + + //pageData = [[NSData alloc] initWithBytes:buffer.constData() length:buffer.size()]; + pageData = [NSData dataWithBytes:buffer.constData() length:buffer.size()]; + + resource.close(); + } + + if (pageData) { + + NSString *encoding = @"utf-8"; + if ([mimeType isEqualToString:@"text/html"]) { + + NSString *content = [[NSString alloc] initWithBytesNoCopy: (char* )[pageData bytes] length:pageData.length encoding:NSISOLatin1StringEncoding freeWhenDone:NO]; + + NSString *charset = [QrcProtocol charsetFromHtml:[QrcProtocol headFromHtml:content]]; + if (charset) { + encoding = charset; + } + [content autorelease]; + } + + NSURLResponse *response = [[NSURLResponse alloc]initWithURL:self.request.URL + MIMEType:mimeType + expectedContentLength:[pageData length] + textEncodingName:encoding]; + + [[self client] URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed]; + [[self client] URLProtocol:self didLoadData:pageData]; + [[self client] URLProtocolDidFinishLoading:self]; + [response autorelease]; + } + else { + [[self client] URLProtocol:self didFailWithError:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:nil]]; + } +} + +- (void)stopLoading +{ + +} + +@end + +#endif diff --git a/client/core/webview/websettings.cpp b/client/core/webview/websettings.cpp new file mode 100644 index 000000000..ad896ae10 --- /dev/null +++ b/client/core/webview/websettings.cpp @@ -0,0 +1,202 @@ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "websettings.h" +#include "amneziawebview_p.h" + +AmneziaWebViewSettings::AmneziaWebViewSettings(AmneziaWebView *view): QObject(view), s(new WebSettings(view)) +{} + +Q_GLOBAL_STATIC(QList, allSettings) + +void WebSettings::apply() +{ + if (view) { + + WebSettings* global = WebSettings::globalSettings(); + + + QString family = fontFamilies.value(WebSettings::StandardFont, + global->fontFamilies.value(WebSettings::StandardFont)); + view->setStandardFontFamily(family); + + + int size = fontSizes.value(WebSettings::DefaultFontSize, + global->fontSizes.value(WebSettings::DefaultFontSize)); + view->setDefaultFontSize(size); + + bool value = attributes.value(WebSettings::AutoLoadImages, + global->attributes.value(WebSettings::AutoLoadImages)); + + QString encoding = !defaultTextEncoding.isEmpty() ? defaultTextEncoding: global->defaultTextEncoding; + + + Q_UNUSED(value) + + } else { + + QList settings = *::allSettings(); + for (int i = 0; i < settings.count(); ++i) + settings[i]->apply(); + } +} + +WebSettings* WebSettings::globalSettings() +{ + static WebSettings *global = nullptr; + if (!global) { + global = new WebSettings; + } + return global; +} + +WebSettings::WebSettings() +{ + // Initialize our global defaults + fontSizes.insert(WebSettings::MinimumFontSize, 0); + fontSizes.insert(WebSettings::MinimumLogicalFontSize, 0); + fontSizes.insert(WebSettings::DefaultFontSize, 16); + fontSizes.insert(WebSettings::DefaultFixedFontSize, 13); + + QFont defaultFont; + defaultFont.setStyleHint(QFont::Serif); + fontFamilies.insert(WebSettings::StandardFont, defaultFont.defaultFamily()); + fontFamilies.insert(WebSettings::SerifFont, defaultFont.defaultFamily()); + + defaultFont.setStyleHint(QFont::Fantasy); + fontFamilies.insert(WebSettings::FantasyFont, defaultFont.defaultFamily()); + + defaultFont.setStyleHint(QFont::Cursive); + fontFamilies.insert(WebSettings::CursiveFont, defaultFont.defaultFamily()); + + defaultFont.setStyleHint(QFont::SansSerif); + fontFamilies.insert(WebSettings::SansSerifFont, defaultFont.defaultFamily()); + + defaultFont.setStyleHint(QFont::Monospace); + fontFamilies.insert(WebSettings::FixedFont, defaultFont.defaultFamily()); + + attributes.insert(WebSettings::AutoLoadImages, true); + attributes.insert(WebSettings::DnsPrefetchEnabled, false); + attributes.insert(WebSettings::JavascriptEnabled, true); + attributes.insert(WebSettings::SpatialNavigationEnabled, false); + attributes.insert(WebSettings::LinksIncludedInFocusChain, true); + attributes.insert(WebSettings::ZoomTextOnly, false); + attributes.insert(WebSettings::PrintElementBackgrounds, true); + attributes.insert(WebSettings::OfflineStorageDatabaseEnabled, false); + attributes.insert(WebSettings::OfflineWebApplicationCacheEnabled, false); + attributes.insert(WebSettings::LocalStorageEnabled, false); + attributes.insert(WebSettings::LocalContentCanAccessRemoteUrls, false); + attributes.insert(WebSettings::LocalContentCanAccessFileUrls, true); + attributes.insert(WebSettings::AcceleratedCompositingEnabled, true); + attributes.insert(WebSettings::WebGLEnabled, true); + attributes.insert(WebSettings::WebAudioEnabled, false); + attributes.insert(WebSettings::CSSRegionsEnabled, true); + attributes.insert(WebSettings::CSSGridLayoutEnabled, false); + attributes.insert(WebSettings::HyperlinkAuditingEnabled, false); + attributes.insert(WebSettings::TiledBackingStoreEnabled, false); + attributes.insert(WebSettings::FrameFlatteningEnabled, false); + attributes.insert(WebSettings::SiteSpecificQuirksEnabled, true); + attributes.insert(WebSettings::ScrollAnimatorEnabled, false); + attributes.insert(WebSettings::CaretBrowsingEnabled, false); + attributes.insert(WebSettings::NotificationsEnabled, true); + +#if defined(Q_OS_WIN32) && defined(DEBUG) + attributes.insert(WebSettings::DeveloperExtrasEnabled,true); +#endif +} + +/*! + \internal +*/ +WebSettings::WebSettings(AmneziaWebView *v) + : view(v) + +{ + allSettings()->append(this); +} + +WebSettings::~WebSettings() +{ + if (view) + allSettings()->removeAll(this); + +} + +void WebSettings::setFontSize(FontSize type, int size) +{ + fontSizes.insert(type, size); + apply(); +} + +int WebSettings::fontSize(FontSize type) const +{ + int defaultValue = 0; + if (view) { + WebSettings* global = WebSettings::globalSettings(); + defaultValue = global->fontSizes.value(type); + } + return fontSizes.value(type, defaultValue); +} + +void WebSettings::resetFontSize(FontSize type) +{ + if (view) { + fontSizes.remove(type); + apply(); + } +} + +void WebSettings::setFontFamily(FontFamily which, const QString& family) +{ + fontFamilies.insert(which, family); + apply(); +} + +QString WebSettings::fontFamily(FontFamily which) const +{ + QString defaultValue; + if (view) { + WebSettings* global = WebSettings::globalSettings(); + defaultValue = global->fontFamilies.value(which); + } + return fontFamilies.value(which, defaultValue); +} + +void WebSettings::resetFontFamily(FontFamily which) +{ + if (view) { + fontFamilies.remove(which); + apply(); + } +} + +void WebSettings::setAttribute(WebAttribute attr, bool on) +{ + attributes.insert(attr, on); + apply(); +} + +bool WebSettings::testAttribute(WebAttribute attr) const +{ + bool defaultValue = false; + if (view) { + WebSettings* global = WebSettings::globalSettings(); + defaultValue = global->attributes.value(attr); + } + return attributes.value(attr, defaultValue); +} + +void WebSettings::resetAttribute(WebAttribute attr) +{ + if (view) { + attributes.remove(attr); + apply(); + } +} diff --git a/client/core/webview/websettings.h b/client/core/webview/websettings.h new file mode 100644 index 000000000..53f8f5b1e --- /dev/null +++ b/client/core/webview/websettings.h @@ -0,0 +1,144 @@ +#ifndef WEBSETTINGS_H +#define WEBSETTINGS_H + +#include + +class AmneziaWebView; +class AmneziaWebViewPrivate; +class WebSettingsData; + +class WebSettings +{ +public: + enum FontFamily { + StandardFont, + FixedFont, + SerifFont, + SansSerifFont, + CursiveFont, + FantasyFont + }; + enum WebAttribute { + AutoLoadImages, + JavascriptEnabled, + JavaEnabled, + PluginsEnabled, + PrivateBrowsingEnabled, + JavascriptCanOpenWindows, + JavascriptCanAccessClipboard, + DeveloperExtrasEnabled, + LinksIncludedInFocusChain, + ZoomTextOnly, + PrintElementBackgrounds, + OfflineStorageDatabaseEnabled, + OfflineWebApplicationCacheEnabled, + LocalStorageEnabled, + LocalContentCanAccessRemoteUrls, + DnsPrefetchEnabled, + XSSAuditingEnabled, + AcceleratedCompositingEnabled, + SpatialNavigationEnabled, + LocalContentCanAccessFileUrls, + TiledBackingStoreEnabled, + FrameFlatteningEnabled, + SiteSpecificQuirksEnabled, + JavascriptCanCloseWindows, + WebGLEnabled, + CSSRegionsEnabled, + HyperlinkAuditingEnabled, + CSSGridLayoutEnabled, + ScrollAnimatorEnabled, + CaretBrowsingEnabled, + NotificationsEnabled, + WebAudioEnabled + }; + enum WebGraphic { + MissingImageGraphic, + MissingPluginGraphic, + DefaultFrameIconGraphic, + TextAreaSizeGripCornerGraphic, + DeleteButtonGraphic, + InputSpeechButtonGraphic, + SearchCancelButtonGraphic, + SearchCancelButtonPressedGraphic + }; + enum FontSize { + MinimumFontSize, + MinimumLogicalFontSize, + DefaultFontSize, + DefaultFixedFontSize + }; + enum ThirdPartyCookiePolicy { + AlwaysAllowThirdPartyCookies, + AlwaysBlockThirdPartyCookies, + AllowThirdPartyWithExistingCookies + }; + + static WebSettings *globalSettings(); + + void setFontSize(FontSize type, int size); + int fontSize(FontSize type) const; + void resetFontSize(FontSize type); + + + void setFontFamily(FontFamily which, const QString &family); + QString fontFamily(FontFamily which) const; + void resetFontFamily(FontFamily which); + + void setAttribute(WebAttribute attr, bool on); + bool testAttribute(WebAttribute attr) const; + void resetAttribute(WebAttribute attr); + + void apply(); + + WebSettings(); + explicit WebSettings(AmneziaWebView *v); + virtual ~WebSettings(); + +private: + friend class WebSettingsData; + friend class AmneziaWebViewPrivate; + friend class WebViewPrivate; + + + + Q_DISABLE_COPY(WebSettings) + + QHash fontFamilies; + QHash fontSizes; + QHash attributes; + QString defaultTextEncoding; + AmneziaWebView *view; + +}; + +class AmneziaWebViewSettings : public QObject +{ + Q_OBJECT + + Q_PROPERTY(int defaultFontSize READ defaultFontSize WRITE setDefaultFontSize) + Q_PROPERTY(QString standardFontFamily READ standardFontFamily WRITE setStandardFontFamily) + Q_PROPERTY(bool developerExtrasEnabled READ developerExtrasEnabled WRITE setDeveloperExtrasEnabled) + +public: + explicit AmneziaWebViewSettings(AmneziaWebView *parent); + + int defaultFontSize() const { return s->fontSize(WebSettings::DefaultFontSize); } + void setDefaultFontSize(int size) { s->setFontSize(WebSettings::DefaultFontSize, size); } + + QString standardFontFamily() const { return s->fontFamily(WebSettings::StandardFont); } + void setStandardFontFamily(const QString& f) { s->setFontFamily(WebSettings::StandardFont, f); } + + bool developerExtrasEnabled() const { return s->testAttribute(WebSettings::DeveloperExtrasEnabled); } + void setDeveloperExtrasEnabled(bool on) { s->setAttribute(WebSettings::DeveloperExtrasEnabled, on); } + + void apply() { s->apply(); } + +private: + QScopedPointer s; +}; + +QML_DECLARE_TYPE(AmneziaWebViewSettings) + + +#endif diff --git a/client/core/webview/webview.json b/client/core/webview/webview.json new file mode 100644 index 000000000..2e09f9898 --- /dev/null +++ b/client/core/webview/webview.json @@ -0,0 +1,3 @@ +{ + "Keys": [ "AmneziaWebView" ] +} diff --git a/client/resources.qrc b/client/resources.qrc index be0a27858..0712e7ac9 100644 --- a/client/resources.qrc +++ b/client/resources.qrc @@ -163,6 +163,7 @@ ui/qml/Controls2/ListViewWithRadioButtonType.qml ui/qml/Controls2/PageType.qml ui/qml/Controls2/PopupType.qml + ui/qml/Controls2/PremiumBannerType.qml ui/qml/Controls2/ProgressBarType.qml ui/qml/Controls2/ScrollBarType.qml ui/qml/Controls2/StackViewType.qml @@ -226,6 +227,7 @@ ui/qml/Pages2/PageProtocolWireGuardClientSettings.qml ui/qml/Pages2/PageSetupWizardApiServiceInfo.qml ui/qml/Pages2/PageSetupWizardApiServicesList.qml + ui/qml/Pages2/PageSetupWizardPremiumWebView.qml ui/qml/Pages2/PageSetupWizardConfigSource.qml ui/qml/Pages2/PageSetupWizardCredentials.qml ui/qml/Pages2/PageSetupWizardEasy.qml @@ -253,6 +255,10 @@ ui/qml/Components/AwgTextField.qml ui/qml/Pages2/PageSettingsApiSubscriptionKey.qml ui/qml/Components/SmartScroll.qml + ui/qml/Components/AmneziaWebViewPanel.qml + + + core/webview/qmldir images/flagKit/ZW.svg diff --git a/client/ui/controllers/pageController.h b/client/ui/controllers/pageController.h index 529106343..2b15ce12c 100644 --- a/client/ui/controllers/pageController.h +++ b/client/ui/controllers/pageController.h @@ -60,6 +60,7 @@ namespace PageLoader PageSetupWizardQrReader, PageSetupWizardApiServicesList, PageSetupWizardApiServiceInfo, + PageSetupWizardPremiumWebView, PageProtocolOpenVpnSettings, PageProtocolShadowSocksSettings, diff --git a/client/ui/qml/Components/AmneziaWebViewPanel.qml b/client/ui/qml/Components/AmneziaWebViewPanel.qml new file mode 100644 index 000000000..f8093cc1b --- /dev/null +++ b/client/ui/qml/Components/AmneziaWebViewPanel.qml @@ -0,0 +1,261 @@ +import QtQuick 2.2 +import AmneziaWebView 1.0 +import Style 1.0 + +Rectangle { + + id: panel + + property alias title: webView.title + property alias icon: webView.icon + property alias progress: webView.progress + property alias html: webView.html + property alias url: webView.url + property alias back: webView.back + property alias stop: webView.stop + property alias reload: webView.reload + property alias forward: webView.forward + property alias pressGrabTime: webView.pressGrabTime + + property string onFailedUrl: "" + property string onFailedHtml: "

" + qsTr("Can`t load page") + "

" + property int preferredWidth: panel.width + property int preferredHeight: panel.height + + property var scriptResult: undefined + property var requestResult: undefined + + property var call_provider: undefined + property var call_data: undefined + + property string status: "unknown" // success, failed + + signal urlCalled(variant url) + signal alertCalled(variant message) + signal scriptCalled(string funName, variant args) + signal reloadCalled() + signal eventSended(variant event) + signal requestSended(variant request) + signal loadFinished() + signal loadFailed() + + color: AmneziaStyle.color.onyxBlack + + function call(funName, args) { + var script + if (panel.call_provider) { + panel.call_data = args + script = "call(" + funName + ")" + } else { + script = funName + "(" + JSON.stringify(args) + ")" + } + + webView.evaluateJavaScript(script) + } + + function event(e) { + call("receiveEvent", e) + } + + function request(r) { + var req = r + req.result = call("receiveRequest", req) + return req + } + + function newEvent(type) { + var e = {} + e.type = type + return e + } + + function newRequest(type) { + var r = {} + r.type = type + r.result = null + return r + } + + function toogleScale() { + webView.doToogleScale() + } + + function zoomIn() { + webView.doZoomIn() + } + + function zoomOut() { + webView.doZoomOut() + } + + + AmneziaWebView { + id: webView + anchors.fill: parent + + property bool loaded: false + focus: true + smooth: false + backgroundColor: AmneziaStyle.color.onyxBlack + + property int panelPreferredWidth: panel.preferredWidth + onPanelPreferredWidthChanged: { + webView.preferredWidth = panel.preferredWidth; + // if (webView.loaded) + // doZoomOrScale(); + } + + preferredWidth: panel.preferredWidth //webView.flexible ? panel.preferredWidth : Math.max(webView.contentsSize.width, 1024) + preferredHeight: panel.preferredHeight + + contentsScale: 1 + // onDoubleClick: { + // if (webView.flexible) + // return + + // async.call(doToogleScale) + // } + + function doToogleScale() { + // webView.needScale = !webView.needScale + // if (webView.needScale) { + // doScale() + // } else { + // webView.contentsScale = webView.zoomvalue + // } + } + + function doZoomOrScale() { + // if (webView.needScale) + // doScale() + // else + // webView.contentsScale = webView.zoomvalue; + } + + function doZoomIn() { + // if (webView.zoomvalue > 0.3) { + // webView.zoomvalue = webView.contentsScale + // webView.zoomvalue -= 0.1 + // webView.contentsScale = webView.zoomvalue + // webView.needScale = false + // } + } + + function doZoomOut() { + // if (webView.zoomvalue < 2.5) { + // webView.zoomvalue = webView.contentsScale + // webView.zoomvalue += 0.1 + // webView.contentsScale = webView.zoomvalue + // webView.needScale = false + // } + } + + function doScale() { + // var zoom = flickableItem.width / webView.preferredWidth + // webView.contentsScale = zoom; + } + + pressGrabTime: 400 + settings.defaultFontSize: 14 + settings.standardFontFamily: "Arial" + //settings.developerExtrasEnabled: isDebugEnabled + onAlert: panel.alertCalled(message) + onUrlChanged: { + //flickableItem.contentX = 0 + //flickableItem.contentY = 0 + panel.urlCalled(url) + } + onLoadStarted: { + webView.loaded = false + //webView.contentsScale = 1 + } + onLoadFinished: { + panel.status = "success" + webView.loaded = true + panel.loadFinished() + //async.call(doZoomOrScale) + } + onLoadFailed: { + console.debug("qml: html load failed: " + html) + if(html == ""){ + if (panel.onFailedUrl !== "") + panel.url = panel.onFailedUrl; + else if (panel.onFailedHtml !== "") + panel.html = panel.onFailedHtml; + } + + panel.status = "failed" + panel.loadFailed() + } + javaScriptWindowObjects: [ + QtObject { + AmneziaWebView.windowObjectName: "script" + function call(functionName, args) { + panel.scriptCalled(functionName, args) + return scriptResult; + } + }, + + QtObject { + AmneziaWebView.windowObjectName: "send" + function event(e) { + panel.eventSended(e) + } + function request(r) { + var req = r + panel.requestSended(req) + req.result = requestResult + return req; + } + }, + QtObject { + AmneziaWebView.windowObjectName: "log" + + function error(msg){ + webView.evaluateJavaScript("console.error('" + msg + "')") + return api.log.error(msg) + } + function warn(msg){ + webView.evaluateJavaScript("console.warn('" + msg + "')") + return api.log.warn(msg) + } + + function info(msg){ + webView.evaluateJavaScript("console.info('" + msg + "')") + return api.log.info(msg) + } + + function debug(msg){ + webView.evaluateJavaScript("console.log('" + msg + "')") + return api.log.debug(msg) + } + + function time(tag, msg){ + webView.evaluateJavaScript("console.log('" + msg + "')") + return api.log.time(tag, msg) + } + }, + + QtObject { + AmneziaWebView.windowObjectName: "webView" + + function scrollUp() { + //flickableItem.contentY = 0; + } + + function reload() { + //panel.reloadCalled() + } + }, + + QtObject { + AmneziaWebView.windowObjectName: "call_provider" + function data(fn) { + return panel.call_provider ? panel.call_provider.data(fn, panel.call_data) : panel.call_data + } + } + ] + } + +} + diff --git a/client/ui/qml/Controls2/PremiumBannerType.qml b/client/ui/qml/Controls2/PremiumBannerType.qml new file mode 100644 index 000000000..7b5600a14 --- /dev/null +++ b/client/ui/qml/Controls2/PremiumBannerType.qml @@ -0,0 +1,123 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import Style 1.0 + +Button { + id: root + + property string headerText: qsTr("Try Amnesia Premium") + property string bodyText: qsTr("High speed and 20 countries to connect to 7 days free.") + + property string hoveredColor: AmneziaStyle.color.charcoalGray + property string defaultColor: AmneziaStyle.color.onyxBlack + property int paddingContent: 20 + + hoverEnabled: true + height: contentItemRoot.implicitHeight + width: contentItemRoot.implicitWidth + + background: Rectangle { + id: backgroundRect + + anchors.fill: parent + radius: 16 + + color: root.hovered ? hoveredColor : defaultColor + + Behavior on color { + PropertyAnimation { duration: 200 } + } + } + + contentItem: Item { + id: contentItemRoot + anchors.fill: parent + implicitHeight: content.implicitHeight + root.paddingContent * 2 + implicitWidth: content.implicitWidth + root.paddingContent * 2 + anchors.margins: root.paddingContent + + GridLayout { + id: content + anchors.fill: parent + columns: 2 + columnSpacing: 5 + + Item { + Layout.fillWidth: true + Layout.minimumWidth: 0 // Позволяет сжиматься + implicitHeight: textColumn.implicitHeight + + Column { + id: textColumn + anchors.left: parent.left + anchors.right: parent.right + spacing: 6 + + // Заголовок с выделением "Premium" + Text { + width: parent.width + text: qsTr("Try Amnezia ") + 'Premium' + textFormat: Text.RichText + color: AmneziaStyle.color.paleGray + font.pixelSize: 20 + font.weight: 700 + font.family: "PT Root UI VF" + wrapMode: Text.WordWrap + } + + // Описание + Text { + text: root.bodyText + wrapMode: Text.WordWrap + color: AmneziaStyle.color.mutedGray + font.pixelSize: 14 + font.weight: 400 + font.family: "PT Root UI VF" + lineHeight: 20 + lineHeightMode: Text.FixedHeight + width: parent.width + } + } + } + + // Стрелка справа + Item { + implicitWidth: 40 + implicitHeight: 40 + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + Layout.minimumWidth: 40 + Layout.maximumWidth: 40 + Layout.preferredWidth: 40 + + Image { + anchors.centerIn: parent + source: "qrc:/images/controls/chevron-right.svg" + sourceSize: Qt.size(24, 24) + } + } + } + } + + + MouseArea { + anchors.fill: parent + + cursorShape: Qt.PointingHandCursor + hoverEnabled: true + enabled: root.enabled + + onEntered: { + backgroundRect.color = root.hoveredColor + } + + onExited: { + backgroundRect.color = root.defaultColor + } + + onClicked: { + root.clicked() + } + } +} diff --git a/client/ui/qml/Pages2/PageHome.qml b/client/ui/qml/Pages2/PageHome.qml index 1615d2761..402845e39 100644 --- a/client/ui/qml/Pages2/PageHome.qml +++ b/client/ui/qml/Pages2/PageHome.qml @@ -204,6 +204,84 @@ PageType { Layout.rightMargin: 16 Layout.topMargin: 22 } + + PremiumBannerType { + id: premiumBannerHome + + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 16 + Layout.bottomMargin: 16 + + property bool isAmneziaFree: { + if (ServersModel.getServersCount() > 0 && ServersModel.isDefaultServerFromApi) { + var apiConfig = ServersModel.getDefaultServerData("apiConfig") + if (apiConfig) { + // Получаем serviceType из apiConfig (ключ "service_type") + var serviceType = "" + if (apiConfig.service_type !== undefined) { + serviceType = apiConfig.service_type + } else if (apiConfig.serviceType !== undefined) { + serviceType = apiConfig.serviceType + } + + if (serviceType === "amnezia-free") { + return true + } + } + // Альтернативная проверка через имя сервера + var serverName = ServersModel.defaultServerName + if (serverName) { + var nameLower = serverName.toString().toLowerCase() + if (nameLower.indexOf("free") >= 0) { + return true + } + } + } + return false + } + + Connections { + target: ServersModel + function onDefaultServerIndexChanged() { + // Пересчитываем isAmneziaFree при изменении сервера + var apiConfig = ServersModel.getDefaultServerData("apiConfig") + var newValue = false + if (ServersModel.getServersCount() > 0 && ServersModel.isDefaultServerFromApi && apiConfig) { + var serviceType = "" + if (apiConfig.service_type !== undefined) { + serviceType = apiConfig.service_type + } else if (apiConfig.serviceType !== undefined) { + serviceType = apiConfig.serviceType + } + + if (serviceType === "amnezia-free") { + newValue = true + } else { + var serverName = ServersModel.defaultServerName + if (serverName) { + var nameLower = serverName.toString().toLowerCase() + if (nameLower.indexOf("free") >= 0) { + newValue = true + } + } + } + } + premiumBannerHome.isAmneziaFree = newValue + } + } + + visible: isAmneziaFree + enabled: visible + + onClicked: { + PageController.goToPage(PageEnum.PageSetupWizardPremiumWebView) + } + + Keys.onEnterPressed: clicked() + Keys.onReturnPressed: clicked() + } } } diff --git a/client/ui/qml/Pages2/PageSetupWizardApiServicesList.qml b/client/ui/qml/Pages2/PageSetupWizardApiServicesList.qml index edf361f77..fd2cb64f4 100644 --- a/client/ui/qml/Pages2/PageSetupWizardApiServicesList.qml +++ b/client/ui/qml/Pages2/PageSetupWizardApiServicesList.qml @@ -16,6 +16,8 @@ import "../Config" PageType { id: root + property bool isAmneziaFreeSelected: false + BackButtonType { id: backButton @@ -31,8 +33,8 @@ PageType { } } - ListViewType { - id: listView + ColumnLayout { + id: mainLayout anchors.top: backButton.bottom anchors.right: parent.right @@ -40,23 +42,46 @@ PageType { anchors.bottom: parent.bottom anchors.topMargin: 16 - header: ColumnLayout { - width: listView.width - - BaseHeaderType { - Layout.fillWidth: true - Layout.rightMargin: 16 - Layout.leftMargin: 16 - Layout.bottomMargin: 24 - - headerText: qsTr("VPN by Amnezia") - descriptionText: qsTr("Choose a VPN service that suits your needs.") - } - } - spacing: 0 - model: SortFilterProxyModel { + BaseHeaderType { + Layout.fillWidth: true + Layout.rightMargin: 16 + Layout.leftMargin: 16 + Layout.bottomMargin: 24 + + headerText: qsTr("VPN by Amnezia") + descriptionText: qsTr("Choose a VPN service that suits your needs.") + } + + PremiumBannerType { + id: premiumBanner + + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 + + visible: root.isAmneziaFreeSelected + enabled: visible + + onClicked: { + PageController.goToPage(PageEnum.PageSetupWizardPremiumWebView) + } + + Keys.onEnterPressed: clicked() + Keys.onReturnPressed: clicked() + } + + ListViewType { + id: listView + + Layout.fillWidth: true + Layout.fillHeight: true + + spacing: 0 + + model: SortFilterProxyModel { id: proxyApiServicesModel sourceModel: ApiServicesModel @@ -88,8 +113,29 @@ PageType { onClicked: { if (isServiceAvailable) { - ApiServicesModel.setServiceIndex(proxyApiServicesModel.mapToSource(index)) - PageController.goToPage(PageEnum.PageSetupWizardApiServiceInfo) + var sourceIndex = proxyApiServicesModel.mapToSource(index) + + // Устанавливаем индекс ПЕРЕД проверкой типа + ApiServicesModel.setServiceIndex(sourceIndex) + + // Проверяем тип через метод + var serviceType = ApiServicesModel.getSelectedServiceType() + + // Также проверяем имя напрямую из делегата + var nameLower = name ? name.toString().toLowerCase() : "" + var nameHasFree = nameLower.indexOf("free") >= 0 + + // Комбинированная проверка + var isAmneziaFree = (serviceType === "amnezia-free") || nameHasFree + + if (isAmneziaFree) { + // Показываем баннер + root.isAmneziaFreeSelected = true + } else { + // Скрываем баннер и переходим на страницу сервиса + root.isAmneziaFreeSelected = false + PageController.goToPage(PageEnum.PageSetupWizardApiServiceInfo) + } } } @@ -97,5 +143,6 @@ PageType { Keys.onReturnPressed: clicked() } } + } } } diff --git a/client/ui/qml/Pages2/PageSetupWizardPremiumWebView.qml b/client/ui/qml/Pages2/PageSetupWizardPremiumWebView.qml new file mode 100644 index 000000000..41bf9b273 --- /dev/null +++ b/client/ui/qml/Pages2/PageSetupWizardPremiumWebView.qml @@ -0,0 +1,69 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import PageEnum 1.0 +import Style 1.0 +import AmneziaWebView 1.0 + +import "./" +import "../Controls2" +import "../Controls2/TextTypes" +import "../Config" +import "../Components" + +PageType { + id: root + + property string premiumUrl: LanguageModel.getCurrentLanguageIndex() === 1 // 1 = Russian + ? "https://storage.googleapis.com/amnezia/amnezia.org?m-path=/ru/premium" + : "https://storage.googleapis.com/amnezia/amnezia.org?m-path=/en/premium" + + BackButtonType { + id: backButton + + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.topMargin: 20 + SettingsController.safeAreaTopMargin + + z: 1000 // Ensure BackButton is above WebView + + onActiveFocusChanged: { + // Focus handling + } + } + + ColumnLayout { + id: webViewContainer + + anchors.top: backButton.bottom + anchors.right: parent.right + anchors.left: parent.left + anchors.bottom: parent.bottom + anchors.topMargin: 16 + + spacing: 0 + + AmneziaWebView { + id: webView + + Layout.fillWidth: true + Layout.fillHeight: true + + preferredWidth: webViewContainer.width + preferredHeight: webViewContainer.height + + url: root.premiumUrl + + onLoadFailed: { + console.error("Failed to load Premium page:", root.premiumUrl) + } + + onLoadFinished: { + console.log("Premium page loaded successfully") + } + } + } +} +