Commit 3dee3669 authored by Thomas Greiner's avatar Thomas Greiner

Issue 7106 - Added configuration options and extension details to issue reports

parent 27fa83f7
Pipeline #44161106 passed with stages
in 8 minutes and 28 seconds
......@@ -198,6 +198,11 @@ html[dir="rtl"] #typeSelectorGroup > p
flex-grow: 1;
}
#showData
{
margin: 0;
}
#showDataValue
{
overflow: auto;
......@@ -471,8 +476,8 @@ body[data-page="commentPage"] #continue
font-size: 0.9rem;
}
#typeSelectorGroup input[type="radio"],
#anonymousSubmission
input[type="checkbox"],
input[type="radio"]
{
vertical-align: top;
}
......@@ -112,6 +112,53 @@
}
ext._Port.prototype.postMessage = postMessage;
// This API is injected at runtime so we can't rely on our promise polyfill
// and therefore only support callbacks
browser.contentSettings = {
cookies: {
get(details, callback)
{
callback({setting: "allow"});
}
},
javascript: {
get(details, callback)
{
callback({setting: "allow"});
}
}
};
// This API is injected at runtime so we can't rely on our promise polyfill
// and therefore only support callbacks
browser.management = {
getAll(callback)
{
callback([
{
enabled: true,
id: "cfhdojbkjhnklbpkdaibdccddilifddb",
name: "Adblock Plus",
type: "extension",
version: "3.4"
}
]);
}
};
browser.permissions = {
request(opts)
{
const granted = confirm(`Grant permissions?\n${opts.permissions}`);
return Promise.resolve(granted);
},
remove(opts)
{
return Promise.resolve(true);
}
};
function connect({name})
{
ext.backgroundPage._sendRawMessage({type: "connect", name});
......@@ -119,7 +166,26 @@
}
browser.runtime.connect = connect;
const getTab = (url) => ({id: ++tabCounter, url});
function getBrowserInfo()
{
return Promise.resolve({
name: "adblockplusfirefox",
buildID: "20161018004015"
});
}
browser.runtime.getBrowserInfo = getBrowserInfo;
browser.runtime.getURL = (path) => path;
function getTab(url)
{
return {
id: ++tabCounter,
incognito: false,
openerTabId: 1,
url
};
}
let tabCounter = 0;
let activeTab = getTab("https://example.com/");
......@@ -153,6 +219,14 @@
return Promise.resolve(tab);
};
function executeScript()
{
return Promise.resolve([
"https://example.com/referrer"
]);
}
browser.tabs.executeScript = executeScript;
browser.tabs.get = (tabId) =>
{
const tab = tabs.get(tabId);
......
......@@ -93,8 +93,12 @@
<label for="comment" class="i18n_issueReporter_comment_label"></label>
<p class="i18n_issueReporter_comment_description"></p>
<textarea id="comment"></textarea>
<p id="includeConfigContainer">
<input id="includeConfig" type="checkbox">
<label for="includeConfig" data-i18n="issueReporter_config_label"></label>
</p>
<p>
<button id="showData" class="i18n_issueReporter_showData_label link" disabled></button>
<button id="showData" class="i18n_issueReporter_showData_label link" disabled></button>
</p>
</div>
......
......@@ -23,6 +23,15 @@ let dataGatheringTabId = null;
let isMinimumTimeMet = false;
const port = browser.runtime.connect({name: "ui"});
function getOriginalTabId()
{
const tabId = parseInt(location.search.replace(/^\?/, ""), 10);
if (!tabId && tabId !== 0)
throw new Error("invalid tab id");
return tabId;
}
port.onMessage.addListener((message) =>
{
switch (message.type)
......@@ -88,20 +97,26 @@ module.exports = {
closeRequestsCollectingTab,
collectData()
{
const tabId = parseInt(location.search.replace(/^\?/, ""), 10);
if (!tabId && tabId !== 0)
return Promise.reject(new Error("invalid tab id"));
let tabId;
try
{
tabId = getOriginalTabId();
}
catch (ex)
{
return Promise.reject(ex);
}
return Promise.all([
retrieveAddonInfo(),
retrieveApplicationInfo(),
retrievePlatformInfo(),
retrieveTabURL(tabId),
retrieveWindowInfo(tabId),
collectRequests(tabId),
retrieveSubscriptions()
]).then(() => reportData);
}
},
updateConfigurationInfo
};
function collectRequests(tabId)
......@@ -198,32 +213,64 @@ function retrieveApplicationInfo()
function retrievePlatformInfo()
{
const element = reportData.createElement("platform");
return browser.runtime.sendMessage({
type: "app.get",
what: "platform"
}).then(platform =>
{
element.setAttribute("name", capitalize(platform));
return browser.runtime.sendMessage({
const {getBrowserInfo, sendMessage} = browser.runtime;
return Promise.all([
// Only Firefox supports browser.runtime.getBrowserInfo()
(getBrowserInfo) ? getBrowserInfo() : null,
sendMessage({
type: "app.get",
what: "platform"
}),
sendMessage({
type: "app.get",
what: "platformVersion"
});
}).then(platformVersion =>
})
])
.then(([browserInfo, platform, platformVersion]) =>
{
if (browserInfo)
{
element.setAttribute("build", browserInfo.buildID);
}
element.setAttribute("name", capitalize(platform));
element.setAttribute("version", platformVersion);
reportData.documentElement.appendChild(element);
});
}
function retrieveTabURL(tabId)
function retrieveWindowInfo(tabId)
{
return browser.tabs.get(tabId).then(tab =>
{
const element = reportData.createElement("window");
if (tab.url)
element.setAttribute("url", censorURL(tab.url));
reportData.documentElement.appendChild(element);
});
return browser.tabs.get(tabId)
.then((tab) =>
{
let openerUrl = null;
if (tab.openerTabId)
{
openerUrl = browser.tabs.get(tab.openerTabId)
.then((openerTab) => openerTab.url);
}
const referrerUrl = browser.tabs.executeScript(tabId, {
code: "document.referrer"
})
.then(([referrer]) => referrer);
return Promise.all([tab.url, openerUrl, referrerUrl]);
})
.then(([url, openerUrl, referrerUrl]) =>
{
const element = reportData.createElement("window");
if (openerUrl)
{
element.setAttribute("opener", censorURL(openerUrl));
}
if (referrerUrl)
{
element.setAttribute("referrer", censorURL(referrerUrl));
}
element.setAttribute("url", censorURL(url));
reportData.documentElement.appendChild(element);
});
}
function retrieveSubscriptions()
......@@ -276,6 +323,238 @@ function retrieveSubscriptions()
});
}
function setConfigurationInfo(configInfo)
{
let extensionsContainer = reportData.querySelector("extensions");
let optionsContainer = reportData.querySelector("options");
if (!configInfo)
{
if (extensionsContainer)
{
extensionsContainer.parentNode.removeChild(extensionsContainer);
}
if (optionsContainer)
{
optionsContainer.parentNode.removeChild(optionsContainer);
}
return;
}
if (!extensionsContainer)
{
extensionsContainer = reportData.createElement("extensions");
reportData.documentElement.appendChild(extensionsContainer);
}
if (!optionsContainer)
{
optionsContainer = reportData.createElement("options");
reportData.documentElement.appendChild(optionsContainer);
}
extensionsContainer.innerHTML = "";
optionsContainer.innerHTML = "";
const {extensions, options} = configInfo;
for (const id in options)
{
const element = reportData.createElement("option");
element.setAttribute("id", id);
element.textContent = options[id];
optionsContainer.appendChild(element);
}
for (const extension of extensions)
{
const element = reportData.createElement("extension");
element.setAttribute("id", extension.id);
element.setAttribute("name", extension.name);
element.setAttribute("type", extension.type);
if (extension.version)
{
element.setAttribute("version", extension.version);
}
extensionsContainer.appendChild(element);
}
}
// Chrome doesn't update the JavaScript context to reflect changes in the
// extension's permissions so we need to proxy our calls through a frame that
// loads after we request the necessary permissions
// https://bugs.chromium.org/p/chromium/issues/detail?id=594703
function proxyApiCall(apiId, ...args)
{
return new Promise((resolve) =>
{
const iframe = document.createElement("iframe");
iframe.hidden = true;
iframe.src = browser.runtime.getURL("proxy.html");
iframe.onload = () =>
{
function callback(...results)
{
document.body.removeChild(iframe);
resolve(results[0]);
}
const api = iframe.contentWindow.chrome || iframe.contentWindow.browser;
switch (apiId)
{
case "contentSettings.cookies":
if ("contentSettings" in browser)
{
// This API is injected at runtime so we can't rely on our
// promise polyfill and therefore only support callbacks
browser.contentSettings.cookies.get(...args, callback);
}
else if ("contentSettings" in api)
{
api.contentSettings.cookies.get(...args, callback);
}
else
{
callback(null);
}
break;
case "contentSettings.javascript":
if ("contentSettings" in browser)
{
// This API is injected at runtime so we can't rely on our
// promise polyfill and therefore only support callbacks
browser.contentSettings.javascript.get(...args, callback);
}
else if ("contentSettings" in api)
{
api.contentSettings.javascript.get(...args, callback);
}
else
{
callback(null);
}
break;
case "management.getAll":
if ("getAll" in browser.management)
{
// This API is injected at runtime so we can't rely on our
// promise polyfill and therefore only support callbacks
browser.management.getAll(...args, callback);
}
else if ("getAll" in api.management)
{
api.management.getAll(...args, callback);
}
else
{
callback(null);
}
break;
}
};
document.body.appendChild(iframe);
});
}
function retrieveExtensions()
{
return proxyApiCall("management.getAll")
.then((installed) =>
{
const extensions = [];
for (const extension of installed)
{
if (!extension.enabled || extension.type != "extension")
continue;
extensions.push({
id: extension.id,
name: extension.name,
type: "extension",
version: extension.version
});
}
const {plugins} = navigator;
for (const plugin of plugins)
{
extensions.push({
id: plugin.filename,
name: plugin.name,
type: "plugin"
});
}
return extensions;
})
.catch((err) =>
{
console.error("Could not retrieve list of extensions");
return [];
});
}
function retrieveOptions()
{
// Firefox doesn't support browser.contentSettings API
if (!("contentSettings" in browser))
return Promise.resolve({});
let tabId;
try
{
tabId = getOriginalTabId();
}
catch (ex)
{
return Promise.reject(ex);
}
return browser.tabs.get(tabId)
.then((tab) =>
{
const details = {primaryUrl: tab.url, incognito: tab.incognito};
return Promise.all([
proxyApiCall("contentSettings.cookies", details),
proxyApiCall("contentSettings.javascript", details),
tab.incognito
]);
})
.then(([cookies, javascript, incognito]) =>
{
return {
cookieBehavior: cookies.setting == "allow" ||
cookies.setting == "session_only",
javascript: javascript.setting == "allow",
privateBrowsing: incognito
};
})
.catch((err) =>
{
console.error("Could not retrieve configuration options");
return {};
});
}
function updateConfigurationInfo(isAccessible)
{
if (!isAccessible)
{
setConfigurationInfo(null);
return Promise.resolve();
}
return Promise.all([
retrieveExtensions(),
retrieveOptions()
])
.then(([extensions, options]) =>
{
setConfigurationInfo({extensions, options});
});
}
function capitalize(str)
{
return str[0].toUpperCase() + str.slice(1);
......
......@@ -21,6 +21,26 @@ const stepsManager = require("./issue-reporter-steps-manager");
const report = require("./issue-reporter-report");
const {$, asIndentedString} = require("./dom");
const optionalPermissions = {
permissions: [
"contentSettings",
"management"
]
};
function containsPermissions()
{
// Firefox doesn't trigger the promise's catch() but instead throws
try
{
return browser.permissions.contains(optionalPermissions);
}
catch (ex)
{
return Promise.reject(ex);
}
}
document.addEventListener("DOMContentLoaded", () =>
{
ext.i18n.setElementLinks(
......@@ -100,6 +120,49 @@ document.addEventListener("DOMContentLoaded", () =>
});
});
// We query our permissions here to find out whether the browser supports them
containsPermissions()
.then(() =>
{
const includeConfig = $("#includeConfig");
includeConfig.addEventListener("change", (event) =>
{
if (!includeConfig.checked)
{
report.updateConfigurationInfo(false);
return;
}
event.preventDefault();
browser.permissions.request(optionalPermissions)
.then((granted) =>
{
return report.updateConfigurationInfo(granted)
.then(() =>
{
includeConfig.checked = granted;
});
})
// Catch error here already to ensure permission is removed
// even if we are unable to update the report data
.catch(console.error)
.then(() => browser.permissions.remove(optionalPermissions))
.then((success) =>
{
if (!success)
throw new Error("Failed to remove permissions");
})
.catch(console.error);
});
})
.catch((err) =>
{
// No need to ask for more data if we won't be able to access it anyway
const includeConfig = $("#includeConfigContainer");
includeConfig.hidden = true;
});
const showDataOverlay = $("#showDataOverlay");
$("#showData").addEventListener("click", event =>
{
......
......@@ -55,22 +55,7 @@ Promise.all([
dataset.application = application;
});
// create the tab object once at the right time
// and make it available per each getTab.then(...)
const getTab = new Promise(
resolve =>
{
document.addEventListener("DOMContentLoaded", () =>
{
browser.tabs.query({active: true, lastFocusedWindow: true}, tabs =>
{
resolve({id: tabs[0].id, url: tabs[0].url});
});
});
}
);
getTab.then(tab =>
activeTab.then(tab =>
{
const urlProtocol = tab.url && new URL(tab.url).protocol;
if (/^https?:$/.test(urlProtocol))
......
......@@ -21,6 +21,9 @@
"issueReporter_comment_label": {
"message": "Comment (optional):"