Commit 2ee44ad3 authored by Kelvin Jiang's avatar Kelvin Jiang Committed by Commit Bot

[Extensions] Add an activity log page (placeholder page)

Add a placeholder page to show an activity log for an extension, hidden behind the
"enable-extension-activity-logging" command line flag, and accessible via
chrome://extensions/?id=<extension_id>. If the flag is disabled and a user
navigates to the page, redirect to the extensions details page.

Bug: 898309
Change-Id: Ib0f50487c323e8c07d6fddcd57df3b8e5477cfc6
Reviewed-on: https://chromium-review.googlesource.com/c/1295296Reviewed-by: default avatarDevlin <rdevlin.cronin@chromium.org>
Commit-Queue: Kelvin Jiang <kelvinjiang@chromium.org>
Cr-Commit-Position: refs/heads/master@{#603176}
parent 1d776440
......@@ -7,6 +7,9 @@
<message name="IDS_EXTENSIONS_ALLOW_ON_ALL_URLS" desc="The checkbox for allowing an extension to run scripts on all websites without explicit permission.">
Allow on all websites
</message>
<message name="IDS_EXTENSIONS_ACTIVITY_LOG" desc="The label of the button to click to view recent extension activity.">
View Activity Log
</message>
<message name="IDS_EXTENSIONS_BACKGROUND_PAGE" desc="Display name for an autogenerated background page.">
background page
</message>
......
......@@ -50,6 +50,7 @@ grit("flattened_resources") {
}
js_type_check("closure_compile") {
deps = [
":activity_log",
":code_section",
":detail_view",
":drag_and_drop_handler",
......@@ -88,6 +89,12 @@ js_library("code_section") {
externs_list = [ "$externs_path/developer_private.js" ]
}
js_library("activity_log") {
deps = [
"//ui/webui/resources/js:cr",
]
}
js_library("detail_view") {
deps = [
":item",
......@@ -228,6 +235,7 @@ js_library("load_error") {
js_library("manager") {
deps = [
":activity_log",
":detail_view",
":item",
":item_list",
......
<link rel="import" href="chrome://resources/html/polymer.html">
<link rel="import" href="chrome://resources/html/cr.html">
<dom-module id="extensions-activity-log">
<template>
<style include="iron-flex cr-shared-style">
#container {
height: 100%;
overflow: overlay;
}
#main {
background-color: white;
margin: auto;
min-height: 100%;
padding-bottom: 64px;
width: var(--cr-toolbar-field-width);
}
</style>
<div id="container">
<div id="main">
Welcome to the new activity log
</div>
</div>
</template>
<script src="activity_log.js"></script>
</dom-module>
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
cr.define('extensions', function() {
'use strict';
const ActivityLog = Polymer({
is: 'extensions-activity-log',
properties: {},
});
return {ActivityLog: ActivityLog};
});
......@@ -386,6 +386,10 @@
icon-class="icon-external" label="$i18n{itemOptions}"
on-click="onExtensionOptionsTap_">
</cr-link-row>
<cr-link-row class="hr" is="" id="extensions-activity-log-link"
hidden$="[[!showActivityLog]]" label="$i18n{viewActivityLog}"
on-click="onActivityLogTap_">
</cr-link-row>
<cr-link-row class="hr" hidden="[[!data.manifestHomePageUrl.length]]"
icon-class="icon-external" id="extensionWebsite"
label="$i18n{extensionWebsite}" on-click="onExtensionWebSiteTap_">
......
......@@ -31,6 +31,9 @@ cr.define('extensions', function() {
/** Whether "allow in incognito" option should be shown. */
incognitoAvailable: Boolean,
/** Whether "View Activity Log" link should be shown. */
showActivityLog: Boolean,
},
observers: [
......@@ -60,6 +63,12 @@ cr.define('extensions', function() {
});
},
/** @private */
onActivityLogTap_: function() {
extensions.navigation.navigateTo(
{page: Page.ACTIVITY_LOG, extensionId: this.data.id});
},
/**
* @param {string} description
* @param {string} fallback
......
......@@ -29,6 +29,12 @@
<structure name="IDR_MD_EXTENSIONS_CODE_SECTION_JS"
file="code_section.js"
type="chrome_html" />
<structure name="IDR_MD_EXTENSIONS_ACTIVITY_LOG_HTML"
file="activity_log.html"
type="chrome_html" />
<structure name="IDR_MD_EXTENSIONS_ACTIVITY_LOG_JS"
file="activity_log.js"
type="chrome_html" />
<structure name="IDR_MD_EXTENSIONS_DETAIL_VIEW_HTML"
file="detail_view.html"
type="chrome_html" />
......
......@@ -7,6 +7,7 @@
<link rel="import" href="chrome://resources/cr_elements/cr_view_manager/cr_view_manager.html">
<link rel="import" href="chrome://resources/html/assert.html">
<link rel="import" href="chrome://resources/html/cr.html">
<link rel="import" href="activity_log.html">
<link rel="import" href="detail_view.html">
<link rel="import" href="drop_overlay.html">
<link rel="import" href="error_page.html">
......@@ -89,11 +90,18 @@
<template>
<extensions-detail-view delegate="[[delegate]]" slot="view"
in-dev-mode="[[inDevMode]]"
show-activity-log="[[showActivityLog]]"
incognito-available="[[incognitoAvailable_]]"
data="[[detailViewItem_]]">
</extensions-detail-view>
</template>
</cr-lazy-render>
<cr-lazy-render id="activity-log">
<template>
<extensions-activity-log slot="view">
</extensions-activity-log>
</template>
</cr-lazy-render>
<cr-lazy-render id="keyboard-shortcuts">
<template>
<extensions-keyboard-shortcuts delegate="[[delegate]]" slot="view"
......
......@@ -51,6 +51,11 @@ cr.define('extensions', function() {
value: () => loadTimeData.getBoolean('inDevMode'),
},
showActivityLog: {
type: Boolean,
value: () => loadTimeData.getBoolean('showActivityLog'),
},
devModeControlledByPolicy: {
type: Boolean,
value: false,
......@@ -400,7 +405,8 @@ cr.define('extensions', function() {
// We should never try and remove a non-existent item.
assert(index >= 0);
this.splice(listId, index, 1);
if ((this.currentPage_.page == Page.DETAILS ||
if ((this.currentPage_.page == Page.ACTIVITY_LOG ||
this.currentPage_.page == Page.DETAILS ||
this.currentPage_.page == Page.ERRORS) &&
this.currentPage_.extensionId == itemId) {
// Leave the details page (the 'list' page is a fine choice).
......@@ -453,6 +459,15 @@ cr.define('extensions', function() {
this.detailViewItem_ = assert(data);
else if (toPage == Page.ERRORS)
this.errorPageItem_ = assert(data);
else if (toPage == Page.ACTIVITY_LOG) {
if (!this.showActivityLog) {
// Redirect back to the details page if we try to view the
// activity log of an extension but the flag is not set.
extensions.navigation.replaceWith(
{page: Page.DETAILS, extensionId: newPage.extensionId});
return;
}
}
if (fromPage != toPage) {
/** @type {CrViewManagerElement} */ (this.$.viewManager)
......@@ -505,7 +520,8 @@ cr.define('extensions', function() {
onViewExitFinish_: function(e) {
const viewType = e.path[0].tagName;
if (viewType == 'EXTENSIONS-ITEM-LIST' ||
viewType == 'EXTENSIONS-KEYBOARD-SHORTCUTS') {
viewType == 'EXTENSIONS-KEYBOARD-SHORTCUTS' ||
viewType == 'EXTENSIONS-ACTIVITY-LOG') {
return;
}
......
......@@ -12,6 +12,7 @@ cr.exportPath('extensions');
const Page = {
LIST: 'items-list',
DETAILS: 'details-view',
ACTIVITY_LOG: 'activity-log',
SHORTCUTS: 'keyboard-shortcuts',
ERRORS: 'error-page',
};
......@@ -99,6 +100,9 @@ cr.define('extensions', function() {
let id = search.get('id');
if (id)
return {page: Page.DETAILS, extensionId: id};
id = search.get('activity');
if (id)
return {page: Page.ACTIVITY_LOG, extensionId: id};
id = search.get('options');
if (id)
return {page: Page.DETAILS, extensionId: id, subpage: Dialog.OPTIONS};
......@@ -180,6 +184,9 @@ cr.define('extensions', function() {
case Page.LIST:
path = '/';
break;
case Page.ACTIVITY_LOG:
path = '/?activity=' + entry.extensionId;
break;
case Page.DETAILS:
if (entry.subpage) {
assert(entry.subpage == Dialog.OPTIONS);
......
......@@ -8,6 +8,7 @@
#include <string>
#include <utility>
#include "base/command_line.h"
#include "base/metrics/histogram_macros.h"
#include "base/strings/utf_string_conversions.h"
#include "base/timer/elapsed_timer.h"
......@@ -16,6 +17,7 @@
#include "chrome/browser/extensions/chrome_extension_browser_constants.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/webui/metrics_handler.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/common/pref_names.h"
#include "chrome/common/url_constants.h"
#include "chrome/grit/browser_resources.h"
......@@ -48,6 +50,7 @@ namespace extensions {
namespace {
constexpr char kInDevModeKey[] = "inDevMode";
constexpr char kShowActivityLogKey[] = "showActivityLog";
constexpr char kLoadTimeClassesKey[] = "loadTimeClasses";
struct LocalizedString {
......@@ -255,6 +258,7 @@ content::WebUIDataSource* CreateMdExtensionsSource(bool in_dev_mode) {
{"toolbarUpdatingToast", IDS_MD_EXTENSIONS_TOOLBAR_UPDATING_TOAST},
{"updateRequiredByPolicy",
IDS_MD_EXTENSIONS_DISABLED_UPDATE_REQUIRED_BY_POLICY},
{"viewActivityLog", IDS_EXTENSIONS_ACTIVITY_LOG},
{"viewBackgroundPage", IDS_EXTENSIONS_BACKGROUND_PAGE},
{"viewIncognito", IDS_EXTENSIONS_VIEW_INCOGNITO},
{"viewInactive", IDS_EXTENSIONS_VIEW_INACTIVE},
......@@ -314,8 +318,10 @@ content::WebUIDataSource* CreateMdExtensionsSource(bool in_dev_mode) {
IDS_MD_EXTENSIONS_HOST_PERMISSIONS_LEARN_MORE,
base::ASCIIToUTF16(
chrome_extension_constants::kRuntimeHostPermissionsHelpURL)));
source->AddBoolean(kInDevModeKey, in_dev_mode);
source->AddBoolean(kShowActivityLogKey,
base::CommandLine::ForCurrentProcess()->HasSwitch(
switches::kEnableExtensionActivityLogging));
source->AddString(kLoadTimeClassesKey, GetLoadTimeClasses(in_dev_mode));
#if BUILDFLAG(OPTIMIZE_WEBUI)
......
......@@ -439,6 +439,29 @@ TEST_F(
extension_manager_tests.TestNames.UrlNavigationToDetails);
});
TEST_F(
'CrExtensionsManagerTestWithIdQueryParam', 'UrlNavigationToActivityLogFail',
function() {
this.runMochaTest(
extension_manager_tests.TestNames.UrlNavigationToActivityLogFail);
});
CrExtensionsManagerTestWithActivityLogFlag =
class extends CrExtensionsManagerTestWithIdQueryParam {
/** @override */
get commandLineSwitches() {
return [{
switchName: 'enable-extension-activity-logging',
}];
}
};
TEST_F(
'CrExtensionsManagerTestWithActivityLogFlag',
'UrlNavigationToActivityLogSuccess', function() {
this.runMochaTest(
extension_manager_tests.TestNames.UrlNavigationToActivityLogSuccess);
});
////////////////////////////////////////////////////////////////////////////////
// Extension Keyboard Shortcuts Tests
......
......@@ -45,6 +45,7 @@ cr.define('extension_detail_view_tests', function() {
item.set('delegate', mockDelegate);
item.set('inDevMode', false);
item.set('incognitoAvailable', true);
item.set('showActivityLog', false);
document.body.appendChild(item);
});
......@@ -115,6 +116,11 @@ cr.define('extension_detail_view_tests', function() {
item.set('data.optionsPage', {openInTab: true, url: optionsUrl});
expectTrue(testIsVisible('#extensions-options'));
expectFalse(testIsVisible('#extensions-activity-log-link'));
item.set('showActivityLog', true);
Polymer.dom.flush();
expectTrue(testIsVisible('#extensions-activity-log-link'));
item.set('data.manifestHomePageUrl', 'http://example.com');
Polymer.dom.flush();
expectTrue(testIsVisible('#extensionWebsite'));
......@@ -200,8 +206,29 @@ cr.define('extension_detail_view_tests', function() {
'chrome-extension://' + extensionData.id + '/options.html';
item.set('data.optionsPage', {openInTab: true, url: optionsUrl});
item.set('data.prettifiedPath', 'foo/bar/baz/');
item.set('showActivityLog', true);
Polymer.dom.flush();
let currentPage = null;
extensions.navigation.addListener(newPage => {
currentPage = newPage;
});
// Even though the command line flag is not set for activity log, we
// still expect to navigate to it after clicking the link as the logic to
// redirect the page back to the details view is in manager.js.
// Since this behavior does not happen in the testing environment,
// we test the behavior in manager_test.js.
MockInteractions.tap(item.$$('#extensions-activity-log-link'));
expectDeepEquals(
currentPage,
{page: Page.ACTIVITY_LOG, extensionId: extensionData.id});
// Reset current page and test delegate calls.
extensions.navigation.navigateTo(
{page: Page.DETAILS, extensionId: extensionData.id});
currentPage = null;
mockDelegate.testClickingCalls(
item.$$('#allow-incognito').getLabel(), 'setItemAllowedIncognito',
[extensionData.id, true]);
......
......@@ -10,6 +10,10 @@ cr.define('extension_manager_tests', function() {
ItemListVisibility: 'item list visibility',
SplitItems: 'split items',
UrlNavigationToDetails: 'url navigation to details',
UrlNavigationToActivityLogFail:
'url navigation to activity log without flag set',
UrlNavigationToActivityLogSuccess:
'url navigation to activity log with flag set',
};
function getDataByName(list, name) {
......@@ -120,6 +124,50 @@ cr.define('extension_manager_tests', function() {
Polymer.dom.flush();
assertViewActive('extensions-detail-view');
});
test(assert(TestNames.UrlNavigationToActivityLogFail), function() {
expectFalse(manager.showActivityLog);
// Try to open activity log with a valid ID.
extensions.navigation.navigateTo({
page: Page.ACTIVITY_LOG,
extensionId: 'ldnnhddmnhbkjipkidpdiheffobcpfmf'
});
Polymer.dom.flush();
// Should be re-routed to details page with showActivityLog set to false.
assertViewActive('extensions-detail-view');
const detailsView = manager.$$('extensions-detail-view');
expectFalse(detailsView.showActivityLog);
// Try to open activity log with an invalid ID.
extensions.navigation.navigateTo(
{page: Page.ACTIVITY_LOG, extensionId: 'z'.repeat(32)});
Polymer.dom.flush();
// Should be re-routed to the main page.
assertViewActive('extensions-item-list');
});
test(assert(TestNames.UrlNavigationToActivityLogSuccess), function() {
expectTrue(manager.showActivityLog);
// Try to open activity log with a valid ID.
extensions.navigation.navigateTo({
page: Page.ACTIVITY_LOG,
extensionId: 'ldnnhddmnhbkjipkidpdiheffobcpfmf'
});
Polymer.dom.flush();
// Should be on activity log page.
assertViewActive('extensions-activity-log');
// Try to open activity log with an invalid ID.
extensions.navigation.navigateTo(
{page: Page.ACTIVITY_LOG, extensionId: 'z'.repeat(32)});
Polymer.dom.flush();
// Should be re-routed to the main page.
assertViewActive('extensions-item-list');
});
});
return {
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment