Commit 6599a4ef authored by Kai Uwe Broulik's avatar Kai Uwe Broulik 🍇

Add browser history runner

Being a DBus runner inside p-b-i has the obvious disadvantage of only working
when the browser is running but I have it open all the time anyway.

It is added as an optional permission as to not cause prompts after upgrading
the extension. Instead, it asks the user to grant the permission either on settings
screen or in KRunner results.

Results are primarily scored by whether the title contains (starts with rated higher),
host contains (+ starts with), path contains (+ starts with) in the same way as is
done in tabs runner, except that the base score is slightly lower. Additionally, the
number of visits to the pages (visited by opening the page, and visited by explicitly
typing into the address bar, the latter of which is scored higher) is taken into account
relative to the result set for added accuracy.
parent b34a8847
......@@ -84,6 +84,11 @@
"message": "Note: The URL may contain sensitive information that could be disclosed when the file is accessible by or shared with others"
},
"options_heading_krunner": {
"description": "Title for settings about KRunner plugins",
"message": "Plasma Search"
},
"options_plugin_tabsrunner_title": {
"description": "Title for Browser Tabs KRunner plugin",
"message": "Find browser tabs in “Run Command” window"
......@@ -93,6 +98,15 @@
"message": "Make sure the “Browser Tabs” module is enabled in <a id=\"$1\" href=\"$2\">Plasma Search settings</a>."
},
"options_plugin_historyrunner_title": {
"description": "Title for Browser History KRunner plugin",
"message": "Search through browser history"
},
"options_plugin_historyrunner_description": {
"description": "Description for Browser History KRunner plugin",
"message": "This feature might need <a id=\"$1\" href=\"$2\">additional permissions</a> to be used."
},
"options_plugin_purpose_title": {
"description": "Title for Purpose / Web Share plugin",
"message": "Content Sharing"
......@@ -185,6 +199,30 @@
"message": "Could not share this content: $1"
},
"permission_request_title": {
"description": "Title for page about requesting additional permissions",
"message": "Additional permissions required"
},
"permission_request_historyrunner_1": {
"description": "Explanation for why additional permissions are needed",
"message": "In order to provide search results for your browser history, additional permissions are required."
},
"permission_request_historyrunner_2": {
"description": "Explanation about disabling history runner",
"message": "You can disable this feature in <a id=\"$1\" href=\"$2\">Plasma search settings</a>."
},
"permission_request_already": {
"message": "You have already granted this permission."
},
"permission_request_button_request": {
"description": "@action:button",
"message": "Request Permission"
},
"permission_request_button_revoke": {
"description": "@action:button",
"message": "Revoke Permission"
},
"general_error_unknown": {
"description": "An unknown error occurred, usually used when an error message by the system is not provided",
"message": "Unknown Error"
......
......@@ -35,6 +35,9 @@ DEFAULT_EXTENSION_SETTINGS = {
tabsrunner: {
enabled: true
},
historyrunner: {
enabled: true
},
purpose: {
enabled: true
},
......
/*
Copyright (C) 2020 Kai Uwe Broulik <kde@broulik.de>
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License as
published by the Free Software Foundation; either version 3 of
the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
let defaultFaviconData = "";
function getFavicon(url) {
return new Promise((resolve) => {
let xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (xhr.readyState !== XMLHttpRequest.DONE) {
return;
}
if (!xhr.response) {
return resolve();
}
let reader = new FileReader();
reader.onloadend = () => {
resolve(reader.result);
}
reader.readAsDataURL(xhr.response);
}
xhr.open("GET", "chrome://favicon/" + url);
xhr.responseType = "blob";
xhr.send();
});
}
addCallback("historyrunner", "find", (message) => {
const query = message.query;
chrome.permissions.contains({
permissions: ["history"]
}, (granted) => {
if (!granted) {
sendPortMessage("historyrunner", "found", {
query,
error: "NO_PERMISSION"
});
return;
}
chrome.history.search({
text: query,
maxResults: 15,
// By default searches only the past 24 hours but we want everything
startTime: 0
}, (results) => {
let promises = [];
// Collect open tabs for each history item URL to filter them out below
results.forEach((result) => {
promises.push(new Promise((resolve) => {
chrome.tabs.query({
url: result.url
}, (tabs) => {
if (!tabs || chrome.runtime.lastError) {
return resolve([]);
}
resolve(tabs);
});
}));
});
Promise.all(promises).then((tabs) => {
// Now filter out the entries with corresponding tabs we found earlier
results = results.filter((result, index) => {
return tabs[index].length === 0;
});
// Now fetch all favicons from special favicon provider URL
// There's no public API for this.
// chrome://favicon/ works on Chrome "by accident", and for
// Firefox' page-icon: scheme there is https://bugzilla.mozilla.org/show_bug.cgi?id=1315616
if (IS_FIREFOX) {
return;
}
promises = [];
results.forEach((result) => {
promises.push(getFavicon(result.url));
});
return Promise.all(promises);
}).then((favicons) => {
if (favicons) {
favicons.forEach((favicon, index) => {
if (favicon) {
results[index].favIconUrl = favicon;
}
});
// Now get the default favicon if we don't have it already...
if (!defaultFaviconData) {
return getFavicon("");
}
}
}).then((faviconData) => {
if (faviconData) {
defaultFaviconData = faviconData;
}
if (defaultFaviconData) {
// ...and remove icon from all results that have the default one
results.forEach((result) => {
if (result.favIconUrl === defaultFaviconData) {
result.favIconUrl = "";
}
});
}
sendPortMessage("historyrunner", "found", {
query,
results
});
});
});
});
});
addCallback("historyrunner", "run", (message) => {
const url = message.url;
chrome.tabs.create({
url
});
});
addCallback("historyrunner", "requestPermission", () => {
chrome.tabs.create({
url: chrome.extension.getURL("permission_request.html") + "?permission=history"
});
});
......@@ -30,6 +30,7 @@
"extension-downloads.js",
"extension-tabsrunner.js",
"extension-purpose.js",
"extension-historyrunner.js",
"extension.js"
],
......@@ -81,6 +82,11 @@
"<all_urls>",
"contextMenus"
],
"optional_permissions": [
"history"
],
"applications": {
"gecko": {
"id": "plasma-browser-integration@kde.org",
......
......@@ -78,11 +78,20 @@
<p data-i18n="options_plugin_downloads_saveOriginUrl_description" data-i18n-html="true">I18N</p>
</li>
<li>
<label data-i18n="options_heading_krunner">I18N</label>
</li>
<li class="dependent">
<label>
<input type="checkbox" data-extension="tabsrunner" data-settings-key="enabled"> <span data-i18n="options_plugin_tabsrunner_title">I18N</span>
</label>
<p data-i18n="options_plugin_tabsrunner_description, open-krunner-settings, #" data-i18n-html="true">I18N</p>
</li>
<li class="dependent" data-requires-extension="historyrunner">
<label>
<input type="checkbox" data-extension="historyrunner" data-settings-key="enabled"> <span data-i18n="options_plugin_historyrunner_title">I18N</span>
</label>
<p data-i18n="options_plugin_historyrunner_description, request-permission-history, #" data-i18n-html="true">I18N</p>
</li>
<li data-requires-extension="purpose">
<label>
<input type="checkbox" data-extension="purpose" data-settings-key="enabled"> <span data-i18n="options_plugin_purpose_title">I18N</span>
......
......@@ -151,6 +151,16 @@ function updateDependencies(control, extension, settingsKey) {
}
}
function askPermission(permission) {
return new Promise((resolve, reject) => {
chrome.permissions.request({
permissions: [permission]
}, (result) => {
resolve(result);
});
});
}
document.addEventListener("DOMContentLoaded", function () {
// poor man's tab widget :)
......@@ -251,6 +261,21 @@ versionInfo.host);
event.preventDefault();
});
document.getElementById("request-permission-history").addEventListener("click", (e) => {
askPermission("history");
e.preventDefault();
});
// When trying to enable historyrunner check if user accepted the permission
const historyRunnerCheckBox = document.querySelector("[data-extension=historyrunner][data-settings-key=enabled]");
historyRunnerCheckBox.addEventListener("click", (e) => {
if (historyRunnerCheckBox.checked) {
askPermission("history").then((granted) => {
historyRunnerCheckBox.checked = granted;
});
}
});
// Make translators credit behave like the one in KAboutData
var translatorsAboutData = "";
......
/*
Copyright (C) 2020 Kai Uwe Broulik <kde@broulik.de>
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License as
published by the Free Software Foundation; either version 3 of
the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
const permissions = {
history: {
rationale: [
chrome.i18n.getMessage("permission_request_historyrunner_1"),
chrome.i18n.getMessage("permission_request_historyrunner_2", ["open-krunner-settings", "#"])
]
}
}
document.addEventListener("DOMContentLoaded", () => {
const urlParams = new URLSearchParams(window.location.search);
const permission = urlParams.get('permission');
const textItem = document.getElementById("permission-text");
const rationaleList = document.getElementById("permission-rationale");
const requestButton = document.getElementById("request-permission");
const revokeButton = document.getElementById("revoke-permission");
if (!permission) {
return;
}
if (!permissions[permission]) {
console.error("Cannot request unknown permission", permission);
return;
}
chrome.permissions.contains({
permissions: [permission]
}, (granted) => {
if (granted) {
textItem.innerText = chrome.i18n.getMessage("permission_request_already");
revokeButton.addEventListener("click", () => {
chrome.permissions.remove({
permissions: [permission]
}, (ok) => {
if (ok) {
window.close();
return;
}
if (chrome.runtime.lastError) {
alert(chrome.runtime.lastError.message);
}
});
});
revokeButton.classList.remove("hidden");
return;
}
(permissions[permission].rationale || []).forEach((rationale) => {
let rationaleItem = document.createElement("li");
rationaleItem.innerHTML = rationale;
rationaleList.appendChild(rationaleItem);
});
const krunnerSettingsLink = document.getElementById("open-krunner-settings");
if (krunnerSettingsLink) {
krunnerSettingsLink.addEventListener("click", (e) => {
sendMessage("settings", "openKRunnerSettings");
e.preventDefault();
});
}
requestButton.addEventListener("click", () => {
chrome.permissions.request({
permissions: [permission]
}, (granted) => {
if (granted) {
window.close();
return;
}
if (chrome.runtime.lastError) {
alert(chrome.runtime.lastError.message);
return;
}
});
});
requestButton.classList.remove("hidden");
});
});
.hidden {
display: none;
}
button {
display: block;
background-color: rgb(239, 240, 241);
/*background-image: linear-gradient(rgb(242, 242, 243), rgb(232, 233, 234));*/
border: 1px rgb(188, 190, 191) solid;
border-radius: 3px;
box-shadow: .5px .5px .5px .5px rgba(35,38,39,.1);
padding: 6px 12px;
color: #232627 !important;
}
button:hover {
border-color: #93cee9 !important;
background-color: #eff0f1 !important;
}
button:focus:not(:active) {
color: #fcfcfc !important;
border-color: #3daee9 !important;
background-image: linear-gradient(180deg,#40afe9,#35abe8);
}
button:active {
background-image: linear-gradient(180deg,#96cfea,#8acae7);
box-shadow: .5px .5px .5px .5px rgba(35,38,39,.1);
transform: translate(1px,1px);
}
a {
color: #316f98;
}
a:hover {
color: #3daefd;
}
h1 {
font-size: 2rem;
margin-bottom: 0.5rem;
}
body {
font-size: 16px;
font-family: Noto Sans
}
.permission-request {
margin-top: 0.5rem;
margin-bottom: 0.5rem;
}
#permission-rationale {
padding-left: 0;
}
#permission-rationale li {
display: block;
}
@media (prefers-color-scheme: dark) {
body {
background-color: #232629 !important;
color: #eff0f1 !important;
}
button {
box-shadow: 0.5px 0.5px 0.5px 0.5px rgb(73 78 80 / 10%);
background-color: #31363b;
color: #eff0f1 !important;
border: 1px #383e43 solid;
}
button:focus, button:active, button:hover {
background-color: #4d4d4d;
background-image: linear-gradient(180deg,#096a9b,#00527d)
}
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title data-i18n="permission_request_title">I18N</title>
<title>Permission Request</title>
<script src="constants.js"></script>
<script src="i18n.js"></script>
<script src="utils.js"></script>
<script src="content-utils.js"></script>
<script src="permission-request.js"></script>
<link rel="stylesheet" href="permission_request.css">
</head>
<body>
<div class="permission-request">
<h1 data-i18n="permission_request_title">I18N</h1>
<p id="permission-text"></p>
<ul id="permission-rationale"></ul>
<button id="request-permission" class="hidden" data-i18n="permission_request_button_request">I18N</button>
<button id="revoke-permission" class="hidden" data-i18n="permission_request_button_revoke">I18N</button>
</div>
</body>
</html>
......@@ -12,6 +12,7 @@ set(HOST_SOURCES main.cpp
kdeconnectplugin.cpp
downloadplugin.cpp
downloadjob.cpp
historyrunnerplugin.cpp
tabsrunnerplugin.cpp
purposeplugin.cpp
)
......@@ -41,3 +42,5 @@ target_link_libraries(
)
install(TARGETS plasma-browser-integration-host ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})
install(FILES plasma-runner-browserhistory.desktop DESTINATION ${KDE_INSTALL_DATAROOTDIR}/krunner/dbusplugins)
/*
* Copyright (C) 2020 Kai Uwe Broulik <kde@broulik.de>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 3 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "historyrunnerplugin.h"
#include "connection.h"
#include "settings.h"
#include <QDBusConnection>
#include <QJsonArray>
#include <QImage>
#include <QSet>
#include <QUrl>
#include <QVariant>
#include <KLocalizedString>
#include <algorithm>
static const auto s_idSeparator = QLatin1String("@@@");
static const auto s_errorNoPermission = QLatin1String("NO_PERMISSION");
static const auto s_idRequestPermission = QLatin1String("REQUEST_PERMISSION");
HistoryRunnerPlugin::HistoryRunnerPlugin(QObject *parent)
: AbstractKRunnerPlugin(QStringLiteral("/HistoryRunner"),
QStringLiteral("historyrunner"),
1,
parent)
{
}
RemoteActions HistoryRunnerPlugin::Actions()
{
return {};
}
RemoteMatches HistoryRunnerPlugin::Match(const QString &searchTerm)
{
if (searchTerm.length() < 3) {
sendErrorReply(QDBusError::InvalidArgs, QStringLiteral("Search term too short"));
return {};
}
setDelayedReply(true);
const bool runQuery = !m_requests.contains(searchTerm);
// It's a multi-hash, so all requests for identical search terms
// will be replied to at once when the results come in
m_requests.insert(searchTerm, message());
if (runQuery) {
sendData(QStringLiteral("find"), {
{QStringLiteral("query"), searchTerm}
});
}
return {};
}
void HistoryRunnerPlugin::Run(const QString &id, const QString &actionId)
{
if (!actionId.isEmpty()) {
sendErrorReply(QDBusError::InvalidArgs, QStringLiteral("Unknown action ID"));
return;
}
if (id == s_idRequestPermission) {
sendData(QStringLiteral("requestPermission"));
return;
}
const int separatorIdx = id.indexOf(s_idSeparator);
if (separatorIdx <= 0) {