- !-1"-4#-7$-%-:&-X`'-(-)-*-^+-],---(.-(/->0->1-D2-3-6-g7-8-9-d:-;-B<-( =- >-%?-&@-H'A-8FB-kC-tD-iE-F-G-uH-I-J--K-L-0M-N-[O-P-:Q-ZR-qS- T-U-PV-jW- X-[ Y-j\Z-[-c\-]-$2^-H_-wK`-r}a-b-2c-0d- e-Of-g-n-=o-p-q-;-------h - -B-R-'t-+--- ----J-c----M------O-Q--- --o$-T)---->-A-C-8L-O-lR-H[-v--O---$--z-> -n -6 - - - - -aA -Bk - -! - -5 -S - - - - - -: - - -{ -; - - -K -) - -h - -N - -u -a -% .O; .{ _ _ _A _ _x _r _6 _= _C _VE _J _7^ _d _ m _n _r _s _8 _ _z _$ _A _ ___tb_f_m_*__n_X__1+_?_N_y__0_W__ ___8_p9_V_a______Z_.)_2_)9_;_H_\_v_W___Ƨ_____q)_`:k;kiks?k@kAk(BkܶCkDkEkFkGkHkKIkJkKkLkMk0NkOkPk`QkRkSkTkUkVkWkXkYk&Zkf[k\kQ]k^kf_k`k2akbkckdkekfkRgkhkikjkkk8lkmknkokpkYqk5rk&sktk4ukvkwkSxk3yk3zk {k|k}k~kkkkkk'kkfkkYkkk#kkk!l0"lq$l%l9l=lDlF lI lR lg lr lTvl lldll2lllԬlkllUllwlll$l\l lZ!l"l #lR$l%l&la'l(lk)lm*l+l,l-l<.l}/l0l1lV2l3l4l5lb6l7l8l79l:l$;lX'lk(?l(@li)Al)Bl8ClADlFElFlGlHl;IlJl<Kl//LlmxMllNlOlPl0Ql)3RlBSlpTlUlVl Wl=XlYlшZl-[l\l]l^l_l=`l\al=cblucldl3elD!flGBglyhliljlklll ml nlq$ oli= pl̉ ql rl !sl!tl6!ulJ!vlW!wldY!xl(d!yl!!zl5!~l!l]!l!l !lP!lj!lA!l?!lV"l "lw"l!"lK""l)"lR*"l*"l6"l6"lU="lzB"lF"l2H"lqO"lS"lV"lc"lc"ll"l#{"l~"l"l"l"l"lj"l"l"l1"l-"l#l?#l*#lb-#lB#l`Q#l+f#lBi#l,s#lu#l#l#l#lF#l#l#l>#l<$l9M$lO$l\P$l)e$le$l|$l$l$l$l*$l$lO$l,%l^%l%lg%l&l'l~'l'lY(l1-(l/(lT(lW(l)ld5)lZ)la)l4i)lk)lvr)lv)l˞)l)l)l)l)lR)l)lM)l)l)lK*lgO*lt*lx*lz*lŀ*l*l7*l>*lդ*l*l)*l*m*m*m*m+m+m!+mC$+m++mz.+ m0+ m;3+ ml+ msn+ my+m+mV+m+me+m+mx+mD+m+mH+m+m٩+m+m`+m+m޶+m+mj+m+ m+!m-+"m+#m+$m+%m-+&mu+'m+(m+)m+*mt++m+,mM+-m+.ms+/m,0m,1m2,2m!,3mv$,4m1,5m4,6ms7,7mS:,8m>@,9mC,:mkE,;m)N,mhY,?m>],@mQ`,Amm,Bmp,Cms,Dm,Em,Fm,Gm,Hmd,Im,Jm7,Km,Lm,Mm-NmfA-Oma-Pmd-Qml-Rm-Sm-Tm-Um-Vmh-Wm-Xm -Ym-Zm-[ma-\m-]m<.^m?._m{C.`mxJ.am3f.bm.cm.dmѠ.emW.fm.gmù.hm.im.jm.km.lm.mm.nm.om.pm.qm.rm].sm/tm/um /vm/wm/xm /ymqD/zmdW/{mW/|m]/}m#l/~m {/m-/m/m/m/m/m}/m/m$0m60m:`0mb0mI0mߖ0mL0m0m 0mm0m,0m0m0m1m -1m71m:1m/C1mK1mZ1m_1ml1m1mh1mظ1mk1m1mD1m 2m1+2m 82mP4m4mY4mi5mXl5mp5_36// Copyright 2013 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. /** * @fileoverview * This is a component extension that implements a text-to-speech (TTS) * engine powered by Google's speech synthesis API. * * This is an "event page", so it's not loaded when the API isn't being used, * and doesn't waste resources. When a web page or web app makes a speech * request and the parameters match one of the voices in this extension's * manifest, it makes a request to Google's API using Chrome's private key * and plays the resulting speech using HTML5 audio. */ /** * The main class for this extension. Adds listeners to * chrome.ttsEngine.onSpeak and chrome.ttsEngine.onStop and implements * them using Google's speech synthesis API. * @constructor */ function TtsExtension() {} TtsExtension.prototype = { /** * The url prefix of the speech server, including static query * parameters that don't change. * @type {string} * @const * @private */ SPEECH_SERVER_URL_: 'https://www.google.com/speech-api/v2/synthesize?' + 'enc=mpeg&client=chromium', /** * A mapping from language and gender to voice name, hardcoded for now * until the speech synthesis server capabilities response provides this. * The key of this map is of the form '-'. * @type {Object} * @private */ LANG_AND_GENDER_TO_VOICE_NAME_: { 'en-gb-male': 'rjs', 'en-gb-female': 'fis', }, /** * The arguments passed to the onSpeak event handler for the utterance * that's currently being spoken. Should be null when no object is * pending. * * @type {?{utterance: string, options: Object, callback: Function}} * @private */ currentUtterance_: null, /** * The HTML5 audio element we use for playing the sound served by the * speech server. * @type {HTMLAudioElement} * @private */ audioElement_: null, /** * A mapping from voice name to language and gender, derived from the * manifest file. This is used in case the speech synthesis request * specifies a voice name but doesn't specify a language code or gender. * @type {Object<{lang: string, gender: string}>} * @private */ voiceNameToLangAndGender_: {}, /** * This is the main function called to initialize this extension. * Initializes data structures and adds event listeners. */ init: function() { // Get voices from manifest. var voices = chrome.app.getDetails().tts_engine.voices; for (var i = 0; i < voices.length; i++) { this.voiceNameToLangAndGender_[voices[i].voice_name] = { lang: voices[i].lang, gender: voices[i].gender }; } // Initialize the audio element and event listeners on it. this.audioElement_ = document.createElement('audio'); document.body.appendChild(this.audioElement_); this.audioElement_.addEventListener( 'ended', this.onStop_.bind(this), false); this.audioElement_.addEventListener( 'canplaythrough', this.onStart_.bind(this), false); // Install event listeners for the ttsEngine API. chrome.ttsEngine.onSpeak.addListener(this.onSpeak_.bind(this)); chrome.ttsEngine.onStop.addListener(this.onStop_.bind(this)); chrome.ttsEngine.onPause.addListener(this.onPause_.bind(this)); chrome.ttsEngine.onResume.addListener(this.onResume_.bind(this)); }, /** * Handler for the chrome.ttsEngine.onSpeak interface. * Gets Chrome's Google API key and then uses it to generate a request * url for the requested speech utterance. Sets that url as the source * of the HTML5 audio element. * @param {string} utterance The text to be spoken. * @param {Object} options Options to control the speech, as defined * in the Chrome ttsEngine extension API. * @private */ onSpeak_: function(utterance, options, callback) { // Truncate the utterance if it's too long. Both Chrome's tts // extension api and the web speech api specify 32k as the // maximum limit for an utterance. if (utterance.length > 32768) utterance = utterance.substr(0, 32768); try { // First, stop any pending audio. this.onStop_(); this.currentUtterance_ = { utterance: utterance, options: options, callback: callback }; var lang = options.lang; var gender = options.gender; if (options.voiceName) { lang = this.voiceNameToLangAndGender_[options.voiceName].lang; gender = this.voiceNameToLangAndGender_[options.voiceName].gender; } if (!lang) lang = navigator.language; // Look up the specific voice name for this language and gender. // If it's not in the map, it doesn't matter - the language will // be used directly. This is only used for languages where more // than one gender is actually available. var key = lang.toLowerCase() + '-' + gender; var voiceName = this.LANG_AND_GENDER_TO_VOICE_NAME_[key]; var url = this.SPEECH_SERVER_URL_; chrome.systemPrivate.getApiKey((function(key) { url += '&key=' + key; url += '&text=' + encodeURIComponent(utterance); url += '&lang=' + lang.toLowerCase(); if (voiceName) url += '&name=' + voiceName; if (options.rate) { // Input rate is between 0.1 and 10.0 with a default of 1.0. // Output speed is between 0.0 and 1.0 with a default of 0.5. url += '&speed=' + (options.rate / 2.0); } if (options.pitch) { // Input pitch is between 0.0 and 2.0 with a default of 1.0. // Output pitch is between 0.0 and 1.0 with a default of 0.5. url += '&pitch=' + (options.pitch / 2.0); } // This begins loading the audio but does not play it. // When enough of the audio has loaded to begin playback, // the 'canplaythrough' handler will call this.onStart_, // which sends a start event to the ttsEngine callback and // then begins playing audio. this.audioElement_.src = url; }).bind(this)); } catch (err) { console.error(String(err)); callback({ 'type': 'error', 'errorMessage': String(err) }); this.currentUtterance_ = null; } }, /** * Handler for the chrome.ttsEngine.onStop interface. * Called either when the ttsEngine API requests us to stop, or when * we reach the end of the audio stream. Pause the audio element to * silence it, and send a callback to the ttsEngine API to let it know * that we've completed. Note that the ttsEngine API manages callback * messages and will automatically replace the 'end' event with a * more specific callback like 'interrupted' when sending it to the * TTS client. * @private */ onStop_: function() { if (this.currentUtterance_) { this.audioElement_.pause(); this.currentUtterance_.callback({ 'type': 'end', 'charIndex': this.currentUtterance_.utterance.length }); } this.currentUtterance_ = null; }, /** * Handler for the canplaythrough event on the audio element. * Called when the audio element has buffered enough audio to begin * playback. Send the 'start' event to the ttsEngine callback and * then begin playing the audio element. * @private */ onStart_: function() { if (this.currentUtterance_) { if (this.currentUtterance_.options.volume !== undefined) { // Both APIs use the same range for volume, between 0.0 and 1.0. this.audioElement_.volume = this.currentUtterance_.options.volume; } this.audioElement_.play(); this.currentUtterance_.callback({ 'type': 'start', 'charIndex': 0 }); } }, /** * Handler for the chrome.ttsEngine.onPause interface. * Pauses audio if we're in the middle of an utterance. * @private */ onPause_: function() { if (this.currentUtterance_) { this.audioElement_.pause(); } }, /** * Handler for the chrome.ttsEngine.onPause interface. * Resumes audio if we're in the middle of an utterance. * @private */ onResume_: function() { if (this.currentUtterance_) { this.audioElement_.play(); } } }; (new TtsExtension()).init(); PNG  IHDRaIDATx^MKQƟ{;cQ-,[>*CIEHV.$!XEҢ$mM"XQ~$"*JQ9fS4L{?{.g3s8@ n'椝a 뚪s7} =p+9d&=>$D zّahh`{'ERGMF3kspoyC+L~\Bd bhXk"-ܿZsF(?,PȨeN J+OA&hp_2/PO_HnIENDB`PNG  IHDRaIDATx^MkEg}&i"F[0JBQLk~.\J݈ d! 5VbQ-lMZ#5 5ͽw朑;̢s6<@x[T'1Sޏ 3%ut CbX}= }_ou/BP ,E2_pރ8b,૞AWTI2ѝtp/"e_F j[d4tcS\YYylc;ÿk٬Ģ5HءJ}a?_Bw Qݏ".=6{v'gs K_s zj< (#*N,qf-"6nġj,ࢆ@D!n5<103K )kǾ񇸴|-5Ӓ@HM} 1NjgV*AHI>v{׋8c<75I,L, p0胜'Nv0r]M,]]VH W*fLr=}3_G|&kW>8 %\Gw3nٯ. !33hhT_o> |$ 0#e1кQj\ W."$@S\j'o\=IENDB`// Copyright (c) 2012 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. (function() { 'use strict'; /** @const */ var BookmarkList = bmm.BookmarkList; /** @const */ var BookmarkTree = bmm.BookmarkTree; /** @const */ var Command = cr.ui.Command; /** @const */ var LinkKind = cr.LinkKind; /** @const */ var ListItem = cr.ui.ListItem; /** @const */ var Menu = cr.ui.Menu; /** @const */ var MenuButton = cr.ui.MenuButton; /** @const */ var Splitter = cr.ui.Splitter; /** @const */ var TreeItem = cr.ui.TreeItem; /** * An array containing the BookmarkTreeNodes that were deleted in the last * deletion action. This is used for implementing undo. * @type {?{nodes: Array>, target: EventTarget}} */ var lastDeleted; /** * * Holds the last DOMTimeStamp when mouse pointer hovers on folder in tree * view. Zero means pointer doesn't hover on folder. * @type {number} */ var lastHoverOnFolderTimeStamp = 0; /** * Holds a function that will undo that last action, if global undo is enabled. * @type {Function} */ var performGlobalUndo; /** * Holds a link controller singleton. Use getLinkController() rarther than * accessing this variabie. * @type {cr.LinkController} */ var linkController; /** * New Windows are not allowed in Windows 8 metro mode. */ var canOpenNewWindows = true; /** * Incognito mode availability can take the following values: , * - 'enabled' for when both normal and incognito modes are available; * - 'disabled' for when incognito mode is disabled; * - 'forced' for when incognito mode is forced (normal mode is unavailable). */ var incognitoModeAvailability = 'enabled'; /** * Whether bookmarks can be modified. * @type {boolean} */ var canEdit = true; /** * @type {TreeItem} * @const */ var searchTreeItem = new TreeItem({ bookmarkId: 'q=' }); /** * Command shortcut mapping. * @const */ var commandShortcutMap = cr.isMac ? { 'edit': 'Enter', // On Mac we also allow Meta+Backspace. 'delete': 'Delete Backspace Meta|Backspace', 'open-in-background-tab': 'Meta|Enter', 'open-in-new-tab': 'Shift|Meta|Enter', 'open-in-same-window': 'Meta|Down', 'open-in-new-window': 'Shift|Enter', 'rename-folder': 'Enter', // Global undo is Command-Z. It is not in any menu. 'undo': 'Meta|z', } : { 'edit': 'F2', 'delete': 'Delete', 'open-in-background-tab': 'Ctrl|Enter', 'open-in-new-tab': 'Shift|Ctrl|Enter', 'open-in-same-window': 'Enter', 'open-in-new-window': 'Shift|Enter', 'rename-folder': 'F2', // Global undo is Ctrl-Z. It is not in any menu. 'undo': 'Ctrl|z', }; /** * Mapping for folder id to suffix of UMA. These names will be appeared * after "BookmarkManager_NavigateTo_" in UMA dashboard. * @const */ var folderMetricsNameMap = { '1': 'BookmarkBar', '2': 'Other', '3': 'Mobile', 'q=': 'Search', 'subfolder': 'SubFolder', }; /** * Adds an event listener to a node that will remove itself after firing once. * @param {!Element} node The DOM node to add the listener to. * @param {string} name The name of the event listener to add to. * @param {function(Event)} handler Function called when the event fires. */ function addOneShotEventListener(node, name, handler) { var f = function(e) { handler(e); node.removeEventListener(name, f); }; node.addEventListener(name, f); } // Get the localized strings from the backend via bookmakrManagerPrivate API. function loadLocalizedStrings(data) { // The strings may contain & which we need to strip. for (var key in data) { data[key] = data[key].replace(/&/, ''); } loadTimeData.data = data; i18nTemplate.process(document, loadTimeData); searchTreeItem.label = loadTimeData.getString('search'); searchTreeItem.icon = isRTL() ? 'images/bookmark_manager_search_rtl.png' : 'images/bookmark_manager_search.png'; } /** * Updates the location hash to reflect the current state of the application. */ function updateHash() { window.location.hash = bmm.tree.selectedItem.bookmarkId; updateAllCommands(); } /** * Navigates to a bookmark ID. * @param {string} id The ID to navigate to. * @param {function()=} opt_callback Function called when list view loaded or * displayed specified folder. */ function navigateTo(id, opt_callback) { window.location.hash = id; var sameParent = bmm.list.parentId == id; if (!sameParent) updateParentId(id); updateAllCommands(); var metricsId = folderMetricsNameMap[id.replace(/^q=.*/, 'q=')] || folderMetricsNameMap['subfolder']; chrome.metricsPrivate.recordUserAction( 'BookmarkManager_NavigateTo_' + metricsId); if (opt_callback) { if (sameParent) opt_callback(); else addOneShotEventListener(bmm.list, 'load', opt_callback); } } /** * Updates the parent ID of the bookmark list and selects the correct tree item. * @param {string} id The id. */ function updateParentId(id) { // Setting list.parentId fires 'load' event. bmm.list.parentId = id; // When tree.selectedItem changed, tree view calls navigatTo() then it // calls updateHash() when list view displayed specified folder. bmm.tree.selectedItem = bmm.treeLookup[id] || bmm.tree.selectedItem; } // Process the location hash. This is called by onhashchange and when the page // is first loaded. function processHash() { var id = window.location.hash.slice(1); if (!id) { // If we do not have a hash, select first item in the tree. id = bmm.tree.items[0].bookmarkId; } var valid = false; if (/^e=/.test(id)) { id = id.slice(2); // If hash contains e=, edit the item specified. chrome.bookmarks.get(id, function(bookmarkNodes) { // Verify the node to edit is a valid node. if (!bookmarkNodes || bookmarkNodes.length != 1) return; var bookmarkNode = bookmarkNodes[0]; // After the list reloads, edit the desired bookmark. var editBookmark = function() { var index = bmm.list.dataModel.findIndexById(bookmarkNode.id); if (index != -1) { var sm = bmm.list.selectionModel; sm.anchorIndex = sm.leadIndex = sm.selectedIndex = index; scrollIntoViewAndMakeEditable(index); } }; var parentId = assert(bookmarkNode.parentId); navigateTo(parentId, editBookmark); }); // We handle the two cases of navigating to the bookmark to be edited // above. Don't run the standard navigation code below. return; } else if (/^q=/.test(id)) { // In case we got a search hash, update the text input and the // bmm.treeLookup to use the new id. setSearch(id.slice(2)); valid = true; } // Navigate to bookmark 'id' (which may be a query of the form q=query). if (valid) { updateParentId(id); } else { // We need to verify that this is a correct ID. chrome.bookmarks.get(id, function(items) { if (items && items.length == 1) updateParentId(id); }); } } // Activate is handled by the open-in-same-window-command. function handleDoubleClickForList(e) { if (e.button == 0) $('open-in-same-window-command').execute(); } // The list dispatches an event when the user clicks on the URL or the Show in // folder part. function handleUrlClickedForList(e) { getLinkController().openUrlFromEvent(e.url, e.originalEvent); chrome.bookmarkManagerPrivate.recordLaunch(); } function handleSearch(e) { setSearch(this.value); } /** * Navigates to the search results for the search text. * @param {string} searchText The text to search for. */ function setSearch(searchText) { if (searchText) { // Only update search item if we have a search term. We never want the // search item to be for an empty search. delete bmm.treeLookup[searchTreeItem.bookmarkId]; var id = searchTreeItem.bookmarkId = 'q=' + searchText; bmm.treeLookup[searchTreeItem.bookmarkId] = searchTreeItem; } var input = $('term'); // Do not update the input if the user is actively using the text input. if (document.activeElement != input) input.value = searchText; if (searchText) { bmm.tree.add(searchTreeItem); bmm.tree.selectedItem = searchTreeItem; } else { // Go "home". bmm.tree.selectedItem = bmm.tree.items[0]; id = bmm.tree.selectedItem.bookmarkId; } navigateTo(id); } /** * This returns the user visible path to the folder where the bookmark is * located. * @param {number} parentId The ID of the parent folder. * @return {string|undefined} The path to the the bookmark, */ function getFolder(parentId) { var parentNode = bmm.tree.getBookmarkNodeById(parentId); if (parentNode) { var s = parentNode.title; if (parentNode.parentId != bmm.ROOT_ID) { return getFolder(parentNode.parentId) + '/' + s; } return s; } } function handleLoadForTree(e) { processHash(); } /** * Returns a promise for all the URLs in the {@code nodes} and the direct * children of {@code nodes}. * @param {!Array} nodes . * @return {!Promise>} . */ function getAllUrls(nodes) { var urls = []; // Adds the node and all its direct children. // TODO(deepak.m1): Here node should exist. When we delete the nodes then // datamodel gets updated but still it shows deleted items as selected items // and accessing those nodes throws chrome.runtime.lastError. This cause // undefined value for node. Please refer https://crbug.com/480935. function addNodes(node) { if (!node || node.id == 'new') return; if (node.children) { node.children.forEach(function(child) { if (!bmm.isFolder(child)) urls.push(child.url); }); } else { urls.push(node.url); } } // Get a future promise for the nodes. var promises = nodes.map(function(node) { if (bmm.isFolder(assert(node))) return bmm.loadSubtree(node.id); // Not a folder so we already have all the data we need. return Promise.resolve(node); }); return Promise.all(promises).then(function(nodes) { nodes.forEach(addNodes); return urls; }); } /** * Returns the nodes (non recursive) to use for the open commands. * @param {HTMLElement} target * @return {!Array} */ function getNodesForOpen(target) { if (target == bmm.tree) { if (bmm.tree.selectedItem != searchTreeItem) return bmm.tree.selectedFolders; // Fall through to use all nodes in the list. } else { var items = bmm.list.selectedItems; if (items.length) return items; } // The list starts off with a null dataModel. We can get here during startup. if (!bmm.list.dataModel) return []; // Return an array based on the dataModel. return bmm.list.dataModel.slice(); } /** * Returns a promise that will contain all URLs of all the selected bookmarks * and the nested bookmarks for use with the open commands. * @param {HTMLElement} target The target list or tree. * @return {Promise>} . */ function getUrlsForOpenCommands(target) { return getAllUrls(getNodesForOpen(target)); } function notNewNode(node) { return node.id != 'new'; } /** * Helper function that updates the canExecute and labels for the open-like * commands. * @param {!cr.ui.CanExecuteEvent} e The event fired by the command system. * @param {!cr.ui.Command} command The command we are currently processing. * @param {string} singularId The string id of singular form of the menu label. * @param {string} pluralId The string id of menu label if the singular form is not used. * @param {boolean} commandDisabled Whether the menu item should be disabled no matter what bookmarks are selected. */ function updateOpenCommand(e, command, singularId, pluralId, commandDisabled) { if (singularId) { // The command label reflects the selection which might not reflect // how many bookmarks will be opened. For example if you right click an // empty area in a folder with 1 bookmark the text should still say "all". var selectedNodes = getSelectedBookmarkNodes(e.target).filter(notNewNode); var singular = selectedNodes.length == 1 && !bmm.isFolder(selectedNodes[0]); command.label = loadTimeData.getString(singular ? singularId : pluralId); } if (commandDisabled) { command.disabled = true; e.canExecute = false; return; } getUrlsForOpenCommands(assertInstanceof(e.target, HTMLElement)).then( function(urls) { var disabled = !urls.length; command.disabled = disabled; e.canExecute = !disabled; }); } /** * Calls the backend to figure out if we can paste the clipboard into the active * folder. * @param {Function=} opt_f Function to call after the state has been updated. */ function updatePasteCommand(opt_f) { function update(commandId, canPaste) { $(commandId).disabled = !canPaste; } var promises = []; // The folders menu. // We can not paste into search item in tree. if (bmm.tree.selectedItem && bmm.tree.selectedItem != searchTreeItem) { promises.push(new Promise(function(resolve) { var id = bmm.tree.selectedItem.bookmarkId; chrome.bookmarkManagerPrivate.canPaste(id, function(canPaste) { update('paste-from-folders-menu-command', canPaste); resolve(canPaste); }); })); } else { // Tree's not loaded yet. update('paste-from-folders-menu-command', false); } // The organize menu. var listId = bmm.list.parentId; if (bmm.list.isSearch() || !listId) { // We cannot paste into search view or the list isn't ready. update('paste-from-organize-menu-command', false); } else { promises.push(new Promise(function(resolve) { chrome.bookmarkManagerPrivate.canPaste(listId, function(canPaste) { update('paste-from-organize-menu-command', canPaste); resolve(canPaste); }); })); } Promise.all(promises).then(function() { var cmd; if (document.activeElement == bmm.list) cmd = 'paste-from-organize-menu-command'; else if (document.activeElement == bmm.tree) cmd = 'paste-from-folders-menu-command'; if (cmd) update('paste-from-context-menu-command', !$(cmd).disabled); if (opt_f) opt_f(); }); } function handleCanExecuteForSearchBox(e) { var command = e.command; switch (command.id) { case 'delete-command': case 'undo-command': // Pass the delete and undo commands through // (fixes http://crbug.com/278112). e.canExecute = false; break; } } function handleCanExecuteForDocument(e) { var command = e.command; switch (command.id) { case 'import-menu-command': e.canExecute = canEdit; break; case 'export-menu-command': // We can always execute the export-menu command. e.canExecute = true; break; case 'sort-command': e.canExecute = !bmm.list.isSearch() && bmm.list.dataModel && bmm.list.dataModel.length > 1 && !isUnmodifiable(bmm.tree.getBookmarkNodeById(bmm.list.parentId)); break; case 'undo-command': // Because the global undo command has no visible UI, always enable it, // and just make it a no-op if undo is not possible. e.canExecute = true; break; default: canExecuteForList(e); if (!e.defaultPrevented) canExecuteForTree(e); break; } } /** * Helper function for handling canExecute for the list and the tree. * @param {!cr.ui.CanExecuteEvent} e Can execute event object. * @param {boolean} isSearch Whether the user is trying to do a command on * search. */ function canExecuteShared(e, isSearch) { var command = e.command; switch (command.id) { case 'paste-from-folders-menu-command': case 'paste-from-organize-menu-command': case 'paste-from-context-menu-command': updatePasteCommand(); break; case 'add-new-bookmark-command': case 'new-folder-command': case 'new-folder-from-folders-menu-command': var parentId = computeParentFolderForNewItem(); var unmodifiable = isUnmodifiable( bmm.tree.getBookmarkNodeById(parentId)); e.canExecute = !isSearch && canEdit && !unmodifiable; break; case 'open-in-new-tab-command': updateOpenCommand(e, command, 'open_in_new_tab', 'open_all', false); break; case 'open-in-background-tab-command': updateOpenCommand(e, command, '', '', false); break; case 'open-in-new-window-command': updateOpenCommand(e, command, 'open_in_new_window', 'open_all_new_window', // Disabled when incognito is forced. incognitoModeAvailability == 'forced' || !canOpenNewWindows); break; case 'open-incognito-window-command': updateOpenCommand(e, command, 'open_incognito', 'open_all_incognito', // Not available when incognito is disabled. incognitoModeAvailability == 'disabled'); break; case 'undo-delete-command': e.canExecute = !!lastDeleted; break; } } /** * Helper function for handling canExecute for the list and document. * @param {!cr.ui.CanExecuteEvent} e Can execute event object. */ function canExecuteForList(e) { function hasSelected() { return !!bmm.list.selectedItem; } function hasSingleSelected() { return bmm.list.selectedItems.length == 1; } function canCopyItem(item) { return item.id != 'new'; } function canCopyItems() { var selectedItems = bmm.list.selectedItems; return selectedItems && selectedItems.some(canCopyItem); } function isSearch() { return bmm.list.isSearch(); } var command = e.command; switch (command.id) { case 'rename-folder-command': // Show rename if a single folder is selected. var items = bmm.list.selectedItems; if (items.length != 1) { e.canExecute = false; command.hidden = true; } else { var isFolder = bmm.isFolder(items[0]); e.canExecute = isFolder && canEdit && !hasUnmodifiable(items); command.hidden = !isFolder; } break; case 'edit-command': // Show the edit command if not a folder. var items = bmm.list.selectedItems; if (items.length != 1) { e.canExecute = false; command.hidden = false; } else { var isFolder = bmm.isFolder(items[0]); e.canExecute = !isFolder && canEdit && !hasUnmodifiable(items); command.hidden = isFolder; } break; case 'show-in-folder-command': e.canExecute = isSearch() && hasSingleSelected(); break; case 'delete-command': case 'cut-command': e.canExecute = canCopyItems() && canEdit && !hasUnmodifiable(bmm.list.selectedItems); break; case 'copy-command': e.canExecute = canCopyItems(); break; case 'open-in-same-window-command': e.canExecute = (e.target == bmm.list) && hasSelected(); break; default: canExecuteShared(e, isSearch()); } } // Update canExecute for the commands when the list is the active element. function handleCanExecuteForList(e) { if (e.target != bmm.list) return; canExecuteForList(e); } // Update canExecute for the commands when the tree is the active element. function handleCanExecuteForTree(e) { if (e.target != bmm.tree) return; canExecuteForTree(e); } function canExecuteForTree(e) { function hasSelected() { return !!bmm.tree.selectedItem; } function isSearch() { return bmm.tree.selectedItem == searchTreeItem; } function isTopLevelItem() { return bmm.tree.selectedItem && bmm.tree.selectedItem.parentNode == bmm.tree; } var command = e.command; switch (command.id) { case 'rename-folder-command': case 'rename-folder-from-folders-menu-command': command.hidden = false; e.canExecute = hasSelected() && !isTopLevelItem() && canEdit && !hasUnmodifiable(bmm.tree.selectedFolders); break; case 'edit-command': command.hidden = true; e.canExecute = false; break; case 'delete-command': case 'delete-from-folders-menu-command': case 'cut-command': case 'cut-from-folders-menu-command': e.canExecute = hasSelected() && !isTopLevelItem() && canEdit && !hasUnmodifiable(bmm.tree.selectedFolders); break; case 'copy-command': case 'copy-from-folders-menu-command': e.canExecute = hasSelected() && !isTopLevelItem(); break; case 'undo-delete-from-folders-menu-command': e.canExecute = lastDeleted && lastDeleted.target == bmm.tree; break; default: canExecuteShared(e, isSearch()); } } /** * Update the canExecute state of all the commands. */ function updateAllCommands() { var commands = document.querySelectorAll('command'); for (var i = 0; i < commands.length; i++) { commands[i].canExecuteChange(); } } function updateEditingCommands() { var editingCommands = [ 'add-new-bookmark', 'cut', 'cut-from-folders-menu', 'delete', 'edit', 'new-folder', 'paste-from-context-menu', 'paste-from-folders-menu', 'paste-from-organize-menu', 'rename-folder', 'sort', ]; chrome.bookmarkManagerPrivate.canEdit(function(result) { if (result != canEdit) { canEdit = result; editingCommands.forEach(function(baseId) { $(baseId + '-command').canExecuteChange(); }); } }); } function handleChangeForTree(e) { navigateTo(bmm.tree.selectedItem.bookmarkId); } function handleMenuButtonClicked(e) { updateEditingCommands(); if (e.currentTarget.id == 'folders-menu') { $('copy-from-folders-menu-command').canExecuteChange(); $('undo-delete-from-folders-menu-command').canExecuteChange(); } else { $('copy-command').canExecuteChange(); } } function handleRename(e) { var item = e.target; var bookmarkNode = item.bookmarkNode; chrome.bookmarks.update(bookmarkNode.id, {title: item.label}); performGlobalUndo = null; // This can't be undone, so disable global undo. } function handleEdit(e) { var item = e.target; var bookmarkNode = item.bookmarkNode; var context = { title: bookmarkNode.title }; if (!bmm.isFolder(bookmarkNode)) context.url = bookmarkNode.url; if (bookmarkNode.id == 'new') { selectItemsAfterUserAction(/** @type {BookmarkList} */(bmm.list)); // New page context.parentId = bookmarkNode.parentId; chrome.bookmarks.create(context, function(node) { // A new node was created and will get added to the list due to the // handler. var dataModel = bmm.list.dataModel; var index = dataModel.indexOf(bookmarkNode); dataModel.splice(index, 1); // Select new item. var newIndex = dataModel.findIndexById(node.id); if (newIndex != -1) { var sm = bmm.list.selectionModel; bmm.list.scrollIndexIntoView(newIndex); sm.leadIndex = sm.anchorIndex = sm.selectedIndex = newIndex; } }); } else { // Edit chrome.bookmarks.update(bookmarkNode.id, context); } performGlobalUndo = null; // This can't be undone, so disable global undo. } function handleCancelEdit(e) { var item = e.target; var bookmarkNode = item.bookmarkNode; if (bookmarkNode.id == 'new') { var dataModel = bmm.list.dataModel; var index = dataModel.findIndexById('new'); dataModel.splice(index, 1); } } /** * Navigates to the folder that the selected item is in and selects it. This is * used for the show-in-folder command. */ function showInFolder() { var bookmarkNode = bmm.list.selectedItem; if (!bookmarkNode) return; var parentId = bookmarkNode.parentId; // After the list is loaded we should select the revealed item. function selectItem() { var index = bmm.list.dataModel.findIndexById(bookmarkNode.id); if (index == -1) return; var sm = bmm.list.selectionModel; sm.anchorIndex = sm.leadIndex = sm.selectedIndex = index; bmm.list.scrollIndexIntoView(index); } var treeItem = bmm.treeLookup[parentId]; treeItem.reveal(); navigateTo(parentId, selectItem); } /** * @return {!cr.LinkController} The link controller used to open links based on * user clicks and keyboard actions. */ function getLinkController() { return linkController || (linkController = new cr.LinkController(loadTimeData)); } /** * Returns the selected bookmark nodes of the provided tree or list. * If |opt_target| is not provided or null the active element is used. * Only call this if the list or the tree is focused. * @param {EventTarget=} opt_target The target list or tree. * @return {!Array} Array of bookmark nodes. */ function getSelectedBookmarkNodes(opt_target) { return (opt_target || document.activeElement) == bmm.tree ? bmm.tree.selectedFolders : bmm.list.selectedItems; } /** * @param {EventTarget=} opt_target The target list or tree. * @return {!Array} An array of the selected bookmark IDs. */ function getSelectedBookmarkIds(opt_target) { var selectedNodes = getSelectedBookmarkNodes(opt_target); selectedNodes.sort(function(a, b) { return a.index - b.index }); return selectedNodes.map(function(node) { return node.id; }); } /** * @param {BookmarkTreeNode} node The node to test. * @return {boolean} Whether the given node is unmodifiable. */ function isUnmodifiable(node) { return !!(node && node.unmodifiable); } /** * @param {Array} nodes A list of BookmarkTreeNodes. * @return {boolean} Whether any of the nodes is managed. */ function hasUnmodifiable(nodes) { return nodes.some(isUnmodifiable); } /** * Opens the selected bookmarks. * @param {cr.LinkKind} kind The kind of link we want to open. * @param {HTMLElement=} opt_eventTarget The target of the user initiated event. */ function openBookmarks(kind, opt_eventTarget) { // If we have selected any folders, we need to find all the bookmarks one // level down. We use multiple async calls to getSubtree instead of getting // the whole tree since we would like to minimize the amount of data sent. var urlsP = getUrlsForOpenCommands(opt_eventTarget ? opt_eventTarget : null); urlsP.then(function(urls) { getLinkController().openUrls(assert(urls), kind); chrome.bookmarkManagerPrivate.recordLaunch(); }); } /** * Opens an item in the list. */ function openItem() { var bookmarkNodes = getSelectedBookmarkNodes(); // If we double clicked or pressed enter on a single folder, navigate to it. if (bookmarkNodes.length == 1 && bmm.isFolder(bookmarkNodes[0])) navigateTo(bookmarkNodes[0].id); else openBookmarks(LinkKind.FOREGROUND_TAB); } /** * Refreshes search results after delete or undo-delete. * This ensures children of deleted folders do not remain in results */ function updateSearchResults() { if (bmm.list.isSearch()) bmm.list.reload(); } /** * Deletes the selected bookmarks. The bookmarks are saved in memory in case * the user needs to undo the deletion. * @param {EventTarget=} opt_target The deleter of bookmarks. */ function deleteBookmarks(opt_target) { var selectedIds = getSelectedBookmarkIds(opt_target); if (!selectedIds.length) return; var filteredIds = getFilteredSelectedBookmarkIds(opt_target); lastDeleted = {nodes: [], target: opt_target || document.activeElement}; function performDelete() { // Only remove filtered ids. chrome.bookmarkManagerPrivate.removeTrees(filteredIds); $('undo-delete-command').canExecuteChange(); $('undo-delete-from-folders-menu-command').canExecuteChange(); performGlobalUndo = undoDelete; } // First, store information about the bookmarks being deleted. // Store all selected ids. selectedIds.forEach(function(id) { chrome.bookmarks.getSubTree(id, function(results) { lastDeleted.nodes.push(results); // When all nodes have been saved, perform the deletion. if (lastDeleted.nodes.length === selectedIds.length) { performDelete(); updateSearchResults(); } }); }); } /** * Restores a tree of bookmarks under a specified folder. * @param {BookmarkTreeNode} node The node to restore. * @param {(string|number)=} opt_parentId If a string is passed, it's the ID of * the folder to restore under. If not specified or a number is passed, the * original parentId of the node will be used. */ function restoreTree(node, opt_parentId) { var bookmarkInfo = { parentId: typeof opt_parentId == 'string' ? opt_parentId : node.parentId, title: node.title, index: node.index, url: node.url }; chrome.bookmarks.create(bookmarkInfo, function(result) { if (!result) { console.error('Failed to restore bookmark.'); return; } if (node.children) { // Restore the children using the new ID for this node. node.children.forEach(function(child) { restoreTree(child, result.id); }); } updateSearchResults(); }); } /** * Restores the last set of bookmarks that was deleted. */ function undoDelete() { lastDeleted.nodes.forEach(function(arr) { arr.forEach(restoreTree); }); lastDeleted = null; $('undo-delete-command').canExecuteChange(); $('undo-delete-from-folders-menu-command').canExecuteChange(); // Only a single level of undo is supported, so disable global undo now. performGlobalUndo = null; } /** * Computes folder for "Add Page" and "Add Folder". * @return {string} The id of folder node where we'll create new page/folder. */ function computeParentFolderForNewItem() { if (document.activeElement == bmm.tree) return bmm.list.parentId; var selectedItem = bmm.list.selectedItem; return selectedItem && bmm.isFolder(selectedItem) ? selectedItem.id : bmm.list.parentId; } /** * Callback for rename folder and edit command. This starts editing for * the passed in target, or the selected item. * @param {EventTarget=} opt_target The target to start editing. If absent or * null, the selected item will be edited instead. */ function editItem(opt_target) { if ((opt_target || document.activeElement) == bmm.tree) { bmm.tree.selectedItem.editing = true; } else { var li = bmm.list.getListItem(bmm.list.selectedItem); if (li) li.editing = true; } } /** * Callback for the new folder command. This creates a new folder and starts * a rename of it. * @param {EventTarget=} opt_target The target to create a new folder in. */ function newFolder(opt_target) { performGlobalUndo = null; // This can't be undone, so disable global undo. var parentId = computeParentFolderForNewItem(); var selectedItems = bmm.list.selectedItems; var newIndex; // Callback is called after tree and list data model updated. function createFolder(callback) { if (selectedItems.length == 1 && document.activeElement != bmm.tree && !bmm.isFolder(selectedItems[0]) && selectedItems[0].id != 'new') { newIndex = bmm.list.dataModel.indexOf(selectedItems[0]) + 1; } chrome.bookmarks.create({ title: loadTimeData.getString('new_folder_name'), parentId: parentId, index: newIndex }, callback); } if ((opt_target || document.activeElement) == bmm.tree) { createFolder(function(newNode) { navigateTo(newNode.id, function() { bmm.treeLookup[newNode.id].editing = true; }); }); return; } function editNewFolderInList() { createFolder(function(newNode) { var index = newNode.index; var sm = bmm.list.selectionModel; sm.anchorIndex = sm.leadIndex = sm.selectedIndex = index; scrollIntoViewAndMakeEditable(index); }); } navigateTo(parentId, editNewFolderInList); } /** * Scrolls the list item into view and makes it editable. * @param {number} index The index of the item to make editable. */ function scrollIntoViewAndMakeEditable(index) { bmm.list.scrollIndexIntoView(index); // onscroll is now dispatched asynchronously so we have to postpone // the rest. setTimeout(function() { var item = bmm.list.getListItemByIndex(index); if (item) item.editing = true; }, 0); } /** * Adds a page to the current folder. This is called by the * add-new-bookmark-command handler. */ function addPage() { var parentId = computeParentFolderForNewItem(); var selectedItems = bmm.list.selectedItems; var newIndex; function editNewBookmark() { if (selectedItems.length == 1 && document.activeElement != bmm.tree && !bmm.isFolder(selectedItems[0])) { newIndex = bmm.list.dataModel.indexOf(selectedItems[0]) + 1; } var fakeNode = { title: '', url: '', parentId: parentId, index: newIndex, id: 'new' }; var dataModel = bmm.list.dataModel; var index = dataModel.length; if (newIndex != undefined) index = newIndex; dataModel.splice(index, 0, fakeNode); var sm = bmm.list.selectionModel; sm.anchorIndex = sm.leadIndex = sm.selectedIndex = index; scrollIntoViewAndMakeEditable(index); }; navigateTo(parentId, editNewBookmark); } /** * This function is used to select items after a user action such as paste, drop * add page etc. * @param {BookmarkList|BookmarkTree} target The target of the user action. * @param {string=} opt_selectedTreeId If provided, then select that tree id. */ function selectItemsAfterUserAction(target, opt_selectedTreeId) { // We get one onCreated event per item so we delay the handling until we get // no more events coming. var ids = []; var timer; function handle(id, bookmarkNode) { clearTimeout(timer); if (opt_selectedTreeId || bmm.list.parentId == bookmarkNode.parentId) ids.push(id); timer = setTimeout(handleTimeout, 50); } function handleTimeout() { chrome.bookmarks.onCreated.removeListener(handle); chrome.bookmarks.onMoved.removeListener(handle); if (opt_selectedTreeId && ids.indexOf(opt_selectedTreeId) != -1) { var index = ids.indexOf(opt_selectedTreeId); if (index != -1 && opt_selectedTreeId in bmm.treeLookup) { bmm.tree.selectedItem = bmm.treeLookup[opt_selectedTreeId]; } } else if (target == bmm.list) { var dataModel = bmm.list.dataModel; var firstIndex = dataModel.findIndexById(ids[0]); var lastIndex = dataModel.findIndexById(ids[ids.length - 1]); if (firstIndex != -1 && lastIndex != -1) { var selectionModel = bmm.list.selectionModel; selectionModel.selectedIndex = -1; selectionModel.selectRange(firstIndex, lastIndex); selectionModel.anchorIndex = selectionModel.leadIndex = lastIndex; bmm.list.focus(); } } bmm.list.endBatchUpdates(); } bmm.list.startBatchUpdates(); chrome.bookmarks.onCreated.addListener(handle); chrome.bookmarks.onMoved.addListener(handle); timer = setTimeout(handleTimeout, 300); } /** * Record user action. * @param {string} name An user action name. */ function recordUserAction(name) { chrome.metricsPrivate.recordUserAction('BookmarkManager_Command_' + name); } /** * The currently selected bookmark, based on where the user is clicking. * @return {string} The ID of the currently selected bookmark (could be from * tree view or list view). */ function getSelectedId() { if (document.activeElement == bmm.tree) return bmm.tree.selectedItem.bookmarkId; var selectedItem = bmm.list.selectedItem; return selectedItem && bmm.isFolder(selectedItem) ? selectedItem.id : bmm.tree.selectedItem.bookmarkId; } /** * Pastes the copied/cutted bookmark into the right location depending whether * if it was called from Organize Menu or from Context Menu. * @param {string} id The id of the element being pasted from. */ function pasteBookmark(id) { recordUserAction('Paste'); selectItemsAfterUserAction(/** @type {BookmarkList} */(bmm.list)); chrome.bookmarkManagerPrivate.paste(id, getSelectedBookmarkIds()); } /** * Returns true if child is contained in another selected folder. * Traces parent nodes up the tree until a selected ancestor or root is found. */ function hasSelectedAncestor(parentNode) { function contains(arr, item) { for (var i = 0; i < arr.length; i++) if (arr[i] === item) return true; return false; } // Don't search top level, cannot select permanent nodes in search. if (parentNode == null || parentNode.id <= 2) return false; // Found selected ancestor. if (contains(getSelectedBookmarkNodes(), parentNode)) return true; // Keep digging. return hasSelectedAncestor( bmm.tree.getBookmarkNodeById(parentNode.parentId)); } /** * @param {EventTarget=} opt_target A target to get bookmark IDs from. * @return {Array} An array of bookmarks IDs. */ function getFilteredSelectedBookmarkIds(opt_target) { // Remove duplicates from filteredIds and return. var filteredIds = []; // Selected nodes to iterate through for matches. var nodes = getSelectedBookmarkNodes(opt_target); for (var i = 0; i < nodes.length; i++) if (!hasSelectedAncestor(bmm.tree.getBookmarkNodeById(nodes[i].parentId))) filteredIds.splice(0, 0, nodes[i].id); return filteredIds; } /** * Handler for the command event. This is used for context menu of list/tree * and organized menu. * @param {!Event} e The event object. */ function handleCommand(e) { var command = e.command; var target = assertInstanceof(e.target, HTMLElement); switch (command.id) { case 'import-menu-command': recordUserAction('Import'); chrome.bookmarks.import(); break; case 'export-menu-command': recordUserAction('Export'); chrome.bookmarks.export(); break; case 'undo-command': if (performGlobalUndo) { recordUserAction('UndoGlobal'); performGlobalUndo(); } else { recordUserAction('UndoNone'); } break; case 'show-in-folder-command': recordUserAction('ShowInFolder'); showInFolder(); break; case 'open-in-new-tab-command': case 'open-in-background-tab-command': recordUserAction('OpenInNewTab'); openBookmarks(LinkKind.BACKGROUND_TAB, target); break; case 'open-in-new-window-command': recordUserAction('OpenInNewWindow'); openBookmarks(LinkKind.WINDOW, target); break; case 'open-incognito-window-command': recordUserAction('OpenIncognito'); openBookmarks(LinkKind.INCOGNITO, target); break; case 'delete-from-folders-menu-command': target = bmm.tree; case 'delete-command': recordUserAction('Delete'); deleteBookmarks(target); break; case 'copy-from-folders-menu-command': target = bmm.tree; case 'copy-command': recordUserAction('Copy'); chrome.bookmarkManagerPrivate.copy(getSelectedBookmarkIds(target), updatePasteCommand); break; case 'cut-from-folders-menu-command': target = bmm.tree; case 'cut-command': recordUserAction('Cut'); chrome.bookmarkManagerPrivate.cut(getSelectedBookmarkIds(target), function() { updatePasteCommand(); updateSearchResults(); }); break; case 'paste-from-organize-menu-command': pasteBookmark(bmm.list.parentId); break; case 'paste-from-folders-menu-command': pasteBookmark(bmm.tree.selectedItem.bookmarkId); break; case 'paste-from-context-menu-command': pasteBookmark(getSelectedId()); break; case 'sort-command': recordUserAction('Sort'); chrome.bookmarkManagerPrivate.sortChildren(bmm.list.parentId); break; case 'rename-folder-from-folders-menu-command': target = bmm.tree; case 'rename-folder-command': editItem(target); break; case 'edit-command': recordUserAction('Edit'); editItem(); break; case 'new-folder-from-folders-menu-command': target = bmm.tree; case 'new-folder-command': recordUserAction('NewFolder'); newFolder(target); break; case 'add-new-bookmark-command': recordUserAction('AddPage'); addPage(); break; case 'open-in-same-window-command': recordUserAction('OpenInSame'); openItem(); break; case 'undo-delete-command': case 'undo-delete-from-folders-menu-command': recordUserAction('UndoDelete'); undoDelete(); break; } } // Execute the copy, cut and paste commands when those events are dispatched by // the browser. This allows us to rely on the browser to handle the keyboard // shortcuts for these commands. function installEventHandlerForCommand(eventName, commandId) { function handle(e) { if (document.activeElement != bmm.list && document.activeElement != bmm.tree) return; var command = $(commandId); if (!command.disabled) { command.execute(); if (e) e.preventDefault(); // Prevent the system beep. } } if (eventName == 'paste') { // Paste is a bit special since we need to do an async call to see if we // can paste because the paste command might not be up to date. document.addEventListener(eventName, function(e) { updatePasteCommand(handle); }); } else { document.addEventListener(eventName, handle); } } function initializeSplitter() { var splitter = document.querySelector('.main > .splitter'); Splitter.decorate(splitter); var splitterStyle = splitter.previousElementSibling.style; // The splitter persists the size of the left component in the local store. if ('treeWidth' in window.localStorage) splitterStyle.width = window.localStorage['treeWidth']; splitter.addEventListener('resize', function(e) { window.localStorage['treeWidth'] = splitterStyle.width; }); } function initializeBookmarkManager() { // Sometimes the extension API is not initialized. if (!chrome.bookmarks) console.error('Bookmarks extension API is not available'); chrome.bookmarkManagerPrivate.getStrings(continueInitializeBookmarkManager); } function continueInitializeBookmarkManager(localizedStrings) { loadLocalizedStrings(localizedStrings); bmm.treeLookup[searchTreeItem.bookmarkId] = searchTreeItem; cr.ui.decorate('cr-menu', Menu); cr.ui.decorate('button[menu]', MenuButton); cr.ui.decorate('command', Command); BookmarkList.decorate($('list')); BookmarkTree.decorate($('tree')); bmm.list.addEventListener('canceledit', handleCancelEdit); bmm.list.addEventListener('canExecute', handleCanExecuteForList); bmm.list.addEventListener('change', updateAllCommands); bmm.list.addEventListener('contextmenu', updateEditingCommands); bmm.list.addEventListener('dblclick', handleDoubleClickForList); bmm.list.addEventListener('edit', handleEdit); bmm.list.addEventListener('rename', handleRename); bmm.list.addEventListener('urlClicked', handleUrlClickedForList); bmm.tree.addEventListener('canExecute', handleCanExecuteForTree); bmm.tree.addEventListener('change', handleChangeForTree); bmm.tree.addEventListener('contextmenu', updateEditingCommands); bmm.tree.addEventListener('rename', handleRename); bmm.tree.addEventListener('load', handleLoadForTree); cr.ui.contextMenuHandler.addContextMenuProperty( /** @type {!Element} */(bmm.tree)); bmm.list.contextMenu = $('context-menu'); bmm.tree.contextMenu = $('context-menu'); // We listen to hashchange so that we can update the currently shown folder // when // the user goes back and forward in the history. window.addEventListener('hashchange', processHash); document.querySelector('header form').onsubmit = /** @type {function(Event=)} */(function(e) { setSearch($('term').value); e.preventDefault(); }); $('term').addEventListener('search', handleSearch); $('term').addEventListener('canExecute', handleCanExecuteForSearchBox); $('folders-button').addEventListener('click', handleMenuButtonClicked); $('organize-button').addEventListener('click', handleMenuButtonClicked); document.addEventListener('canExecute', handleCanExecuteForDocument); document.addEventListener('command', handleCommand); // Listen to copy, cut and paste events and execute the associated commands. installEventHandlerForCommand('copy', 'copy-command'); installEventHandlerForCommand('cut', 'cut-command'); installEventHandlerForCommand('paste', 'paste-from-organize-menu-command'); // Install shortcuts for (var name in commandShortcutMap) { $(name + '-command').shortcut = commandShortcutMap[name]; } // Disable almost all commands at startup. var commands = document.querySelectorAll('command'); for (var i = 0, command; command = commands[i]; ++i) { if (command.id != 'import-menu-command' && command.id != 'export-menu-command') { command.disabled = true; } } chrome.bookmarkManagerPrivate.canEdit(function(result) { canEdit = result; }); chrome.systemPrivate.getIncognitoModeAvailability(function(result) { // TODO(rustema): propagate policy value to the bookmark manager when it // changes. incognitoModeAvailability = result; }); chrome.bookmarkManagerPrivate.canOpenNewWindows(function(result) { canOpenNewWindows = result; }); cr.ui.FocusOutlineManager.forDocument(document); initializeSplitter(); bmm.addBookmarkModelListeners(); dnd.init(selectItemsAfterUserAction); bmm.tree.reload(); } initializeBookmarkManager(); })(); // Copyright (c) 2012 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. // TODO(arv): Now that this is driven by a data model, implement a data model // that handles the loading and the events from the bookmark backend. /** * @typedef {{childIds: Array}} * * @see chrome/common/extensions/api/bookmarks.json */ var ReorderInfo; /** * @typedef {{parentId: string, * index: number, * oldParentId: string, * oldIndex: number}} * * @see chrome/common/extensions/api/bookmarks.json */ var MoveInfo; cr.define('bmm', function() { 'use strict'; var List = cr.ui.List; var ListItem = cr.ui.ListItem; var ArrayDataModel = cr.ui.ArrayDataModel; var ContextMenuButton = cr.ui.ContextMenuButton; /** * Basic array data model for use with bookmarks. * @param {!Array} items The bookmark items. * @constructor * @extends {ArrayDataModel} */ function BookmarksArrayDataModel(items) { ArrayDataModel.call(this, items); } BookmarksArrayDataModel.prototype = { __proto__: ArrayDataModel.prototype, /** * Finds the index of the bookmark with the given ID. * @param {string} id The ID of the bookmark node to find. * @return {number} The index of the found node or -1 if not found. */ findIndexById: function(id) { for (var i = 0; i < this.length; i++) { if (this.item(i).id == id) return i; } return -1; } }; /** * Removes all children and appends a new child. * @param {!Node} parent The node to remove all children from. * @param {!Node} newChild The new child to append. */ function replaceAllChildren(parent, newChild) { var n; while ((n = parent.lastChild)) { parent.removeChild(n); } parent.appendChild(newChild); } /** * Creates a new bookmark list. * @param {Object=} opt_propertyBag Optional properties. * @constructor * @extends {cr.ui.List} */ var BookmarkList = cr.ui.define('list'); BookmarkList.prototype = { __proto__: List.prototype, /** @override */ decorate: function() { List.prototype.decorate.call(this); this.addEventListener('mousedown', this.handleMouseDown_); // HACK(arv): http://crbug.com/40902 window.addEventListener('resize', this.redraw.bind(this)); // We could add the ContextMenuButton in the BookmarkListItem but it slows // down redraws a lot so we do this on mouseovers instead. this.addEventListener('mouseover', this.handleMouseOver_.bind(this)); bmm.list = this; }, /** * @param {!BookmarkTreeNode} bookmarkNode * @override */ createItem: function(bookmarkNode) { return new BookmarkListItem(bookmarkNode); }, /** @private {string} */ parentId_: '', /** @private {number} */ loadCount_: 0, /** * Reloads the list from the bookmarks backend. */ reload: function() { var parentId = this.parentId; var callback = this.handleBookmarkCallback_.bind(this); this.loadCount_++; if (!parentId) callback([]); else if (/^q=/.test(parentId)) chrome.bookmarks.search(parentId.slice(2), callback); else chrome.bookmarks.getChildren(parentId, callback); }, /** * Callback function for loading items. * @param {Array} items The loaded items. * @private */ handleBookmarkCallback_: function(items) { this.loadCount_--; if (this.loadCount_) return; if (!items) { // Failed to load bookmarks. Most likely due to the bookmark being // removed. cr.dispatchSimpleEvent(this, 'invalidId'); return; } this.dataModel = new BookmarksArrayDataModel(items); this.fixWidth_(); cr.dispatchSimpleEvent(this, 'load'); // Use the same histogram configuration as UMA_HISTOGRAM_COUNTS_1000(). chrome.metricsPrivate.recordValue({ 'metricName': 'Bookmarks.BookmarksInFolder', 'type': chrome.metricsPrivate.MetricTypeType.HISTOGRAM_LOG, 'min': 1, 'max': 1000, 'buckets': 50 }, this.dataModel.length); }, /** * The bookmark node that the list is currently displaying. If we are * currently displaying search this returns null. * @type {BookmarkTreeNode} */ get bookmarkNode() { if (this.isSearch()) return null; var treeItem = bmm.treeLookup[this.parentId]; return treeItem && treeItem.bookmarkNode; }, /** * @return {boolean} Whether we are currently showing search results. */ isSearch: function() { return this.parentId_[0] == 'q'; }, /** * @return {boolean} Whether we are editing an ephemeral item. */ hasEphemeral: function() { var dataModel = this.dataModel; for (var i = 0; i < dataModel.array_.length; i++) { if (dataModel.array_[i].id == 'new') return true; } return false; }, /** * Handles mouseover on the list so that we can add the context menu button * lazily. * @private * @param {!Event} e The mouseover event object. */ handleMouseOver_: function(e) { var el = e.target; while (el && el.parentNode != this) { el = el.parentNode; } if (el && el.parentNode == this && !el.editing && !(el.lastChild instanceof ContextMenuButton)) { el.appendChild(new ContextMenuButton); } }, /** * Dispatches an urlClicked event which is used to open URLs in new * tabs etc. * @private * @param {string} url The URL that was clicked. * @param {!Event} originalEvent The original click event object. */ dispatchUrlClickedEvent_: function(url, originalEvent) { var event = new Event('urlClicked', {bubbles: true}); event.url = url; event.originalEvent = originalEvent; this.dispatchEvent(event); }, /** * Handles mousedown events so that we can prevent the auto scroll as * necessary. * @private * @param {!Event} e The mousedown event object. */ handleMouseDown_: function(e) { e = /** @type {!MouseEvent} */(e); if (e.button == 1) { // WebKit no longer fires click events for middle clicks so we manually // listen to mouse up to dispatch a click event. this.addEventListener('mouseup', this.handleMiddleMouseUp_); // When the user does a middle click we need to prevent the auto scroll // in case the user is trying to middle click to open a bookmark in a // background tab. // We do not do this in case the target is an input since middle click // is also paste on Linux and we don't want to break that. if (e.target.tagName != 'INPUT') e.preventDefault(); } }, /** * WebKit no longer dispatches click events for middle clicks so we need * to emulate it. * @private * @param {!Event} e The mouse up event object. */ handleMiddleMouseUp_: function(e) { e = /** @type {!MouseEvent} */(e); this.removeEventListener('mouseup', this.handleMiddleMouseUp_); if (e.button == 1) { var el = e.target; while (el.parentNode != this) { el = el.parentNode; } var node = el.bookmarkNode; if (node && !bmm.isFolder(node)) this.dispatchUrlClickedEvent_(node.url, e); } e.preventDefault(); }, // Bookmark model update callbacks handleBookmarkChanged: function(id, changeInfo) { var dataModel = this.dataModel; var index = dataModel.findIndexById(id); if (index != -1) { var bookmarkNode = this.dataModel.item(index); bookmarkNode.title = changeInfo.title; if ('url' in changeInfo) bookmarkNode.url = changeInfo['url']; dataModel.updateIndex(index); } }, /** * @param {string} id * @param {ReorderInfo} reorderInfo */ handleChildrenReordered: function(id, reorderInfo) { if (this.parentId == id) { // We create a new data model with updated items in the right order. var dataModel = this.dataModel; var items = {}; for (var i = this.dataModel.length - 1; i >= 0; i--) { var bookmarkNode = dataModel.item(i); items[bookmarkNode.id] = bookmarkNode; } var newArray = []; for (var i = 0; i < reorderInfo.childIds.length; i++) { newArray[i] = items[reorderInfo.childIds[i]]; newArray[i].index = i; } this.dataModel = new BookmarksArrayDataModel(newArray); } }, handleCreated: function(id, bookmarkNode) { if (this.parentId == bookmarkNode.parentId) this.dataModel.splice(bookmarkNode.index, 0, bookmarkNode); }, /** * @param {string} id * @param {MoveInfo} moveInfo */ handleMoved: function(id, moveInfo) { if (moveInfo.parentId == this.parentId || moveInfo.oldParentId == this.parentId) { var dataModel = this.dataModel; if (moveInfo.oldParentId == moveInfo.parentId) { // Reorder within this folder this.startBatchUpdates(); var bookmarkNode = this.dataModel.item(moveInfo.oldIndex); this.dataModel.splice(moveInfo.oldIndex, 1); this.dataModel.splice(moveInfo.index, 0, bookmarkNode); this.endBatchUpdates(); } else { if (moveInfo.oldParentId == this.parentId) { // Move out of this folder var index = dataModel.findIndexById(id); if (index != -1) dataModel.splice(index, 1); } if (moveInfo.parentId == this.parentId) { // Move to this folder var self = this; chrome.bookmarks.get(id, function(bookmarkNodes) { var bookmarkNode = bookmarkNodes[0]; dataModel.splice(bookmarkNode.index, 0, bookmarkNode); }); } } } }, handleRemoved: function(id, removeInfo) { var dataModel = this.dataModel; var index = dataModel.findIndexById(id); if (index != -1) dataModel.splice(index, 1); }, /** * Workaround for http://crbug.com/40902 * @private */ fixWidth_: function() { var list = bmm.list; if (this.loadCount_ || !list) return; // The width of the list is wrong after its content has changed. // Fortunately the reported offsetWidth is correct so we can detect the //incorrect width. if (list.offsetWidth != list.parentNode.clientWidth - list.offsetLeft) { // Set the width to the correct size. This causes the relayout. list.style.width = list.parentNode.clientWidth - list.offsetLeft + 'px'; // Remove the temporary style.width in a timeout. Once the timer fires // the size should not change since we already fixed the width. window.setTimeout(function() { list.style.width = ''; }, 0); } } }; /** * The ID of the bookmark folder we are displaying. */ cr.defineProperty(BookmarkList, 'parentId', cr.PropertyKind.JS, function() { this.reload(); }); /** * The contextMenu property. */ cr.ui.contextMenuHandler.addContextMenuProperty(BookmarkList); /** @type {cr.ui.Menu} */ BookmarkList.prototype.contextMenu; /** * Creates a new bookmark list item. * @param {!BookmarkTreeNode} bookmarkNode The bookmark node this represents. * @constructor * @extends {cr.ui.ListItem} */ function BookmarkListItem(bookmarkNode) { var el = cr.doc.createElement('div'); el.bookmarkNode = bookmarkNode; BookmarkListItem.decorate(el); return el; } /** * Decorates an element as a bookmark list item. * @param {!HTMLElement} el The element to decorate. */ BookmarkListItem.decorate = function(el) { el.__proto__ = BookmarkListItem.prototype; el.decorate(); }; BookmarkListItem.prototype = { __proto__: ListItem.prototype, /** @override */ decorate: function() { ListItem.prototype.decorate.call(this); var bookmarkNode = this.bookmarkNode; this.draggable = true; var labelEl = this.ownerDocument.createElement('div'); labelEl.className = 'label'; var labelImgWrapper = this.ownerDocument.createElement('div'); labelImgWrapper.className = 'label-img-wrapper'; var labelImg = this.ownerDocument.createElement('div'); var labelText = this.ownerDocument.createElement('div'); labelText.className = 'label-text'; labelText.textContent = bookmarkNode.title; var urlEl = this.ownerDocument.createElement('div'); urlEl.className = 'url'; if (bmm.isFolder(bookmarkNode)) { this.className = 'folder'; // TODO(pkasting): Condense folder icon resources together. labelImg.style.content = cr.icon.getImage( cr.isMac ? 'chrome://theme/IDR_BOOKMARK_BAR_FOLDER' : 'chrome://theme/IDR_FOLDER_CLOSED'); } else { labelImg.style.content = cr.icon.getFavicon(bookmarkNode.url); urlEl.textContent = bookmarkNode.url; } labelImgWrapper.appendChild(labelImg); labelEl.appendChild(labelImgWrapper); labelEl.appendChild(labelText); this.appendChild(labelEl); this.appendChild(urlEl); // Initially the ContextMenuButton was added here but it slowed down // rendering a lot so it is now added using mouseover. }, /** * The ID of the bookmark folder we are currently showing or loading. * @type {string} */ get bookmarkId() { return this.bookmarkNode.id; }, /** * Whether the user is currently able to edit the list item. * @type {boolean} */ get editing() { return this.hasAttribute('editing'); }, set editing(editing) { var oldEditing = this.editing; if (oldEditing == editing) return; var url = this.bookmarkNode.url; var title = this.bookmarkNode.title; var isFolder = bmm.isFolder(this.bookmarkNode); var listItem = this; var labelInput, urlInput; // Handles enter and escape which trigger reset and commit respectively. function handleKeydown(e) { // Make sure that the tree does not handle the key. e.stopPropagation(); // Calling list.focus blurs the input which will stop editing the list // item. switch (e.key) { case 'Escape': // Esc labelInput.value = title; if (!isFolder) urlInput.value = url; // fall through cr.dispatchSimpleEvent(listItem, 'canceledit', true); case 'Enter': if (listItem.parentNode) listItem.parentNode.focus(); break; case 'Tab': // Tab // urlInput is the last focusable element in the page. If we // allowed Tab focus navigation and the page loses focus, we // couldn't give focus on urlInput programatically. So, we prevent // Tab focus navigation. if (document.activeElement == urlInput && !e.ctrlKey && !e.metaKey && !e.shiftKey && !getValidURL(urlInput)) { e.preventDefault(); urlInput.blur(); } break; } } function getValidURL(input) { var originalValue = input.value; if (!originalValue) return null; if (input.validity.valid) return originalValue; // Blink does not do URL fix up so we manually test if prepending // 'http://' would make the URL valid. // https://bugs.webkit.org/show_bug.cgi?id=29235 input.value = 'http://' + originalValue; if (input.validity.valid) return input.value; // still invalid input.value = originalValue; return null; } function handleBlur(e) { // When the blur event happens we do not know who is getting focus so we // delay this a bit since we want to know if the other input got focus // before deciding if we should exit edit mode. var doc = e.target.ownerDocument; window.setTimeout(function() { var activeElement = doc.hasFocus() && doc.activeElement; if (activeElement != urlInput && activeElement != labelInput) { listItem.editing = false; } }, 50); } var doc = this.ownerDocument; var labelTextEl = queryRequiredElement('.label-text', this); var urlEl = queryRequiredElement('.url', this); if (editing) { this.setAttribute('editing', ''); this.draggable = false; labelInput = /** @type {HTMLElement} */(doc.createElement('input')); labelInput.placeholder = loadTimeData.getString('name_input_placeholder'); replaceAllChildren(labelTextEl, labelInput); labelInput.value = title; if (!isFolder) { urlInput = /** @type {HTMLElement} */(doc.createElement('input')); urlInput.type = 'url'; urlInput.required = true; urlInput.placeholder = loadTimeData.getString('url_input_placeholder'); // We also need a name for the input for the CSS to work. urlInput.name = '-url-input-' + cr.createUid(); replaceAllChildren(assert(urlEl), urlInput); urlInput.value = url; } var stopPropagation = function(e) { e.stopPropagation(); }; var eventsToStop = ['mousedown', 'mouseup', 'contextmenu', 'dblclick', 'paste']; eventsToStop.forEach(function(type) { labelInput.addEventListener(type, stopPropagation); }); labelInput.addEventListener('keydown', handleKeydown); labelInput.addEventListener('blur', handleBlur); cr.ui.limitInputWidth(labelInput, this, 100, 0.5); labelInput.focus(); labelInput.select(); if (!isFolder) { eventsToStop.forEach(function(type) { urlInput.addEventListener(type, stopPropagation); }); urlInput.addEventListener('keydown', handleKeydown); urlInput.addEventListener('blur', handleBlur); cr.ui.limitInputWidth(urlInput, this, 200, 0.5); } } else { // Check that we have a valid URL and if not we do not change the // editing mode. if (!isFolder) { var urlInput = this.querySelector('.url input'); var newUrl = urlInput.value; if (!newUrl) { cr.dispatchSimpleEvent(this, 'canceledit', true); return; } newUrl = getValidURL(urlInput); if (!newUrl) { // In case the item was removed before getting here we should // not alert. if (listItem.parentNode) { // Select the item again. var dataModel = this.parentNode.dataModel; var index = dataModel.indexOf(this.bookmarkNode); var sm = this.parentNode.selectionModel; sm.selectedIndex = sm.leadIndex = sm.anchorIndex = index; alert(loadTimeData.getString('invalid_url')); } urlInput.focus(); urlInput.select(); return; } urlEl.textContent = this.bookmarkNode.url = newUrl; } this.removeAttribute('editing'); this.draggable = true; labelInput = this.querySelector('.label input'); var newLabel = labelInput.value; labelTextEl.textContent = this.bookmarkNode.title = newLabel; if (isFolder) { if (newLabel != title) { cr.dispatchSimpleEvent(this, 'rename', true); } } else if (newLabel != title || newUrl != url) { cr.dispatchSimpleEvent(this, 'edit', true); } } } }; return { BookmarkList: BookmarkList, list: /** @type {Element} */(null), // Set when decorated. }; }); // Copyright (c) 2011 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('bmm', function() { 'use strict'; /** * The id of the bookmark root. * @type {string} * @const */ var ROOT_ID = '0'; /** @const */ var Tree = cr.ui.Tree; /** @const */ var TreeItem = cr.ui.TreeItem; /** @const */ var localStorage = window.localStorage; var treeLookup = {}; // Manager for persisting the expanded state. var expandedManager = /** @type {EventListener} */({ /** * A map of the collapsed IDs. * @type {Object} */ map: 'bookmarkTreeState' in localStorage ? /** @type {Object} */(JSON.parse(localStorage['bookmarkTreeState'])) : {}, /** * Set the collapsed state for an ID. * @param {string} id The bookmark ID of the tree item that was expanded or * collapsed. * @param {boolean} expanded Whether the tree item was expanded. */ set: function(id, expanded) { if (expanded) delete this.map[id]; else this.map[id] = 1; this.save(); }, /** * @param {string} id The bookmark ID. * @return {boolean} Whether the tree item should be expanded. */ get: function(id) { return !(id in this.map); }, /** * Callback for the expand and collapse events from the tree. * @param {!Event} e The collapse or expand event. */ handleEvent: function(e) { this.set(e.target.bookmarkId, e.type == 'expand'); }, /** * Cleans up old bookmark IDs. */ cleanUp: function() { for (var id in this.map) { // If the id is no longer in the treeLookup the bookmark no longer // exists. if (!(id in treeLookup)) delete this.map[id]; } this.save(); }, timer: null, /** * Saves the expanded state to the localStorage. */ save: function() { clearTimeout(this.timer); var map = this.map; // Save in a timeout so that we can coalesce multiple changes. this.timer = setTimeout(function() { localStorage['bookmarkTreeState'] = JSON.stringify(map); }, 100); } }); // Clean up once per session but wait until things settle down a bit. setTimeout(expandedManager.cleanUp.bind(expandedManager), 1e4); /** * Creates a new tree item for a bookmark node. * @param {!Object} bookmarkNode The bookmark node. * @constructor * @extends {TreeItem} */ function BookmarkTreeItem(bookmarkNode) { var ti = new TreeItem({ label: bookmarkNode.title, bookmarkNode: bookmarkNode, // Bookmark toolbar and Other bookmarks are not draggable. draggable: bookmarkNode.parentId != ROOT_ID }); ti.__proto__ = BookmarkTreeItem.prototype; treeLookup[bookmarkNode.id] = ti; return ti; } BookmarkTreeItem.prototype = { __proto__: TreeItem.prototype, /** * The ID of the bookmark this tree item represents. * @type {string} */ get bookmarkId() { return this.bookmarkNode.id; } }; /** * Asynchronousy adds a tree item at the correct index based on the bookmark * backend. * * Since the bookmark tree only contains folders the index we get from certain * callbacks is not very useful so we therefore have this async call which * gets the children of the parent and adds the tree item at the desired * index. * * This also exoands the parent so that newly added children are revealed. * * @param {!cr.ui.TreeItem} parent The parent tree item. * @param {!cr.ui.TreeItem} treeItem The tree item to add. * @param {Function=} opt_f A function which gets called after the item has * been added at the right index. */ function addTreeItem(parent, treeItem, opt_f) { chrome.bookmarks.getChildren(parent.bookmarkNode.id, function(children) { var isFolder = /** * @type {function (BookmarkTreeNode, number, * Array<(BookmarkTreeNode)>)} */(bmm.isFolder); var index = children.filter(isFolder).map(function(item) { return item.id; }).indexOf(treeItem.bookmarkNode.id); parent.addAt(treeItem, index); parent.expanded = true; if (opt_f) opt_f(); }); } /** * Creates a new bookmark list. * @param {Object=} opt_propertyBag Optional properties. * @constructor * @extends {cr.ui.Tree} */ var BookmarkTree = cr.ui.define('tree'); BookmarkTree.prototype = { __proto__: Tree.prototype, decorate: function() { Tree.prototype.decorate.call(this); this.addEventListener('expand', expandedManager); this.addEventListener('collapse', expandedManager); bmm.tree = this; }, handleBookmarkChanged: function(id, changeInfo) { var treeItem = treeLookup[id]; if (treeItem) treeItem.label = treeItem.bookmarkNode.title = changeInfo.title; }, /** * @param {string} id * @param {ReorderInfo} reorderInfo */ handleChildrenReordered: function(id, reorderInfo) { var parentItem = treeLookup[id]; // The tree only contains folders. var dirIds = reorderInfo.childIds.filter(function(id) { return id in treeLookup; }).forEach(function(id, i) { parentItem.addAt(treeLookup[id], i); }); }, handleCreated: function(id, bookmarkNode) { if (bmm.isFolder(bookmarkNode)) { var parentItem = treeLookup[bookmarkNode.parentId]; var newItem = new BookmarkTreeItem(bookmarkNode); addTreeItem(parentItem, newItem); } }, /** * @param {string} id * @param {MoveInfo} moveInfo */ handleMoved: function(id, moveInfo) { var treeItem = treeLookup[id]; if (treeItem) { var oldParentItem = treeLookup[moveInfo.oldParentId]; oldParentItem.remove(treeItem); var newParentItem = treeLookup[moveInfo.parentId]; // The tree only shows folders so the index is not the index we want. We // therefore get the children need to adjust the index. addTreeItem(newParentItem, treeItem); } }, handleRemoved: function(id, removeInfo) { var parentItem = treeLookup[removeInfo.parentId]; var itemToRemove = treeLookup[id]; if (parentItem && itemToRemove) parentItem.remove(itemToRemove); }, insertSubtree: function(folder) { if (!bmm.isFolder(folder)) return; var children = folder.children; this.handleCreated(folder.id, folder); for (var i = 0; i < children.length; i++) { var child = children[i]; this.insertSubtree(child); } }, /** * Returns the bookmark node with the given ID. The tree only maintains * folder nodes. * @param {string} id The ID of the node to find. * @return {BookmarkTreeNode} The bookmark tree node or null if not found. */ getBookmarkNodeById: function(id) { var treeItem = treeLookup[id]; if (treeItem) return treeItem.bookmarkNode; return null; }, /** * Returns the selected bookmark folder node as an array. * @type {!Array} Array of bookmark nodes. */ get selectedFolders() { return this.selectedItem && this.selectedItem.bookmarkNode ? [this.selectedItem.bookmarkNode] : []; }, /** * Fetches the bookmark items and builds the tree control. */ reload: function() { /** * Recursive helper function that adds all the directories to the * parentTreeItem. * @param {!cr.ui.Tree|!cr.ui.TreeItem} parentTreeItem The parent tree * element to append to. * @param {!Array} bookmarkNodes A list of bookmark * nodes to be added. * @return {boolean} Whether any directories where added. */ function buildTreeItems(parentTreeItem, bookmarkNodes) { var hasDirectories = false; for (var i = 0, bookmarkNode; bookmarkNode = bookmarkNodes[i]; i++) { if (bmm.isFolder(bookmarkNode)) { hasDirectories = true; var item = new BookmarkTreeItem(bookmarkNode); parentTreeItem.add(item); var children = assert(bookmarkNode.children); var anyChildren = buildTreeItems(item, children); item.expanded = anyChildren && expandedManager.get(bookmarkNode.id); } } return hasDirectories; } var self = this; chrome.bookmarkManagerPrivate.getSubtree('', true, function(root) { self.clear(); buildTreeItems(self, root[0].children); cr.dispatchSimpleEvent(self, 'load'); }); }, /** * Clears the tree. */ clear: function() { // Remove all fields without recreating the object since other code // references it. for (var id in treeLookup) { delete treeLookup[id]; } this.textContent = ''; }, /** @override */ remove: function(child) { Tree.prototype.remove.call(this, child); if (child.bookmarkNode) delete treeLookup[child.bookmarkNode.id]; } }; return { BookmarkTree: BookmarkTree, BookmarkTreeItem: BookmarkTreeItem, treeLookup: treeLookup, tree: /** @type {Element} */(null), // Set when decorated. ROOT_ID: ROOT_ID }; }); // Copyright 2013 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('dnd', function() { 'use strict'; /** @const */ var BookmarkList = bmm.BookmarkList; /** @const */ var ListItem = cr.ui.ListItem; /** @const */ var TreeItem = cr.ui.TreeItem; /** * Enumeration of valid drop locations relative to an element. These are * bit masks to allow combining multiple locations in a single value. * @enum {number} * @const */ var DropPosition = { NONE: 0, ABOVE: 1, ON: 2, BELOW: 4 }; /** * @type {Object} Drop information calculated in |handleDragOver|. */ var dropDestination = null; /** * @type {number} Timer id used to help minimize flicker. */ var removeDropIndicatorTimer; /** * The element currently targeted by a touch. * @type {Element} */ var currentTouchTarget; /** * The element that had a style applied it to indicate the drop location. * This is used to easily remove the style when necessary. * @type {Element} */ var lastIndicatorElement; /** * The style that was applied to indicate the drop location. * @type {?string} */ var lastIndicatorClassName; var dropIndicator = { /** * Applies the drop indicator style on the target element and stores that * information to easily remove the style in the future. */ addDropIndicatorStyle: function(indicatorElement, position) { var indicatorStyleName = position == DropPosition.ABOVE ? 'drag-above' : position == DropPosition.BELOW ? 'drag-below' : 'drag-on'; lastIndicatorElement = indicatorElement; lastIndicatorClassName = indicatorStyleName; indicatorElement.classList.add(indicatorStyleName); }, /** * Clears the drop indicator style from the last element was the drop target * so the drop indicator is no longer for that element. */ removeDropIndicatorStyle: function() { if (!lastIndicatorElement || !lastIndicatorClassName) return; lastIndicatorElement.classList.remove(lastIndicatorClassName); lastIndicatorElement = null; lastIndicatorClassName = null; }, /** * Displays the drop indicator on the current drop target to give the * user feedback on where the drop will occur. */ update: function(dropDest) { window.clearTimeout(removeDropIndicatorTimer); var indicatorElement = dropDest.element; var position = dropDest.position; if (dropDest.element instanceof BookmarkList) { // For an empty bookmark list use 'drop-above' style. position = DropPosition.ABOVE; } else if (dropDest.element instanceof TreeItem) { indicatorElement = indicatorElement.querySelector('.tree-row'); } dropIndicator.removeDropIndicatorStyle(); dropIndicator.addDropIndicatorStyle(indicatorElement, position); }, /** * Stop displaying the drop indicator. */ finish: function() { // The use of a timeout is in order to reduce flickering as we move // between valid drop targets. window.clearTimeout(removeDropIndicatorTimer); removeDropIndicatorTimer = window.setTimeout(function() { dropIndicator.removeDropIndicatorStyle(); }, 100); } }; /** * Delay for expanding folder when pointer hovers on folder in tree view in * milliseconds. * @type {number} * @const */ // TODO(yosin): EXPAND_FOLDER_DELAY should follow system settings. 400ms is // taken from Windows default settings. var EXPAND_FOLDER_DELAY = 400; /** * The timestamp when the mouse was over a folder during a drag operation. * Used to open the hovered folder after a certain time. * @type {number} */ var lastHoverOnFolderTimeStamp = 0; /** * Expand a folder if the user has hovered for longer than the specified * time during a drag action. */ function updateAutoExpander(eventTimeStamp, overElement) { // Expands a folder in tree view when pointer hovers on it longer than // EXPAND_FOLDER_DELAY. var hoverOnFolderTimeStamp = lastHoverOnFolderTimeStamp; lastHoverOnFolderTimeStamp = 0; if (hoverOnFolderTimeStamp) { if (eventTimeStamp - hoverOnFolderTimeStamp >= EXPAND_FOLDER_DELAY) overElement.expanded = true; else lastHoverOnFolderTimeStamp = hoverOnFolderTimeStamp; } else if (overElement instanceof TreeItem && bmm.isFolder(overElement.bookmarkNode) && overElement.hasChildren && !overElement.expanded) { lastHoverOnFolderTimeStamp = eventTimeStamp; } } /** * Stores the information about the bookmark and folders being dragged. * @type {Object} */ var dragData = null; var dragInfo = { handleChromeDragEnter: function(newDragData) { dragData = newDragData; }, clearDragData: function() { dragData = null; }, isDragValid: function() { return !!dragData; }, isSameProfile: function() { return dragData && dragData.sameProfile; }, isDraggingFolders: function() { return dragData && dragData.elements.some(function(node) { return !node.url; }); }, isDraggingBookmark: function(bookmarkId) { return dragData && dragData.elements.some(function(node) { return node.id == bookmarkId; }); }, isDraggingChildBookmark: function(folderId) { return dragData && dragData.elements.some(function(node) { return node.parentId == folderId; }); }, isDraggingFolderToDescendant: function(bookmarkNode) { return dragData && dragData.elements.some(function(node) { var dragFolder = bmm.treeLookup[node.id]; var dragFolderNode = dragFolder && dragFolder.bookmarkNode; return dragFolderNode && bmm.contains(dragFolderNode, bookmarkNode); }); } }; /** * External function to select folders or bookmarks after a drop action. * @type {?Function} */ var selectItemsAfterUserAction = null; function getBookmarkElement(el) { while (el && !el.bookmarkNode) { el = el.parentNode; } return el; } // If we are over the list and the list is showing search result, we cannot // drop. function isOverSearch(overElement) { return bmm.list.isSearch() && bmm.list.contains(overElement); } /** * Determines the valid drop positions for the given target element. * @param {!HTMLElement} overElement The element that we are currently * dragging over. * @return {DropPosition} An bit field enumeration of valid drop locations. */ function calculateValidDropTargets(overElement) { // Don't allow dropping if there is an ephemeral item being edited. if (bmm.list.hasEphemeral()) return DropPosition.NONE; if (!dragInfo.isDragValid() || isOverSearch(overElement)) return DropPosition.NONE; if (dragInfo.isSameProfile() && (dragInfo.isDraggingBookmark(overElement.bookmarkNode.id) || dragInfo.isDraggingFolderToDescendant(overElement.bookmarkNode))) { return DropPosition.NONE; } var canDropInfo = calculateDropAboveBelow(overElement); if (canDropOn(overElement)) canDropInfo |= DropPosition.ON; return canDropInfo; } function calculateDropAboveBelow(overElement) { if (overElement instanceof BookmarkList) return DropPosition.NONE; // We cannot drop between Bookmarks bar and Other bookmarks. if (overElement.bookmarkNode.parentId == bmm.ROOT_ID) return DropPosition.NONE; var isOverTreeItem = overElement instanceof TreeItem; var isOverExpandedTree = isOverTreeItem && overElement.expanded; var isDraggingFolders = dragInfo.isDraggingFolders(); // We can only drop between items in the tree if we have any folders. if (isOverTreeItem && !isDraggingFolders) return DropPosition.NONE; // When dragging from a different profile we do not need to consider // conflicts between the dragged items and the drop target. if (!dragInfo.isSameProfile()) { // Don't allow dropping below an expanded tree item since it is confusing // to the user anyway. return isOverExpandedTree ? DropPosition.ABOVE : (DropPosition.ABOVE | DropPosition.BELOW); } var resultPositions = DropPosition.NONE; // Cannot drop above if the item above is already in the drag source. var previousElem = overElement.previousElementSibling; if (!previousElem || !dragInfo.isDraggingBookmark(previousElem.bookmarkId)) resultPositions |= DropPosition.ABOVE; // Don't allow dropping below an expanded tree item since it is confusing // to the user anyway. if (isOverExpandedTree) return resultPositions; // Cannot drop below if the item below is already in the drag source. var nextElement = overElement.nextElementSibling; if (!nextElement || !dragInfo.isDraggingBookmark(nextElement.bookmarkId)) resultPositions |= DropPosition.BELOW; return resultPositions; } /** * Determine whether we can drop the dragged items on the drop target. * @param {!HTMLElement} overElement The element that we are currently * dragging over. * @return {boolean} Whether we can drop the dragged items on the drop * target. */ function canDropOn(overElement) { // We can only drop on a folder. if (!bmm.isFolder(overElement.bookmarkNode)) return false; if (!dragInfo.isSameProfile()) return true; if (overElement instanceof BookmarkList) { // We are trying to drop an item past the last item. This is // only allowed if dragged item is different from the last item // in the list. var listItems = bmm.list.items; var len = listItems.length; if (!len || !dragInfo.isDraggingBookmark(listItems[len - 1].bookmarkId)) return true; } return !dragInfo.isDraggingChildBookmark(overElement.bookmarkNode.id); } /** * Callback for the dragstart event. * @param {Event} e The dragstart event. */ function handleDragStart(e) { // Determine the selected bookmarks. var target = e.target; var draggedNodes = []; var isFromTouch = target == currentTouchTarget; if (target instanceof ListItem) { // Use selected items. draggedNodes = target.parentNode.selectedItems; } else if (target instanceof TreeItem) { draggedNodes.push(target.bookmarkNode); } // We manage starting the drag by using the extension API. e.preventDefault(); // Do not allow dragging if there is an ephemeral item being edited at the // moment. if (bmm.list.hasEphemeral()) return; if (draggedNodes.length) { // If we are dragging a single link, we can do the *Link* effect. // Otherwise, we only allow copy and move. e.dataTransfer.effectAllowed = draggedNodes.length == 1 && !bmm.isFolder(draggedNodes[0]) ? 'copyMoveLink' : 'copyMove'; chrome.bookmarkManagerPrivate.startDrag(draggedNodes.map(function(node) { return node.id; }), isFromTouch); var dragTarget = getBookmarkElement(e.target); if (dragTarget instanceof ListItem || dragTarget instanceof BookmarkList) { chrome.metricsPrivate.recordUserAction( 'BookmarkManager_StartDragFromList'); } else if (dragTarget instanceof TreeItem) { chrome.metricsPrivate.recordUserAction( 'BookmarkManager_StartDragFromTree'); } chrome.metricsPrivate.recordSmallCount( 'BookmarkManager.NumDragged', draggedNodes.length); } } function handleDragEnter(e) { e.preventDefault(); } /** * Calback for the dragover event. * @param {Event} e The dragover event. */ function handleDragOver(e) { // Allow DND on text inputs. if (e.target.tagName != 'INPUT') { // The default operation is to allow dropping links etc to do navigation. // We never want to do that for the bookmark manager. e.preventDefault(); // Set to none. This will get set to something if we can do the drop. e.dataTransfer.dropEffect = 'none'; } if (!dragInfo.isDragValid()) return; var overElement = getBookmarkElement(e.target) || (e.target == bmm.list ? bmm.list : null); if (!overElement) return; updateAutoExpander(e.timeStamp, overElement); var canDropInfo = calculateValidDropTargets(overElement); if (canDropInfo == DropPosition.NONE) return; // Now we know that we can drop. Determine if we will drop above, on or // below based on mouse position etc. dropDestination = calcDropPosition(e.clientY, overElement, canDropInfo); if (!dropDestination) { e.dataTransfer.dropEffect = 'none'; return; } e.dataTransfer.dropEffect = dragInfo.isSameProfile() ? 'move' : 'copy'; dropIndicator.update(dropDestination); } /** * This function determines where the drop will occur relative to the element. * @return {?Object} If no valid drop position is found, null, otherwise * an object containing the following parameters: * element - The target element that will receive the drop. * position - A |DropPosition| relative to the |element|. */ function calcDropPosition(elementClientY, overElement, canDropInfo) { if (overElement instanceof BookmarkList) { // Dropping on the BookmarkList either means dropping below the last // bookmark element or on the list itself if it is empty. var length = overElement.items.length; if (length) return { element: overElement.getListItemByIndex(length - 1), position: DropPosition.BELOW }; return {element: overElement, position: DropPosition.ON}; } var above = canDropInfo & DropPosition.ABOVE; var below = canDropInfo & DropPosition.BELOW; var on = canDropInfo & DropPosition.ON; var rect = overElement.getBoundingClientRect(); var yRatio = (elementClientY - rect.top) / rect.height; if (above && (yRatio <= .25 || yRatio <= .5 && (!below || !on))) return {element: overElement, position: DropPosition.ABOVE}; if (below && (yRatio > .75 || yRatio > .5 && (!above || !on))) return {element: overElement, position: DropPosition.BELOW}; if (on) return {element: overElement, position: DropPosition.ON}; return null; } function calculateDropInfo(eventTarget, dropDestination) { if (!dropDestination || !dragInfo.isDragValid()) return null; var dropPos = dropDestination.position; var relatedNode = dropDestination.element.bookmarkNode; var dropInfoResult = { selectTarget: null, selectedTreeId: -1, parentId: dropPos == DropPosition.ON ? relatedNode.id : relatedNode.parentId, index: -1, relatedIndex: -1 }; // Try to find the index in the dataModel so we don't have to always keep // the index for the list items up to date. var overElement = getBookmarkElement(eventTarget); if (overElement instanceof ListItem) { dropInfoResult.relatedIndex = overElement.parentNode.dataModel.indexOf(relatedNode); dropInfoResult.selectTarget = bmm.list; } else if (overElement instanceof BookmarkList) { dropInfoResult.relatedIndex = overElement.dataModel.length - 1; dropInfoResult.selectTarget = bmm.list; } else { // Tree dropInfoResult.relatedIndex = relatedNode.index; dropInfoResult.selectTarget = bmm.tree; dropInfoResult.selectedTreeId = bmm.tree.selectedItem ? bmm.tree.selectedItem.bookmarkId : null; } if (dropPos == DropPosition.ABOVE) dropInfoResult.index = dropInfoResult.relatedIndex; else if (dropPos == DropPosition.BELOW) dropInfoResult.index = dropInfoResult.relatedIndex + 1; return dropInfoResult; } function handleDragLeave(e) { dropIndicator.finish(); } function handleDrop(e) { var dropInfo = calculateDropInfo(e.target, dropDestination); if (dropInfo) { selectItemsAfterUserAction(dropInfo.selectTarget, dropInfo.selectedTreeId); if (dropInfo.index != -1) chrome.bookmarkManagerPrivate.drop(dropInfo.parentId, dropInfo.index); else chrome.bookmarkManagerPrivate.drop(dropInfo.parentId); e.preventDefault(); var dragTarget = getBookmarkElement(e.target); var action; if (dragTarget instanceof ListItem || dragTarget instanceof BookmarkList) { action = 'BookmarkManager_DropToList'; if (dropDestination.position == DropPosition.ON) action = 'BookmarkManager_DropToListItem'; } else if (dragTarget instanceof TreeItem) { action = 'BookmarkManager_DropToTree'; if (dropDestination.position == DropPosition.ON) action = 'BookmarkManager_DropToTreeItem'; } if (action) chrome.metricsPrivate.recordUserAction(action); } dropDestination = null; dropIndicator.finish(); } function setCurrentTouchTarget(e) { // Only set a new target for a single touch point. if (e.touches.length == 1) currentTouchTarget = getBookmarkElement(e.target); } function clearCurrentTouchTarget(e) { if (getBookmarkElement(e.target) == currentTouchTarget) currentTouchTarget = null; } function clearDragData() { dragInfo.clearDragData(); dropDestination = null; } function init(selectItemsAfterUserActionFunction) { function deferredClearData() { setTimeout(clearDragData, 0); } selectItemsAfterUserAction = selectItemsAfterUserActionFunction; document.addEventListener('dragstart', handleDragStart); document.addEventListener('dragenter', handleDragEnter); document.addEventListener('dragover', handleDragOver); document.addEventListener('dragleave', handleDragLeave); document.addEventListener('drop', handleDrop); document.addEventListener('dragend', deferredClearData); document.addEventListener('mouseup', deferredClearData); document.addEventListener('mousedown', clearCurrentTouchTarget); document.addEventListener('touchcancel', clearCurrentTouchTarget); document.addEventListener('touchend', clearCurrentTouchTarget); document.addEventListener('touchstart', setCurrentTouchTarget); chrome.bookmarkManagerPrivate.onDragEnter.addListener( dragInfo.handleChromeDragEnter); chrome.bookmarkManagerPrivate.onDragLeave.addListener(deferredClearData); chrome.bookmarkManagerPrivate.onDrop.addListener(deferredClearData); } return {init: init}; }); // Copyright (c) 2012 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('bmm', function() { 'use strict'; /** * Whether a node contains another node. * TODO(yosin): Once JavaScript style guide is updated and linter follows * that, we'll remove useless documentations for |parent| and |descendant|. * TODO(yosin): bmm.contains() should be method of BookmarkTreeNode. * @param {!BookmarkTreeNode} parent . * @param {!BookmarkTreeNode} descendant . * @return {boolean} Whether the parent contains the descendant. */ function contains(parent, descendant) { if (descendant.parentId == parent.id) return true; // the bmm.treeLookup contains all folders var parentTreeItem = bmm.treeLookup[descendant.parentId]; if (!parentTreeItem || !parentTreeItem.bookmarkNode) return false; return this.contains(parent, parentTreeItem.bookmarkNode); } /** * @param {!BookmarkTreeNode} node The node to test. * @return {boolean} Whether a bookmark node is a folder. */ function isFolder(node) { return !('url' in node); } var loadingPromises = {}; /** * Promise version of chrome.bookmarkManagerPrivate.getSubtree. * @param {string} id . * @param {boolean} foldersOnly . * @return {!Promise>} . */ function getSubtreePromise(id, foldersOnly) { return new Promise(function(resolve, reject) { chrome.bookmarkManagerPrivate.getSubtree(id, foldersOnly, function(node) { if (chrome.runtime.lastError) { reject(new Error(chrome.runtime.lastError.message)); return; } resolve(node); }); }); } /** * Loads a subtree of the bookmark tree and returns a {@code Promise} that * will be fulfilled when done. This reuses multiple loads so that we do not * load the same subtree more than once at the same time. * @return {!Promise} The future promise for the load. */ function loadSubtree(id) { if (!loadingPromises[id]) { loadingPromises[id] = getSubtreePromise(id, false).then(function(nodes) { return nodes && nodes[0]; }, function(error) { console.error(error.message); }); loadingPromises[id].then(function() { delete loadingPromises[id]; }); } return loadingPromises[id]; } /** * Loads the entire bookmark tree and returns a {@code Promise} that will * be fulfilled when done. This reuses multiple loads so that we do not load * the same tree more than once at the same time. * @return {!Promise} The future promise for the load. */ function loadTree() { return loadSubtree(''); } var bookmarkCache = { /** * Removes the cached item from both the list and tree lookups. */ remove: function(id) { var treeItem = bmm.treeLookup[id]; if (treeItem) { var items = treeItem.items; // is an HTMLCollection for (var i = 0; i < items.length; ++i) { var item = items[i]; var bookmarkNode = item.bookmarkNode; delete bmm.treeLookup[bookmarkNode.id]; } delete bmm.treeLookup[id]; } }, /** * Updates the underlying bookmark node for the tree items and list items by * querying the bookmark backend. * @param {string} id The id of the node to update the children for. * @param {Function=} opt_f A funciton to call when done. */ updateChildren: function(id, opt_f) { function updateItem(bookmarkNode) { var treeItem = bmm.treeLookup[bookmarkNode.id]; if (treeItem) { treeItem.bookmarkNode = bookmarkNode; } } chrome.bookmarks.getChildren(id, function(children) { if (children) children.forEach(updateItem); if (opt_f) opt_f(children); }); } }; /** * Called when the title of a bookmark changes. * @param {string} id The id of changed bookmark node. * @param {!Object} changeInfo The information about how the node changed. */ function handleBookmarkChanged(id, changeInfo) { if (bmm.tree) bmm.tree.handleBookmarkChanged(id, changeInfo); if (bmm.list) bmm.list.handleBookmarkChanged(id, changeInfo); } /** * Callback for when the user reorders by title. * @param {string} id The id of the bookmark folder that was reordered. * @param {!Object} reorderInfo The information about how the items where * reordered. */ function handleChildrenReordered(id, reorderInfo) { if (bmm.tree) bmm.tree.handleChildrenReordered(id, reorderInfo); if (bmm.list) bmm.list.handleChildrenReordered(id, reorderInfo); bookmarkCache.updateChildren(id); } /** * Callback for when a bookmark node is created. * @param {string} id The id of the newly created bookmark node. * @param {!Object} bookmarkNode The new bookmark node. */ function handleCreated(id, bookmarkNode) { if (bmm.list) bmm.list.handleCreated(id, bookmarkNode); if (bmm.tree) bmm.tree.handleCreated(id, bookmarkNode); bookmarkCache.updateChildren(bookmarkNode.parentId); } /** * Callback for when a bookmark node is moved. * @param {string} id The id of the moved bookmark node. * @param {!Object} moveInfo The information about move. */ function handleMoved(id, moveInfo) { if (bmm.list) bmm.list.handleMoved(id, moveInfo); if (bmm.tree) bmm.tree.handleMoved(id, moveInfo); bookmarkCache.updateChildren(moveInfo.parentId); if (moveInfo.parentId != moveInfo.oldParentId) bookmarkCache.updateChildren(moveInfo.oldParentId); } /** * Callback for when a bookmark node is removed. * @param {string} id The id of the removed bookmark node. * @param {!Object} removeInfo The information about removed. */ function handleRemoved(id, removeInfo) { if (bmm.list) bmm.list.handleRemoved(id, removeInfo); if (bmm.tree) bmm.tree.handleRemoved(id, removeInfo); bookmarkCache.updateChildren(removeInfo.parentId); bookmarkCache.remove(id); } /** * Callback for when all bookmark nodes have been deleted. */ function handleRemoveAll() { // Reload the list and the tree. if (bmm.list) bmm.list.reload(); if (bmm.tree) bmm.tree.reload(); } /** * Callback for when importing bookmark is started. */ function handleImportBegan() { chrome.bookmarks.onCreated.removeListener(handleCreated); chrome.bookmarks.onChanged.removeListener(handleBookmarkChanged); } /** * Callback for when importing bookmark node is finished. */ function handleImportEnded() { // When importing is done we reload the tree and the list. function f() { bmm.tree.removeEventListener('load', f); chrome.bookmarks.onCreated.addListener(handleCreated); chrome.bookmarks.onChanged.addListener(handleBookmarkChanged); if (!bmm.list) return; // TODO(estade): this should navigate to the newly imported folder, which // may be the bookmark bar if there were no previous bookmarks. bmm.list.reload(); } if (bmm.tree) { bmm.tree.addEventListener('load', f); bmm.tree.reload(); } } /** * Adds the listeners for the bookmark model change events. */ function addBookmarkModelListeners() { chrome.bookmarks.onChanged.addListener(handleBookmarkChanged); chrome.bookmarks.onChildrenReordered.addListener(handleChildrenReordered); chrome.bookmarks.onCreated.addListener(handleCreated); chrome.bookmarks.onMoved.addListener(handleMoved); chrome.bookmarks.onRemoved.addListener(handleRemoved); chrome.bookmarks.onImportBegan.addListener(handleImportBegan); chrome.bookmarks.onImportEnded.addListener(handleImportEnded); }; return { contains: contains, isFolder: isFolder, loadSubtree: loadSubtree, loadTree: loadTree, addBookmarkModelListeners: addBookmarkModelListeners }; }); $i18n{title} // Copyright (c) 2012 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. /** * Authenticator class wraps the communications between Gaia and its host. */ function Authenticator() { } /** * Gaia auth extension url origin. * @type {string} */ Authenticator.THIS_EXTENSION_ORIGIN = 'chrome-extension://mfffpogegjflfpflabcdkioaeobkgjik'; /** * The lowest version of the credentials passing API supported. * @type {number} */ Authenticator.MIN_API_VERSION_VERSION = 1; /** * The highest version of the credentials passing API supported. * @type {number} */ Authenticator.MAX_API_VERSION_VERSION = 1; /** * The key types supported by the credentials passing API. * @type {Array} Array of strings. */ Authenticator.API_KEY_TYPES = [ 'KEY_TYPE_PASSWORD_PLAIN', ]; /** * Allowed origins of the hosting page. * @type {Array} */ Authenticator.ALLOWED_PARENT_ORIGINS = [ 'chrome://oobe', 'chrome://chrome-signin' ]; /** * Singleton getter of Authenticator. * @return {Object} The singleton instance of Authenticator. */ Authenticator.getInstance = function() { if (!Authenticator.instance_) { Authenticator.instance_ = new Authenticator(); } return Authenticator.instance_; }; Authenticator.prototype = { email_: null, gaiaId_: null, // Depending on the key type chosen, this will contain the plain text password // or a credential derived from it along with the information required to // repeat the derivation, such as a salt. The information will be encoded so // that it contains printable ASCII characters only. The exact encoding is TBD // when support for key types other than plain text password is added. passwordBytes_: null, needPassword_: false, chooseWhatToSync_: false, skipForNow_: false, sessionIndex_: null, attemptToken_: null, // Input params from extension initialization URL. inputLang_: undefined, intputEmail_: undefined, isSAMLFlow_: false, gaiaLoaded_: false, supportChannel_: null, useEafe_: false, clientId_: '', GAIA_URL: 'https://accounts.google.com/', GAIA_PAGE_PATH: 'ServiceLogin?skipvpage=true&sarp=1&rm=hide', SERVICE_ID: 'chromeoslogin', CONTINUE_URL: Authenticator.THIS_EXTENSION_ORIGIN + '/success.html', CONSTRAINED_FLOW_SOURCE: 'chrome', initialize: function() { var handleInitializeMessage = function(e) { if (Authenticator.ALLOWED_PARENT_ORIGINS.indexOf(e.origin) == -1) { console.error('Unexpected parent message, origin=' + e.origin); return; } window.removeEventListener('message', handleInitializeMessage); var params = e.data; params.parentPage = e.origin; this.initializeFromParent_(params); this.onPageLoad_(); }.bind(this); document.addEventListener('DOMContentLoaded', function() { window.addEventListener('message', handleInitializeMessage); window.parent.postMessage({'method': 'loginUIDOMContentLoaded'}, '*'); }); }, initializeFromParent_: function(params) { this.parentPage_ = params.parentPage; this.gaiaUrl_ = params.gaiaUrl || this.GAIA_URL; this.gaiaPath_ = params.gaiaPath || this.GAIA_PAGE_PATH; this.inputLang_ = params.hl; this.inputEmail_ = params.email; this.service_ = params.service || this.SERVICE_ID; this.continueUrl_ = params.continueUrl || this.CONTINUE_URL; this.desktopMode_ = params.desktopMode == '1'; this.isConstrainedWindow_ = params.constrained == '1'; this.useEafe_ = params.useEafe || false; this.clientId_ = params.clientId || ''; this.initialFrameUrl_ = params.frameUrl || this.constructInitialFrameUrl_(); this.initialFrameUrlWithoutParams_ = stripParams(this.initialFrameUrl_); this.needPassword_ = params.needPassword == '1'; // For CrOS 'ServiceLogin' we assume that Gaia is loaded if we recieved // 'clearOldAttempts' message. For other scenarios Gaia doesn't send this // message so we have to rely on 'load' event. // TODO(dzhioev): Do not rely on 'load' event after b/16313327 is fixed. this.assumeLoadedOnLoadEvent_ = !this.gaiaPath_.startsWith('ServiceLogin') || this.service_ !== 'chromeoslogin' || this.useEafe_; }, isGaiaMessage_: function(msg) { // Not quite right, but good enough. return this.gaiaUrl_.startsWith(msg.origin) || this.GAIA_URL.startsWith(msg.origin); }, isParentMessage_: function(msg) { return msg.origin == this.parentPage_; }, constructInitialFrameUrl_: function() { var url = this.gaiaUrl_ + this.gaiaPath_; url = appendParam(url, 'service', this.service_); // Easy bootstrap use auth_code message as success signal instead of // continue URL. if (!this.useEafe_) url = appendParam(url, 'continue', this.continueUrl_); if (this.inputLang_) url = appendParam(url, 'hl', this.inputLang_); if (this.inputEmail_) url = appendParam(url, 'Email', this.inputEmail_); if (this.isConstrainedWindow_) url = appendParam(url, 'source', this.CONSTRAINED_FLOW_SOURCE); return url; }, onPageLoad_: function() { window.addEventListener('message', this.onMessage.bind(this), false); this.initSupportChannel_(); if (this.assumeLoadedOnLoadEvent_) { var gaiaFrame = $('gaia-frame'); var handler = function() { gaiaFrame.removeEventListener('load', handler); if (!this.gaiaLoaded_) { this.gaiaLoaded_ = true; this.maybeInitialized_(); if (this.useEafe_ && this.clientId_) { // Sends initial handshake message to EAFE. Note this fails with // SSO redirect because |gaiaFrame| sits on a different origin. gaiaFrame.contentWindow.postMessage({ clientId: this.clientId_ }, this.gaiaUrl_); } } }.bind(this); gaiaFrame.addEventListener('load', handler); } }, initSupportChannel_: function() { var supportChannel = new Channel(); supportChannel.connect('authMain'); supportChannel.registerMessage('channelConnected', function() { // Load the gaia frame after the background page indicates that it is // ready, so that the webRequest handlers are all setup first. var gaiaFrame = $('gaia-frame'); gaiaFrame.src = this.initialFrameUrl_; if (this.supportChannel_) { console.error('Support channel is already initialized.'); return; } this.supportChannel_ = supportChannel; if (this.desktopMode_) { this.supportChannel_.send({ name: 'initDesktopFlow', gaiaUrl: this.gaiaUrl_, continueUrl: stripParams(this.continueUrl_), isConstrainedWindow: this.isConstrainedWindow_, initialFrameUrlWithoutParams: this.initialFrameUrlWithoutParams_ }); this.supportChannel_.registerMessage( 'switchToFullTab', this.switchToFullTab_.bind(this)); } this.supportChannel_.registerMessage( 'completeLogin', this.onCompleteLogin_.bind(this)); this.initSAML_(); this.supportChannel_.send({name: 'resetAuth'}); this.maybeInitialized_(); }.bind(this)); window.setTimeout(function() { if (!this.supportChannel_) { // Give up previous channel and bind its 'channelConnected' to a no-op. supportChannel.registerMessage('channelConnected', function() {}); // Re-initialize the channel if it is not connected properly, e.g. // connect may be called before background script started running. this.initSupportChannel_(); } }.bind(this), 200); }, /** * Called when one of the initialization stages has finished. If all the * needed parts are initialized, notifies parent about successfull * initialization. */ maybeInitialized_: function() { if (!this.gaiaLoaded_ || !this.supportChannel_) return; var msg = { 'method': 'loginUILoaded' }; window.parent.postMessage(msg, this.parentPage_); }, /** * Invoked when the background script sends a message to indicate that the * current content does not fit in a constrained window. * @param {Object=} msg Extra info to send. */ switchToFullTab_: function(msg) { var parentMsg = { 'method': 'switchToFullTab', 'url': msg.url }; window.parent.postMessage(parentMsg, this.parentPage_); }, /** * Invoked when the signin flow is complete. * @param {Object=} opt_extraMsg Optional extra info to send. */ completeLogin_: function(opt_extraMsg) { var msg = { 'method': 'completeLogin', 'email': (opt_extraMsg && opt_extraMsg.email) || this.email_, 'password': this.passwordBytes_ || (opt_extraMsg && opt_extraMsg.password), 'usingSAML': this.isSAMLFlow_, 'chooseWhatToSync': this.chooseWhatToSync_ || false, 'skipForNow': (opt_extraMsg && opt_extraMsg.skipForNow) || this.skipForNow_, 'sessionIndex': (opt_extraMsg && opt_extraMsg.sessionIndex) || this.sessionIndex_, 'gaiaId': (opt_extraMsg && opt_extraMsg.gaiaId) || this.gaiaId_ }; window.parent.postMessage(msg, this.parentPage_); this.supportChannel_.send({name: 'resetAuth'}); }, /** * Invoked when support channel is connected. */ initSAML_: function() { this.isSAMLFlow_ = false; this.supportChannel_.registerMessage( 'onAuthPageLoaded', this.onAuthPageLoaded_.bind(this)); this.supportChannel_.registerMessage( 'onInsecureContentBlocked', this.onInsecureContentBlocked_.bind(this)); this.supportChannel_.registerMessage( 'apiCall', this.onAPICall_.bind(this)); this.supportChannel_.send({ name: 'setGaiaUrl', gaiaUrl: this.gaiaUrl_ }); if (!this.desktopMode_ && this.gaiaUrl_.startsWith('https://')) { // Abort the login flow when content served over an unencrypted connection // is detected on Chrome OS. This does not apply to tests that explicitly // set a non-https GAIA URL and want to perform all authentication over // http. this.supportChannel_.send({ name: 'setBlockInsecureContent', blockInsecureContent: true }); } }, /** * Invoked when the background page sends 'onHostedPageLoaded' message. * @param {!Object} msg Details sent with the message. */ onAuthPageLoaded_: function(msg) { if (msg.isSAMLPage && !this.isSAMLFlow_) { // GAIA redirected to a SAML login page. The credentials provided to this // page will determine what user gets logged in. The credentials obtained // from the GAIA login form are no longer relevant and can be discarded. this.isSAMLFlow_ = true; this.email_ = null; this.gaiaId_ = null; this.passwordBytes_ = null; } window.parent.postMessage({ 'method': 'authPageLoaded', 'isSAML': this.isSAMLFlow_, 'domain': extractDomain(msg.url) }, this.parentPage_); }, /** * Invoked when the background page sends an 'onInsecureContentBlocked' * message. * @param {!Object} msg Details sent with the message. */ onInsecureContentBlocked_: function(msg) { window.parent.postMessage({ 'method': 'insecureContentBlocked', 'url': stripParams(msg.url) }, this.parentPage_); }, /** * Invoked when one of the credential passing API methods is called by a SAML * provider. * @param {!Object} msg Details of the API call. */ onAPICall_: function(msg) { var call = msg.call; if (call.method == 'initialize') { if (!Number.isInteger(call.requestedVersion) || call.requestedVersion < Authenticator.MIN_API_VERSION_VERSION) { this.sendInitializationFailure_(); return; } this.apiVersion_ = Math.min(call.requestedVersion, Authenticator.MAX_API_VERSION_VERSION); this.initialized_ = true; this.sendInitializationSuccess_(); return; } if (call.method == 'add') { if (Authenticator.API_KEY_TYPES.indexOf(call.keyType) == -1) { console.error('Authenticator.onAPICall_: unsupported key type'); return; } // Not setting |email_| and |gaiaId_| because this API call will // eventually be followed by onCompleteLogin_() which does set it. this.apiToken_ = call.token; this.passwordBytes_ = call.passwordBytes; } else if (call.method == 'confirm') { if (call.token != this.apiToken_) console.error('Authenticator.onAPICall_: token mismatch'); } else { console.error('Authenticator.onAPICall_: unknown message'); } }, onGotAuthCode_: function(authCode) { window.parent.postMessage({ 'method': 'completeAuthenticationAuthCodeOnly', 'authCode': authCode }, this.parentPage_); }, sendInitializationSuccess_: function() { this.supportChannel_.send({name: 'apiResponse', response: { result: 'initialized', version: this.apiVersion_, keyTypes: Authenticator.API_KEY_TYPES }}); }, sendInitializationFailure_: function() { this.supportChannel_.send({ name: 'apiResponse', response: {result: 'initialization_failed'} }); }, /** * Callback invoked for 'completeLogin' message. * @param {Object=} msg Message sent from background page. */ onCompleteLogin_: function(msg) { if (!msg.email || !msg.gaiaId || !msg.sessionIndex) { // On desktop, if the skipForNow message field is set, send it to handler. // This does not require the email, gaiaid or session to be valid. if (this.desktopMode_ && msg.skipForNow) { this.completeLogin_(msg); } else { console.error('Missing fields to complete login.'); window.parent.postMessage({method: 'missingGaiaInfo'}, this.parentPage_); return; } } // Skip SAML extra steps for desktop flow and non-SAML flow. if (!this.isSAMLFlow_ || this.desktopMode_) { this.completeLogin_(msg); return; } this.email_ = msg.email; this.gaiaId_ = msg.gaiaId; // Password from |msg| is not used because ChromeOS SAML flow // gets password by asking user to confirm. this.skipForNow_ = msg.skipForNow; this.sessionIndex_ = msg.sessionIndex; if (this.passwordBytes_) { // If the credentials passing API was used, login is complete. window.parent.postMessage({method: 'samlApiUsed'}, this.parentPage_); this.completeLogin_(msg); } else if (!this.needPassword_) { // If the credentials passing API was not used, the password was obtained // by scraping. It must be verified before use. However, the host may not // be interested in the password at all. In that case, verification is // unnecessary and login is complete. this.completeLogin_(msg); } else { this.supportChannel_.sendWithCallback( {name: 'getScrapedPasswords'}, function(passwords) { if (passwords.length == 0) { window.parent.postMessage( {method: 'noPassword', email: this.email_}, this.parentPage_); } else { window.parent.postMessage({method: 'confirmPassword', email: this.email_, passwordCount: passwords.length}, this.parentPage_); } }.bind(this)); } }, onVerifyConfirmedPassword_: function(password) { this.supportChannel_.sendWithCallback( {name: 'getScrapedPasswords'}, function(passwords) { for (var i = 0; i < passwords.length; ++i) { if (passwords[i] == password) { this.passwordBytes_ = passwords[i]; // SAML login is complete when the user has successfully // confirmed the password. if (this.passwordBytes_ !== null) this.completeLogin_(); return; } } window.parent.postMessage( {method: 'confirmPassword', email: this.email_}, this.parentPage_); }.bind(this)); }, onMessage: function(e) { var msg = e.data; if (this.useEafe_) { if (msg == '!_{h:\'gaia-frame\'}' && this.isGaiaMessage_(e)) { // Sends client ID again on the hello message to work around the SSO // signin issue. // TODO(xiyuan): Revisit this when EAFE is integrated or for webview. $('gaia-frame').contentWindow.postMessage({ clientId: this.clientId_ }, this.gaiaUrl_); } else if (typeof msg == 'object' && msg.type == 'authorizationCode' && this.isGaiaMessage_(e)) { this.onGotAuthCode_(msg.authorizationCode); } else { console.error('Authenticator.onMessage: unknown message' + ', msg=' + JSON.stringify(msg)); } return; } if (msg.method == 'attemptLogin' && this.isGaiaMessage_(e)) { // At this point GAIA does not yet know the gaiaId, so its not set here. this.email_ = msg.email; this.passwordBytes_ = msg.password; this.attemptToken_ = msg.attemptToken; this.chooseWhatToSync_ = msg.chooseWhatToSync; this.isSAMLFlow_ = false; if (this.supportChannel_) this.supportChannel_.send({name: 'startAuth'}); else console.error('Support channel is not initialized.'); } else if (msg.method == 'clearOldAttempts' && this.isGaiaMessage_(e)) { if (!this.gaiaLoaded_) { this.gaiaLoaded_ = true; this.maybeInitialized_(); } this.email_ = null; this.gaiaId_ = null; this.sessionIndex_ = false; this.passwordBytes_ = null; this.attemptToken_ = null; this.isSAMLFlow_ = false; this.skipForNow_ = false; this.chooseWhatToSync_ = false; if (this.supportChannel_) { this.supportChannel_.send({name: 'resetAuth'}); // This message is for clearing saml properties in gaia_auth_host and // oobe_screen_oauth_enrollment. window.parent.postMessage({ 'method': 'resetAuthFlow', }, this.parentPage_); } } else if (msg.method == 'verifyConfirmedPassword' && this.isParentMessage_(e)) { this.onVerifyConfirmedPassword_(msg.password); } else if (msg.method == 'redirectToSignin' && this.isParentMessage_(e)) { $('gaia-frame').src = this.constructInitialFrameUrl_(); } else { console.error('Authenticator.onMessage: unknown message + origin!?'); } } }; Authenticator.getInstance().initialize(); /* Copyright 2013 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. */ html, body, iframe { height: 100%; margin: 0; padding: 0; width: 100%; } iframe { overflow: hidden; } webview { display: inline-block; height: 100%; margin: 0; padding: 0; width: 100%; }
// Copyright (c) 2012 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. /** * @fileoverview Offline login implementation. */ /** * Initialize the offline page. * @param {Object} params Intialization params passed from parent page. */ function load(params) { // Setup localized strings. $('sign-in-title').textContent = decodeURIComponent(params['stringSignIn']); $('email-label').textContent = decodeURIComponent(params['stringEmail']); $('password-label').textContent = decodeURIComponent(params['stringPassword']); $('submit-button').value = decodeURIComponent(params['stringSignIn']); $('empty-email-alert').textContent = decodeURIComponent(params['stringEmptyEmail']); $('empty-password-alert').textContent = decodeURIComponent(params['stringEmptyPassword']); $('errormsg-alert').textContent = decodeURIComponent(params['stringError']); // Setup actions. var form = $('offline-login-form'); form.addEventListener('submit', function(e) { // Clear all previous errors. form.email.classList.remove('field-error'); form.password.classList.remove('field-error'); form.password.classList.remove('form-error'); if (form.email.value == '') { form.email.classList.add('field-error'); form.email.focus(); } else if (form.password.value == '') { form.password.classList.add('field-error'); form.password.focus(); } else { var msg = { 'method': 'offlineLogin', 'email': form.email.value, 'password': form.password.value }; window.parent.postMessage(msg, 'chrome://oobe/'); } e.preventDefault(); }); var email = params['email']; if (email) { // Email is present, which means that unsuccessful login attempt has been // made. Try to mimic Gaia's behaviour. form.email.value = email; form.password.classList.add('form-error'); form.password.focus(); } else { form.email.focus(); } window.parent.postMessage({'method': 'loginUILoaded'}, 'chrome://oobe/'); } /** * Handles initialization message from parent page. * @param {MessageEvent} e */ function handleInitializeMessage(e) { var ALLOWED_PARENT_ORIGINS = [ 'chrome://oobe', 'chrome://chrome-signin' ]; if (ALLOWED_PARENT_ORIGINS.indexOf(e.origin) == -1) return; window.removeEventListener('message', handleInitializeMessage); var params = e.data; params.parentPage = e.origin; load(params); } document.addEventListener('DOMContentLoaded', function() { window.addEventListener('message', handleInitializeMessage); window.parent.postMessage({'method': 'loginUIDOMContentLoaded'}, '*'); }); /* Copyright 2014 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. */ /* TODO(dbeam): what's wrong with * here? Specificity issues? */ audio, body, canvas, command, dd, div, dl, dt, embed, form, group, h1, h2, h3, h4, h5, h6, html, img, mark, meter, object, output, progress, summary, td, time, tr, video { border: 0; margin: 0; padding: 0; } html { background: #fff; color: #333; direction: ltr; font: 81.25% arial, helvetica, sans-serif; line-height: 1; } h1, h2, h3, h4, h5, h6 { color: #222; font-size: 1.54em; font-weight: normal; line-height: 24px; margin: 0 0 .46em; } strong { color: #222; } body, html { height: 100%; min-width: 100%; position: absolute; } .wrapper { min-height: 100%; position: relative; } .content { padding: 0 44px; } .main { margin: 0 auto; padding-bottom: 100px; padding-top: 23px; width: 650px; } button, input, select, textarea { font-family: inherit; font-size: inherit; } input[type=email], input[type=number], input[type=password], input[type=text], input[type=url] { -webkit-box-sizing: border-box; background: #fff; border: 1px solid #d9d9d9; border-radius: 1px; border-top: 1px solid #c0c0c0; box-sizing: border-box; display: inline-block; height: 29px; margin: 0; padding-left: 8px; } input[type=email]:hover, input[type=number]:hover, input[type=password]:hover, input[type=text]:hover, input[type=url]:hover { border: 1px solid #b9b9b9; border-top: 1px solid #a0a0a0; box-shadow: inset 0 1px 2px rgba(0,0,0,0.1); } input[type=email]:focus, input[type=number]:focus, input[type=password]:focus, input[type=text]:focus, input[type=url]:focus { border: 1px solid rgb(77, 144, 254); box-shadow: inset 0 1px 2px rgba(0,0,0,0.3); outline: none; } input[type=email][disabled=disabled], input[type=number][disabled=disabled], input[type=password][disabled=disabled], input[type=text][disabled=disabled], input[type=url][disabled=disabled] { background: #f5f5f5; border: 1px solid #e5e5e5; } input[type=email][disabled=disabled]:hover, input[type=number][disabled=disabled]:hover, input[type=password][disabled=disabled]:hover, input[type=text][disabled=disabled]:hover, input[type=url][disabled=disabled]:hover { box-shadow: none; } .g-button { -webkit-transition: all 218ms; -webkit-user-select: none; background-color: #f5f5f5; background-image: linear-gradient(to bottom, #f5f5f5, #f1f1f1); border: 1px solid rgba(0,0,0,0.1); border-radius: 2px; color: #555; cursor: default; display: inline-block; font-size: 11px; font-weight: bold; height: 27px; line-height: 27px; min-width: 54px; padding: 0 8px; text-align: center; transition: all 218ms; user-select: none; } *+html .g-button { min-width: 70px; } button.g-button, input[type=submit].g-button { height: 29px; line-height: 29px; margin: 0; vertical-align: bottom; } *+html button.g-button, *+html input[type=submit].g-button { overflow: visible; } .g-button:hover { -webkit-transition: all 0ms; background-color: #f8f8f8; background-image: linear-gradient(to bottom, #f8f8f8, #f1f1f1); border: 1px solid #c6c6c6; box-shadow: 0 1px 1px rgba(0,0,0,0.1); color: #333; text-decoration: none; transition: all 0ms; } .g-button:active { background-color: #f6f6f6; background-image: linear-gradient(to bottom, #f6f6f6, #f1f1f1); box-shadow: inset 0 1px 2px rgba(0,0,0,0.1); } .g-button:visited { color: #666; } .g-button-submit { background-color: rgb(77, 144, 254); background-image: linear-gradient(to bottom, rgb(77, 144, 254), rgb(71, 135, 237)); border: 1px solid rgb(48, 121, 237); color: #fff; text-shadow: 0 1px rgba(0,0,0,0.1); } .g-button-submit:hover { background-color: rgb(53, 122, 232); background-image: linear-gradient(to bottom, rgb(77, 144, 254), rgb(53, 122, 232)); border: 1px solid rgb(47, 91, 183); color: #fff; text-shadow: 0 1px rgba(0,0,0,0.3); } .g-button-submit:active { box-shadow: inset 0 1px 2px rgba(0,0,0,0.3); } .g-button-submit:visited { color: #fff; } .g-button-submit:focus { box-shadow: inset 0 0 0 1px #fff; } .g-button-submit:focus:hover { box-shadow: inset 0 0 0 1px #fff, 0 1px 1px rgba(0,0,0,0.1); } .g-button:hover img { opacity: .72; } .g-button:active img { opacity: 1; } .errormsg { color: rgb(221, 75, 57); display: block; line-height: 17px; margin: .5em 0 0; } input[type=email].form-error, input[type=number].form-error, input[type=password].form-error, input[type=text].form-error, input[type=url].form-error, input[type=text].field-error, input[type=password].field-error { border: 1px solid rgb(221, 75, 57); } html { background: transparent; } .content { width: auto; } .main { padding-bottom: 12px; padding-top: 23px; } .signin-box h2 { font-size: 16px; height: 16px; line-height: 17px; margin: 0 0 1.2em; position: relative; } .signin-box label { display: block; margin: 0 0 1.5em; } .signin-box input[type=text], .signin-box input[type=password] { font-size: 15px; height: 32px; width: 100%; } .signin-box .email-label, .signin-box .passwd-label { -webkit-user-select: none; display: block; font-weight: bold; margin: 0 0 .5em; user-select: none; } .signin-box input[type=submit] { font-size: 13px; height: 32px; margin: 0 1.5em 1.2em 0; } .errormsg { display: none; } .form-error + .errormsg, .field-error + .errormsg { display: block; } // Copyright (c) 2012 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. /** * Alias for document.getElementById. * @param {string} id The ID of the element to find. * @return {HTMLElement} The found element or null if not found. */ function $(id) { return document.getElementById(id); } /** * Creates a new URL which is the old URL with a GET param of key=value. * Copied from ui/webui/resources/js/util.js. * @param {string} url The base URL. There is not sanity checking on the URL so * it must be passed in a proper format. * @param {string} key The key of the param. * @param {string} value The value of the param. * @return {string} The new URL. */ function appendParam(url, key, value) { var param = encodeURIComponent(key) + '=' + encodeURIComponent(value); if (url.indexOf('?') == -1) return url + '?' + param; return url + '&' + param; } /** * Creates a new URL by striping all query parameters. * @param {string} url The original URL. * @return {string} The new URL with all query parameters stripped. */ function stripParams(url) { return url.substring(0, url.indexOf('?')) || url; } /** * Extract domain name from an URL. * @param {string} url An URL string. * @return {string} The host name of the URL. */ function extractDomain(url) { var a = document.createElement('a'); a.href = url; return a.hostname; } // Copyright 2013 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. /** * @fileoverview * A background script of the auth extension that bridges the communication * between the main and injected scripts. * * Here is an overview of the communication flow when SAML is being used: * 1. The main script sends the |startAuth| signal to this background script, * indicating that the authentication flow has started and SAML pages may be * loaded from now on. * 2. A script is injected into each SAML page. The injected script sends three * main types of messages to this background script: * a) A |pageLoaded| message is sent when the page has been loaded. This is * forwarded to the main script as |onAuthPageLoaded|. * b) If the SAML provider supports the credential passing API, the API calls * are sent to this background script as |apiCall| messages. These * messages are forwarded unmodified to the main script. * c) The injected script scrapes passwords. They are sent to this background * script in |updatePassword| messages. The main script can request a list * of the scraped passwords by sending the |getScrapedPasswords| message. */ /** * BackgroundBridgeManager maintains an array of BackgroundBridge, indexed by * the associated tab id. */ function BackgroundBridgeManager() { this.bridges_ = {}; } BackgroundBridgeManager.prototype = { CONTINUE_URL_BASE: 'chrome-extension://mfffpogegjflfpflabcdkioaeobkgjik' + '/success.html', // Maps a tab id to its associated BackgroundBridge. bridges_: null, run: function() { chrome.runtime.onConnect.addListener(this.onConnect_.bind(this)); chrome.webRequest.onBeforeRequest.addListener( function(details) { if (this.bridges_[details.tabId]) return this.bridges_[details.tabId].onInsecureRequest(details.url); }.bind(this), {urls: ['http://*/*', 'file://*/*', 'ftp://*/*']}, ['blocking']); chrome.webRequest.onBeforeSendHeaders.addListener( function(details) { if (this.bridges_[details.tabId]) return this.bridges_[details.tabId].onBeforeSendHeaders(details); else return {requestHeaders: details.requestHeaders}; }.bind(this), {urls: ['*://*/*'], types: ['sub_frame']}, ['blocking', 'requestHeaders']); chrome.webRequest.onHeadersReceived.addListener( function(details) { if (this.bridges_[details.tabId]) return this.bridges_[details.tabId].onHeadersReceived(details); }.bind(this), {urls: ['*://*/*'], types: ['sub_frame']}, ['blocking', 'responseHeaders']); chrome.webRequest.onCompleted.addListener( function(details) { if (this.bridges_[details.tabId]) this.bridges_[details.tabId].onCompleted(details); }.bind(this), {urls: ['*://*/*', this.CONTINUE_URL_BASE + '*'], types: ['sub_frame']}, ['responseHeaders']); }, onConnect_: function(port) { var tabId = this.getTabIdFromPort_(port); if (!this.bridges_[tabId]) this.bridges_[tabId] = new BackgroundBridge(tabId); if (port.name == 'authMain') { this.bridges_[tabId].setupForAuthMain(port); port.onDisconnect.addListener(function() { delete this.bridges_[tabId]; }.bind(this)); } else if (port.name == 'injected') { this.bridges_[tabId].setupForInjected(port); } else { console.error('Unexpected connection, port.name=' + port.name); } }, getTabIdFromPort_: function(port) { return port.sender.tab ? port.sender.tab.id : -1; } }; /** * BackgroundBridge allows the main script and the injected script to * collaborate. It forwards credentials API calls to the main script and * maintains a list of scraped passwords. * @param {string} tabId The associated tab ID. */ function BackgroundBridge(tabId) { this.tabId_ = tabId; this.passwordStore_ = {}; } BackgroundBridge.prototype = { // The associated tab ID. Only used for debugging now. tabId: null, // The initial URL loaded in the gaia iframe. We only want to handle // onCompleted() for the frame that loaded this URL. initialFrameUrlWithoutParams: null, // On process onCompleted() requests that come from this frame Id. frameId: -1, isDesktopFlow_: false, // Whether the extension is loaded in a constrained window. // Set from main auth script. isConstrainedWindow_: null, // Email of the newly authenticated user based on the gaia response header // 'google-accounts-signin'. email_: null, // Gaia Id of the newly authenticated user based on the gaia response // header 'google-accounts-signin'. gaiaId_: null, // Session index of the newly authenticated user based on the gaia response // header 'google-accounts-signin'. sessionIndex_: null, // Gaia URL base that is set from main auth script. gaiaUrl_: null, // Whether to abort the authentication flow and show an error messagen when // content served over an unencrypted connection is detected. blockInsecureContent_: false, // Whether auth flow has started. It is used as a signal of whether the // injected script should scrape passwords. authStarted_: false, // Whether SAML flow is going. isSAML_: false, passwordStore_: null, channelMain_: null, channelInjected_: null, /** * Sets up the communication channel with the main script. */ setupForAuthMain: function(port) { this.channelMain_ = new Channel(); this.channelMain_.init(port); // Registers for desktop related messages. this.channelMain_.registerMessage( 'initDesktopFlow', this.onInitDesktopFlow_.bind(this)); // Registers for SAML related messages. this.channelMain_.registerMessage( 'setGaiaUrl', this.onSetGaiaUrl_.bind(this)); this.channelMain_.registerMessage( 'setBlockInsecureContent', this.onSetBlockInsecureContent_.bind(this)); this.channelMain_.registerMessage( 'resetAuth', this.onResetAuth_.bind(this)); this.channelMain_.registerMessage( 'startAuth', this.onAuthStarted_.bind(this)); this.channelMain_.registerMessage( 'getScrapedPasswords', this.onGetScrapedPasswords_.bind(this)); this.channelMain_.registerMessage( 'apiResponse', this.onAPIResponse_.bind(this)); this.channelMain_.send({ 'name': 'channelConnected' }); }, /** * Sets up the communication channel with the injected script. */ setupForInjected: function(port) { this.channelInjected_ = new Channel(); this.channelInjected_.init(port); this.channelInjected_.registerMessage( 'apiCall', this.onAPICall_.bind(this)); this.channelInjected_.registerMessage( 'updatePassword', this.onUpdatePassword_.bind(this)); this.channelInjected_.registerMessage( 'pageLoaded', this.onPageLoaded_.bind(this)); this.channelInjected_.registerMessage( 'getSAMLFlag', this.onGetSAMLFlag_.bind(this)); }, /** * Handler for 'initDesktopFlow' signal sent from the main script. * Only called in desktop mode. */ onInitDesktopFlow_: function(msg) { this.isDesktopFlow_ = true; this.gaiaUrl_ = msg.gaiaUrl; this.isConstrainedWindow_ = msg.isConstrainedWindow; this.initialFrameUrlWithoutParams = msg.initialFrameUrlWithoutParams; }, /** * Handler for webRequest.onCompleted. It 1) detects loading of continue URL * and notifies the main script of signin completion; 2) detects if the * current page could be loaded in a constrained window and signals the main * script of switching to full tab if necessary. */ onCompleted: function(details) { // Only monitors requests in the gaia frame. The gaia frame is the one // where the initial frame URL completes. if (details.url.lastIndexOf( this.initialFrameUrlWithoutParams, 0) == 0) { this.frameId = details.frameId; } if (this.frameId == -1) { // If for some reason the frameId could not be set above, just make sure // the frame is more than two levels deep (since the gaia frame is at // least three levels deep). if (details.parentFrameId <= 0) return; } else if (details.frameId != this.frameId) { return; } if (details.url.lastIndexOf(backgroundBridgeManager.CONTINUE_URL_BASE, 0) == 0) { var skipForNow = false; if (details.url.indexOf('ntp=1') >= 0) skipForNow = true; // TOOD(guohui): For desktop SAML flow, show password confirmation UI. var passwords = this.onGetScrapedPasswords_(); var msg = { 'name': 'completeLogin', 'email': this.email_, 'gaiaId': this.gaiaId_, 'password': passwords[0], 'sessionIndex': this.sessionIndex_, 'skipForNow': skipForNow }; this.channelMain_.send(msg); } else if (this.isConstrainedWindow_) { // The header google-accounts-embedded is only set on gaia domain. if (this.gaiaUrl_ && details.url.lastIndexOf(this.gaiaUrl_) == 0) { var headers = details.responseHeaders; for (var i = 0; headers && i < headers.length; ++i) { if (headers[i].name.toLowerCase() == 'google-accounts-embedded') return; } } var msg = { 'name': 'switchToFullTab', 'url': details.url }; this.channelMain_.send(msg); } }, /** * Handler for webRequest.onBeforeRequest, invoked when content served over an * unencrypted connection is detected. Determines whether the request should * be blocked and if so, signals that an error message needs to be shown. * @param {string} url The URL that was blocked. * @return {!Object} Decision whether to block the request. */ onInsecureRequest: function(url) { if (!this.blockInsecureContent_) return {}; this.channelMain_.send({name: 'onInsecureContentBlocked', url: url}); return {cancel: true}; }, /** * Handler or webRequest.onHeadersReceived. It reads the authenticated user * email from google-accounts-signin-header. * @return {!Object} Modified request headers. */ onHeadersReceived: function(details) { var headers = details.responseHeaders; if (this.gaiaUrl_ && details.url.lastIndexOf(this.gaiaUrl_) == 0) { for (var i = 0; headers && i < headers.length; ++i) { if (headers[i].name.toLowerCase() == 'google-accounts-signin') { var headerValues = headers[i].value.toLowerCase().split(','); var signinDetails = {}; headerValues.forEach(function(e) { var pair = e.split('='); signinDetails[pair[0].trim()] = pair[1].trim(); }); // Remove "" around. this.email_ = signinDetails['email'].slice(1, -1); this.gaiaId_ = signinDetails['obfuscatedid'].slice(1, -1); this.sessionIndex_ = signinDetails['sessionindex']; break; } } } if (!this.isDesktopFlow_) { // Check whether GAIA headers indicating the start or end of a SAML // redirect are present. If so, synthesize cookies to mark these points. for (var i = 0; headers && i < headers.length; ++i) { if (headers[i].name.toLowerCase() == 'google-accounts-saml') { var action = headers[i].value.toLowerCase(); if (action == 'start') { this.isSAML_ = true; // GAIA is redirecting to a SAML IdP. Any cookies contained in the // current |headers| were set by GAIA. Any cookies set in future // requests will be coming from the IdP. Append a cookie to the // current |headers| that marks the point at which the redirect // occurred. headers.push({name: 'Set-Cookie', value: 'google-accounts-saml-start=now'}); return {responseHeaders: headers}; } else if (action == 'end') { this.isSAML_ = false; // The SAML IdP has redirected back to GAIA. Add a cookie that marks // the point at which the redirect occurred occurred. It is // important that this cookie be prepended to the current |headers| // because any cookies contained in the |headers| were already set // by GAIA, not the IdP. Due to limitations in the webRequest API, // it is not trivial to prepend a cookie: // // The webRequest API only allows for deleting and appending // headers. To prepend a cookie (C), three steps are needed: // 1) Delete any headers that set cookies (e.g., A, B). // 2) Append a header which sets the cookie (C). // 3) Append the original headers (A, B). // // Due to a further limitation of the webRequest API, it is not // possible to delete a header in step 1) and append an identical // header in step 3). To work around this, a trailing semicolon is // added to each header before appending it. Trailing semicolons are // ignored by Chrome in cookie headers, causing the modified headers // to actually set the original cookies. var otherHeaders = []; var cookies = [{name: 'Set-Cookie', value: 'google-accounts-saml-end=now'}]; for (var j = 0; j < headers.length; ++j) { if (headers[j].name.toLowerCase().startsWith('set-cookie')) { var header = headers[j]; header.value += ';'; cookies.push(header); } else { otherHeaders.push(headers[j]); } } return {responseHeaders: otherHeaders.concat(cookies)}; } } } } return {}; }, /** * Handler for webRequest.onBeforeSendHeaders. * @return {!Object} Modified request headers. */ onBeforeSendHeaders: function(details) { if (!this.isDesktopFlow_ && this.gaiaUrl_ && details.url.startsWith(this.gaiaUrl_)) { details.requestHeaders.push({ name: 'X-Cros-Auth-Ext-Support', value: 'SAML' }); } return {requestHeaders: details.requestHeaders}; }, /** * Handler for 'setGaiaUrl' signal sent from the main script. */ onSetGaiaUrl_: function(msg) { this.gaiaUrl_ = msg.gaiaUrl; }, /** * Handler for 'setBlockInsecureContent' signal sent from the main script. */ onSetBlockInsecureContent_: function(msg) { this.blockInsecureContent_ = msg.blockInsecureContent; }, /** * Handler for 'resetAuth' signal sent from the main script. */ onResetAuth_: function() { this.authStarted_ = false; this.passwordStore_ = {}; this.isSAML_ = false; }, /** * Handler for 'authStarted' signal sent from the main script. */ onAuthStarted_: function() { this.authStarted_ = true; this.passwordStore_ = {}; this.isSAML_ = false; }, /** * Handler for 'getScrapedPasswords' request sent from the main script. * @return {Array} The array with de-duped scraped passwords. */ onGetScrapedPasswords_: function() { var passwords = {}; for (var property in this.passwordStore_) { passwords[this.passwordStore_[property]] = true; } return Object.keys(passwords); }, /** * Handler for 'apiResponse' signal sent from the main script. Passes on the * |msg| to the injected script. */ onAPIResponse_: function(msg) { this.channelInjected_.send(msg); }, onAPICall_: function(msg) { this.channelMain_.send(msg); }, onUpdatePassword_: function(msg) { if (!this.authStarted_) return; this.passwordStore_[msg.id] = msg.password; }, onPageLoaded_: function(msg) { if (this.channelMain_) this.channelMain_.send({name: 'onAuthPageLoaded', url: msg.url, isSAMLPage: this.isSAML_}); }, onGetSAMLFlag_: function(msg) { return this.isSAML_; } }; var backgroundBridgeManager = new BackgroundBridgeManager(); backgroundBridgeManager.run(); // Copyright 2013 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. /** * @fileoverview * Script to be injected into SAML provider pages, serving three main purposes: * 1. Signal hosting extension that an external page is loaded so that the * UI around it should be changed accordingly; * 2. Provide an API via which the SAML provider can pass user credentials to * Chrome OS, allowing the password to be used for encrypting user data and * offline login. * 3. Scrape password fields, making the password available to Chrome OS even if * the SAML provider does not support the credential passing API. */ (function() { function APICallForwarder() { } /** * The credential passing API is used by sending messages to the SAML page's * |window| object. This class forwards API calls from the SAML page to a * background script and API responses from the background script to the SAML * page. Communication with the background script occurs via a |Channel|. */ APICallForwarder.prototype = { // Channel to which API calls are forwarded. channel_: null, /** * Initialize the API call forwarder. * @param {!Object} channel Channel to which API calls should be forwarded. */ init: function(channel) { this.channel_ = channel; this.channel_.registerMessage('apiResponse', this.onAPIResponse_.bind(this)); window.addEventListener('message', this.onMessage_.bind(this)); }, onMessage_: function(event) { if (event.source != window || typeof event.data != 'object' || !event.data.hasOwnProperty('type') || event.data.type != 'gaia_saml_api') { return; } // Forward API calls to the background script. this.channel_.send({name: 'apiCall', call: event.data.call}); }, onAPIResponse_: function(msg) { // Forward API responses to the SAML page. window.postMessage({type: 'gaia_saml_api_reply', response: msg.response}, '/'); } }; /** * A class to scrape password from type=password input elements under a given * docRoot and send them back via a Channel. */ function PasswordInputScraper() { } PasswordInputScraper.prototype = { // URL of the page. pageURL_: null, // Channel to send back changed password. channel_: null, // An array to hold password fields. passwordFields_: null, // An array to hold cached password values. passwordValues_: null, // A MutationObserver to watch for dynamic password field creation. passwordFieldsObserver: null, /** * Initialize the scraper with given channel and docRoot. Note that the * scanning for password fields happens inside the function and does not * handle DOM tree changes after the call returns. * @param {!Object} channel The channel to send back password. * @param {!string} pageURL URL of the page. * @param {!HTMLElement} docRoot The root element of the DOM tree that * contains the password fields of interest. */ init: function(channel, pageURL, docRoot) { this.pageURL_ = pageURL; this.channel_ = channel; this.passwordFields_ = []; this.passwordValues_ = []; this.findAndTrackChildren(docRoot); this.passwordFieldsObserver = new MutationObserver(function(mutations) { mutations.forEach(function(mutation) { Array.prototype.forEach.call( mutation.addedNodes, function(addedNode) { if (addedNode.nodeType != Node.ELEMENT_NODE) return; if (addedNode.matches('input[type=password]')) { this.trackPasswordField(addedNode); } else { this.findAndTrackChildren(addedNode); } }.bind(this)); }.bind(this)); }.bind(this)); this.passwordFieldsObserver.observe(docRoot, {subtree: true, childList: true}); }, /** * Find and track password fields that are descendants of the given element. * @param {!HTMLElement} element The parent element to search from. */ findAndTrackChildren: function(element) { Array.prototype.forEach.call( element.querySelectorAll('input[type=password]'), function(field) { this.trackPasswordField(field); }.bind(this)); }, /** * Start tracking value changes of the given password field if it is * not being tracked yet. * @param {!HTMLInputElement} passworField The password field to track. */ trackPasswordField: function(passwordField) { var existing = this.passwordFields_.filter(function(element) { return element === passwordField; }); if (existing.length != 0) return; var index = this.passwordFields_.length; var fieldId = passwordField.id || passwordField.name || ''; passwordField.addEventListener( 'input', this.onPasswordChanged_.bind(this, index, fieldId)); this.passwordFields_.push(passwordField); this.passwordValues_.push(passwordField.value); }, /** * Check if the password field at |index| has changed. If so, sends back * the updated value. */ maybeSendUpdatedPassword: function(index, fieldId) { var newValue = this.passwordFields_[index].value; if (newValue == this.passwordValues_[index]) return; this.passwordValues_[index] = newValue; // Use an invalid char for URL as delimiter to concatenate page url, // password field index and id to construct a unique ID for the password // field. var passwordId = this.pageURL_.split('#')[0].split('?')[0] + '|' + index + '|' + fieldId; this.channel_.send({ name: 'updatePassword', id: passwordId, password: newValue }); }, /** * Handles 'change' event in the scraped password fields. * @param {number} index The index of the password fields in * |passwordFields_|. * @param {string} fieldId The id or name of the password field or blank. */ onPasswordChanged_: function(index, fieldId) { this.maybeSendUpdatedPassword(index, fieldId); } }; function onGetSAMLFlag(channel, isSAMLPage) { if (!isSAMLPage) return; var pageURL = window.location.href; channel.send({name: 'pageLoaded', url: pageURL}); var initPasswordScraper = function() { var passwordScraper = new PasswordInputScraper(); passwordScraper.init(channel, pageURL, document.documentElement); }; if (document.readyState == 'loading') { window.addEventListener('readystatechange', function listener(event) { if (document.readyState == 'loading') return; initPasswordScraper(); window.removeEventListener(event.type, listener, true); }, true); } else { initPasswordScraper(); } } var channel = Channel.create(); channel.connect('injected'); channel.sendWithCallback({name: 'getSAMLFlag'}, onGetSAMLFlag.bind(undefined, channel)); var apiCallForwarder = new APICallForwarder(); apiCallForwarder.init(channel); })(); // Copyright 2013 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. /** * Channel to the background script. */ function Channel() { this.messageCallbacks_ = {}; this.internalRequestCallbacks_ = {}; } /** @const */ Channel.INTERNAL_REQUEST_MESSAGE = 'internal-request-message'; /** @const */ Channel.INTERNAL_REPLY_MESSAGE = 'internal-reply-message'; Channel.prototype = { // Message port to use to communicate with background script. port_: null, // Registered message callbacks. messageCallbacks_: null, // Internal request id to track pending requests. nextInternalRequestId_: 0, // Pending internal request callbacks. internalRequestCallbacks_: null, /** * Initialize the channel with given port for the background script. */ init: function(port) { this.port_ = port; this.port_.onMessage.addListener(this.onMessage_.bind(this)); }, /** * Connects to the background script with the given name. */ connect: function(name) { this.port_ = chrome.runtime.connect({name: name}); this.port_.onMessage.addListener(this.onMessage_.bind(this)); }, /** * Associates a message name with a callback. When a message with the name * is received, the callback will be invoked with the message as its arg. * Note only the last registered callback will be invoked. */ registerMessage: function(name, callback) { this.messageCallbacks_[name] = callback; }, /** * Sends a message to the other side of the channel. */ send: function(msg) { this.port_.postMessage(msg); }, /** * Sends a message to the other side and invokes the callback with * the replied object. Useful for message that expects a returned result. */ sendWithCallback: function(msg, callback) { var requestId = this.nextInternalRequestId_++; this.internalRequestCallbacks_[requestId] = callback; this.send({ name: Channel.INTERNAL_REQUEST_MESSAGE, requestId: requestId, payload: msg }); }, /** * Invokes message callback using given message. * @return {*} The return value of the message callback or null. */ invokeMessageCallbacks_: function(msg) { var name = msg.name; if (this.messageCallbacks_[name]) return this.messageCallbacks_[name](msg); console.error('Error: Unexpected message, name=' + name); return null; }, /** * Invoked when a message is received. */ onMessage_: function(msg) { var name = msg.name; if (name == Channel.INTERNAL_REQUEST_MESSAGE) { var payload = msg.payload; var result = this.invokeMessageCallbacks_(payload); this.send({ name: Channel.INTERNAL_REPLY_MESSAGE, requestId: msg.requestId, result: result }); } else if (name == Channel.INTERNAL_REPLY_MESSAGE) { var callback = this.internalRequestCallbacks_[msg.requestId]; delete this.internalRequestCallbacks_[msg.requestId]; if (callback) callback(msg.result); } else { this.invokeMessageCallbacks_(msg); } } }; /** * Class factory. * @return {Channel} */ Channel.create = function() { return new Channel(); }; // Copyright 2014 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. var appId = 'hotword_audio_verification'; chrome.app.runtime.onLaunched.addListener(function() { // We need to focus the window if it already exists, since it // is created as 'hidden'. // // Note: If we ever launch on another platform, make sure that this works // with window managers that support hiding (e.g. Cmd+h on an app window on // Mac). var appWindow = chrome.app.window.get(appId); if (appWindow) { appWindow.focus(); return; } chrome.app.window.create('main.html', { 'frame': 'none', 'resizable': false, 'hidden': true, 'id': appId, 'innerBounds': { 'width': 784, 'height': 448 } }); }); // Copyright 2014 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. var appWindow = chrome.app.window.current(); document.addEventListener('DOMContentLoaded', function() { chrome.hotwordPrivate.getLocalizedStrings(function(strings) { loadTimeData.data = strings; i18nTemplate.process(document, loadTimeData); var flow = new Flow(); flow.startFlow(); var pressFunction = function(e) { // Only respond to 'Enter' key presses. if (e.type == 'keyup' && e.key != 'Enter') return; var classes = e.target.classList; if (classes.contains('close') || classes.contains('finish-button')) { flow.stopTraining(); appWindow.close(); e.preventDefault(); } if (classes.contains('retry-button')) { flow.handleRetry(); e.preventDefault(); } }; $('steps').addEventListener('click', pressFunction); $('steps').addEventListener('keyup', pressFunction); $('audio-history-agree').addEventListener('click', function(e) { flow.enableAudioHistory(); e.preventDefault(); }); $('hotword-start').addEventListener('click', function(e) { flow.advanceStep(); e.preventDefault(); }); $('settings-link').addEventListener('click', function(e) { chrome.browser.openTab({'url': 'chrome://settings'}, function() {}); e.preventDefault(); }); }); }); // Copyright 2014 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. (function() { // Correspond to steps in the hotword opt-in flow. /** @const */ var START = 'start-container'; /** @const */ var AUDIO_HISTORY = 'audio-history-container'; /** @const */ var SPEECH_TRAINING = 'speech-training-container'; /** @const */ var FINISH = 'finish-container'; /** * These flows correspond to the three LaunchModes as defined in * chrome/browser/search/hotword_service.h and should be kept in sync * with them. * @const */ var FLOWS = [ [START, SPEECH_TRAINING, FINISH], [START, AUDIO_HISTORY, SPEECH_TRAINING, FINISH], [SPEECH_TRAINING, FINISH] ]; /** * The launch mode. This enum needs to be kept in sync with that of * the same name in hotword_service.h. * @enum {number} */ var LaunchMode = { HOTWORD_ONLY: 0, HOTWORD_AND_AUDIO_HISTORY: 1, RETRAIN: 2 }; /** * The training state. * @enum {string} */ var TrainingState = { RESET: 'reset', TIMEOUT: 'timeout', ERROR: 'error', }; /** * Class to control the page flow of the always-on hotword and * Audio History opt-in process. * @constructor */ function Flow() { this.currentStepIndex_ = -1; this.currentFlow_ = []; /** * The mode that this app was launched in. * @private {LaunchMode} */ this.launchMode_ = LaunchMode.HOTWORD_AND_AUDIO_HISTORY; /** * Whether this flow is currently in the process of training a voice model. * @private {boolean} */ this.training_ = false; /** * The current training state. * @private {?TrainingState} */ this.trainingState_ = null; /** * Whether an expected hotword trigger has been received, indexed by * training step. * @private {boolean[]} */ this.hotwordTriggerReceived_ = []; /** * Prefix of the element ids for the page that is currently training. * @private {string} */ this.trainingPagePrefix_ = 'speech-training'; /** * Whether the speaker model for this flow has been finalized. * @private {boolean} */ this.speakerModelFinalized_ = false; /** * ID of the currently active timeout. * @private {?number} */ this.timeoutId_ = null; /** * Listener for the speakerModelSaved event. * @private {Function} */ this.speakerModelFinalizedListener_ = this.onSpeakerModelFinalized_.bind(this); /** * Listener for the hotword trigger event. * @private {Function} */ this.hotwordTriggerListener_ = this.handleHotwordTrigger_.bind(this); // Listen for the user locking the screen. chrome.idle.onStateChanged.addListener( this.handleIdleStateChanged_.bind(this)); // Listen for hotword settings changes. This used to detect when the user // switches to a different profile. if (chrome.hotwordPrivate.onEnabledChanged) { chrome.hotwordPrivate.onEnabledChanged.addListener( this.handleEnabledChanged_.bind(this)); } } /** * Advances the current step. Begins training if the speech-training * page has been reached. */ Flow.prototype.advanceStep = function() { this.currentStepIndex_++; if (this.currentStepIndex_ < this.currentFlow_.length) { if (this.currentFlow_[this.currentStepIndex_] == SPEECH_TRAINING) this.startTraining(); this.showStep_.apply(this); } }; /** * Gets the appropriate flow and displays its first page. */ Flow.prototype.startFlow = function() { if (chrome.hotwordPrivate && chrome.hotwordPrivate.getLaunchState) chrome.hotwordPrivate.getLaunchState(this.startFlowForMode_.bind(this)); }; /** * Starts the training process. */ Flow.prototype.startTraining = function() { // Don't start a training session if one already exists. if (this.training_) return; this.training_ = true; if (chrome.hotwordPrivate.onHotwordTriggered && !chrome.hotwordPrivate.onHotwordTriggered.hasListener( this.hotwordTriggerListener_)) { chrome.hotwordPrivate.onHotwordTriggered.addListener( this.hotwordTriggerListener_); } this.waitForHotwordTrigger_(0); if (chrome.hotwordPrivate.startTraining) chrome.hotwordPrivate.startTraining(); }; /** * Stops the training process. */ Flow.prototype.stopTraining = function() { if (!this.training_) return; this.training_ = false; if (chrome.hotwordPrivate.onHotwordTriggered) { chrome.hotwordPrivate.onHotwordTriggered. removeListener(this.hotwordTriggerListener_); } if (chrome.hotwordPrivate.stopTraining) chrome.hotwordPrivate.stopTraining(); }; /** * Attempts to enable audio history for the signed-in account. */ Flow.prototype.enableAudioHistory = function() { // Update UI $('audio-history-agree').disabled = true; $('audio-history-cancel').disabled = true; $('audio-history-error').hidden = true; $('audio-history-wait').hidden = false; if (chrome.hotwordPrivate.setAudioHistoryEnabled) { chrome.hotwordPrivate.setAudioHistoryEnabled( true, this.onAudioHistoryRequestCompleted_.bind(this)); } }; // ---- private methods: /** * Shows an error if the audio history setting was not enabled successfully. * @private */ Flow.prototype.handleAudioHistoryError_ = function() { $('audio-history-agree').disabled = false; $('audio-history-cancel').disabled = false; $('audio-history-wait').hidden = true; $('audio-history-error').hidden = false; // Set a timeout before focusing the Enable button so that screenreaders // have time to announce the error first. this.setTimeout_(function() { $('audio-history-agree').focus(); }.bind(this), 50); }; /** * Callback for when an audio history request completes. * @param {chrome.hotwordPrivate.AudioHistoryState} state The audio history * request state. * @private */ Flow.prototype.onAudioHistoryRequestCompleted_ = function(state) { if (!state.success || !state.enabled) { this.handleAudioHistoryError_(); return; } this.advanceStep(); }; /** * Shows an error if the speaker model has not been finalized. * @private */ Flow.prototype.handleSpeakerModelFinalizedError_ = function() { if (!this.training_) return; if (this.speakerModelFinalized_) return; this.updateTrainingState_(TrainingState.ERROR); this.stopTraining(); }; /** * Handles the speaker model finalized event. * @private */ Flow.prototype.onSpeakerModelFinalized_ = function() { this.speakerModelFinalized_ = true; if (chrome.hotwordPrivate.onSpeakerModelSaved) { chrome.hotwordPrivate.onSpeakerModelSaved.removeListener( this.speakerModelFinalizedListener_); } this.stopTraining(); this.setTimeout_(this.finishFlow_.bind(this), 2000); }; /** * Completes the training process. * @private */ Flow.prototype.finishFlow_ = function() { if (chrome.hotwordPrivate.setHotwordAlwaysOnSearchEnabled) { chrome.hotwordPrivate.setHotwordAlwaysOnSearchEnabled(true, this.advanceStep.bind(this)); } }; /** * Handles a user clicking on the retry button. */ Flow.prototype.handleRetry = function() { if (!(this.trainingState_ == TrainingState.TIMEOUT || this.trainingState_ == TrainingState.ERROR)) return; this.startTraining(); this.updateTrainingState_(TrainingState.RESET); }; // ---- private methods: /** * Completes the training process. * @private */ Flow.prototype.finalizeSpeakerModel_ = function() { if (!this.training_) return; // Listen for the success event from the NaCl module. if (chrome.hotwordPrivate.onSpeakerModelSaved && !chrome.hotwordPrivate.onSpeakerModelSaved.hasListener( this.speakerModelFinalizedListener_)) { chrome.hotwordPrivate.onSpeakerModelSaved.addListener( this.speakerModelFinalizedListener_); } this.speakerModelFinalized_ = false; this.setTimeout_(this.handleSpeakerModelFinalizedError_.bind(this), 30000); if (chrome.hotwordPrivate.finalizeSpeakerModel) chrome.hotwordPrivate.finalizeSpeakerModel(); }; /** * Returns the current training step. * @param {string} curStepClassName The name of the class of the current * training step. * @return {Object} The current training step, its index, and an array of * all training steps. Any of these can be undefined. * @private */ Flow.prototype.getCurrentTrainingStep_ = function(curStepClassName) { var steps = $(this.trainingPagePrefix_ + '-training').querySelectorAll('.train'); var curStep = $(this.trainingPagePrefix_ + '-training').querySelector('.listening'); return {current: curStep, index: Array.prototype.indexOf.call(steps, curStep), steps: steps}; }; /** * Updates the training state. * @param {TrainingState} state The training state. * @private */ Flow.prototype.updateTrainingState_ = function(state) { this.trainingState_ = state; this.updateErrorUI_(); }; /** * Waits two minutes and then checks for a training error. * @param {number} index The index of the training step. * @private */ Flow.prototype.waitForHotwordTrigger_ = function(index) { if (!this.training_) return; this.hotwordTriggerReceived_[index] = false; this.setTimeout_(this.handleTrainingTimeout_.bind(this, index), 120000); }; /** * Checks for and handles a training error. * @param {number} index The index of the training step. * @private */ Flow.prototype.handleTrainingTimeout_ = function(index) { if (this.hotwordTriggerReceived_[index]) return; this.timeoutTraining_(); }; /** * Times out training and updates the UI to show a "retry" message, if * currently training. * @private */ Flow.prototype.timeoutTraining_ = function() { if (!this.training_) return; this.clearTimeout_(); this.updateTrainingState_(TrainingState.TIMEOUT); this.stopTraining(); }; /** * Sets a timeout. If any timeout is active, clear it. * @param {Function} func The function to invoke when the timeout occurs. * @param {number} delay Timeout delay in milliseconds. * @private */ Flow.prototype.setTimeout_ = function(func, delay) { this.clearTimeout_(); this.timeoutId_ = setTimeout(function() { this.timeoutId_ = null; func(); }, delay); }; /** * Clears any currently active timeout. * @private */ Flow.prototype.clearTimeout_ = function() { if (this.timeoutId_ != null) { clearTimeout(this.timeoutId_); this.timeoutId_ = null; } }; /** * Updates the training error UI. * @private */ Flow.prototype.updateErrorUI_ = function() { if (!this.training_) return; var trainingSteps = this.getCurrentTrainingStep_('listening'); var steps = trainingSteps.steps; $(this.trainingPagePrefix_ + '-toast').hidden = this.trainingState_ != TrainingState.TIMEOUT; if (this.trainingState_ == TrainingState.RESET) { // We reset the training to begin at the first step. // The first step is reset to 'listening', while the rest // are reset to 'not-started'. var prompt = loadTimeData.getString('trainingFirstPrompt'); for (var i = 0; i < steps.length; ++i) { steps[i].classList.remove('recorded'); if (i == 0) { steps[i].classList.remove('not-started'); steps[i].classList.add('listening'); } else { steps[i].classList.add('not-started'); if (i == steps.length - 1) prompt = loadTimeData.getString('trainingLastPrompt'); else prompt = loadTimeData.getString('trainingMiddlePrompt'); } steps[i].querySelector('.text').textContent = prompt; } // Reset the buttonbar. $(this.trainingPagePrefix_ + '-processing').hidden = true; $(this.trainingPagePrefix_ + '-wait').hidden = false; $(this.trainingPagePrefix_ + '-error').hidden = true; $(this.trainingPagePrefix_ + '-retry').hidden = true; } else if (this.trainingState_ == TrainingState.TIMEOUT) { var curStep = trainingSteps.current; if (curStep) { curStep.classList.remove('listening'); curStep.classList.add('not-started'); } // Set a timeout before focusing the Retry button so that screenreaders // have time to announce the timeout first. this.setTimeout_(function() { $(this.trainingPagePrefix_ + '-toast').children[1].focus(); }.bind(this), 50); } else if (this.trainingState_ == TrainingState.ERROR) { // Update the buttonbar. $(this.trainingPagePrefix_ + '-wait').hidden = true; $(this.trainingPagePrefix_ + '-error').hidden = false; $(this.trainingPagePrefix_ + '-retry').hidden = false; $(this.trainingPagePrefix_ + '-processing').hidden = false; // Set a timeout before focusing the Retry button so that screenreaders // have time to announce the error first. this.setTimeout_(function() { $(this.trainingPagePrefix_ + '-retry').children[0].focus(); }.bind(this), 50); } }; /** * Handles a hotword trigger event and updates the training UI. * @private */ Flow.prototype.handleHotwordTrigger_ = function() { var trainingSteps = this.getCurrentTrainingStep_('listening'); if (!trainingSteps.current) return; var index = trainingSteps.index; this.hotwordTriggerReceived_[index] = true; trainingSteps.current.querySelector('.text').textContent = loadTimeData.getString('trainingRecorded'); trainingSteps.current.classList.remove('listening'); trainingSteps.current.classList.add('recorded'); if (trainingSteps.steps[index + 1]) { trainingSteps.steps[index + 1].classList.remove('not-started'); trainingSteps.steps[index + 1].classList.add('listening'); this.waitForHotwordTrigger_(index + 1); return; } // Only the last step makes it here. var buttonElem = $(this.trainingPagePrefix_ + '-processing').hidden = false; this.finalizeSpeakerModel_(); }; /** * Handles a chrome.idle.onStateChanged event and times out the training if * the state is "locked". * @param {!string} state State, one of "active", "idle", or "locked". * @private */ Flow.prototype.handleIdleStateChanged_ = function(state) { if (state == 'locked') this.timeoutTraining_(); }; /** * Handles a chrome.hotwordPrivate.onEnabledChanged event and times out * training if the user is no longer the active user (user switches profiles). * @private */ Flow.prototype.handleEnabledChanged_ = function() { if (chrome.hotwordPrivate.getStatus) { chrome.hotwordPrivate.getStatus(function(status) { if (status.userIsActive) return; this.timeoutTraining_(); }.bind(this)); } }; /** * Gets and starts the appropriate flow for the launch mode. * @param {chrome.hotwordPrivate.LaunchState} state Launch state of the * Hotword Audio Verification App. * @private */ Flow.prototype.startFlowForMode_ = function(state) { this.launchMode_ = state.launchMode; assert(state.launchMode >= 0 && state.launchMode < FLOWS.length, 'Invalid Launch Mode.'); this.currentFlow_ = FLOWS[state.launchMode]; if (state.launchMode == LaunchMode.HOTWORD_ONLY) { $('intro-description-audio-history-enabled').hidden = false; } else if (state.launchMode == LaunchMode.HOTWORD_AND_AUDIO_HISTORY) { $('intro-description').hidden = false; } this.advanceStep(); }; /** * Displays the current step. If the current step is not the first step, * also hides the previous step. Focuses the current step's first button. * @private */ Flow.prototype.showStep_ = function() { var currentStepId = this.currentFlow_[this.currentStepIndex_]; var currentStep = document.getElementById(currentStepId); currentStep.hidden = false; cr.ui.setInitialFocus(currentStep); var previousStep = null; if (this.currentStepIndex_ > 0) previousStep = this.currentFlow_[this.currentStepIndex_ - 1]; if (previousStep) document.getElementById(previousStep).hidden = true; chrome.app.window.current().show(); }; window.Flow = Flow; })(); /* Copyright 2014 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. */ /* TODO(xdai): Remove hard-coded font-family for 'Roboto'. */ * { box-sizing: border-box; color: rgba(0, 0, 0, .54); font-family: Roboto, 'Noto Sans', sans-serif; font-size: 13px; margin: 0; padding: 0; } #start-container * { color: #fff; } #start-container h2 { font-size: 15px; font-weight: normal; line-height: 24px; margin-top: 16px; } #start-container h3 { font-weight: normal; margin: 42px 16px 24px 16px; } #start-container div.container { background: rgb(66, 133, 244); } div.intro-image { background: -webkit-image-set( url(../images/intro-1x.png) 1x, url(../images/intro-2x.png) 2x) no-repeat; height: 152px; left: 24px; position: absolute; top: 122px; width: 304px; } div.intro-text { left: 328px; position: absolute; text-align: center; top: 116px; width: 432px; } #start-container div.buttonbar { background-color: rgb(51, 103, 214); height: 56px; padding: 0; text-align: center; } #start-container .buttonbar button { height: 100%; margin: 0; padding: 0 8px; width: 100%; } a { -webkit-app-region: no-drag; color: rgb(51, 103, 214); text-decoration: none; } button { -webkit-app-region: no-drag; } body { -webkit-app-region: drag; background: #ddd; } h1 { font-size: 20px; font-weight: normal; line-height: 32px; } h3 { font-size: 13px; line-height: 20px; } div.container { background: #fff; height: 448px; position: relative; width: 784px; } div.header { background: -webkit-image-set( url(../images/gradient-1x.png) 1x, url(../images/gradient-2x.png) 2x) no-repeat; height: 128px; padding: 70px 42px 0 42px; } div.header h1 { color: #fff; } div.content { height: 264px; line-height: 20px; padding: 32px 42px 0 42px; } div.content h3 { color: rgba(0, 0, 0, .87); margin-bottom: 16px; } div.col-2 { color: rgba(0, 0, 0, .54); float: left; width: 320px; } div.col-spacing { float: left; height: 216px; width: 60px; } div.v-spacing { height: 8px; } a[is='action-link'] { display: inline-block; font-size: 14px; margin-top: 22px; text-decoration: none; text-transform: uppercase; } .train { clear: both; line-height: 18px; margin-bottom: 24px; } .train .icon { display: inline-block; height: 18px; margin-right: 8px; vertical-align: top; width: 18px; } .train .text { color: rgba(0, 0, 0, .54); display: inline-block; line-height: 13px; padding-top: 3px; vertical-align: top; } .train.recorded .text { color: rgba(66, 133, 244, 1); } @-webkit-keyframes rotate { from { -webkit-transform: rotate(0); } to { -webkit-transform: rotate(359deg); } } .train.listening .icon { -webkit-animation: rotate 2s linear infinite; background: -webkit-image-set( url(../images/placeholder-loader-1x.png) 1x, url(../images/placeholder-loader-2x.png) 2x) no-repeat; } .train.not-started .icon { background: -webkit-image-set( url(../images/ic-check-gray-1x.png) 1x, url(../images/ic-check-gray-2x.png) 2x) no-repeat; } .train.recorded .icon { background: -webkit-image-set( url(../images/ic-check-blue-1x.png) 1x, url(../images/ic-check-blue-2x.png) 2x) no-repeat; } .check { clear: both; height: 18px; margin-bottom: 24px; } .check .icon { background: -webkit-image-set( url(../images/ic-check-blue-1x.png) 1x, url(../images/ic-check-blue-2x.png) 2x) no-repeat; display: inline-block; height: 18px; margin-right: 8px; vertical-align: top; width: 18px; } .check .text { color: rgba(0, 0, 0, .54); display: inline-block; height: 18px; line-height: 18px; padding-top: 2px; vertical-align: top; } div.buttonbar { background-color: rgba(236,239, 241, 1); bottom: 0; height: 56px; padding: 12px; position: absolute; width: 100%; } .buttonbar button { background: none; border: none; display: inline-block; font-weight: 700; height: 32px; line-height: 32px; margin-left: 8px; min-width: 56px; padding: 1px 8px 0 8px; text-transform: uppercase; } .buttonbar button:disabled { opacity: .5; } .buttonbar button.grayed-out { color: rgba(0, 0, 0, .28); text-transform: none; } .buttonbar button.primary { color: rgb(51, 103, 214); } .buttonbar .left { float: left; text-align: left; } .buttonbar .left button:first-child { margin-left: 0; } .buttonbar .right { float: right; text-align: right; } .buttonbar .message { margin: 7px 0 0 2px; } .buttonbar .message .icon { display: inline-block; height: 18px; margin-right: 8px; vertical-align: top; width: 18px; } .buttonbar .message.wait .icon { -webkit-animation: rotate 2s linear infinite; background: -webkit-image-set( url(../images/placeholder-loader-1x.png) 1x, url(../images/placeholder-loader-2x.png) 2x) no-repeat; } .buttonbar .message.error .icon { background: -webkit-image-set( url(../images/ic-error-1x.png) 1x, url(../images/ic-error-2x.png) 2x) no-repeat; } .buttonbar .message .text { color: rgba(0, 0, 0, .54); display: inline-block; line-height: 18px; padding-top: 2px; vertical-align: top; } .buttonbar .message.error .text { color: rgb(213, 0, 0); } .close { -webkit-app-region: no-drag; background: -webkit-image-set( url(../images/ic-x-white-1x.png) 1x, url(../images/ic-x-white-2x.png) 2x) center center no-repeat; border: none; float: right; height: 42px; opacity: .54; width: 42px; } .close:hover { opacity: 1; } .toast { background-color: rgb(38, 50, 56); bottom: 0; height: 52px; padding: 10px 12px 0 42px; position: absolute; width: 100%; } .toast .message { color: #fff; float: left; padding: 9px 0 0 0; } .toast button { background: none; border: none; color: rgb(58, 218, 255); float: right; height: 32px; margin-left: 18px; min-width: 56px; padding: 0 8px 0 8px; text-transform: uppercase; } PNG  IHDRFgAMA asRGB cHRMz&u0`:pQ< pHYsHHFk>bKGD̿kIDATx^!0DQ0=QMOF=BNTSQLXu$~b8hdG #Iz"Ld̓DbH  %$Ґd]I@L]q[ %tEXtdate:create2014-12-17T17:17:47-08:00z_%tEXtdate:modify2014-12-17T17:15:16-08:00GEwIENDB`PNG  IHDR$$K PIDATx^ Fp ңB?_cϜ$8wM  )P^3N90y g&A 0 J f([9*욷S8Qv~2`FIENDB`PNG  IHDR0cõIDATx]O`EP"b1$(c~} ;%!!4tP ^ LqckF٣G& ɕ\{w*"@JٓJR>ۖ7r܎X27*-3xJML&{7bj||/c9 VBcܜ#j2uy}ȟ?7–SKW")/5"!5Mų` Ȭ}aWF5M+7"KMF9=VvPf _&aWWUo,OU?簵dlΟI,}nB;RK^\=c๩zSZHu1u]]]+DX`WOL9Uyx+iL~{|ţ,L-[Fl"+!+݅`OE]\Aɐ })+ 7ynew QWZ1H){w֘H؛Z_w\6ضZ.s2Ψ $fAu;$^3m}xQ(aXq uZ&2U,J2˫o3nLE(:ꈣx8. (.AaD+B%K"I[T€ K4I .ItiifҒfi=oIԫtI~^kr9}^ivh ͱHsW֢hIU%V[wT/ߓc$ɶ֊ՖKԖ,K#ɶHYI-"""f`#߰GeKnb]*@-V۞ L/u} "A'qm9,ZٶIyK Fu=rŪ'Od꺾Yvg UPtWIWp)t2] 窏4,""_G\ Xn0t-0"ACjCNl[{eG)iU&lV wLI?Ƃ骬Hs+}"f}~'Q厧/y 5CVW NeR۔Zz322F^LJ/e 8T[SZPy@&[Y~`~(tfyq)Y..{a0$5yKS:oHU]4@\b\7S|O?ixH$8#O.cNvp^{Y]"".Yz ~JZbL`e` biŋ~8v*m<%H""My%}/ ӊ|"@U_ قt{]`i*6ډ,"0\ J7< ?V}מ3'#W"IJB#L(E$ؑI"w%ƇtoN+m|Fzpm*iY D#Wq>X8m6pns԰V(5D$6$eቦPT9<&1l&7`yPR4"IGpa(Lj:˝ޅ=R:\*\cL`{Vi_ w꣢_H-&օ'6'$z@9Lb WO[Y ɣ+Z9A=1{Q[Vdݨw-ˤ /)v]<{ F4XS^ŞEQ{xv)#Ll| "E$c0j&De%$%ݛq%9]+RUMpx~ 1|5 /䃺FT؎r,-h[Avk EK9w?F& ؞ShA Xe:h~Quz;=epl0G\8%oKMdAu1? Oyz;:ahB<%D\fցP:ڒ_?ֵ!Gч*H=)@K%VLHH)bشZ1މW``xOn$R׵̮Cu^Xj<3*S@T./ 0o>\ulW/+BqDg̵V~+ >7Ζm<ݭ~9Po2*t6.2CJ/saB|hMJt/Vs`?Նt6ujx]#}:Tڀ 4[QSul-XnĎYDD)\лzb>3ḽKM04uX: -@3jhvf BMMPP{E<$|5Q@~ A ,0{_nPrW&>͟6C;-mXZu|U3.P 61us(&r$u1 o)5< W~S,*Tԙ'UU+ By; ]Bz, KѿU=8:3&镈& ņϻC `G5ٻO"$qTVG1MN] K3B+`^w _IDDc¥7 !SHo[*4|?CLS |?|n&]naޫKD$J`M1X?[X9v D>ؔ͠Õ&"/GNN%󹙏r3!xۏ$-n`^o pd?g. wͫWM)]'a7{Lm/G4 h7zP ˽d{hpv1ʺ!*e *PfNϢh&SJ'I܈DQ?㊮7`^ !`Oumd=G~*O8ƾhG+ќåߣw={ < aCmI=OPVPZ:W ew 1l>ߔ%2_y FU•׺.hZ4):Mܕ AvIQ|4`6?j?DctWI.Ye / )?N1ʗjiZgvxXT'6?݂V& %MS:Yz swd^ ~=20T[b4\ؔNф%NVwaJ=dS,t_#bH!" U-t64HNM-Ȱ "~ *^ E3lQtB=%hܾ%bX1cqr,lC,Ѱmoj|4>-Y.'N*FL.@+z}<=?b ~n.<@6ND" hʁR&[f* =Qs8DapL۶P>cMVgzsf s+?Ìn޵(TfAkN"Fˇqk)8aqEw0 ych|tUQ=;ȸ UpA>8&y__}uoc!-z9cxգ_wV7&&6rJ!,$v58*k6`EK(' *Mؑ3@E3{?^ۓ+a8Z8l52;De2)O+<"l`p :PbXҽcK_XKoh> {҈|ʼna.STRSjjBRkSm\i}:sh0#as4My:AB9z5{*,`t iXqt+ ᷥ;엘5íʶS,GTm.(΅"m¯'λ6; 3d鮡 Dc\ g )5BdEh!u+=,(ez()W@ ({)P?P7-cGLɇ=qqQx˗ɕj⟸x$l,_&ҿ9 `  R#/}5~|aϾrظ,L\y԰fD> 6O%o2 7.߳)z?E >[%_n.ZnZn9gv\kYVgWgA[Wy 5,MAMA2{ç K%óX_pk tBmM_~= X]usNFuT+߷?knPpuBdV 0)zh-l)s[AoY(]_H9T9&Sgɛ.5$G]CsDWq}A))ȋ|N< ՟z+nHp(Cxwp4}5RjpZ.E?fUX{ 2u\4^y91G-;[ٜ𸳹Q_:/R{f,1'ScNF}/ Z}ٍ?yv3F]:~OrMޔ]i[cRܕvyδųhgLV_'7?oY_G3&?hb=1!P'977>ssNkbOKc~\477w$Ik1O|tvvE9֌'cSǵTfw̉19L; jDs?6 +i}_I8u>whsH"26NŶ1Ę{O33) ɜ"ʼX#r\,G6n?E#9! PS d>:u޸aFCCH7}x=<\˥]?p$ˢf:/.i á}3sҺ;3B0a†) o$ .j0'''s[.He5!~˵T)Q==nX[:C<kimuwB:eݼ :3|A03鼮3r.;Qg@0$mjjڸAʩ%@dH0E^9U^+sTyP&[llll[7 mRlɳ yŋCU&^; 殇KTpEب1 ,qZozp!"˼RNMg7%0*Ve)\ȼL?? B ZB qw=  *d jd ¥f@E ָYTW'Ӯ31uD a涫3:}a%  ^J'bޮk׫Lc`ttzvĄfޛ1AXP 饒mо;'ܡkN-'IvrO*:4x=G}!镰 Roԩxxɱ% SQSMb*ly%t[ e%OOu 52_ Y]i 8lc;ͺh(i%mîwy aL?kN/?Gε]m~XWcY7d2l6wK=u8隇5{ku}>wo^qu#g*.: # vuZ=_۶Ov(S^c3_&AEWx ki>?[UMǫfujW rE27]Eq.kggUbX_)ם} ^. ^Tw:.E{[/<55IENDB`PNG  IHDR`1Q%IDATxne?0MD@DOMn|r[x8jd|Poe1ΪMIR{%MwJɠc{+r%*q%;(H^__rYx:J<`ȟaAk⮱0,%a,!ۿ^/ql|5wg:(_O3[i X*%h@p=[ v{e빉ײarvmIws9IvP0@S)Z΀ p%Mw+ZkoY/t?&! 3gY]C׻)UBj4z`@^\X>|zxPm<>? ZƱ|ݪOlYx'pܼ9F4<2WY9J\mRyM^O{7O\ &\dyi]>1Ul~=]jV͞u*5g/{ގ0xه'5ţ?rؼODGKʆSn_I{`ӥ攒!Rl^2B6n L(o|͙.5& h'_`s*4aL>:8:̼.EFjY".#,XE}O"e6ZmhKiY@*A2S!-m$7<ߧ'}>=w,e%'VìM<(xa*f9e\Xm$.SZ{=|@U樭b*pj^!|Cⵏ EiQYE0iTrB"K+fcxdxßC!"$'\Whf] ^+9q_s񑋀5Ny 5/ I@@/'$)y\p.`ͣj5,i@\QUYJ^L X4Y A@\h>tC/d'."`ͣ)['H$ `XK8擔Xfۓ $\0F7(xGWI|Q/# T^x0_8G } `#y깇mvTAE4_PJ$3)KIag*u) x]+eS?n)JL ;غ}ۍ0%/J1f@NKF)KJ%m\A@rjvO( `E ߳@̻k mnSns++_}҉%G L `&6# ~2^&ZKAT[0L#rJe ̷-+ba ;VD +bx\/F `-}*XDþ !t"V`R̷7ܪ1/! :#:),lX\҆ÿ%KˏP%bGip)]% .xjyk58=Weͥ"A$_UL `MFbK2$VvPg]6NsB#-l~ 9UL `ْh>tH{E*L˅ K=Յ+s 0@PK]WKaۖ? 0bCAY{~3^kȯ(0X 'U(BEĄm _J IeG+y +[Hk6" 1 $sXnKzzhB6&2v$r `0YC {XE M%a 0E.V!`4N H. ._,E#&Q  :Bw@Hj-H&;<5,dI7:_`L_rB-h& ڣOhT{wEWN18/CԜ0@<]w1=_R%URgblu_sʎI{}w;'|/ Uw/|g l\ےEcYBjEJN<h!eФ|Ye`H4t}![nIvBjt%Cg LedN/V¼6Յ0a*9,ZW擰)'Ik`EsHmKҶx%Ї@4nߪ5هBږe[1*`'7R?:zV6o+N#?6Qx]CE|χv? `~$)'h ;D.?{㧒D YZj79Oqhc/U?OA `-iGg\~X1ێ0$͞vΚ6˞#9yVNn7aBpC+*39A `H5 xYT"v4n(9'E|.*$(,B4^8Zc w#]l;Nq* QXו"̈́去ƊO!Poퟕa?M[*8E޳ۉuFVD"0D͋)y?L'(kfȲȆmU %G?dUrW{XȈ 8TW;ϑXAbzל[^'ҁ;hG `hTatރ/`|] Ji'R I\D̘Sw#i^D(v}31a̮hFuHРߐb 2{Ũ|Ggp؞^/D(0 k<_&,sƢ39G-fr-}iXB _UM_SFf7ۤi3Sv~KiR9Q`O yct\ٖdųsԼgG&פl}ǾNe^A-R7;Q{p\.S_ f^^A4@kjiOߊWpa+glkR?P- @0 GYyeuFy ?T +% =OmrїWUe+_L:0I\GVXR½ _fm`Ֆebe}bZ MQTҷm9vRqi뫯'}vZ&VcK+iB:2s `ksK*,'OϷE\BR7|!Kw)9*YM,mH:tQT<3}ÁE:en+_,-'}¼:}߰Ն|[涌 A 49kbȎ3%gyy,en=;|F Ufԭ.: S^*YwS*ؔӓwF4%'TZl}- gnoY| ^cC[8+RkQ KSO@ɘQ|Y29Fg+:]š[/3_WCl䟱̩.Ɏ\ɆCӁ[C"h)yt=1ZP[ŽJ\u-@c(^grӛ~SvSЄ<;AϳJiB3E=$ #όQ4[_#2|0DLFgoN_[拚NYJ~YzS;4{R}Yt4 &+N5VXW[R[[ ANpfapRS绱[aL/W>.`_ $z=Ez8T N,v #TXC=ub1s"'0`!yrVi؟oB,u;~_+VDe0" ,sl@ JK+a6)2bTD Ib5Sc~UjO, \[AhV[Fz|KCq'ΫuY'w:9-=i =,?5s>_J]bt VVM]Od}xW\a{QdOi5|&15jNb /)N|DPMα,ÖO|1y^St HPW*?]Wc#y.4#.dp-JH V;U#x}eud"ZVH0,~M{nH58_߻woZ[ DXۊw ˩qC{/uv2 .dQW&}?GtӥW˩"ix7TZy?Iy$P+e[ҳ՚%6KzQkjBB3dcl>,I3Qv`I[+V*`5Q#.xBD- w lg?ႀE?WV~#leI3,l1 Bzoi8?BcyjҎkρXVSީJhk?0$:s/)n'+՚*9\.a DʩcWW-fQk ݉hyYR6IY쨋j{$ kj#!jR"/e)SZ<%[==ͮ~4̗5v]&n( <0:ћg=uu&LF燾f( ,ۙT[U5Gj)kn5ŏȩSsxؿӒ;" |NIaMl ^`Y^3\W^Q)d'LQqHySaʦ9oDNvs9K|N'|r˹&~b0t݁XR8ͩ†*,[OkԖ-O򾥶jy_惯}}FU V >T|vSX)$Y-mM|1YNS5{!XJVgLhޏ͎Fo8<2_GJ[(j_dw,u]TW,>ji[x*2@(i.`6b0_틔UC)iAd=DdeFd=`VEafjD* Vs? 939:̪WQ/ "@jkW;3žyuO3™Y(Y\%/{mj70.5EQʱg>;q,Oc[Cm7(~3Ca~`fL}h00)/*%c7& ԣNOVy4-ɗz]8ugBޣ%V-dEɳ#t5TZkkۨP%u/'f~dw񪏏U\s՗>. c}>u),َxw*S>Pʲ9{{mK 0Eyݷ=3gx!UcߩO߮ $L>?+ڮE9s7aar$zƮX|eR-pDc?ujj ̒pDS%f 9P|KOy^ɭ E"Bd)ס\<]2֘g>"371j,.?J༬]Qr܍27&')`vs-u>hA_5(A~65z >M)Կjbƽۺგ]]u`p=FJT*u}5pVRUCCߓWiAMI|`, y(?0SMn mMNyWpmM EM/i{^4IW`Dpj^W(^KQ.߯9@j)kw>8?v13df+e*fS|312RYtYiE4ˤj_Mis>U5)))0)8jzw8>lO?t[M)n~>z?{@|jmqddH@}pkn^cjmI]]_dZReF=yaPmBsCfex! &Q29o|oq931N @d򽑶}> 8 /kw E)m9uv6;oÀi "n:oap{Ყ'0D̄S@jb8o%@fp;D4e'DT S>3(ۃ-G>132v!< =.Wk¦ lbv_b&BVqpb1e]bQ^d"-Ǽ @3ar8T1 B;<=Pā] ƧCŢUA)GH Bk7\" ~#c];ڋ fN@h3v1BήKRuA&BS7>?X Q}aJC@ Ϥ?x j؉!(ơ@):=␮LTD_:I"JNA̶fVr8b& 45~ HCzwf0xs` J@ *ҏƅ E3  Nhҡ{Ap{A<5 MZ7= ?_"L[:(l{gP C:} J-W:=:Ӂ@8l@}AJ7ǃ c-[WpdtIENDB`PNG  IHDR  NPLTEDCB~CB~A}iA~A}BBlhefc`b_jd]o@|nEk>zgaA~eg^CcD?{q=y`E^;vmNuye[iG B ZK B B 3G B ƹ @)/@yn1B :!$^B :! 5Z B >-y^B -@VJ@@W-@D "jS "Z B G B !f@@o<!G B ෾B ":!:X!!@Pc D$ynB 6@$!@yB @!U} D$B [ 6"jG "[ "S B 4#~_[ "H>B "jlD S "-@P}G B <#"jlB #@NB >"ښ!|Ì0#B 1B HM6 B k 6B OjK B  S B S B -K B O6@@I)!@ >B  jc @{3B 0#B B }`j%K09- @BHE P >>5.[dMs2 MwSvD Ğ:.@,?\G@Bzwt A #O|gG9K"fm~gG\D M7@x@.xi"6!bϺ;q8&BӼ_Nu@@x7<8;J #g4A мukVA bWxcB-(D E<{p*ѿ&kA ; ~&#-_Mw<3@@h5r@ʌ3-*A D |ߞHV @"M;U)dx{ǟ1+A Zw&jD Ă0`#Œ @X]??_&D /|۟PPA Ή@Ͽ-D ]ܲ&("3@ծjI(`F!W<H@E[OD I ]d_?q &A ĞH(E A j:l,&Qu @iZ nDcwO q U o<qNd@\F'΃0DMBӆ׾7EՁ@(k@u.<|\#K @hZu^: Q}Ї:|@ _K0M D ˭iRoK@gpc +ބ*DB>vMth_!*D4B'DzI5Py\#?ꒆe_!*L6B󊭻#dX| . X;/K& `A \ͷN'M$./؇DɁ{?!vQ ֑ږ:'hKƚOw,SoKA !?Tگޖ, @8Uai û '! {6UupJ @%ѯzsz["x ymC0@k]kH* H@)Pi:> $&<709B=%y ܚQyվj$@0"=?C* i }<Р@ 6btݚ~27>dJǂ @0DLs1SաJB2HK~aT' %x{YD 0#O6ݩPI DZ'>S@@v]jf @ D t]T[#g`M 2ůRf_ @ 0#n]7m A JZu?J< 6םyU.=%$ RVu nh#!?≠:L  [v@ *!#0M * tg ݱ`KKP!uq,  ,&!i}K|MBvՁ@ph ռTa$@Gf-aФtj 0EՁ@ tb 2W^x^-aI7nH@ @d|/<%DHG"}Xvu&F,u Lz@8ȥ G[yCgn@8\@dWL%@8i{ o%- &¾P,rwg{~j$E0Ϯq'"x@ EӫN54K[f!@4¸t xjx2O?+ HqX?>A dd,3=DlcH!/Mu+#P$D 1/3:q xm<QDa"@4c֏Fu@xzB@ /8@0ꮜh@۽!z /M}h8G ӿ Gy@k@8L hڮ7Ձ@ݭB h&"A @huGn{@X!kΒ< %u ֒<B۶<C0+@Ц_d<bxr`Ii;+@/튑t@|[d<"7ҷ%A @x`8 Xk:G Dڷ]q*8׿@ ud<*aA 59 iMԁ@m<@%oK@@Nm=9ku?y"9ԁ@ZM̍Q@ @xw]A /i֘B _Wor[eIENDB`PNG  IHDRVΎWIDATxc郱Jmv qh` %͟ԁw usI[/Z|odI! Z?_ul˾' 84|T&Wh % @v9|y`FY8 +P|'K?~Xɱ !̦͟/1 5yHOy1%M!+A.#3}n 7톫le/E 1} lFIENDB`PNG  IHDR$$IDATxK(EABDQ Y˜# FF3RYHYNVRX`EDqf(BkY47 $B&;C٘COQ=tI5h0[k!YH'm6ʬblf,$Ah-jR**TP >YRh1ѡ&F ~=^MB91` ,ͤ&sf;0Lfje"X [3f|>$IENDB`PNG  IHDRFIDATxc)/}*ưQCl6"Pj ĭ]@,yB%yg R@A(̣OWl>n. TW+$sb2WN5<=b'Hʕ[*g: 9|0~P3Q*w͖*%,__#,j埘IENDB`PNG  IHDRVΎWIDATx^Փ 1 EY9;dvaXJװ^9"})'8ެc=s6esFlŸS1r {'r*fW*6dw@vĝ_P$cNۿC.9Xݴsq_IENDB`PNG  IHDR$$h6PLTEC7C6C5D4C7C6C6>6D6A5C6C7C6D7C6A7D7ԢltRNS1T!?׮3vsIDATx^K EQ0V^°w| @eB!B2K96^ju7GO[GJ}X-5wO,#GA 0"A"h`4LHwya$(`ԟy?ͳh~U1/}}!O~ T$#IENDB`PNG  IHDRa}IDATxc<@?Uaf†`@" ʀe18~a 5;1 #ހ_}* (Fʮ bclh8!Q },M4IENDB`PNG  IHDR00` PLTEBPtK\SV[GCWXFSQRpo#tRNSIJTIDATx^շ0C!kzy<t3\sy )o&S#As(Xh?G!HRbHnۀ';D802qܢRP1 2VW߿S? f7CYJ.Kc9Ѷ\IENDB`PNG  IHDR?B\DPLTEBx\YIDECOKF[}hjcHGl^rZ|QP_{jtvfeNkxJ tRNSrp UIDATxEo\1;ffN1)33}o@3fSJx兏ewb@oǀcX51@,c0 `mr`# T%<LR~-sŌ;|9x /Ss>ʀ `@и$kPx.X >wňVZ ؃z/F >g1 Y/*7ŀY7} n8{`Tldމ'sQ񼁺e5 ?OM#yU_XCݷ}.Bƥx jQlס*REjxP>r1f&iwEq3 #aII G\mr.D >D$Zj>wm*'& `@pe/0 `g}bU_ǀkbW-""""j5鱨,IENDB`// Copyright 2014 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('hotword', function() { 'use strict'; /** * Class used to manage always-on hotwording. Automatically starts hotwording * on startup, if always-on is enabled, and starts/stops hotwording at * appropriate times. * @param {!hotword.StateManager} stateManager * @constructor * @extends {hotword.BaseSessionManager} */ function AlwaysOnManager(stateManager) { hotword.BaseSessionManager.call( this, stateManager, hotword.constants.SessionSource.ALWAYS); } AlwaysOnManager.prototype = { __proto__: hotword.BaseSessionManager.prototype, /** @override */ enabled: function() { return this.stateManager.isAlwaysOnEnabled(); }, /** @override */ updateListeners: function() { hotword.BaseSessionManager.prototype.updateListeners.call(this); if (this.enabled()) this.startSession(); } }; return {AlwaysOnManager: AlwaysOnManager}; }); // Copyright 2014 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. 'use strict'; /** * @fileoverview This is the audio client content script injected into eligible * Google.com and New tab pages for interaction between the Webpage and the * Hotword extension. */ (function() { /** * @constructor */ var AudioClient = function() { /** @private {Element} */ this.speechOverlay_ = null; /** @private {number} */ this.checkSpeechUiRetries_ = 0; /** * Port used to communicate with the audio manager. * @private {?Port} */ this.port_ = null; /** * Keeps track of the effects of different commands. Used to verify that * proper UIs are shown to the user. * @private {Object} */ this.uiStatus_ = null; /** * Bound function used to handle commands sent from the page to this script. * @private {Function} */ this.handleCommandFromPageFunc_ = null; }; /** * Messages sent to the page to control the voice search UI. * @enum {string} */ AudioClient.CommandToPage = { HOTWORD_VOICE_TRIGGER: 'vt', HOTWORD_STARTED: 'hs', HOTWORD_ENDED: 'hd', HOTWORD_TIMEOUT: 'ht', HOTWORD_ERROR: 'he' }; /** * Messages received from the page used to indicate voice search state. * @enum {string} */ AudioClient.CommandFromPage = { SPEECH_START: 'ss', SPEECH_END: 'se', SPEECH_RESET: 'sr', SHOWING_HOTWORD_START: 'shs', SHOWING_ERROR_MESSAGE: 'sem', SHOWING_TIMEOUT_MESSAGE: 'stm', CLICKED_RESUME: 'hcc', CLICKED_RESTART: 'hcr', CLICKED_DEBUG: 'hcd' }; /** * Errors that are sent to the hotword extension. * @enum {string} */ AudioClient.Error = { NO_SPEECH_UI: 'ac1', NO_HOTWORD_STARTED_UI: 'ac2', NO_HOTWORD_TIMEOUT_UI: 'ac3', NO_HOTWORD_ERROR_UI: 'ac4' }; /** * @const {string} * @private */ AudioClient.HOTWORD_EXTENSION_ID_ = 'nbpagnldghgfoolbancepceaanlmhfmd'; /** * Number of times to retry checking a transient error. * @const {number} * @private */ AudioClient.MAX_RETRIES = 3; /** * Delay to wait in milliseconds before rechecking for any transient errors. * @const {number} * @private */ AudioClient.RETRY_TIME_MS_ = 2000; /** * DOM ID for the speech UI overlay. * @const {string} * @private */ AudioClient.SPEECH_UI_OVERLAY_ID_ = 'spch'; /** * @const {string} * @private */ AudioClient.HELP_CENTER_URL_ = 'https://support.google.com/chrome/?p=ui_hotword_search'; /** * @const {string} * @private */ AudioClient.CLIENT_PORT_NAME_ = 'chwcpn'; /** * Existence of the Audio Client. * @const {string} * @private */ AudioClient.EXISTS_ = 'chwace'; /** * Checks for the presence of speech overlay UI DOM elements. * @private */ AudioClient.prototype.checkSpeechOverlayUi_ = function() { if (!this.speechOverlay_) { window.setTimeout( this.delayedCheckSpeechOverlayUi_.bind(this), AudioClient.RETRY_TIME_MS_); } else { this.checkSpeechUiRetries_ = 0; } }; /** * Function called to check for the speech UI overlay after some time has * passed since an initial check. Will either retry triggering the speech * or sends an error message depending on the number of retries. * @private */ AudioClient.prototype.delayedCheckSpeechOverlayUi_ = function() { this.speechOverlay_ = document.getElementById(AudioClient.SPEECH_UI_OVERLAY_ID_); if (!this.speechOverlay_) { if (this.checkSpeechUiRetries_++ < AudioClient.MAX_RETRIES) { this.sendCommandToPage_(AudioClient.CommandToPage.VOICE_TRIGGER); this.checkSpeechOverlayUi_(); } else { this.sendCommandToExtension_(AudioClient.Error.NO_SPEECH_UI); } } else { this.checkSpeechUiRetries_ = 0; } }; /** * Checks that the triggered UI is actually displayed. * @param {AudioClient.CommandToPage} command Command that was send. * @private */ AudioClient.prototype.checkUi_ = function(command) { this.uiStatus_[command].timeoutId = window.setTimeout( this.failedCheckUi_.bind(this, command), AudioClient.RETRY_TIME_MS_); }; /** * Function called when the UI verification is not called in time. Will either * retry the command or sends an error message, depending on the number of * retries for the command. * @param {AudioClient.CommandToPage} command Command that was sent. * @private */ AudioClient.prototype.failedCheckUi_ = function(command) { if (this.uiStatus_[command].tries++ < AudioClient.MAX_RETRIES) { this.sendCommandToPage_(command); this.checkUi_(command); } else { this.sendCommandToExtension_(this.uiStatus_[command].error); } }; /** * Confirm that an UI element has been shown. * @param {AudioClient.CommandToPage} command UI to confirm. * @private */ AudioClient.prototype.verifyUi_ = function(command) { if (this.uiStatus_[command].timeoutId) { window.clearTimeout(this.uiStatus_[command].timeoutId); this.uiStatus_[command].timeoutId = null; this.uiStatus_[command].tries = 0; } }; /** * Sends a command to the audio manager. * @param {string} commandStr command to send to plugin. * @private */ AudioClient.prototype.sendCommandToExtension_ = function(commandStr) { if (this.port_) this.port_.postMessage({'cmd': commandStr}); }; /** * Handles a message from the audio manager. * @param {{cmd: string}} commandObj Command from the audio manager. * @private */ AudioClient.prototype.handleCommandFromExtension_ = function(commandObj) { var command = commandObj['cmd']; if (command) { switch (command) { case AudioClient.CommandToPage.HOTWORD_VOICE_TRIGGER: this.sendCommandToPage_(command); this.checkSpeechOverlayUi_(); break; case AudioClient.CommandToPage.HOTWORD_STARTED: this.sendCommandToPage_(command); this.checkUi_(command); break; case AudioClient.CommandToPage.HOTWORD_ENDED: this.sendCommandToPage_(command); break; case AudioClient.CommandToPage.HOTWORD_TIMEOUT: this.sendCommandToPage_(command); this.checkUi_(command); break; case AudioClient.CommandToPage.HOTWORD_ERROR: this.sendCommandToPage_(command); this.checkUi_(command); break; } } }; /** * @param {AudioClient.CommandToPage} commandStr Command to send. * @private */ AudioClient.prototype.sendCommandToPage_ = function(commandStr) { window.postMessage({'type': commandStr}, '*'); }; /** * Handles a message from the html window. * @param {!MessageEvent} messageEvent Message event from the window. * @private */ AudioClient.prototype.handleCommandFromPage_ = function(messageEvent) { if (messageEvent.source == window && messageEvent.data.type) { var command = messageEvent.data.type; switch (command) { case AudioClient.CommandFromPage.SPEECH_START: this.speechActive_ = true; this.sendCommandToExtension_(command); break; case AudioClient.CommandFromPage.SPEECH_END: this.speechActive_ = false; this.sendCommandToExtension_(command); break; case AudioClient.CommandFromPage.SPEECH_RESET: this.speechActive_ = false; this.sendCommandToExtension_(command); break; case 'SPEECH_RESET': // Legacy, for embedded NTP. this.speechActive_ = false; this.sendCommandToExtension_(AudioClient.CommandFromPage.SPEECH_END); break; case AudioClient.CommandFromPage.CLICKED_RESUME: this.sendCommandToExtension_(command); break; case AudioClient.CommandFromPage.CLICKED_RESTART: this.sendCommandToExtension_(command); break; case AudioClient.CommandFromPage.CLICKED_DEBUG: window.open(AudioClient.HELP_CENTER_URL_, '_blank'); break; case AudioClient.CommandFromPage.SHOWING_HOTWORD_START: this.verifyUi_(AudioClient.CommandToPage.HOTWORD_STARTED); break; case AudioClient.CommandFromPage.SHOWING_ERROR_MESSAGE: this.verifyUi_(AudioClient.CommandToPage.HOTWORD_ERROR); break; case AudioClient.CommandFromPage.SHOWING_TIMEOUT_MESSAGE: this.verifyUi_(AudioClient.CommandToPage.HOTWORD_TIMEOUT); break; } } }; /** * Initialize the content script. */ AudioClient.prototype.initialize = function() { if (AudioClient.EXISTS_ in window) return; window[AudioClient.EXISTS_] = true; // UI verification object. this.uiStatus_ = {}; this.uiStatus_[AudioClient.CommandToPage.HOTWORD_STARTED] = { timeoutId: null, tries: 0, error: AudioClient.Error.NO_HOTWORD_STARTED_UI }; this.uiStatus_[AudioClient.CommandToPage.HOTWORD_TIMEOUT] = { timeoutId: null, tries: 0, error: AudioClient.Error.NO_HOTWORD_TIMEOUT_UI }; this.uiStatus_[AudioClient.CommandToPage.HOTWORD_ERROR] = { timeoutId: null, tries: 0, error: AudioClient.Error.NO_HOTWORD_ERROR_UI }; this.handleCommandFromPageFunc_ = this.handleCommandFromPage_.bind(this); window.addEventListener('message', this.handleCommandFromPageFunc_, false); this.initPort_(); }; /** * Initialize the communications port with the audio manager. This * function will be also be called again if the audio-manager * disconnects for some reason (such as the extension * background.html page being reloaded). * @private */ AudioClient.prototype.initPort_ = function() { this.port_ = chrome.runtime.connect( AudioClient.HOTWORD_EXTENSION_ID_, {'name': AudioClient.CLIENT_PORT_NAME_}); // Note that this listen may have to be destroyed manually if AudioClient // is ever destroyed on this tab. this.port_.onDisconnect.addListener( (function(e) { if (this.handleCommandFromPageFunc_) { window.removeEventListener( 'message', this.handleCommandFromPageFunc_, false); } delete window[AudioClient.EXISTS_]; }).bind(this)); // See note above. this.port_.onMessage.addListener( this.handleCommandFromExtension_.bind(this)); if (this.speechActive_) { this.sendCommandToExtension_(AudioClient.CommandFromPage.SPEECH_START); } else { // It's possible for this script to be injected into the page after it has // completed loaded (i.e. when prerendering). In this case, this script // won't receive a SPEECH_RESET from the page to forward onto the // extension. To make up for this, always send a SPEECH_RESET. This means // in most cases, the extension will receive SPEECH_RESET twice, one from // this sendCommandToExtension_ and the one forwarded from the page. But // that's OK and the extension can handle it. this.sendCommandToExtension_(AudioClient.CommandFromPage.SPEECH_RESET); } }; // Initializes as soon as the code is ready, do not wait for the page. new AudioClient().initialize(); })(); // Copyright 2014 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('hotword', function() { 'use strict'; /** * Base class for managing hotwording sessions. * @param {!hotword.StateManager} stateManager Manager of global hotwording * state. * @param {!hotword.constants.SessionSource} sessionSource Source of the * hotword session request. * @constructor */ function BaseSessionManager(stateManager, sessionSource) { /** * Manager of global hotwording state. * @protected {!hotword.StateManager} */ this.stateManager = stateManager; /** * Source of the hotword session request. * @private {!hotword.constants.SessionSource} */ this.sessionSource_ = sessionSource; /** * Chrome event listeners. Saved so that they can be de-registered when * hotwording is disabled. * @private */ this.sessionRequestedListener_ = this.handleSessionRequested_.bind(this); this.sessionStoppedListener_ = this.handleSessionStopped_.bind(this); // Need to setup listeners on startup, otherwise events that caused the // event page to start up, will be lost. this.setupListeners_(); this.stateManager.onStatusChanged.addListener(function() { hotword.debug('onStatusChanged'); this.updateListeners(); }.bind(this)); } BaseSessionManager.prototype = { /** * Return whether or not this session type is enabled. * @protected * @return {boolean} */ enabled: assertNotReached, /** * Called when the hotwording session is stopped. * @protected */ onSessionStop: function() {}, /** * Starts a launcher hotwording session. * @param {hotword.constants.TrainingMode=} opt_mode The mode to start the * recognizer in. */ startSession: function(opt_mode) { this.stateManager.startSession(this.sessionSource_, function() { chrome.hotwordPrivate.setHotwordSessionState(true, function() {}); }, this.handleHotwordTrigger.bind(this), opt_mode); }, /** * Stops a launcher hotwording session. * @private */ stopSession_: function() { this.stateManager.stopSession(this.sessionSource_); this.onSessionStop(); }, /** * Handles a hotword triggered event. * @param {?Object} log Audio log data, if audio logging is enabled. * @protected */ handleHotwordTrigger: function(log) { hotword.debug('Hotword triggered: ' + this.sessionSource_, log); chrome.hotwordPrivate.notifyHotwordRecognition( 'search', log, function() {}); }, /** * Handles a hotwordPrivate.onHotwordSessionRequested event. * @private */ handleSessionRequested_: function() { hotword.debug('handleSessionRequested_: ' + this.sessionSource_); this.startSession(); }, /** * Handles a hotwordPrivate.onHotwordSessionStopped event. * @private */ handleSessionStopped_: function() { hotword.debug('handleSessionStopped_: ' + this.sessionSource_); this.stopSession_(); }, /** * Set up event listeners. * @private */ setupListeners_: function() { if (chrome.hotwordPrivate.onHotwordSessionRequested.hasListener( this.sessionRequestedListener_)) { return; } chrome.hotwordPrivate.onHotwordSessionRequested.addListener( this.sessionRequestedListener_); chrome.hotwordPrivate.onHotwordSessionStopped.addListener( this.sessionStoppedListener_); }, /** * Remove event listeners. * @private */ removeListeners_: function() { chrome.hotwordPrivate.onHotwordSessionRequested.removeListener( this.sessionRequestedListener_); chrome.hotwordPrivate.onHotwordSessionStopped.removeListener( this.sessionStoppedListener_); }, /** * Update event listeners based on the current hotwording state. * @protected */ updateListeners: function() { if (this.enabled()) { this.setupListeners_(); } else { this.removeListeners_(); this.stopSession_(); } } }; return {BaseSessionManager: BaseSessionManager}; }); // Copyright 2014 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('hotword.constants', function() { 'use strict'; /** * Number of seconds of audio to record when logging is enabled. * @const {number} */ var AUDIO_LOG_SECONDS = 2; /** * Timeout in seconds, for detecting false positives with a hotword stream. * @const {number} */ var HOTWORD_STREAM_TIMEOUT_SECONDS = 2; /** * Hotword data shared module extension's ID. * @const {string} */ var SHARED_MODULE_ID = 'lccekmodgklaepjeofjdjpbminllajkg'; /** * Path to shared module data. * @const {string} */ var SHARED_MODULE_ROOT = '_modules/' + SHARED_MODULE_ID; /** * Name used by the content scripts to create communications Ports. * @const {string} */ var CLIENT_PORT_NAME = 'chwcpn'; /** * The field name to specify the command among pages. * @const {string} */ var COMMAND_FIELD_NAME = 'cmd'; /** * The speaker model file name. * @const {string} */ var SPEAKER_MODEL_FILE_NAME = 'speaker_model.data'; /** * The training utterance file name prefix. * @const {string} */ var UTTERANCE_FILE_PREFIX = 'utterance-'; /** * The training utterance file extension. * @const {string} */ var UTTERANCE_FILE_EXTENSION = '.raw'; /** * The number of training utterances required to train the speaker model. * @const {number} */ var NUM_TRAINING_UTTERANCES = 3; /** * The size of the file system requested for reading the speaker model and * utterances. This number should always be larger than the combined file * size, * currently 576338 bytes as of February 2015. * @const {number} */ var FILE_SYSTEM_SIZE_BYTES = 1048576; /** * Time to wait for expected messages, in milliseconds. * @enum {number} */ var TimeoutMs = {SHORT: 200, NORMAL: 500, LONG: 2000}; /** * The URL of the files used by the plugin. * @enum {string} */ var File = { RECOGNIZER_CONFIG: 'hotword.data', }; /** * Errors emitted by the NaClManager. * @enum {string} */ var Error = { NACL_CRASH: 'nacl_crash', TIMEOUT: 'timeout', }; /** * Event types supported by NaClManager. * @enum {string} */ var Event = { READY: 'ready', TRIGGER: 'trigger', SPEAKER_MODEL_SAVED: 'speaker model saved', ERROR: 'error', TIMEOUT: 'timeout', }; /** * Messages for communicating with the NaCl recognizer plugin. These must * match * constants in /hotword_plugin.c * @enum {string} */ var NaClPlugin = { RESTART: 'r', SAMPLE_RATE_PREFIX: 'h', MODEL_PREFIX: 'm', STOP: 's', LOG: 'l', DSP: 'd', BEGIN_SPEAKER_MODEL: 'b', ADAPT_SPEAKER_MODEL: 'a', FINISH_SPEAKER_MODEL: 'f', SPEAKER_MODEL_SAVED: 'sm_saved', REQUEST_MODEL: 'model', MODEL_LOADED: 'model_loaded', READY_FOR_AUDIO: 'audio', STOPPED: 'stopped', HOTWORD_DETECTED: 'hotword', MS_CONFIGURED: 'ms_configured', TIMEOUT: 'timeout' }; /** * Messages sent from the injected scripts to the Google page. * @enum {string} */ var CommandToPage = { HOTWORD_VOICE_TRIGGER: 'vt', HOTWORD_STARTED: 'hs', HOTWORD_ENDED: 'hd', HOTWORD_TIMEOUT: 'ht', HOTWORD_ERROR: 'he' }; /** * Messages sent from the Google page to the extension or to the * injected script and then passed to the extension. * @enum {string} */ var CommandFromPage = { SPEECH_START: 'ss', SPEECH_END: 'se', SPEECH_RESET: 'sr', SHOWING_HOTWORD_START: 'shs', SHOWING_ERROR_MESSAGE: 'sem', SHOWING_TIMEOUT_MESSAGE: 'stm', CLICKED_RESUME: 'hcc', CLICKED_RESTART: 'hcr', CLICKED_DEBUG: 'hcd', WAKE_UP_HELPER: 'wuh', // Command specifically for the opt-in promo below this line. // User has explicitly clicked 'no'. CLICKED_NO_OPTIN: 'hcno', // User has opted in. CLICKED_OPTIN: 'hco', // User clicked on the microphone. PAGE_WAKEUP: 'wu' }; /** * Source of a hotwording session request. * @enum {string} */ var SessionSource = { LAUNCHER: 'launcher', NTP: 'ntp', ALWAYS: 'always', TRAINING: 'training' }; /** * The mode to start the hotword recognizer in. * @enum {string} */ var RecognizerStartMode = { NORMAL: 'normal', NEW_MODEL: 'new model', ADAPT_MODEL: 'adapt model' }; /** * MediaStream open success/errors to be reported via UMA. * DO NOT remove or renumber values in this enum. Only add new ones. * @enum {number} */ var UmaMediaStreamOpenResult = { SUCCESS: 0, UNKNOWN: 1, NOT_SUPPORTED: 2, PERMISSION_DENIED: 3, CONSTRAINT_NOT_SATISFIED: 4, OVERCONSTRAINED: 5, NOT_FOUND: 6, ABORT: 7, SOURCE_UNAVAILABLE: 8, PERMISSION_DISMISSED: 9, INVALID_STATE: 10, DEVICES_NOT_FOUND: 11, INVALID_SECURITY_ORIGIN: 12, MAX: 12 }; /** * UMA metrics. * DO NOT change these enum values. * @enum {string} */ var UmaMetrics = { TRIGGER: 'Hotword.HotwordTrigger', MEDIA_STREAM_RESULT: 'Hotword.HotwordMediaStreamResult', NACL_PLUGIN_LOAD_RESULT: 'Hotword.HotwordNaClPluginLoadResult', NACL_MESSAGE_TIMEOUT: 'Hotword.HotwordNaClMessageTimeout', TRIGGER_SOURCE: 'Hotword.HotwordTriggerSource' }; /** * Message waited for by NaCl plugin, to be reported via UMA. * DO NOT remove or renumber values in this enum. Only add new ones. * @enum {number} */ var UmaNaClMessageTimeout = { REQUEST_MODEL: 0, MODEL_LOADED: 1, READY_FOR_AUDIO: 2, STOPPED: 3, HOTWORD_DETECTED: 4, MS_CONFIGURED: 5, MAX: 5 }; /** * NaCl plugin load success/errors to be reported via UMA. * DO NOT remove or renumber values in this enum. Only add new ones. * @enum {number} */ var UmaNaClPluginLoadResult = {SUCCESS: 0, UNKNOWN: 1, CRASH: 2, NO_MODULE_FOUND: 3, MAX: 3}; /** * Source of hotword triggering, to be reported via UMA. * DO NOT remove or renumber values in this enum. Only add new ones. * @enum {number} */ var UmaTriggerSource = {LAUNCHER: 0, NTP_GOOGLE_COM: 1, ALWAYS_ON: 2, TRAINING: 3, MAX: 3}; /** * The browser UI language. * @const {string} */ var UI_LANGUAGE = (chrome.i18n && chrome.i18n.getUILanguage) ? chrome.i18n.getUILanguage() : ''; return { AUDIO_LOG_SECONDS: AUDIO_LOG_SECONDS, CLIENT_PORT_NAME: CLIENT_PORT_NAME, COMMAND_FIELD_NAME: COMMAND_FIELD_NAME, FILE_SYSTEM_SIZE_BYTES: FILE_SYSTEM_SIZE_BYTES, HOTWORD_STREAM_TIMEOUT_SECONDS: HOTWORD_STREAM_TIMEOUT_SECONDS, NUM_TRAINING_UTTERANCES: NUM_TRAINING_UTTERANCES, SHARED_MODULE_ID: SHARED_MODULE_ID, SHARED_MODULE_ROOT: SHARED_MODULE_ROOT, SPEAKER_MODEL_FILE_NAME: SPEAKER_MODEL_FILE_NAME, UI_LANGUAGE: UI_LANGUAGE, UTTERANCE_FILE_EXTENSION: UTTERANCE_FILE_EXTENSION, UTTERANCE_FILE_PREFIX: UTTERANCE_FILE_PREFIX, CommandToPage: CommandToPage, CommandFromPage: CommandFromPage, Error: Error, Event: Event, File: File, NaClPlugin: NaClPlugin, RecognizerStartMode: RecognizerStartMode, SessionSource: SessionSource, TimeoutMs: TimeoutMs, UmaMediaStreamOpenResult: UmaMediaStreamOpenResult, UmaMetrics: UmaMetrics, UmaNaClMessageTimeout: UmaNaClMessageTimeout, UmaNaClPluginLoadResult: UmaNaClPluginLoadResult, UmaTriggerSource: UmaTriggerSource }; }); // Copyright 2014 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('hotword', function() { 'use strict'; /** * Class used to keep this extension alive. When started, this calls an * extension API on a regular basis which resets the event page keep-alive * timer. * @constructor */ function KeepAlive() { this.timeoutId_ = null; } KeepAlive.prototype = { /** * Start the keep alive process. Safe to call multiple times. */ start: function() { if (this.timeoutId_ == null) this.timeoutId_ = setTimeout(this.handleTimeout_.bind(this), 1000); }, /** * Stops the keep alive process. Safe to call multiple times. */ stop: function() { if (this.timeoutId_ != null) { clearTimeout(this.timeoutId_); this.timeoutId_ = null; } }, /** * Handle the timer timeout. Calls an extension API and schedules the next * timeout. * @private */ handleTimeout_: function() { // Dummy extensions API call used to keep this event page alive by // resetting the shutdown timer. chrome.runtime.getPlatformInfo(function(info) {}); this.timeoutId_ = setTimeout(this.handleTimeout_.bind(this), 1000); } }; return {KeepAlive: KeepAlive}; }); // Copyright 2014 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('hotword', function() { 'use strict'; /** * Class used to manage the interaction between hotwording and the launcher * (app list). * @param {!hotword.StateManager} stateManager * @constructor * @extends {hotword.BaseSessionManager} */ function LauncherManager(stateManager) { hotword.BaseSessionManager.call( this, stateManager, hotword.constants.SessionSource.LAUNCHER); } LauncherManager.prototype = { __proto__: hotword.BaseSessionManager.prototype, /** @override */ enabled: function() { return this.stateManager.isSometimesOnEnabled(); }, /** @override */ onSessionStop: function() { chrome.hotwordPrivate.setHotwordSessionState(false, function() {}); } }; return {LauncherManager: LauncherManager}; }); // Copyright 2014 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('hotword', function() { 'use strict'; /** * Wrapper around console.log allowing debug log message to be enabled during * development. * @param {...*} varArgs */ function debug(varArgs) { if (hotword.DEBUG || window.localStorage['hotword.DEBUG']) console.log.apply(console, arguments); } return {DEBUG: false, debug: debug}; }); // Copyright 2014 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. (function() { 'use strict'; /** * @fileoverview This extension provides hotword triggering capabilites to * Chrome. * * This extension contains all the JavaScript for loading and managing the * hotword detector. The hotword detector and language model data will be * provided by a shared module loaded from the web store. * * IMPORTANT! Whenever adding new events, the extension version number MUST be * incremented. */ // Hotwording state. var stateManager = new hotword.StateManager(); var pageAudioManager = new hotword.PageAudioManager(stateManager); var alwaysOnManager = new hotword.AlwaysOnManager(stateManager); var launcherManager = new hotword.LauncherManager(stateManager); var trainingManager = new hotword.TrainingManager(stateManager); // Detect when hotword settings have changed. chrome.hotwordPrivate.onEnabledChanged.addListener(function() { stateManager.updateStatus(); }); // Detect a request to delete the speaker model. chrome.hotwordPrivate.onDeleteSpeakerModel.addListener(function() { hotword.TrainingManager.handleDeleteSpeakerModel(); }); // Detect a request for the speaker model existence. chrome.hotwordPrivate.onSpeakerModelExists.addListener(function() { hotword.TrainingManager.handleSpeakerModelExists(); }); // Detect when the shared module containing the NaCL module and language model // is installed. chrome.management.onInstalled.addListener(function(info) { if (info.id == hotword.constants.SHARED_MODULE_ID) { hotword.debug('Shared module installed, reloading extension.'); chrome.runtime.reload(); } }); }()); // Copyright 2014 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('hotword.metrics', function() { 'use strict'; /** * Helper function to record enum values in UMA. * @param {!string} name * @param {!number} value * @param {!number} maxValue */ function recordEnum(name, value, maxValue) { var metricDesc = { 'metricName': name, 'type': chrome.metricsPrivate.MetricTypeType.HISTOGRAM_LINEAR, 'min': 1, 'max': maxValue, 'buckets': maxValue + 1 }; chrome.metricsPrivate.recordValue(metricDesc, value); } return {recordEnum: recordEnum}; }); // Copyright 2014 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('hotword', function() { 'use strict'; /** * Class used to manage the state of the NaCl recognizer plugin. Handles all * control of the NaCl plugin, including creation, start, stop, trigger, and * shutdown. * * @param {boolean} loggingEnabled Whether audio logging is enabled. * @param {boolean} hotwordStream Whether the audio input stream is from a * hotword stream. * @constructor * @extends {cr.EventTarget} */ function NaClManager(loggingEnabled, hotwordStream) { /** * Current state of this manager. * @private {hotword.NaClManager.ManagerState_} */ this.recognizerState_ = ManagerState_.UNINITIALIZED; /** * The window.timeout ID associated with a pending message. * @private {?number} */ this.naclTimeoutId_ = null; /** * The expected message that will cancel the current timeout. * @private {?string} */ this.expectingMessage_ = null; /** * Whether the plugin will be started as soon as it stops. * @private {boolean} */ this.restartOnStop_ = false; /** * NaCl plugin element on extension background page. * @private {?HTMLEmbedElement} */ this.plugin_ = null; /** * URL containing hotword-model data file. * @private {string} */ this.modelUrl_ = ''; /** * Media stream containing an audio input track. * @private {?MediaStream} */ this.stream_ = null; /** * The mode to start the recognizer in. * @private {?chrome.hotwordPrivate.RecognizerStartMode} */ this.startMode_ = hotword.constants.RecognizerStartMode.NORMAL; /** * Whether audio logging is enabled. * @private {boolean} */ this.loggingEnabled_ = loggingEnabled; /** * Whether the audio input stream is from a hotword stream. * @private {boolean} */ this.hotwordStream_ = hotwordStream; /** * Audio log of X seconds before hotword triggered. * @private {?Object} */ this.preambleLog_ = null; }; /** * States this manager can be in. Since messages to/from the plugin are * asynchronous (and potentially queued), it's not possible to know what state * the plugin is in. However, track a state machine for NaClManager based on * what messages are sent/received. * @enum {number} * @private */ NaClManager.ManagerState_ = { UNINITIALIZED: 0, LOADING: 1, STOPPING: 2, STOPPED: 3, STARTING: 4, RUNNING: 5, ERROR: 6, SHUTDOWN: 7, }; var ManagerState_ = NaClManager.ManagerState_; var Error_ = hotword.constants.Error; var UmaNaClMessageTimeout_ = hotword.constants.UmaNaClMessageTimeout; var UmaNaClPluginLoadResult_ = hotword.constants.UmaNaClPluginLoadResult; NaClManager.prototype.__proto__ = cr.EventTarget.prototype; /** * Called when an error occurs. Dispatches an event. * @param {!hotword.constants.Error} error * @private */ NaClManager.prototype.handleError_ = function(error) { var event = new Event(hotword.constants.Event.ERROR); event.data = error; this.dispatchEvent(event); }; /** * Record the result of loading the NaCl plugin to UMA. * @param {!hotword.constants.UmaNaClPluginLoadResult} error * @private */ NaClManager.prototype.logPluginLoadResult_ = function(error) { hotword.metrics.recordEnum( hotword.constants.UmaMetrics.NACL_PLUGIN_LOAD_RESULT, error, UmaNaClPluginLoadResult_.MAX); }; /** * Set a timeout. Only allow one timeout to exist at any given time. * @param {!function()} func * @param {number} timeout * @private */ NaClManager.prototype.setTimeout_ = function(func, timeout) { assert(!this.naclTimeoutId_, 'Timeout already exists'); this.naclTimeoutId_ = window.setTimeout(function() { this.naclTimeoutId_ = null; func(); }.bind(this), timeout); }; /** * Clears the current timeout. * @private */ NaClManager.prototype.clearTimeout_ = function() { window.clearTimeout(this.naclTimeoutId_); this.naclTimeoutId_ = null; }; /** * Starts a stopped or stopping hotword recognizer (NaCl plugin). * @param {hotword.constants.RecognizerStartMode} mode The mode to start the * recognizer in. */ NaClManager.prototype.startRecognizer = function(mode) { this.startMode_ = mode; if (this.recognizerState_ == ManagerState_.STOPPED) { this.preambleLog_ = null; this.recognizerState_ = ManagerState_.STARTING; if (mode == hotword.constants.RecognizerStartMode.NEW_MODEL) { hotword.debug('Starting Recognizer in START training mode'); this.sendDataToPlugin_( hotword.constants.NaClPlugin.BEGIN_SPEAKER_MODEL); } else if (mode == hotword.constants.RecognizerStartMode.ADAPT_MODEL) { hotword.debug('Starting Recognizer in ADAPT training mode'); this.sendDataToPlugin_( hotword.constants.NaClPlugin.ADAPT_SPEAKER_MODEL); } else { hotword.debug('Starting Recognizer in NORMAL mode'); this.sendDataToPlugin_(hotword.constants.NaClPlugin.RESTART); } // Normally, there would be a waitForMessage_(READY_FOR_AUDIO) here. // However, this message is sent the first time audio data is read and in // some cases (ie. using the hotword stream), this won't happen until a // potential hotword trigger is seen. Having a waitForMessage_() would // time // out in this case, so just leave it out. This ends up sacrificing a bit // of // error detection in the non-hotword-stream case, but I think we can live // with that. } else if (this.recognizerState_ == ManagerState_.STOPPING) { // Wait until the plugin is stopped before trying to start it. this.restartOnStop_ = true; } else { throw 'Attempting to start NaCl recogniser not in STOPPED or STOPPING ' + 'state'; } }; /** * Stops the hotword recognizer. */ NaClManager.prototype.stopRecognizer = function() { if (this.recognizerState_ == ManagerState_.STARTING) { // If the recognizer is stopped before it finishes starting, it causes an // assertion to be raised in waitForMessage_() since we're waiting for the // READY_FOR_AUDIO message. Clear the current timeout and expecting // message // since we no longer expect it and may never receive it. this.clearTimeout_(); this.expectingMessage_ = null; } this.sendDataToPlugin_(hotword.constants.NaClPlugin.STOP); this.recognizerState_ = ManagerState_.STOPPING; this.waitForMessage_( hotword.constants.TimeoutMs.NORMAL, hotword.constants.NaClPlugin.STOPPED); }; /** * Saves the speaker model. */ NaClManager.prototype.finalizeSpeakerModel = function() { if (this.recognizerState_ == ManagerState_.UNINITIALIZED || this.recognizerState_ == ManagerState_.ERROR || this.recognizerState_ == ManagerState_.SHUTDOWN || this.recognizerState_ == ManagerState_.LOADING) { return; } this.sendDataToPlugin_(hotword.constants.NaClPlugin.FINISH_SPEAKER_MODEL); }; /** * Checks whether the file at the given path exists. * @param {!string} path Path to a file. Can be any valid URL. * @return {boolean} True if the patch exists. * @private */ NaClManager.prototype.fileExists_ = function(path) { var xhr = new XMLHttpRequest(); xhr.open('HEAD', path, false); try { xhr.send(); } catch (err) { return false; } if (xhr.readyState != xhr.DONE || xhr.status != 200) { return false; } return true; }; /** * Creates and returns a list of possible languages to check for hotword * support. * @return {!Array} Array of languages. * @private */ NaClManager.prototype.getPossibleLanguages_ = function() { // Create array used to search first for language-country, if not found then // search for language, if not found then no language (empty string). // For example, search for 'en-us', then 'en', then ''. var langs = new Array(); if (hotword.constants.UI_LANGUAGE) { // Chrome webstore doesn't support uppercase path: crbug.com/353407 var language = hotword.constants.UI_LANGUAGE.toLowerCase(); langs.push(language); // Example: 'en-us'. // Remove country to add just the language to array. var hyphen = language.lastIndexOf('-'); if (hyphen >= 0) { langs.push(language.substr(0, hyphen)); // Example: 'en'. } } langs.push(''); return langs; }; /** * Creates a NaCl plugin object and attaches it to the page. * @param {!string} src Location of the plugin. * @return {!HTMLEmbedElement} NaCl plugin DOM object. * @private */ NaClManager.prototype.createPlugin_ = function(src) { var plugin = /** @type {HTMLEmbedElement} */ (document.createElement('embed')); plugin.src = src; plugin.type = 'application/x-nacl'; document.body.appendChild(plugin); return plugin; }; /** * Initializes the NaCl manager. * @param {!string} naclArch Either 'arm', 'x86-32' or 'x86-64'. * @param {!MediaStream} stream A stream containing an audio source track. * @return {boolean} True if the successful. */ NaClManager.prototype.initialize = function(naclArch, stream) { assert( this.recognizerState_ == ManagerState_.UNINITIALIZED, 'Recognizer not in uninitialized state. State: ' + this.recognizerState_); assert(this.plugin_ == null); var langs = this.getPossibleLanguages_(); var i, j; // For country-lang variations. For example, when combined with path it will // attempt to find: '/x86-32_en-gb/', else '/x86-32_en/', else '/x86-32_/'. for (i = 0; i < langs.length; i++) { var folder = hotword.constants.SHARED_MODULE_ROOT + '/_platform_specific/' + naclArch + '_' + langs[i] + '/'; var dataSrc = folder + hotword.constants.File.RECOGNIZER_CONFIG; var pluginSrc = hotword.constants.SHARED_MODULE_ROOT + '/hotword_' + langs[i] + '.nmf'; var dataExists = this.fileExists_(dataSrc) && this.fileExists_(pluginSrc); if (!dataExists) { continue; } var plugin = this.createPlugin_(pluginSrc); if (!plugin || !plugin.postMessage) { document.body.removeChild(plugin); this.recognizerState_ = ManagerState_.ERROR; return false; } this.plugin_ = plugin; this.modelUrl_ = chrome.extension.getURL(dataSrc); this.stream_ = stream; this.recognizerState_ = ManagerState_.LOADING; plugin.addEventListener( 'message', this.handlePluginMessage_.bind(this), false); plugin.addEventListener('crash', function() { this.handleError_(Error_.NACL_CRASH); this.logPluginLoadResult_(UmaNaClPluginLoadResult_.CRASH); }.bind(this), false); return true; } this.recognizerState_ = ManagerState_.ERROR; this.logPluginLoadResult_(UmaNaClPluginLoadResult_.NO_MODULE_FOUND); return false; }; /** * Shuts down the NaCl plugin and frees all resources. */ NaClManager.prototype.shutdown = function() { if (this.plugin_ != null) { document.body.removeChild(this.plugin_); this.plugin_ = null; } this.clearTimeout_(); this.recognizerState_ = ManagerState_.SHUTDOWN; if (this.stream_) this.stream_.getAudioTracks()[0].stop(); this.stream_ = null; }; /** * Sends data to the NaCl plugin. * @param {!string|!MediaStreamTrack} data Command to be sent to NaCl plugin. * @private */ NaClManager.prototype.sendDataToPlugin_ = function(data) { assert( this.recognizerState_ != ManagerState_.UNINITIALIZED, 'Recognizer in uninitialized state'); this.plugin_.postMessage(data); }; /** * Waits, with a timeout, for a message to be received from the plugin. If the * message is not seen within the timeout, dispatch an 'error' event and go * into * the ERROR state. * @param {number} timeout Timeout, in milliseconds, to wait for the message. * @param {!string} message Message to wait for. * @private */ NaClManager.prototype.waitForMessage_ = function(timeout, message) { assert( this.expectingMessage_ == null, 'Cannot wait for message: ' + message + ', already waiting for message ' + this.expectingMessage_); this.setTimeout_(function() { this.recognizerState_ = ManagerState_.ERROR; this.handleError_(Error_.TIMEOUT); switch (this.expectingMessage_) { case hotword.constants.NaClPlugin.REQUEST_MODEL: var metricValue = UmaNaClMessageTimeout_.REQUEST_MODEL; break; case hotword.constants.NaClPlugin.MODEL_LOADED: var metricValue = UmaNaClMessageTimeout_.MODEL_LOADED; break; case hotword.constants.NaClPlugin.READY_FOR_AUDIO: var metricValue = UmaNaClMessageTimeout_.READY_FOR_AUDIO; break; case hotword.constants.NaClPlugin.STOPPED: var metricValue = UmaNaClMessageTimeout_.STOPPED; break; case hotword.constants.NaClPlugin.HOTWORD_DETECTED: var metricValue = UmaNaClMessageTimeout_.HOTWORD_DETECTED; break; case hotword.constants.NaClPlugin.MS_CONFIGURED: var metricValue = UmaNaClMessageTimeout_.MS_CONFIGURED; break; } hotword.metrics.recordEnum( hotword.constants.UmaMetrics.NACL_MESSAGE_TIMEOUT, metricValue, UmaNaClMessageTimeout_.MAX); }.bind(this), timeout); this.expectingMessage_ = message; }; /** * Called when a message is received from the plugin. If we're waiting for * that * message, cancel the pending timeout. * @param {string} message Message received. * @private */ NaClManager.prototype.receivedMessage_ = function(message) { if (message == this.expectingMessage_) { this.clearTimeout_(); this.expectingMessage_ = null; } }; /** * Handle a REQUEST_MODEL message from the plugin. * The plugin sends this message immediately after starting. * @private */ NaClManager.prototype.handleRequestModel_ = function() { if (this.recognizerState_ != ManagerState_.LOADING) { return; } this.logPluginLoadResult_(UmaNaClPluginLoadResult_.SUCCESS); this.sendDataToPlugin_( hotword.constants.NaClPlugin.MODEL_PREFIX + this.modelUrl_); this.waitForMessage_( hotword.constants.TimeoutMs.LONG, hotword.constants.NaClPlugin.MODEL_LOADED); // Configure logging in the plugin. This can be configured any time before // starting the recognizer, and now is as good a time as any. if (this.loggingEnabled_) { this.sendDataToPlugin_( hotword.constants.NaClPlugin.LOG + ':' + hotword.constants.AUDIO_LOG_SECONDS); } // If the audio stream is from a hotword stream, tell the plugin. if (this.hotwordStream_) { this.sendDataToPlugin_( hotword.constants.NaClPlugin.DSP + ':' + hotword.constants.HOTWORD_STREAM_TIMEOUT_SECONDS); } }; /** * Handle a MODEL_LOADED message from the plugin. * The plugin sends this message after successfully loading the language * model. * @private */ NaClManager.prototype.handleModelLoaded_ = function() { if (this.recognizerState_ != ManagerState_.LOADING) { return; } this.sendDataToPlugin_(this.stream_.getAudioTracks()[0]); // The plugin will send a MS_CONFIGURED, but don't set a timeout waiting for // it. MediaStreamAudioTrack::Configure() will remain pending until the // first // audio buffer is received. When the audio source is a DSP for always-on // detection, no audio sample is sent until the DSP detects a potential // hotword trigger. Thus, Configure would remain pending indefinitely if we // were to wait here. See https://crbug.com/616203 }; /** * Handle a MS_CONFIGURED message from the plugin. * The plugin sends this message after successfully configuring the audio * input * stream. * @private */ NaClManager.prototype.handleMsConfigured_ = function() { if (this.recognizerState_ != ManagerState_.LOADING) { return; } this.recognizerState_ = ManagerState_.STOPPED; this.dispatchEvent(new Event(hotword.constants.Event.READY)); }; /** * Handle a READY_FOR_AUDIO message from the plugin. * The plugin sends this message after the recognizer is started and * successfully receives and processes audio data. * @private */ NaClManager.prototype.handleReadyForAudio_ = function() { if (this.recognizerState_ != ManagerState_.STARTING) { return; } this.recognizerState_ = ManagerState_.RUNNING; }; /** * Handle a HOTWORD_DETECTED message from the plugin. * The plugin sends this message after detecting the hotword. * @private */ NaClManager.prototype.handleHotwordDetected_ = function() { if (this.recognizerState_ != ManagerState_.RUNNING) { return; } // We'll receive a STOPPED message very soon. this.recognizerState_ = ManagerState_.STOPPING; this.waitForMessage_( hotword.constants.TimeoutMs.NORMAL, hotword.constants.NaClPlugin.STOPPED); var event = new Event(hotword.constants.Event.TRIGGER); event.log = this.preambleLog_; this.dispatchEvent(event); }; /** * Handle a STOPPED message from the plugin. * This plugin sends this message after stopping the recognizer. This can * happen * either in response to a stop request, or after the hotword is detected. * @private */ NaClManager.prototype.handleStopped_ = function() { this.recognizerState_ = ManagerState_.STOPPED; if (this.restartOnStop_) { this.restartOnStop_ = false; this.startRecognizer(this.startMode_); } }; /** * Handle a TIMEOUT message from the plugin. * The plugin sends this message when it thinks the stream is from a DSP and * a hotword wasn't detected within a timeout period after arrival of the * first * audio samples. * @private */ NaClManager.prototype.handleTimeout_ = function() { if (this.recognizerState_ != ManagerState_.RUNNING) { return; } this.recognizerState_ = ManagerState_.STOPPED; this.dispatchEvent(new Event(hotword.constants.Event.TIMEOUT)); }; /** * Handle a SPEAKER_MODEL_SAVED message from the plugin. * The plugin sends this message after writing the model to a file. * @private */ NaClManager.prototype.handleSpeakerModelSaved_ = function() { this.dispatchEvent(new Event(hotword.constants.Event.SPEAKER_MODEL_SAVED)); }; /** * Handles a message from the NaCl plugin. * @param {!Event} msg Message from NaCl plugin. * @private */ NaClManager.prototype.handlePluginMessage_ = function(msg) { if (msg['data']) { if (typeof(msg['data']) == 'object') { // Save the preamble for delivery to the trigger handler when the // trigger // message arrives. this.preambleLog_ = msg['data']; return; } this.receivedMessage_(msg['data']); switch (msg['data']) { case hotword.constants.NaClPlugin.REQUEST_MODEL: this.handleRequestModel_(); break; case hotword.constants.NaClPlugin.MODEL_LOADED: this.handleModelLoaded_(); break; case hotword.constants.NaClPlugin.MS_CONFIGURED: this.handleMsConfigured_(); break; case hotword.constants.NaClPlugin.READY_FOR_AUDIO: this.handleReadyForAudio_(); break; case hotword.constants.NaClPlugin.HOTWORD_DETECTED: this.handleHotwordDetected_(); break; case hotword.constants.NaClPlugin.STOPPED: this.handleStopped_(); break; case hotword.constants.NaClPlugin.TIMEOUT: this.handleTimeout_(); break; case hotword.constants.NaClPlugin.SPEAKER_MODEL_SAVED: this.handleSpeakerModelSaved_(); break; } } }; return {NaClManager: NaClManager}; }); // Copyright 2014 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('hotword', function() { 'use strict'; /** * Class used to manage the interaction between hotwording and the * NTP/google.com. Injects a content script to interact with NTP/google.com * and updates the global hotwording state based on interaction with those * pages. * @param {!hotword.StateManager} stateManager * @constructor */ function PageAudioManager(stateManager) { /** * Manager of global hotwording state. * @private {!hotword.StateManager} */ this.stateManager_ = stateManager; /** * Mapping between tab ID and port that is connected from the injected * content script. * @private {!Object} */ this.portMap_ = {}; /** * Chrome event listeners. Saved so that they can be de-registered when * hotwording is disabled. */ this.connectListener_ = this.handleConnect_.bind(this); this.tabCreatedListener_ = this.handleCreatedTab_.bind(this); this.tabUpdatedListener_ = this.handleUpdatedTab_.bind(this); this.tabActivatedListener_ = this.handleActivatedTab_.bind(this); this.microphoneStateChangedListener_ = this.handleMicrophoneStateChanged_.bind(this); this.windowFocusChangedListener_ = this.handleChangedWindow_.bind(this); this.messageListener_ = this.handleMessageFromPage_.bind(this); // Need to setup listeners on startup, otherwise events that caused the // event page to start up, will be lost. this.setupListeners_(); this.stateManager_.onStatusChanged.addListener(function() { this.updateListeners_(); this.updateTabState_(); }.bind(this)); }; var CommandToPage = hotword.constants.CommandToPage; var CommandFromPage = hotword.constants.CommandFromPage; PageAudioManager.prototype = { /** * Helper function to test if a URL path is eligible for hotwording. * @param {!string} url URL to check. * @param {!string} base Base URL to compare against.. * @return {boolean} True if url is an eligible hotword URL. * @private */ checkUrlPathIsEligible_: function(url, base) { if (url == base || url == base + '/' || url.startsWith(base + '/_/chrome/newtab?') || // Appcache NTP. url.startsWith(base + '/?') || url.startsWith(base + '/#') || url.startsWith(base + '/webhp') || url.startsWith(base + '/search') || url.startsWith(base + '/imghp')) { return true; } return false; }, /** * Determines if a URL is eligible for hotwording. For now, the valid pages * are the Google HP and SERP (this will include the NTP). * @param {!string} url URL to check. * @return {boolean} True if url is an eligible hotword URL. * @private */ isEligibleUrl_: function(url) { if (!url) return false; var baseGoogleUrls = [ 'https://encrypted.google.', 'https://images.google.', 'https://www.google.' ]; // TODO(amistry): Get this list from a file in the shared module instead. var tlds = [ 'at', 'ca', 'com', 'com.au', 'com.mx', 'com.br', 'co.jp', 'co.kr', 'co.nz', 'co.uk', 'co.za', 'de', 'es', 'fr', 'it', 'ru' ]; // Check for the new tab page first. if (this.checkUrlPathIsEligible_(url, 'chrome://newtab')) return true; // Check URLs with each type of local-based TLD. for (var i = 0; i < baseGoogleUrls.length; i++) { for (var j = 0; j < tlds.length; j++) { var base = baseGoogleUrls[i] + tlds[j]; if (this.checkUrlPathIsEligible_(url, base)) return true; } } return false; }, /** * Locates the current active tab in the current focused window and * performs a callback with the tab as the parameter. * @param {function(?Tab)} callback Function to call with the * active tab or null if not found. The function's |this| will be set to * this object. * @private */ findCurrentTab_: function(callback) { chrome.windows.getAll({'populate': true}, function(windows) { for (var i = 0; i < windows.length; ++i) { if (!windows[i].focused) continue; for (var j = 0; j < windows[i].tabs.length; ++j) { var tab = windows[i].tabs[j]; if (tab.active) { callback.call(this, tab); return; } } } callback.call(this, null); }.bind(this)); }, /** * This function is called when a tab is activated (comes into focus). * @param {Tab} tab Current active tab. * @private */ activateTab_: function(tab) { if (!tab) { this.stopHotwording_(); return; } if (tab.id in this.portMap_) { this.startHotwordingIfEligible_(); return; } this.stopHotwording_(); this.prepareTab_(tab); }, /** * Prepare a new or updated tab by injecting the content script. * @param {!Tab} tab Newly updated or created tab. * @private */ prepareTab_: function(tab) { if (!this.isEligibleUrl_(tab.url)) return; chrome.tabs.executeScript( tab.id, {'file': 'audio_client.js'}, function(results) { if (chrome.runtime.lastError) { // Ignore this error. For new tab pages, even though the URL is // reported to be chrome://newtab/, the actual URL is a // country-specific google domain. Since we don't have permission // to inject on every page, an error will happen when the user is // in an unsupported country. // // The property still needs to be accessed so that the error // condition is cleared. If it isn't, exectureScript will log an // error the next time it is called. } }); }, /** * Updates hotwording state based on the state of current tabs/windows. * @private */ updateTabState_: function() { this.findCurrentTab_(this.activateTab_); }, /** * Handles a newly created tab. * @param {!Tab} tab Newly created tab. * @private */ handleCreatedTab_: function(tab) { this.prepareTab_(tab); }, /** * Handles an updated tab. * @param {number} tabId Id of the updated tab. * @param {{status: string}} info Change info of the tab. * @param {!Tab} tab Updated tab. * @private */ handleUpdatedTab_: function(tabId, info, tab) { // Chrome fires multiple update events: undefined, loading and completed. // We perform content injection on loading state. if (info['status'] != 'loading') return; this.prepareTab_(tab); }, /** * Handles a tab that has just become active. * @param {{tabId: number}} info Information about the activated tab. * @private */ handleActivatedTab_: function(info) { this.updateTabState_(); }, /** * Handles the microphone state changing. * @param {boolean} enabled Whether the microphone is now enabled. * @private */ handleMicrophoneStateChanged_: function(enabled) { if (enabled) { this.updateTabState_(); return; } this.stopHotwording_(); }, /** * Handles a change in Chrome windows. * Note: this does not always trigger in Linux. * @param {number} windowId Id of newly focused window. * @private */ handleChangedWindow_: function(windowId) { this.updateTabState_(); }, /** * Handles a content script attempting to connect. * @param {!Port} port Communications port from the client. * @private */ handleConnect_: function(port) { if (port.name != hotword.constants.CLIENT_PORT_NAME) return; var tab = /** @type {!Tab} */ (port.sender.tab); // An existing port from the same tab might already exist. But that port // may be from the previous page, so just overwrite the port. this.portMap_[tab.id] = port; port.onDisconnect.addListener(function() { this.handleClientDisconnect_(port); }.bind(this)); port.onMessage.addListener(function(msg) { this.handleMessage_(msg, port.sender, port.postMessage); }.bind(this)); }, /** * Handles a client content script disconnect. * @param {Port} port Disconnected port. * @private */ handleClientDisconnect_: function(port) { var tabId = port.sender.tab.id; if (tabId in this.portMap_ && this.portMap_[tabId] == port) { // Due to a race between port disconnection and tabs.onUpdated messages, // the port could have changed. delete this.portMap_[port.sender.tab.id]; } this.stopHotwordingIfIneligibleTab_(); }, /** * Disconnect all connected clients. * @private */ disconnectAllClients_: function() { for (var id in this.portMap_) { var port = this.portMap_[id]; port.disconnect(); delete this.portMap_[id]; } }, /** * Sends a command to the client content script on an eligible tab. * @param {hotword.constants.CommandToPage} command Command to send. * @param {number} tabId Id of the target tab. * @private */ sendClient_: function(command, tabId) { if (tabId in this.portMap_) { var message = {}; message[hotword.constants.COMMAND_FIELD_NAME] = command; this.portMap_[tabId].postMessage(message); } }, /** * Sends a command to all connected clients. * @param {hotword.constants.CommandToPage} command Command to send. * @private */ sendAllClients_: function(command) { for (var idStr in this.portMap_) { var id = parseInt(idStr, 10); assert(!isNaN(id), 'Tab ID is not a number: ' + idStr); this.sendClient_(command, id); } }, /** * Handles a hotword trigger. Sends a trigger message to the currently * active tab. * @private */ hotwordTriggered_: function() { this.findCurrentTab_(function(tab) { if (tab) this.sendClient_(CommandToPage.HOTWORD_VOICE_TRIGGER, tab.id); }); }, /** * Starts hotwording. * @private */ startHotwording_: function() { this.stateManager_.startSession( hotword.constants.SessionSource.NTP, function() { this.sendAllClients_(CommandToPage.HOTWORD_STARTED); }.bind(this), this.hotwordTriggered_.bind(this)); }, /** * Starts hotwording if the currently active tab is eligible for hotwording * (e.g. google.com). * @private */ startHotwordingIfEligible_: function() { this.findCurrentTab_(function(tab) { if (!tab) { this.stopHotwording_(); return; } if (this.isEligibleUrl_(tab.url)) this.startHotwording_(); }); }, /** * Stops hotwording. * @private */ stopHotwording_: function() { this.stateManager_.stopSession(hotword.constants.SessionSource.NTP); this.sendAllClients_(CommandToPage.HOTWORD_ENDED); }, /** * Stops hotwording if the currently active tab is not eligible for * hotwording (i.e. google.com). * @private */ stopHotwordingIfIneligibleTab_: function() { this.findCurrentTab_(function(tab) { if (!tab) { this.stopHotwording_(); return; } if (!this.isEligibleUrl_(tab.url)) this.stopHotwording_(); }); }, /** * Handles a message from the content script injected into the page. * @param {!Object} request Request from the content script. * @param {!MessageSender} sender Message sender. * @param {!function(Object)} sendResponse Function for sending a response. * @private */ handleMessage_: function(request, sender, sendResponse) { switch (request[hotword.constants.COMMAND_FIELD_NAME]) { // TODO(amistry): Handle other messages such as CLICKED_RESUME and // CLICKED_RESTART, if necessary. case CommandFromPage.SPEECH_START: this.stopHotwording_(); break; case CommandFromPage.SPEECH_END: case CommandFromPage.SPEECH_RESET: this.startHotwording_(); break; } }, /** * Handles a message directly from the NTP/HP/SERP. * @param {!Object} request Message from the sender. * @param {!MessageSender} sender Information about the sender. * @param {!function(HotwordStatus)} sendResponse Callback to respond * to sender. * @return {boolean} Whether to maintain the port open to call sendResponse. * @private */ handleMessageFromPage_: function(request, sender, sendResponse) { switch (request.type) { case CommandFromPage.PAGE_WAKEUP: if (sender.tab && this.isEligibleUrl_(sender.tab.url)) { chrome.hotwordPrivate.getStatus( true /* getOptionalFields */, this.statusDone_.bind( this, request.tab || sender.tab || {incognito: true}, sendResponse)); return true; } // Do not show the opt-in promo for ineligible urls. this.sendResponse_({'doNotShowOptinMessage': true}, sendResponse); break; case CommandFromPage.CLICKED_OPTIN: chrome.hotwordPrivate.setEnabled(true); break; // User has explicitly clicked 'no thanks'. case CommandFromPage.CLICKED_NO_OPTIN: chrome.hotwordPrivate.setEnabled(false); break; } return false; }, /** * Sends a message directly to the sending page. * @param {!HotwordStatus} response The response to send to the sender. * @param {!function(HotwordStatus)} sendResponse Callback to respond * to sender. * @private */ sendResponse_: function(response, sendResponse) { try { sendResponse(response); } catch (err) { // Suppress the exception thrown by sendResponse() when the page doesn't // specify a response callback in the call to // chrome.runtime.sendMessage(). // Unfortunately, there doesn't appear to be a way to detect one-way // messages without explicitly saying in the message itself. This // message is defined as a constant in // extensions/renderer/messaging_bindings.cc if (err.message == 'Attempting to use a disconnected port object') return; throw err; } }, /** * Sends the response to the tab. * @param {Tab} tab The tab that the request was sent from. * @param {function(HotwordStatus)} sendResponse Callback function to * respond to sender. * @param {HotwordStatus} hotwordStatus Status of the hotword extension. * @private */ statusDone_: function(tab, sendResponse, hotwordStatus) { var response = {'doNotShowOptinMessage': true}; // If always-on is available, then we do not show the promo, as the promo // only works with the sometimes-on pref. if (!tab.incognito && hotwordStatus.available && !hotwordStatus.enabledSet && !hotwordStatus.alwaysOnAvailable) { response = hotwordStatus; } this.sendResponse_(response, sendResponse); }, /** * Set up event listeners. * @private */ setupListeners_: function() { if (chrome.runtime.onConnect.hasListener(this.connectListener_)) return; chrome.runtime.onConnect.addListener(this.connectListener_); chrome.tabs.onCreated.addListener(this.tabCreatedListener_); chrome.tabs.onUpdated.addListener(this.tabUpdatedListener_); chrome.tabs.onActivated.addListener(this.tabActivatedListener_); chrome.windows.onFocusChanged.addListener( this.windowFocusChangedListener_); chrome.hotwordPrivate.onMicrophoneStateChanged.addListener( this.microphoneStateChangedListener_); if (chrome.runtime.onMessage.hasListener(this.messageListener_)) return; chrome.runtime.onMessageExternal.addListener(this.messageListener_); }, /** * Remove event listeners. * @private */ removeListeners_: function() { chrome.runtime.onConnect.removeListener(this.connectListener_); chrome.tabs.onCreated.removeListener(this.tabCreatedListener_); chrome.tabs.onUpdated.removeListener(this.tabUpdatedListener_); chrome.tabs.onActivated.removeListener(this.tabActivatedListener_); chrome.windows.onFocusChanged.removeListener( this.windowFocusChangedListener_); chrome.hotwordPrivate.onMicrophoneStateChanged.removeListener( this.microphoneStateChangedListener_); // Don't remove the Message listener, as we want them listening all // the time, }, /** * Update event listeners based on the current hotwording state. * @private */ updateListeners_: function() { if (this.stateManager_.isSometimesOnEnabled()) { this.setupListeners_(); } else { this.removeListeners_(); this.stopHotwording_(); this.disconnectAllClients_(); } } }; return {PageAudioManager: PageAudioManager}; }); // Copyright 2014 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('hotword', function() { 'use strict'; /** * Trivial container class for session information. * @param {!hotword.constants.SessionSource} source Source of the hotword * session. * @param {!function()} triggerCb Callback invoked when the hotword has * triggered. * @param {!function()} startedCb Callback invoked when the session has * been started successfully. * @param {function()=} opt_modelSavedCb Callback invoked when the speaker * model has been saved successfully. * @constructor * @struct * @private */ function Session_(source, triggerCb, startedCb, opt_modelSavedCb) { /** * Source of the hotword session request. * @private {!hotword.constants.SessionSource} */ this.source_ = source; /** * Callback invoked when the hotword has triggered. * @private {!function()} */ this.triggerCb_ = triggerCb; /** * Callback invoked when the session has been started successfully. * @private {?function()} */ this.startedCb_ = startedCb; /** * Callback invoked when the session has been started successfully. * @private {?function()} */ this.speakerModelSavedCb_ = opt_modelSavedCb; } /** * Class to manage hotwording state. Starts/stops the hotword detector based * on user settings, session requests, and any other factors that play into * whether or not hotwording should be running. * @constructor */ function StateManager() { /** * Current state. * @private {hotword.StateManager.State_} */ this.state_ = State_.STOPPED; /** * Current hotwording status. * @private {?chrome.hotwordPrivate.StatusDetails} */ this.hotwordStatus_ = null; /** * NaCl plugin manager. * @private {?hotword.NaClManager} */ this.pluginManager_ = null; /** * Currently active hotwording sessions. * @private {!Array} */ this.sessions_ = []; /** * The mode to start the recognizer in. * @private {!hotword.constants.RecognizerStartMode} */ this.startMode_ = hotword.constants.RecognizerStartMode.NORMAL; /** * Event that fires when the hotwording status has changed. * @type {!ChromeEvent} */ this.onStatusChanged = new chrome.Event(); /** * Hotword trigger audio notification... a.k.a The Chime (tm). * @private {!HTMLAudioElement} */ this.chime_ = /** @type {!HTMLAudioElement} */ (document.createElement('audio')); /** * Chrome event listeners. Saved so that they can be de-registered when * hotwording is disabled. * @private */ this.idleStateChangedListener_ = this.handleIdleStateChanged_.bind(this); this.startupListener_ = this.handleStartup_.bind(this); /** * Whether this user is locked. * @private {boolean} */ this.isLocked_ = false; /** * Current state of audio logging. * This is tracked separately from hotwordStatus_ because we need to restart * the hotword detector when this value changes. * @private {boolean} */ this.loggingEnabled_ = false; /** * Current state of training. * This is tracked separately from |hotwordStatus_| because we need to * restart the hotword detector when this value changes. * @private {!boolean} */ this.trainingEnabled_ = false; /** * Helper class to keep this extension alive while the hotword detector is * running in always-on mode. * @private {!hotword.KeepAlive} */ this.keepAlive_ = new hotword.KeepAlive(); // Get the initial status. chrome.hotwordPrivate.getStatus(this.handleStatus_.bind(this)); // Setup the chime and insert into the page. // Set preload=none to prevent an audio output stream from being created // when the extension loads. this.chime_.preload = 'none'; this.chime_.src = chrome.extension.getURL( hotword.constants.SHARED_MODULE_ROOT + '/audio/chime.wav'); document.body.appendChild(this.chime_); // In order to remove this listener, it must first be added. This handles // the case on first Chrome startup where this event is never registered, // so can't be removed when it's determined that hotwording is disabled. // Why not only remove the listener if it exists? Extension API events have // two parts to them, the Javascript listeners, and a browser-side component // that wakes up the extension if it's an event page. The browser-side // wake-up event is only removed when the number of javascript listeners // becomes 0. To clear the browser wake-up event, a listener first needs to // be added, then removed in order to drop the count to 0 and remove the // event. chrome.runtime.onStartup.addListener(this.startupListener_); } /** * @enum {number} * @private */ StateManager.State_ = { STOPPED: 0, STARTING: 1, RUNNING: 2, ERROR: 3, }; var State_ = StateManager.State_; var UmaMediaStreamOpenResults_ = { // These first error are defined by the MediaStream spec: // http://w3c.github.io/mediacapture-main/getusermedia.html#idl-def-MediaStreamError 'NotSupportedError': hotword.constants.UmaMediaStreamOpenResult.NOT_SUPPORTED, 'PermissionDeniedError': hotword.constants.UmaMediaStreamOpenResult.PERMISSION_DENIED, 'ConstraintNotSatisfiedError': hotword.constants.UmaMediaStreamOpenResult.CONSTRAINT_NOT_SATISFIED, 'OverconstrainedError': hotword.constants.UmaMediaStreamOpenResult.OVERCONSTRAINED, 'NotFoundError': hotword.constants.UmaMediaStreamOpenResult.NOT_FOUND, 'AbortError': hotword.constants.UmaMediaStreamOpenResult.ABORT, 'SourceUnavailableError': hotword.constants.UmaMediaStreamOpenResult.SOURCE_UNAVAILABLE, // The next few errors are chrome-specific. See: // content/renderer/media/user_media_client_impl.cc // (UserMediaClientImpl::GetUserMediaRequestFailed) 'PermissionDismissedError': hotword.constants.UmaMediaStreamOpenResult.PERMISSION_DISMISSED, 'InvalidStateError': hotword.constants.UmaMediaStreamOpenResult.INVALID_STATE, 'DevicesNotFoundError': hotword.constants.UmaMediaStreamOpenResult.DEVICES_NOT_FOUND, 'InvalidSecurityOriginError': hotword.constants.UmaMediaStreamOpenResult.INVALID_SECURITY_ORIGIN }; var UmaTriggerSources_ = { 'launcher': hotword.constants.UmaTriggerSource.LAUNCHER, 'ntp': hotword.constants.UmaTriggerSource.NTP_GOOGLE_COM, 'always': hotword.constants.UmaTriggerSource.ALWAYS_ON, 'training': hotword.constants.UmaTriggerSource.TRAINING }; StateManager.prototype = { /** * Request status details update. Intended to be called from the * hotwordPrivate.onEnabledChanged() event. */ updateStatus: function() { chrome.hotwordPrivate.getStatus(this.handleStatus_.bind(this)); }, /** * @return {boolean} True if google.com/NTP/launcher hotwording is enabled. */ isSometimesOnEnabled: function() { assert( this.hotwordStatus_, 'No hotwording status (isSometimesOnEnabled)'); // Although the two settings are supposed to be mutually exclusive, it's // possible for both to be set. In that case, always-on takes precedence. return this.hotwordStatus_.enabled && !this.hotwordStatus_.alwaysOnEnabled; }, /** * @return {boolean} True if always-on hotwording is enabled. */ isAlwaysOnEnabled: function() { assert(this.hotwordStatus_, 'No hotword status (isAlwaysOnEnabled)'); return this.hotwordStatus_.alwaysOnEnabled && !this.hotwordStatus_.trainingEnabled; }, /** * @return {boolean} True if training is enabled. */ isTrainingEnabled: function() { assert(this.hotwordStatus_, 'No hotword status (isTrainingEnabled)'); return this.hotwordStatus_.trainingEnabled; }, /** * Callback for hotwordPrivate.getStatus() function. * @param {chrome.hotwordPrivate.StatusDetails} status Current hotword * status. * @private */ handleStatus_: function(status) { hotword.debug('New hotword status', status); this.hotwordStatus_ = status; this.updateStateFromStatus_(); this.onStatusChanged.dispatch(); }, /** * Updates state based on the current status. * @private */ updateStateFromStatus_: function() { if (!this.hotwordStatus_) return; if (this.hotwordStatus_.enabled || this.hotwordStatus_.alwaysOnEnabled || this.hotwordStatus_.trainingEnabled) { // Detect changes to audio logging and kill the detector if that setting // has changed. if (this.hotwordStatus_.audioLoggingEnabled != this.loggingEnabled_) this.shutdownDetector_(); this.loggingEnabled_ = this.hotwordStatus_.audioLoggingEnabled; // If the training state has changed, we need to first shut down the // detector so that we can restart in a different mode. if (this.hotwordStatus_.trainingEnabled != this.trainingEnabled_) this.shutdownDetector_(); this.trainingEnabled_ = this.hotwordStatus_.trainingEnabled; // Start the detector if there's a session and the user is unlocked, and // stops it otherwise. if (this.sessions_.length && !this.isLocked_ && this.hotwordStatus_.userIsActive) { this.startDetector_(); } else { this.shutdownDetector_(); } if (!chrome.idle.onStateChanged.hasListener( this.idleStateChangedListener_)) { chrome.idle.onStateChanged.addListener( this.idleStateChangedListener_); } if (!chrome.runtime.onStartup.hasListener(this.startupListener_)) chrome.runtime.onStartup.addListener(this.startupListener_); } else { // Not enabled. Shut down if running. this.shutdownDetector_(); chrome.idle.onStateChanged.removeListener( this.idleStateChangedListener_); // If hotwording isn't enabled, don't start this component extension on // Chrome startup. If a user enables hotwording, the status change // event will be fired and the onStartup event will be registered. chrome.runtime.onStartup.removeListener(this.startupListener_); } }, /** * Starts the hotword detector. * @private */ startDetector_: function() { // Last attempt to start detector resulted in an error. if (this.state_ == State_.ERROR) { // TODO(amistry): Do some error rate tracking here and disable the // extension if we error too often. } if (!this.pluginManager_) { this.state_ = State_.STARTING; var isHotwordStream = this.isAlwaysOnEnabled() && this.hotwordStatus_.hotwordHardwareAvailable; this.pluginManager_ = new hotword.NaClManager(this.loggingEnabled_, isHotwordStream); this.pluginManager_.addEventListener( hotword.constants.Event.READY, this.onReady_.bind(this)); this.pluginManager_.addEventListener( hotword.constants.Event.ERROR, this.onError_.bind(this)); this.pluginManager_.addEventListener( hotword.constants.Event.TRIGGER, this.onTrigger_.bind(this)); this.pluginManager_.addEventListener( hotword.constants.Event.TIMEOUT, this.onTimeout_.bind(this)); this.pluginManager_.addEventListener( hotword.constants.Event.SPEAKER_MODEL_SAVED, this.onSpeakerModelSaved_.bind(this)); chrome.runtime.getPlatformInfo(function(platform) { var naclArch = platform.nacl_arch; // googDucking set to false so that audio output level from other tabs // is not affected when hotword is enabled. https://crbug.com/357773 // content/common/media/media_stream_options.cc // When always-on is enabled, request the hotword stream. // Optional because we allow power users to bypass the hardware // detection via a flag, and hence the hotword stream may not be // available. var constraints = /** @type {googMediaStreamConstraints} */ ({ audio: { optional: [ {googDucking: false}, {googHotword: this.isAlwaysOnEnabled()} ] } }); navigator.webkitGetUserMedia( /** @type {MediaStreamConstraints} */ (constraints), function(stream) { hotword.metrics.recordEnum( hotword.constants.UmaMetrics.MEDIA_STREAM_RESULT, hotword.constants.UmaMediaStreamOpenResult.SUCCESS, hotword.constants.UmaMediaStreamOpenResult.MAX); // The detector could have been shut down before the stream // finishes opening. if (this.pluginManager_ == null) { stream.getAudioTracks()[0].stop(); return; } if (this.isAlwaysOnEnabled()) this.keepAlive_.start(); if (!this.pluginManager_.initialize(naclArch, stream)) { this.state_ = State_.ERROR; this.shutdownPluginManager_(); } }.bind(this), function(error) { if (error.name in UmaMediaStreamOpenResults_) { var metricValue = UmaMediaStreamOpenResults_[error.name]; } else { var metricValue = hotword.constants.UmaMediaStreamOpenResult.UNKNOWN; } hotword.metrics.recordEnum( hotword.constants.UmaMetrics.MEDIA_STREAM_RESULT, metricValue, hotword.constants.UmaMediaStreamOpenResult.MAX); this.state_ = State_.ERROR; this.pluginManager_ = null; }.bind(this)); }.bind(this)); } else if (this.state_ != State_.STARTING) { // Don't try to start a starting detector. this.startRecognizer_(); } }, /** * Start the recognizer plugin. Assumes the plugin has been loaded and is * ready to start. * @private */ startRecognizer_: function() { assert(this.pluginManager_, 'No NaCl plugin loaded'); if (this.state_ != State_.RUNNING) { this.state_ = State_.RUNNING; if (this.isAlwaysOnEnabled()) this.keepAlive_.start(); this.pluginManager_.startRecognizer(this.startMode_); } for (var i = 0; i < this.sessions_.length; i++) { var session = this.sessions_[i]; if (session.startedCb_) { session.startedCb_(); session.startedCb_ = null; } } }, /** * Stops the hotword detector, if it's running. * @private */ stopDetector_: function() { this.keepAlive_.stop(); if (this.pluginManager_ && this.state_ == State_.RUNNING) { this.state_ = State_.STOPPED; this.pluginManager_.stopRecognizer(); } }, /** * Shuts down and removes the plugin manager, if it exists. * @private */ shutdownPluginManager_: function() { this.keepAlive_.stop(); if (this.pluginManager_) { this.pluginManager_.shutdown(); this.pluginManager_ = null; } }, /** * Shuts down the hotword detector. * @private */ shutdownDetector_: function() { this.state_ = State_.STOPPED; this.shutdownPluginManager_(); }, /** * Finalizes the speaker model. Assumes the plugin has been loaded and * started. */ finalizeSpeakerModel: function() { assert( this.pluginManager_, 'Cannot finalize speaker model: No NaCl plugin loaded'); if (this.state_ != State_.RUNNING) { hotword.debug('Cannot finalize speaker model: NaCl plugin not started'); return; } this.pluginManager_.finalizeSpeakerModel(); }, /** * Handle the hotword plugin being ready to start. * @private */ onReady_: function() { if (this.state_ != State_.STARTING) { // At this point, we should not be in the RUNNING state. Doing so would // imply the hotword detector was started without being ready. assert(this.state_ != State_.RUNNING, 'Unexpected RUNNING state'); this.shutdownPluginManager_(); return; } this.startRecognizer_(); }, /** * Handle an error from the hotword plugin. * @private */ onError_: function() { this.state_ = State_.ERROR; this.shutdownPluginManager_(); }, /** * Handle hotword triggering. * @param {!Event} event Event containing audio log data. * @private */ onTrigger_: function(event) { this.keepAlive_.stop(); hotword.debug('Hotword triggered!'); chrome.metricsPrivate.recordUserAction( hotword.constants.UmaMetrics.TRIGGER); assert(this.pluginManager_, 'No NaCl plugin loaded on trigger'); // Detector implicitly stops when the hotword is detected. this.state_ = State_.STOPPED; // Play the chime. this.chime_.play(); // Implicitly clear the top session. A session needs to be started in // order to restart the detector. if (this.sessions_.length) { var session = this.sessions_.pop(); session.triggerCb_(event.log); hotword.metrics.recordEnum( hotword.constants.UmaMetrics.TRIGGER_SOURCE, UmaTriggerSources_[session.source_], hotword.constants.UmaTriggerSource.MAX); } // If we're in always-on mode, shut down the hotword detector. The hotword // stream requires that we close and re-open it after a trigger, and the // only way to accomplish this is to shut everything down. if (this.isAlwaysOnEnabled()) this.shutdownDetector_(); }, /** * Handle hotword timeout. * @private */ onTimeout_: function() { hotword.debug('Hotword timeout!'); // We get this event when the hotword detector thinks there's a false // trigger. In this case, we need to shut down and restart the detector to // re-arm the DSP. this.shutdownDetector_(); this.updateStateFromStatus_(); }, /** * Handle speaker model saved. * @private */ onSpeakerModelSaved_: function() { hotword.debug('Speaker model saved!'); if (this.sessions_.length) { // Only call the callback of the the top session. var session = this.sessions_[this.sessions_.length - 1]; if (session.speakerModelSavedCb_) session.speakerModelSavedCb_(); } }, /** * Remove a hotwording session from the given source. * @param {!hotword.constants.SessionSource} source Source of the hotword * session request. * @private */ removeSession_: function(source) { for (var i = 0; i < this.sessions_.length; i++) { if (this.sessions_[i].source_ == source) { this.sessions_.splice(i, 1); break; } } }, /** * Start a hotwording session. * @param {!hotword.constants.SessionSource} source Source of the hotword * session request. * @param {!function()} startedCb Callback invoked when the session has * been started successfully. * @param {!function()} triggerCb Callback invoked when the hotword has * @param {function()=} modelSavedCb Callback invoked when the speaker model * has been saved. * @param {hotword.constants.RecognizerStartMode=} opt_mode The mode to * start the recognizer in. */ startSession: function( source, startedCb, triggerCb, opt_modelSavedCb, opt_mode) { if (this.isTrainingEnabled() && opt_mode) { this.startMode_ = opt_mode; } else { this.startMode_ = hotword.constants.RecognizerStartMode.NORMAL; } hotword.debug('Starting session for source: ' + source); this.removeSession_(source); this.sessions_.push( new Session_(source, triggerCb, startedCb, opt_modelSavedCb)); this.updateStateFromStatus_(); }, /** * Stops a hotwording session. * @param {!hotword.constants.SessionSource} source Source of the hotword * session request. */ stopSession: function(source) { hotword.debug('Stopping session for source: ' + source); this.removeSession_(source); // If this is a training session then switch the start mode back to // normal. if (source == hotword.constants.SessionSource.TRAINING) this.startMode_ = hotword.constants.RecognizerStartMode.NORMAL; this.updateStateFromStatus_(); }, /** * Handles a chrome.idle.onStateChanged event. * @param {!string} state State, one of "active", "idle", or "locked". * @private */ handleIdleStateChanged_: function(state) { hotword.debug('Idle state changed: ' + state); var oldLocked = this.isLocked_; if (state == 'locked') this.isLocked_ = true; else this.isLocked_ = false; if (oldLocked != this.isLocked_) this.updateStateFromStatus_(); }, /** * Handles a chrome.runtime.onStartup event. * @private */ handleStartup_: function() { // Nothing specific needs to be done here. This function exists solely to // be registered on the startup event. } }; return {StateManager: StateManager}; }); // Copyright 2014 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('hotword', function() { 'use strict'; /** * Class used to manage speaker training. Starts a hotwording session * if training is on, and automatically restarts the detector when a * a hotword is triggered. * @param {!hotword.StateManager} stateManager * @constructor * @extends {hotword.BaseSessionManager} */ function TrainingManager(stateManager) { /** * Chrome event listeners. Saved so that they can be de-registered when * hotwording is disabled. * @private */ this.finalizedSpeakerModelListener_ = this.handleFinalizeSpeakerModel_.bind(this); hotword.BaseSessionManager.call( this, stateManager, hotword.constants.SessionSource.TRAINING); } /** * Handles a success event on mounting the file system event and deletes * the user data files. * @param {FileSystem} fs The FileSystem object. * @private */ TrainingManager.deleteFiles_ = function(fs) { fs.root.getFile( hotword.constants.SPEAKER_MODEL_FILE_NAME, {create: false}, TrainingManager.deleteFile_, TrainingManager.fileErrorHandler_); for (var i = 0; i < hotword.constants.NUM_TRAINING_UTTERANCES; ++i) { fs.root.getFile( hotword.constants.UTTERANCE_FILE_PREFIX + i + hotword.constants.UTTERANCE_FILE_EXTENSION, {create: false}, TrainingManager.deleteFile_, TrainingManager.fileErrorHandler_); } }; /** * Deletes a file. * @param {FileEntry} fileEntry The FileEntry object. * @private */ TrainingManager.deleteFile_ = function(fileEntry) { if (fileEntry.isFile) { hotword.debug('File found: ' + fileEntry.fullPath); if (hotword.DEBUG || window.localStorage['hotword.DEBUG']) { fileEntry.getMetadata(function(md) { hotword.debug('File size: ' + md.size); }); } fileEntry.remove(function() { hotword.debug('File removed: ' + fileEntry.fullPath); }, TrainingManager.fileErrorHandler_); } }; /** * Handles a failure event on mounting the file system event. * @param {FileError} e The FileError object. * @private */ TrainingManager.fileErrorHandler_ = function(e) { hotword.debug('File error: ' + e.code); }; /** * Handles a failure event on checking for the existence of the speaker model. * @param {FileError} e The FileError object. * @private */ TrainingManager.sendNoSpeakerModelResponse_ = function(e) { chrome.hotwordPrivate.speakerModelExistsResult(false); }; /** * Handles a success event on mounting the file system and checks for the * existence of the speaker model. * @param {FileSystem} fs The FileSystem object. * @private */ TrainingManager.speakerModelExists_ = function(fs) { fs.root.getFile( hotword.constants.SPEAKER_MODEL_FILE_NAME, {create: false}, TrainingManager.sendSpeakerModelExistsResponse_, TrainingManager.sendNoSpeakerModelResponse_); }; /** * Sends a response through the HotwordPrivateApi indicating whether * the speaker model exists. * @param {FileEntry} fileEntry The FileEntry object. * @private */ TrainingManager.sendSpeakerModelExistsResponse_ = function(fileEntry) { if (fileEntry.isFile) { hotword.debug('File found: ' + fileEntry.fullPath); if (hotword.DEBUG || window.localStorage['hotword.DEBUG']) { fileEntry.getMetadata(function(md) { hotword.debug('File size: ' + md.size); }); } } chrome.hotwordPrivate.speakerModelExistsResult(fileEntry.isFile); }; /** * Handles a request to delete the speaker model. */ TrainingManager.handleDeleteSpeakerModel = function() { window.webkitRequestFileSystem( PERSISTENT, hotword.constants.FILE_SYSTEM_SIZE_BYTES, TrainingManager.deleteFiles_, TrainingManager.fileErrorHandler_); }; /** * Handles a request for the speaker model existence. */ TrainingManager.handleSpeakerModelExists = function() { window.webkitRequestFileSystem( PERSISTENT, hotword.constants.FILE_SYSTEM_SIZE_BYTES, TrainingManager.speakerModelExists_, TrainingManager.fileErrorHandler_); }; TrainingManager.prototype = { __proto__: hotword.BaseSessionManager.prototype, /** @override */ enabled: function() { return this.stateManager.isTrainingEnabled(); }, /** @override */ updateListeners: function() { hotword.BaseSessionManager.prototype.updateListeners.call(this); if (this.enabled()) { // Detect when the speaker model needs to be finalized. if (!chrome.hotwordPrivate.onFinalizeSpeakerModel.hasListener( this.finalizedSpeakerModelListener_)) { chrome.hotwordPrivate.onFinalizeSpeakerModel.addListener( this.finalizedSpeakerModelListener_); } this.startSession(hotword.constants.RecognizerStartMode.NEW_MODEL); } else { chrome.hotwordPrivate.onFinalizeSpeakerModel.removeListener( this.finalizedSpeakerModelListener_); } }, /** @override */ handleHotwordTrigger: function(log) { if (this.enabled()) { hotword.BaseSessionManager.prototype.handleHotwordTrigger.call( this, log); this.startSession(hotword.constants.RecognizerStartMode.ADAPT_MODEL); } }, /** @override */ startSession: function(opt_mode) { this.stateManager.startSession( this.sessionSource_, function() { chrome.hotwordPrivate.setHotwordSessionState(true, function() {}); }, this.handleHotwordTrigger.bind(this), this.handleSpeakerModelSaved_.bind(this), opt_mode); }, /** * Handles a hotwordPrivate.onFinalizeSpeakerModel event. * @private */ handleFinalizeSpeakerModel_: function() { if (this.enabled()) this.stateManager.finalizeSpeakerModel(); }, /** * Handles a hotwordPrivate.onFinalizeSpeakerModel event. * @private */ handleSpeakerModelSaved_: function() { if (this.enabled()) chrome.hotwordPrivate.notifySpeakerModelSaved(); }, }; return {TrainingManager: TrainingManager}; });

screenshot

// Copyright 2016 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. /** @type {string} * @const */ var FEEDBACK_LANDING_PAGE = 'https://support.google.com/chrome/go/feedback_confirmation'; /** * Feedback flow defined in feedback_private.idl. * @enum {string} */ var FeedbackFlow = { REGULAR: 'regular', // Flow in a regular user session. LOGIN: 'login', // Flow on the login screen. SHOW_SRT_PROMPT: 'showSrtPrompt' // Prompt user to try Software Removal Tool }; /** * The status of sending the feedback report as defined in feedback_private.idl. * @enum {string} */ var ReportStatus = { SUCCESS: 'success', DELAYED: 'delayed' }; // Copyright 2013 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. // // Copyright 2016 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. /** @type {string} * @const */ var FEEDBACK_LANDING_PAGE = 'https://support.google.com/chrome/go/feedback_confirmation'; /** * Feedback flow defined in feedback_private.idl. * @enum {string} */ var FeedbackFlow = { REGULAR: 'regular', // Flow in a regular user session. LOGIN: 'login', // Flow on the login screen. SHOW_SRT_PROMPT: 'showSrtPrompt' // Prompt user to try Software Removal Tool }; /** * The status of sending the feedback report as defined in feedback_private.idl. * @enum {string} */ var ReportStatus = { SUCCESS: 'success', DELAYED: 'delayed' }; /** * @type {number} * @const */ var FEEDBACK_WIDTH = 500; /** * @type {number} * @const */ var FEEDBACK_HEIGHT = 610; /** * @type {string} * @const */ var FEEDBACK_DEFAULT_WINDOW_ID = 'default_window'; // To generate a hashed extension ID, use a sha-1 hash, all in lower case. // Example: // echo -n 'abcdefghijklmnopqrstuvwxyzabcdef' | sha1sum | \ // awk '{print toupper($1)}' var whitelistedExtensionIds = [ '12E618C3C6E97495AAECF2AC12DEB082353241C6', // QuickOffice '3727DD3E564B6055387425027AD74C58784ACC15', // QuickOffice '2FC374607C2DF285634B67C64A2E356C607091C3', // QuickOffice '2843C1E82A9B6C6FB49308FDDF4E157B6B44BC2B', // G+ Photos '5B5DA6D054D10DB917AF7D9EAE3C56044D1B0B03', // G+ Photos '986913085E3E3C3AFDE9B7A943149C4D3F4C937B', // Feedback Extension '7AE714FFD394E073F0294CFA134C9F91DB5FBAA4', // Connectivity Diagnostics 'C7DA3A55C2355F994D3FDDAD120B426A0DF63843', // Connectivity Diagnostics '75E3CFFFC530582C583E4690EF97C70B9C8423B7', // Connectivity Diagnostics '32A1BA997F8AB8DE29ED1BA94AAF00CF2A3FEFA7', // Connectivity Diagnostics 'A291B26E088FA6BA53FFD72F0916F06EBA7C585A', // Chrome OS Recovery Tool 'D7986543275120831B39EF28D1327552FC343960', // Chrome OS Recovery Tool '8EBDF73405D0B84CEABB8C7513C9B9FA9F1DC2CE', // GetHelp app. '97B23E01B2AA064E8332EE43A7A85C628AADC3F2', // Chrome Remote Desktop Dev '9E527CDA9D7C50844E8A5DB964A54A640AE48F98', // Chrome Remote Desktop Stable 'DF52618D0B040D8A054D8348D2E84DDEEE5974E7', // Chrome Remote Desktop QA '269D721F163E587BC53C6F83553BF9CE2BB143CD', // Chrome Remote Desktop QA backup 'C449A798C495E6CF7D6AF10162113D564E67AD12', // Chrome Remote Desktop Apps V2 '981974CD1832B87BE6B21BE78F7249BB501E0DE6', // Play Movies Dev '32FD7A816E47392C92D447707A89EB07EEDE6FF7', // Play Movies Nightly '3F3CEC4B9B2B5DC2F820CE917AABDF97DB2F5B49', // Play Movies Beta 'F92FAC70AB68E1778BF62D9194C25979596AA0E6', // Play Movies Stable '0F585FB1D0FDFBEBCE1FEB5E9DFFB6DA476B8C9B', // Hangouts Extension '2D22CDB6583FD0A13758AEBE8B15E45208B4E9A7', // Hangouts Extension '49DA0B9CCEEA299186C6E7226FD66922D57543DC', // Hangouts Extension 'E7E2461CE072DF036CF9592740196159E2D7C089', // Hangouts Extension 'A74A4D44C7CFCD8844830E6140C8D763E12DD8F3', // Hangouts Extension '312745D9BF916161191143F6490085EEA0434997', // Hangouts Extension '53041A2FA309EECED01FFC751E7399186E860B2C', // Hangouts Extension '0F42756099D914A026DADFA182871C015735DD95', // Hangouts Extension '1B7734733E207CCE5C33BFAA544CA89634BF881F', // GLS nightly 'E2ACA3D943A3C96310523BCDFD8C3AF68387E6B7', // GLS stable '11B478CEC461C766A2DC1E5BEEB7970AE06DC9C2', // http://crbug.com/463552 '0EFB879311E9EFBB7C45251F89EC655711B1F6ED', // http://crbug.com/463552 '9193D3A51E2FE33B496CDA53EA330423166E7F02', // http://crbug.com/463552 'F9119B8B18C7C82B51E7BC6FF816B694F2EC3E89', // http://crbug.com/463552 'BA007D8D52CC0E2632EFCA03ACD003B0F613FD71', // http://crbug.com/470411 '5260FA31DE2007A837B7F7B0EB4A47CE477018C8', // http://crbug.com/470411 '4F4A25F31413D9B9F80E61D096DEB09082515267', // http://crbug.com/470411 'FBA0DE4D3EFB5485FC03760F01F821466907A743', // http://crbug.com/470411 'E216473E4D15C5FB14522D32C5F8DEAAB2CECDC6', // http://crbug.com/470411 '676A08383D875E51CE4C2308D875AE77199F1413', // http://crbug.com/473845 '869A23E11B308AF45A68CC386C36AADA4BE44A01', // http://crbug.com/473845 'E9CE07C7EDEFE70B9857B312E88F94EC49FCC30F', // http://crbug.com/473845 'A4577D8C2AF4CF26F40CBCA83FFA4251D6F6C8F8', // http://crbug.com/478929 'A8208CCC87F8261AFAEB6B85D5E8D47372DDEA6B', // http://crbug.com/478929 // TODO (ntang) Remove the following 2 hashes by 12/31/2017. 'B620CF4203315F9F2E046EDED22C7571A935958D', // http://crbug.com/510270 'B206D8716769728278D2D300349C6CB7D7DE2EF9', // http://crbug.com/510270 'EFCF5358672FEE04789FD2EC3638A67ADEDB6C8C', // http://crbug.com/514696 'FAD85BC419FE00995D196312F53448265EFA86F1', // http://crbug.com/516527 'F33B037DEDA65F226B7409C2ADB0CF3F8565AB03', // http://crbug.com/541769 '969C788BCBC82FBBE04A17360CA165C23A419257', // http://crbug.com/541769 '3BC3740BFC58F06088B300274B4CFBEA20136342', // http://crbug.com/541769 '2B6C6A4A5940017146F3E58B7F90116206E84685', // http://crbug.com/642141 '96FF2FFA5C9173C76D47184B3E86D267B37781DE', // http://crbug.com/642141 ]; /** * Used to generate unique IDs for FeedbackRequest objects. * @type {number} */ var lastUsedId = 0; /** * A FeedbackRequest object represents a unique feedback report, requested by an * instance of the feedback window. It contains the system information specific * to this report, the full feedbackInfo, and callbacks to send the report upon * request. */ class FeedbackRequest { constructor(feedbackInfo) { this.id_ = ++lastUsedId; this.feedbackInfo_ = feedbackInfo; this.onSystemInfoReadyCallback_ = null; this.isSystemInfoReady_ = false; this.reportIsBeingSent_ = false; this.isRequestCanceled_ = false; this.useSystemInfo_ = false; } /** * Called when the system information is sent from the C++ side. * @param {Object} sysInfo The received system information. */ getSystemInformationCallback(sysInfo) { if (this.isRequestCanceled_) { // If the window had been closed before the system information was // received, we skip the rest of the operations and return immediately. return; } this.isSystemInfoReady_ = true; // Combine the newly received system information with whatever system // information we have in the feedback info (if any). if (this.feedbackInfo_.systemInformation) { this.feedbackInfo_.systemInformation = this.feedbackInfo_.systemInformation.concat(sysInfo); } else { this.feedbackInfo_.systemInformation = sysInfo; } if (this.onSystemInfoReadyCallback_ != null) { this.onSystemInfoReadyCallback_(); this.onSystemInfoReadyCallback_ = null; } } /** * Retrieves the system information for this request object. * @param {function()} callback Invoked to notify the listener that the system * information has been received. */ getSystemInformation(callback) { if (this.isSystemInfoReady_) { callback(); return; } this.onSystemInfoReadyCallback_ = callback; // The C++ side must reply to the callback specific to this object. var boundCallback = this.getSystemInformationCallback.bind(this); chrome.feedbackPrivate.getSystemInformation(boundCallback); } /** * Sends the feedback report represented by the object, either now if system * information is ready, or later once it is. * @param {boolean} useSystemInfo True if the user would like the system * information to be sent with the report. */ sendReport(useSystemInfo) { this.reportIsBeingSent_ = true; this.useSystemInfo_ = useSystemInfo; if (useSystemInfo && !this.isSystemInfoReady_) { this.onSystemInfoReadyCallback_ = this.sendReportNow; return; } this.sendReportNow(); } /** * Sends the report immediately and removes this object once the report is * sent. */ sendReportNow() { if (!this.useSystemInfo_) { // Clear the system information if the user doesn't want it to be sent. this.feedbackInfo_.systemInformation = null; } /** @const */ var ID = this.id_; /** @const */ var FLOW = this.feedbackInfo_.flow; chrome.feedbackPrivate.sendFeedback(this.feedbackInfo_, function(result) { if (result == ReportStatus.SUCCESS) { console.log('Feedback: Report sent for request with ID ' + ID); if (FLOW != FeedbackFlow.LOGIN) window.open(FEEDBACK_LANDING_PAGE, '_blank'); } else { console.log('Feedback: Report for request with ID ' + ID + ' will be sent later.'); } }); } /** * Handles the event when the feedback UI window corresponding to this * FeedbackRequest instance is closed. */ onWindowClosed() { if (!this.reportIsBeingSent_) this.isRequestCanceled_ = true; } }; /** * Function to determine whether or not a given extension id is whitelisted to * invoke the feedback UI. If the extension is whitelisted, the callback to * start the Feedback UI will be called. * @param {string} id the id of the sender extension. * @param {Function} startFeedbackCallback The callback function that will * will start the feedback UI. * @param {Object} feedbackInfo The feedback info object to pass to the * start feedback UI callback. */ function senderWhitelisted(id, startFeedbackCallback, feedbackInfo) { crypto.subtle.digest('SHA-1', new TextEncoder().encode(id)).then( function(hashBuffer) { var hashString = ''; var hashView = new Uint8Array(hashBuffer); for (var i = 0; i < hashView.length; ++i) { var n = hashView[i]; hashString += n < 0x10 ? '0' : ''; hashString += n.toString(16); } if (whitelistedExtensionIds.indexOf(hashString.toUpperCase()) != -1) startFeedbackCallback(feedbackInfo); }); } /** * Callback which gets notified once our feedback UI has loaded and is ready to * receive its initial feedback info object. * @param {Object} request The message request object. * @param {Object} sender The sender of the message. * @param {function(Object)} sendResponse Callback for sending a response. */ function feedbackReadyHandler(request, sender, sendResponse) { if (request.ready) chrome.runtime.sendMessage({sentFromEventPage: true}); } /** * Callback which gets notified if another extension is requesting feedback. * @param {Object} request The message request object. * @param {Object} sender The sender of the message. * @param {function(Object)} sendResponse Callback for sending a response. */ function requestFeedbackHandler(request, sender, sendResponse) { if (request.requestFeedback) senderWhitelisted(sender.id, startFeedbackUI, request.feedbackInfo); } /** * Callback which starts up the feedback UI. * @param {Object} feedbackInfo Object containing any initial feedback info. */ function startFeedbackUI(feedbackInfo) { var win = chrome.app.window.get(FEEDBACK_DEFAULT_WINDOW_ID); if (win) { win.show(); return; } chrome.app.window.create('html/default.html', { frame: feedbackInfo.useSystemWindowFrame ? 'chrome' : 'none', id: FEEDBACK_DEFAULT_WINDOW_ID, innerBounds: { minWidth: FEEDBACK_WIDTH, minHeight: FEEDBACK_HEIGHT, }, hidden: true, resizable: false }, function(appWindow) { var request = new FeedbackRequest(feedbackInfo); // The feedbackInfo member of the new window should refer to the one in // its corresponding FeedbackRequest object to avoid copying and // duplicatations. appWindow.contentWindow.feedbackInfo = request.feedbackInfo_; // Define some functions for the new window so that it can call back // into here. // Define a function for the new window to get the system information. appWindow.contentWindow.getSystemInformation = function(callback) { request.getSystemInformation(callback); }; // Define a function to request sending the feedback report. appWindow.contentWindow.sendFeedbackReport = function(useSystemInfo) { request.sendReport(useSystemInfo); }; // Observe when the window is closed. appWindow.onClosed.addListener(function() { request.onWindowClosed(); }); }); } chrome.runtime.onMessage.addListener(feedbackReadyHandler); chrome.runtime.onMessageExternal.addListener(requestFeedbackHandler); chrome.feedbackPrivate.onFeedbackRequested.addListener(startFeedbackUI); // Copyright 2013 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. /** * @type {string} * @const */ var SRT_DOWNLOAD_PAGE = 'https://www.google.com/chrome/cleanup-tool/'; /** @type {number} * @const */ var MAX_ATTACH_FILE_SIZE = 3 * 1024 * 1024; /** * @type {number} * @const */ var FEEDBACK_MIN_WIDTH = 500; /** * @type {number} * @const */ var FEEDBACK_MIN_HEIGHT = 585; /** * @type {number} * @const */ var FEEDBACK_MIN_HEIGHT_LOGIN = 482; /** @type {number} * @const */ var CONTENT_MARGIN_HEIGHT = 40; /** @type {number} * @const */ var MAX_SCREENSHOT_WIDTH = 100; /** @type {string} * @const */ var SYSINFO_WINDOW_ID = 'sysinfo_window'; /** @type {string} * @const */ var STATS_WINDOW_ID = 'stats_window'; /** * SRT Prompt Result defined in feedback_private.idl. * @enum {string} */ var SrtPromptResult = { ACCEPTED: 'accepted', // User accepted prompt. DECLINED: 'declined', // User declined prompt. CLOSED: 'closed', // User closed window without responding to prompt. }; var attachedFileBlob = null; var lastReader = null; /** * Determines whether the system information associated with this instance of * the feedback window has been received. * @type {boolean} */ var isSystemInfoReady = false; /** * Indicates whether the SRT Prompt is currently being displayed. * @type {boolean} */ var isShowingSrtPrompt = false; /** * The callback used by the sys_info_page to receive the event that the system * information is ready. * @type {function(sysInfo)} */ var sysInfoPageOnSysInfoReadyCallback = null; /** * Reads the selected file when the user selects a file. * @param {Event} fileSelectedEvent The onChanged event for the file input box. */ function onFileSelected(fileSelectedEvent) { $('attach-error').hidden = true; var file = fileSelectedEvent.target.files[0]; if (!file) { // User canceled file selection. attachedFileBlob = null; return; } if (file.size > MAX_ATTACH_FILE_SIZE) { $('attach-error').hidden = false; // Clear our selected file. $('attach-file').value = ''; attachedFileBlob = null; return; } attachedFileBlob = file.slice(); } /** * Clears the file that was attached to the report with the initial request. * Instead we will now show the attach file button in case the user wants to * attach another file. */ function clearAttachedFile() { $('custom-file-container').hidden = true; attachedFileBlob = null; feedbackInfo.attachedFile = null; $('attach-file').hidden = false; } /** * Creates a closure that creates or shows a window with the given url. * @param {string} windowId A string with the ID of the window we are opening. * @param {string} url The destination URL of the new window. * @return {function()} A function to be called to open the window. */ function windowOpener(windowId, url) { return function(e) { e.preventDefault(); chrome.app.window.create(url, {id: windowId}); }; } /** * Opens a new window with chrome://slow_trace, downloading performance data. */ function openSlowTraceWindow() { chrome.app.window.create( 'chrome://slow_trace/tracing.zip#' + feedbackInfo.traceId); } /** * Sends the report; after the report is sent, we need to be redirected to * the landing page, but we shouldn't be able to navigate back, hence * we open the landing page in a new tab and sendReport closes this tab. * @return {boolean} True if the report was sent. */ function sendReport() { if ($('description-text').value.length == 0) { var description = $('description-text'); description.placeholder = loadTimeData.getString('no-description'); description.focus(); return false; } // Prevent double clicking from sending additional reports. $('send-report-button').disabled = true; console.log('Feedback: Sending report'); if (!feedbackInfo.attachedFile && attachedFileBlob) { feedbackInfo.attachedFile = { name: $('attach-file').value, data: attachedFileBlob }; } feedbackInfo.description = $('description-text').value; feedbackInfo.pageUrl = $('page-url-text').value; feedbackInfo.email = $('user-email-text').value; var useSystemInfo = false; var useHistograms = false; if ($('sys-info-checkbox') != null && $('sys-info-checkbox').checked) { // Send histograms along with system info. useSystemInfo = useHistograms = true; } // feedbackInfo.sendHistograms = useHistograms; // If the user doesn't want to send the screenshot. if (!$('screenshot-checkbox').checked) feedbackInfo.screenshot = null; var productId = parseInt('' + feedbackInfo.productId); if (isNaN(productId)) { // For apps that still use a string value as the |productId|, we must clear // that value since the API uses an integer value, and a conflict in data // types will cause the report to fail to be sent. productId = null; } feedbackInfo.productId = productId; // Request sending the report, show the landing page (if allowed), and close // this window right away. The FeedbackRequest object that represents this // report will take care of sending the report in the background. sendFeedbackReport(useSystemInfo); window.close(); return true; } /** * Click listener for the cancel button. * @param {Event} e The click event being handled. */ function cancel(e) { e.preventDefault(); window.close(); } /** * Converts a blob data URL to a blob object. * @param {string} url The data URL to convert. * @return {Blob} Blob object containing the data. */ function dataUrlToBlob(url) { var mimeString = url.split(',')[0].split(':')[1].split(';')[0]; var data = atob(url.split(',')[1]); var dataArray = []; for (var i = 0; i < data.length; ++i) dataArray.push(data.charCodeAt(i)); return new Blob([new Uint8Array(dataArray)], {type: mimeString}); } // function resizeAppWindow() { // We pick the width from the titlebar, which has no margins. var width = $('title-bar').scrollWidth; if (width < FEEDBACK_MIN_WIDTH) width = FEEDBACK_MIN_WIDTH; // We get the height by adding the titlebar height and the content height + // margins. We can't get the margins for the content-pane here by using // style.margin - the variable seems to not exist. var height = $('title-bar').scrollHeight + $('content-pane').scrollHeight + CONTENT_MARGIN_HEIGHT; var minHeight = FEEDBACK_MIN_HEIGHT; if (feedbackInfo.flow == FeedbackFlow.LOGIN) minHeight = FEEDBACK_MIN_HEIGHT_LOGIN; height = Math.max(height, minHeight); chrome.app.window.current().resizeTo(width, height); } /** * A callback to be invoked when the background page of this extension receives * the system information. */ function onSystemInformation() { isSystemInfoReady = true; // In case the sys_info_page needs to be notified by this event, do so. if (sysInfoPageOnSysInfoReadyCallback != null) { sysInfoPageOnSysInfoReadyCallback(feedbackInfo.systemInformation); sysInfoPageOnSysInfoReadyCallback = null; } } /** * Initializes our page. * Flow: * .) DOMContent Loaded -> . Request feedbackInfo object * . Setup page event handlers * .) Feedback Object Received -> . take screenshot * . request email * . request System info * . request i18n strings * .) Screenshot taken -> . Show Feedback window. */ function initialize() { // Add listener to receive the feedback info object. chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) { if (request.sentFromEventPage) { if (!feedbackInfo.flow) feedbackInfo.flow = FeedbackFlow.REGULAR; if (feedbackInfo.flow == FeedbackFlow.SHOW_SRT_PROMPT) { isShowingSrtPrompt = true; $('content-pane').hidden = true; $('srt-decline-button').onclick = function() { isShowingSrtPrompt = false; chrome.feedbackPrivate.logSrtPromptResult(SrtPromptResult.DECLINED); $('srt-prompt').hidden = true; $('content-pane').hidden = false; }; $('srt-accept-button').onclick = function() { chrome.feedbackPrivate.logSrtPromptResult(SrtPromptResult.ACCEPTED); window.open(SRT_DOWNLOAD_PAGE, '_blank'); window.close(); }; $('close-button').addEventListener('click', function() { if (isShowingSrtPrompt) { chrome.feedbackPrivate.logSrtPromptResult(SrtPromptResult.CLOSED); } }); } else { $('srt-prompt').hidden = true; } $('description-text').textContent = feedbackInfo.description; if (feedbackInfo.pageUrl) $('page-url-text').value = feedbackInfo.pageUrl; takeScreenshot(function(screenshotCanvas) { // We've taken our screenshot, show the feedback page without any // further delay. window.webkitRequestAnimationFrame(function() { resizeAppWindow(); }); chrome.app.window.current().show(); var screenshotDataUrl = screenshotCanvas.toDataURL('image/png'); $('screenshot-image').src = screenshotDataUrl; $('screenshot-image').classList.toggle('wide-screen', $('screenshot-image').width > MAX_SCREENSHOT_WIDTH); feedbackInfo.screenshot = dataUrlToBlob(screenshotDataUrl); }); chrome.feedbackPrivate.getUserEmail(function(email) { $('user-email-text').value = email; }); // Initiate getting the system info. isSystemInfoReady = false; getSystemInformation(onSystemInformation); // An extension called us with an attached file. if (feedbackInfo.attachedFile) { $('attached-filename-text').textContent = feedbackInfo.attachedFile.name; attachedFileBlob = feedbackInfo.attachedFile.data; $('custom-file-container').hidden = false; $('attach-file').hidden = true; } // No URL and file attachment for login screen feedback. if (feedbackInfo.flow == FeedbackFlow.LOGIN) { $('page-url').hidden = true; $('attach-file-container').hidden = true; $('attach-file-note').hidden = true; } // chrome.feedbackPrivate.getStrings(function(strings) { loadTimeData.data = strings; i18nTemplate.process(document, loadTimeData); if ($('sys-info-url')) { // Opens a new window showing the full anonymized system+app // information. $('sys-info-url').onclick = function() { var win = chrome.app.window.get(SYSINFO_WINDOW_ID); if (win) { win.show(); return; } chrome.app.window.create( '/html/sys_info.html', { frame: 'chrome', id: SYSINFO_WINDOW_ID, width: 640, height: 400, hidden: false, resizable: true }, function(appWindow) { // Define functions for the newly created window. // Gets the full system information for the new window. appWindow.contentWindow.getFullSystemInfo = function(callback) { if (isSystemInfoReady) { callback(feedbackInfo.systemInformation); return; } sysInfoPageOnSysInfoReadyCallback = callback; }; // Returns the loadTimeData for the new window. appWindow.contentWindow.getLoadTimeData = function() { return loadTimeData; }; }); }; } if ($('histograms-url')) { // Opens a new window showing the histogram metrics. $('histograms-url').onclick = windowOpener(STATS_WINDOW_ID, 'chrome://histograms'); } // Make sure our focus starts on the description field. $('description-text').focus(); }); } }); window.addEventListener('DOMContentLoaded', function() { // Ready to receive the feedback object. chrome.runtime.sendMessage({ready: true}); // Setup our event handlers. $('attach-file').addEventListener('change', onFileSelected); $('send-report-button').onclick = sendReport; $('cancel-button').onclick = cancel; $('remove-attached-file').onclick = clearAttachedFile; // }); } initialize(); // Copyright 2015 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. /** * The global load time data that contains the localized strings that we will * get from the main page when this page first loads. */ var loadTimeData = null; /** * A queue of a sequence of closures that will incrementally build the sys info * html table. */ var tableCreationClosuresQueue = []; /** * The time used to post delayed tasks in MS. Currently set to be enough for two * frames. */ var STANDARD_DELAY_MS = 32; function getValueDivForButton(button) { return $(button.id.substr(0, button.id.length - 4)); } function getButtonForValueDiv(valueDiv) { return $(valueDiv.id + '-btn'); } /** * Expands the multiline table cell that contains the given valueDiv. * @param {HTMLElement} button The expand button. * @param {HTMLElement} valueDiv The div that contains the multiline logs. * @param {number} delayFactor A value used for increasing the delay after which * the cell will be expanded. Useful for expandAll() since it expands the * multiline cells one after another with each expension done slightly after * the previous one. */ function expand(button, valueDiv, delayFactor) { button.textContent = loadTimeData.getString('sysinfoPageCollapseBtn'); // Show the spinner container. var valueCell = valueDiv.parentNode; valueCell.firstChild.hidden = false; // Expanding huge logs can take a very long time, so we do it after a delay // to have a chance to render the spinner. setTimeout(function() { valueCell.className = 'number-expanded'; // Hide the spinner container. valueCell.firstChild.hidden = true; }, STANDARD_DELAY_MS * delayFactor); } /** * Collapses the multiline table cell that contains the given valueDiv. * @param {HTMLElement} button The expand button. * @param {HTMLElement} valueDiv The div that contains the multiline logs. */ function collapse(button, valueDiv) { button.textContent = loadTimeData.getString('sysinfoPageExpandBtn'); valueDiv.parentNode.className = 'number-collapsed'; } /** * Toggles whether an item is collapsed or expanded. */ function changeCollapsedStatus() { var valueDiv = getValueDivForButton(this); if (valueDiv.parentNode.className == 'number-collapsed') expand(this, valueDiv, 1); else collapse(this, valueDiv); } /** * Collapses all log items. */ function collapseAll() { var valueDivs = document.getElementsByClassName('stat-value'); for (var i = 0; i < valueDivs.length; ++i) { if (valueDivs[i].parentNode.className != 'number-expanded') continue; var button = getButtonForValueDiv(valueDivs[i]); if (button) collapse(button, valueDivs[i]); } } /** * Expands all log items. */ function expandAll() { var valueDivs = document.getElementsByClassName('stat-value'); for (var i = 0; i < valueDivs.length; ++i) { if (valueDivs[i].parentNode.className != 'number-collapsed') continue; var button = getButtonForValueDiv(valueDivs[i]); if (button) expand(button, valueDivs[i], i + 1); } } function createNameCell(key) { var nameCell = document.createElement('td'); nameCell.setAttribute('class', 'name'); var nameDiv = document.createElement('div'); nameDiv.setAttribute('class', 'stat-name'); nameDiv.appendChild(document.createTextNode(key)); nameCell.appendChild(nameDiv); return nameCell; } function createButtonCell(key, isMultiLine) { var buttonCell = document.createElement('td'); buttonCell.setAttribute('class', 'button-cell'); if (isMultiLine) { var button = document.createElement('button'); button.setAttribute('id', '' + key + '-value-btn'); button.onclick = changeCollapsedStatus; button.textContent = loadTimeData.getString('sysinfoPageExpandBtn'); buttonCell.appendChild(button); } return buttonCell; } function createValueCell(key, value, isMultiLine) { var valueCell = document.createElement('td'); var valueDiv = document.createElement('div'); valueDiv.setAttribute('class', 'stat-value'); valueDiv.setAttribute('id', '' + key + '-value'); valueDiv.appendChild(document.createTextNode(value)); if (isMultiLine) { valueCell.className = 'number-collapsed'; var loadingContainer = $('spinner-container').cloneNode(true); loadingContainer.setAttribute('id', '' + key + '-value-loading'); loadingContainer.hidden = true; valueCell.appendChild(loadingContainer); } else { valueCell.className = 'number'; } valueCell.appendChild(valueDiv); return valueCell; } function createTableRow(key, value) { var row = document.createElement('tr'); // Avoid using element.scrollHeight as it's very slow. crbug.com/653968. var isMultiLine = value.split('\n').length > 2 || value.length > 1000; row.appendChild(createNameCell(key)); row.appendChild(createButtonCell(key, isMultiLine)); row.appendChild(createValueCell(key, value, isMultiLine)); return row; } /** * Finalize the page after the content has been loaded. */ function finishPageLoading() { $('collapseAllBtn').onclick = collapseAll; $('expandAllBtn').onclick = expandAll; $('spinner-container').hidden = true; } /** * Pops a closure from the front of the queue and executes it. */ function processQueue() { var closure = tableCreationClosuresQueue.shift(); if (closure) closure(); if (tableCreationClosuresQueue.length > 0) { // Post a task to process the next item in the queue. setTimeout(processQueue, STANDARD_DELAY_MS); } } /** * Creates a closure that creates a table row for the given key and value. * @param {string} key The name of the log. * @param {string} value The contents of the log. * @return {function():void} A closure that creates a row for the given log. */ function createTableRowWrapper(key, value) { return function() { $('detailsTable').appendChild(createTableRow(key, value)); }; } /** * Creates closures to build the system information table row by row * incrementally. * @param {Object} systemInfo The system information that will be used to fill * the table. */ function createTable(systemInfo) { for (var key in systemInfo) { var item = systemInfo[key]; tableCreationClosuresQueue.push( createTableRowWrapper(item['key'], item['value'])); } tableCreationClosuresQueue.push(finishPageLoading); processQueue(); } /** * Initializes the page when the window is loaded. */ window.onload = function() { loadTimeData = getLoadTimeData(); i18nTemplate.process(document, loadTimeData); getFullSystemInfo(createTable); }; // Copyright 2013 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. /** * Function to take the screenshot of the current screen. * @param {function(HTMLCanvasElement)} callback Callback for returning the * canvas with the screenshot on it. */ function takeScreenshot(callback) { var screenshotStream = null; var video = document.createElement('video'); video.addEventListener('canplay', function(e) { if (screenshotStream) { var canvas = document.createElement('canvas'); canvas.setAttribute('width', video.videoWidth); canvas.setAttribute('height', video.videoHeight); canvas.getContext('2d').drawImage( video, 0, 0, video.videoWidth, video.videoHeight); video.pause(); video.src = ''; screenshotStream.getVideoTracks()[0].stop(); screenshotStream = null; callback(canvas); } }, false); navigator.webkitGetUserMedia( { video: { mandatory: { chromeMediaSource: 'screen', maxWidth: 4096, maxHeight: 2560 } } }, function(stream) { if (stream) { screenshotStream = stream; video.src = window.URL.createObjectURL(screenshotStream); video.play(); } }, function(err) { console.error('takeScreenshot failed: ' + err.name + '; ' + err.message + '; ' + err.constraintName); } ); } // Copyright 2013 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. /** * Setup handlers for the minimize and close topbar buttons. */ function initializeHandlers() { // If this dialog is using system window controls, these elements aren't // needed at all. if (window.feedbackInfo.useSystemWindowFrame) { $('minimize-button').hidden = true; $('close-button').hidden = true; return; } $('minimize-button').addEventListener('click', function(e) { e.preventDefault(); chrome.app.window.current().minimize(); }); $('minimize-button').addEventListener('mousedown', function(e) { e.preventDefault(); }); $('close-button').addEventListener('click', function() { window.close(); }); $('close-button').addEventListener('mousedown', function(e) { e.preventDefault(); }); } window.addEventListener('DOMContentLoaded', initializeHandlers); /* Copyright 2013 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. */ html { height: 100%; } body { background-color: #fbfbfb; height: 100%; margin: 0; overflow: auto; padding: 0; width: 100%; } [hidden] { display: none !important; } .title-bar { -webkit-align-items: center; -webkit-app-region: drag; background-color: #fff; box-shadow: 0 1px #d0d0d0; color: rgb(80, 80, 82); display: -webkit-flex; font-size: 15px; height: 48px; } .title-bar #page-title { -webkit-flex: 1 1 auto; -webkit-margin-start: 20px; } .title-bar .button-bar { -webkit-flex: 0 1 auto; } .content { color: #646464; font-size: 12px; margin: 20px; } .content #description-text { border-color: #c8c8c8; box-sizing: border-box; height: 120px; line-height: 18px; padding: 10px; resize: none; width: 100%; } .content #additional-info-label { -webkit-margin-start: 10px; } .content .text-field-container { -webkit-align-items: center; -webkit-padding-start: 10px; display: -webkit-flex; height: 29px; margin-top: 10px; } .content .text-field-container > label { -webkit-flex: 0 1 auto; width: 100px; } .content .text-field-container > input[type=text] { -webkit-flex: 1 1 auto; -webkit-padding-start: 5px; border: 1px solid; border-color: #c8c8c8; color: #585858; height: 100%; } .content .text-field-container > input[type=checkbox] { margin-right: 9px; } .content .checkbox-field-container { -webkit-align-items: center; display: -webkit-flex; height: 29px; } #screenshot-container { margin-top: 10px; } .content #screenshot-image { -webkit-margin-start: 150px; display: block; height: 60px; margin-top: 40px; transition: all 250ms ease; } .content #screenshot-image:hover { -webkit-margin-start: 80px; height: 125px; margin-top: 80px; z-index: 1; } .content #screenshot-image.wide-screen { height: auto; width: 100px; } .content #screenshot-image.wide-screen:hover { height: auto; width: 200px; } .content #privacy-note { color: #969696; font-size: 10px; line-height: 15px; margin-bottom: 20px; margin-top: 20px; } .content .buttons-pane { bottom: 20px; display: -webkit-flex; justify-content: flex-end; left: 20px; right: 20px; } .content .top-buttons { position: absolute; } .content .remove-file-button { -webkit-margin-start: 5px; background-color: transparent; background-image: -webkit-image-set( url(chrome://resources/images/apps/button_butter_bar_close.png) 1x, url(chrome://resources/images/2x/apps/button_butter_bar_close.png) 2x); background-position: 50% 80%; background-repeat: no-repeat; border: none; height: 16px; pointer-events: auto; width: 16px; } .content .remove-file-button:hover { background-image: -webkit-image-set( url(chrome://resources/images/apps/button_butter_bar_close_hover.png) 1x, url(chrome://resources/images/2x/apps/button_butter_bar_close_hover.png) 2x); } .content .remove-file-button:active { background-image: -webkit-image-set( url(chrome://resources/images/apps/button_butter_bar_close_pressed.png) 1x, url(chrome://resources/images/2x/apps/button_butter_bar_close_pressed.png) 2x); } .content #attach-file-note { -webkit-margin-start: 112px; margin-bottom: 10px; margin-top: 10px; } .content .attach-file-notification { color: rgb(204, 0, 0); font-weight: bold; } button.white-button { -webkit-margin-end: 10px; color: #000; } button.blue-button { color: #fff; text-shadow: 1px sharp drop shadow rgb(45, 106, 218); } .srt-image { -webkit-margin-end: auto; -webkit-margin-start: 40px; display: block; height: 50px; margin-bottom: 20px; margin-top: 120px; } .srt-body { font-size: 14px; line-height: 24px; margin: 0 40px; } /* Copyright 2016 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. */ html, body { overflow: visible; } #detailsTable { margin-top: .5em; } #status { color: rgb(66, 133, 244); display: inline-block; margin: .5em .5em; }PNG  IHDR DcPLTEZZ\``baacǗbbdk0e tRNSMNIDATx^ 0DQQw)Dzm!,((++g ecz,3yAw 闖5io`hЪ/XTp tH(t͎y0@\}dg0Y>pOapS e\ ^*S%IENDB`PNG  IHDR@@PLTEZZ\[[]]]_ٺېڏÒ{{|nnpmmonnoݗzz{``b\\^||}oop__aooqM8ntRNS*'*+jT0IDATx^N@^轾k!,lٙ0{]F 56ZРN V@QQ? Ԁ6-|V_v(R#A5;ڜc@0M(EjJLے3`|k
// Copyright 2013 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. var webview; /** * Points the webview to the starting URL of a scope authorization * flow, and unhides the dialog once the page has loaded. * @param {string} url The url of the authorization entry point. * @param {Object} win The dialog window that contains this page. Can * be left undefined if the caller does not want to display the * window. */ function loadAuthUrlAndShowWindow(url, win) { // Send popups from the webview to a normal browser window. webview.addEventListener('newwindow', function(e) { e.window.discard(); window.open(e.targetUrl); }); // Request a customized view from GAIA. webview.request.onBeforeSendHeaders.addListener(function(details) { headers = details.requestHeaders || []; headers.push({'name': 'X-Browser-View', 'value': 'embedded'}); return { requestHeaders: headers }; }, { urls: ['https://accounts.google.com/*'], }, ['blocking', 'requestHeaders']); if (!url.toLowerCase().startsWith('https://accounts.google.com/')) document.querySelector('.titlebar').classList.add('titlebar-border'); webview.src = url; if (win) { webview.addEventListener('loadstop', function() { win.show(); }); } } document.addEventListener('DOMContentLoaded', function() { webview = document.querySelector('webview'); document.querySelector('.titlebar-close-button').onclick = function() { window.close(); }; chrome.resourcesPrivate.getStrings('identity', function(strings) { document.title = strings['window-title']; }); }); /* Copyright 2015 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. */ body { background-color: rgb(82, 86, 89); font-family: 'Roboto', 'Noto', sans-serif; margin: 0; } viewer-page-indicator { visibility: hidden; z-index: 2; } viewer-pdf-toolbar { position: fixed; width: 100%; z-index: 4; } #plugin { height: 100%; position: fixed; width: 100%; z-index: 1; } #sizer { position: absolute; z-index: 0; } @media(max-height: 250px) { viewer-pdf-toolbar { display: none; } } @media(max-height: 200px) { viewer-zoom-toolbar { display: none; } } @media(max-width: 300px) { viewer-zoom-toolbar { display: none; } }
// Copyright 2014 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. 'use strict'; /** * Global PDFViewer object, accessible for testing. * @type Object */ var viewer; (function() { /** * Stores any pending messages received which should be passed to the * PDFViewer when it is created. * @type Array */ var pendingMessages = []; /** * Handles events that are received prior to the PDFViewer being created. * @param {Object} message A message event received. */ function handleScriptingMessage(message) { pendingMessages.push(message); } /** * Initialize the global PDFViewer and pass any outstanding messages to it. * @param {Object} browserApi An object providing an API to the browser. */ function initViewer(browserApi) { // PDFViewer will handle any messages after it is created. window.removeEventListener('message', handleScriptingMessage, false); viewer = new PDFViewer(browserApi); while (pendingMessages.length > 0) viewer.handleScriptingMessage(pendingMessages.shift()); } /** * Entrypoint for starting the PDF viewer. This function obtains the browser * API for the PDF and constructs a PDFViewer object with it. */ function main() { // Set up an event listener to catch scripting messages which are sent prior // to the PDFViewer being created. window.addEventListener('message', handleScriptingMessage, false); createBrowserApi().then(initViewer); }; main(); })(); // Copyright 2013 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. 'use strict'; /** * @return {number} Width of a scrollbar in pixels */ function getScrollbarWidth() { var div = document.createElement('div'); div.style.visibility = 'hidden'; div.style.overflow = 'scroll'; div.style.width = '50px'; div.style.height = '50px'; div.style.position = 'absolute'; document.body.appendChild(div); var result = div.offsetWidth - div.clientWidth; div.parentNode.removeChild(div); return result; } /** * Return the filename component of a URL, percent decoded if possible. * @param {string} url The URL to get the filename from. * @return {string} The filename component. */ function getFilenameFromURL(url) { // Ignore the query and fragment. var mainUrl = url.split(/#|\?/)[0]; var components = mainUrl.split(/\/|\\/); var filename = components[components.length - 1]; try { return decodeURIComponent(filename); } catch (e) { if (e instanceof URIError) return filename; throw e; } } /** * Whether keydown events should currently be ignored. Events are ignored when * an editable element has focus, to allow for proper editing controls. * @param {HTMLElement} activeElement The currently selected DOM node. * @return {boolean} True if keydown events should be ignored. */ function shouldIgnoreKeyEvents(activeElement) { while (activeElement.shadowRoot != null && activeElement.shadowRoot.activeElement != null) { activeElement = activeElement.shadowRoot.activeElement; } return (activeElement.isContentEditable || activeElement.tagName == 'INPUT' || activeElement.tagName == 'TEXTAREA'); } /** * The minimum number of pixels to offset the toolbar by from the bottom and * right side of the screen. */ PDFViewer.MIN_TOOLBAR_OFFSET = 15; /** * The height of the toolbar along the top of the page. The document will be * shifted down by this much in the viewport. */ PDFViewer.MATERIAL_TOOLBAR_HEIGHT = 56; /** * Minimum height for the material toolbar to show (px). Should match the media * query in index-material.css. If the window is smaller than this at load, * leave no space for the toolbar. */ PDFViewer.TOOLBAR_WINDOW_MIN_HEIGHT = 250; /** * The light-gray background color used for print preview. */ PDFViewer.LIGHT_BACKGROUND_COLOR = '0xFFCCCCCC'; /** * The dark-gray background color used for the regular viewer. */ PDFViewer.DARK_BACKGROUND_COLOR = '0xFF525659'; /** * Creates a new PDFViewer. There should only be one of these objects per * document. * @constructor * @param {!BrowserApi} browserApi An object providing an API to the browser. */ function PDFViewer(browserApi) { this.browserApi_ = browserApi; this.originalUrl_ = this.browserApi_.getStreamInfo().originalUrl; this.loadState_ = LoadState.LOADING; this.parentWindow_ = null; this.parentOrigin_ = null; this.isFormFieldFocused_ = false; this.delayedScriptingMessages_ = []; this.isPrintPreview_ = location.origin === 'chrome://print'; // Parse open pdf parameters. this.paramsParser_ = new OpenPDFParamsParser(this.getNamedDestination_.bind(this)); var toolbarEnabled = this.paramsParser_.getUiUrlParams(this.originalUrl_).toolbar && !this.isPrintPreview_; // The sizer element is placed behind the plugin element to cause scrollbars // to be displayed in the window. It is sized according to the document size // of the pdf and zoom level. this.sizer_ = $('sizer'); if (this.isPrintPreview_) this.pageIndicator_ = $('page-indicator'); this.passwordScreen_ = $('password-screen'); this.passwordScreen_.addEventListener('password-submitted', this.onPasswordSubmitted_.bind(this)); this.errorScreen_ = $('error-screen'); // Can only reload if we are in a normal tab. if (chrome.tabs && this.browserApi_.getStreamInfo().tabId != -1) { this.errorScreen_.reloadFn = function() { chrome.tabs.reload(this.browserApi_.getStreamInfo().tabId); }.bind(this); } // Create the viewport. var shortWindow = window.innerHeight < PDFViewer.TOOLBAR_WINDOW_MIN_HEIGHT; var topToolbarHeight = (toolbarEnabled) ? PDFViewer.MATERIAL_TOOLBAR_HEIGHT : 0; var defaultZoom = this.browserApi_.getZoomBehavior() == BrowserApi.ZoomBehavior.MANAGE ? this.browserApi_.getDefaultZoom() : 1.0; this.viewport_ = new Viewport(window, this.sizer_, this.viewportChanged_.bind(this), this.beforeZoom_.bind(this), this.afterZoom_.bind(this), getScrollbarWidth(), defaultZoom, topToolbarHeight); // Create the plugin object dynamically so we can set its src. The plugin // element is sized to fill the entire window and is set to be fixed // positioning, acting as a viewport. The plugin renders into this viewport // according to the scroll position of the window. this.plugin_ = document.createElement('embed'); // NOTE: The plugin's 'id' field must be set to 'plugin' since // chrome/renderer/printing/print_web_view_helper.cc actually references it. this.plugin_.id = 'plugin'; this.plugin_.type = 'application/x-google-chrome-pdf'; this.plugin_.addEventListener('message', this.handlePluginMessage_.bind(this), false); // Handle scripting messages from outside the extension that wish to interact // with it. We also send a message indicating that extension has loaded and // is ready to receive messages. window.addEventListener('message', this.handleScriptingMessage.bind(this), false); this.plugin_.setAttribute('src', this.originalUrl_); this.plugin_.setAttribute('stream-url', this.browserApi_.getStreamInfo().streamUrl); var headers = ''; for (var header in this.browserApi_.getStreamInfo().responseHeaders) { headers += header + ': ' + this.browserApi_.getStreamInfo().responseHeaders[header] + '\n'; } this.plugin_.setAttribute('headers', headers); var backgroundColor = PDFViewer.DARK_BACKGROUND_COLOR; this.plugin_.setAttribute('background-color', backgroundColor); this.plugin_.setAttribute('top-toolbar-height', topToolbarHeight); if (this.browserApi_.getStreamInfo().embedded) { this.plugin_.setAttribute('top-level-url', this.browserApi_.getStreamInfo().tabUrl); } else { this.plugin_.setAttribute('full-frame', ''); } document.body.appendChild(this.plugin_); // Setup the button event listeners. this.zoomToolbar_ = $('zoom-toolbar'); this.zoomToolbar_.addEventListener('fit-to-width', this.viewport_.fitToWidth.bind(this.viewport_)); this.zoomToolbar_.addEventListener('fit-to-page', this.fitToPage_.bind(this)); this.zoomToolbar_.addEventListener('zoom-in', this.viewport_.zoomIn.bind(this.viewport_)); this.zoomToolbar_.addEventListener('zoom-out', this.viewport_.zoomOut.bind(this.viewport_)); this.gestureDetector_ = new GestureDetector(this.plugin_); this.gestureDetector_.addEventListener( 'pinchstart', this.viewport_.pinchZoomStart.bind(this.viewport_)); this.sentPinchEvent_ = false; this.gestureDetector_.addEventListener( 'pinchupdate', this.onPinchUpdate_.bind(this)); this.gestureDetector_.addEventListener( 'pinchend', this.onPinchEnd_.bind(this)); if (toolbarEnabled) { this.toolbar_ = $('toolbar'); this.toolbar_.hidden = false; this.toolbar_.addEventListener('save', this.save_.bind(this)); this.toolbar_.addEventListener('print', this.print_.bind(this)); this.toolbar_.addEventListener('rotate-right', this.rotateClockwise_.bind(this)); // Must attach to mouseup on the plugin element, since it eats mousedown // and click events. this.plugin_.addEventListener('mouseup', this.toolbar_.hideDropdowns.bind(this.toolbar_)); this.toolbar_.docTitle = getFilenameFromURL(this.originalUrl_); } document.body.addEventListener('change-page', function(e) { this.viewport_.goToPage(e.detail.page); }.bind(this)); document.body.addEventListener('navigate', function(e) { var disposition = e.detail.newtab ? Navigator.WindowOpenDisposition.NEW_BACKGROUND_TAB : Navigator.WindowOpenDisposition.CURRENT_TAB; this.navigator_.navigate(e.detail.uri, disposition); }.bind(this)); this.toolbarManager_ = new ToolbarManager(window, this.toolbar_, this.zoomToolbar_); // Set up the ZoomManager. this.zoomManager_ = ZoomManager.create( this.browserApi_.getZoomBehavior(), this.viewport_, this.browserApi_.setZoom.bind(this.browserApi_), this.browserApi_.getInitialZoom()); this.viewport_.zoomManager = this.zoomManager_; this.browserApi_.addZoomEventListener( this.zoomManager_.onBrowserZoomChange.bind(this.zoomManager_)); // Setup the keyboard event listener. document.addEventListener('keydown', this.handleKeyEvent_.bind(this)); document.addEventListener('mousemove', this.handleMouseEvent_.bind(this)); document.addEventListener('mouseout', this.handleMouseEvent_.bind(this)); var tabId = this.browserApi_.getStreamInfo().tabId; this.navigator_ = new Navigator( this.originalUrl_, this.viewport_, this.paramsParser_, new NavigatorDelegate(tabId)); this.viewportScroller_ = new ViewportScroller(this.viewport_, this.plugin_, window); // Request translated strings. chrome.resourcesPrivate.getStrings('pdf', this.handleStrings_.bind(this)); } PDFViewer.prototype = { /** * @private * Handle key events. These may come from the user directly or via the * scripting API. * @param {KeyboardEvent} e the event to handle. */ handleKeyEvent_: function(e) { var position = this.viewport_.position; // Certain scroll events may be sent from outside of the extension. var fromScriptingAPI = e.fromScriptingAPI; if (shouldIgnoreKeyEvents(document.activeElement) || e.defaultPrevented) return; this.toolbarManager_.hideToolbarsAfterTimeout(e); var pageUpHandler = function() { // Go to the previous page if we are fit-to-page. if (this.viewport_.fittingType == Viewport.FittingType.FIT_TO_PAGE) { this.viewport_.goToPage(this.viewport_.getMostVisiblePage() - 1); // Since we do the movement of the page. e.preventDefault(); } else if (fromScriptingAPI) { position.y -= this.viewport.size.height; this.viewport.position = position; } }.bind(this); var pageDownHandler = function() { // Go to the next page if we are fit-to-page. if (this.viewport_.fittingType == Viewport.FittingType.FIT_TO_PAGE) { this.viewport_.goToPage(this.viewport_.getMostVisiblePage() + 1); // Since we do the movement of the page. e.preventDefault(); } else if (fromScriptingAPI) { position.y += this.viewport.size.height; this.viewport.position = position; } }.bind(this); switch (e.keyCode) { case 9: // Tab key. this.toolbarManager_.showToolbarsForKeyboardNavigation(); return; case 27: // Escape key. if (!this.isPrintPreview_) { this.toolbarManager_.hideSingleToolbarLayer(); return; } break; // Ensure escape falls through to the print-preview handler. case 32: // Space key. if (e.shiftKey) pageUpHandler(); else pageDownHandler(); return; case 33: // Page up key. pageUpHandler(); return; case 34: // Page down key. pageDownHandler(); return; case 37: // Left arrow key. if (!hasKeyModifiers(e)) { // Go to the previous page if there are no horizontal scrollbars and // no form field is focused. if (!(this.viewport_.documentHasScrollbars().horizontal || this.isFormFieldFocused_)) { this.viewport_.goToPage(this.viewport_.getMostVisiblePage() - 1); // Since we do the movement of the page. e.preventDefault(); } else if (fromScriptingAPI) { position.x -= Viewport.SCROLL_INCREMENT; this.viewport.position = position; } } return; case 38: // Up arrow key. if (fromScriptingAPI) { position.y -= Viewport.SCROLL_INCREMENT; this.viewport.position = position; } return; case 39: // Right arrow key. if (!hasKeyModifiers(e)) { // Go to the next page if there are no horizontal scrollbars and no // form field is focused. if (!(this.viewport_.documentHasScrollbars().horizontal || this.isFormFieldFocused_)) { this.viewport_.goToPage(this.viewport_.getMostVisiblePage() + 1); // Since we do the movement of the page. e.preventDefault(); } else if (fromScriptingAPI) { position.x += Viewport.SCROLL_INCREMENT; this.viewport.position = position; } } return; case 40: // Down arrow key. if (fromScriptingAPI) { position.y += Viewport.SCROLL_INCREMENT; this.viewport.position = position; } return; case 65: // 'a' key. if (e.ctrlKey || e.metaKey) { this.plugin_.postMessage({ type: 'selectAll' }); // Since we do selection ourselves. e.preventDefault(); } return; case 71: // 'g' key. if (this.toolbar_ && (e.ctrlKey || e.metaKey) && e.altKey) { this.toolbarManager_.showToolbars(); this.toolbar_.selectPageNumber(); } return; case 219: // Left bracket key. if (e.ctrlKey) this.rotateCounterClockwise_(); return; case 220: // Backslash key. if (e.ctrlKey) this.zoomToolbar_.fitToggleFromHotKey(); return; case 221: // Right bracket key. if (e.ctrlKey) this.rotateClockwise_(); return; } // Give print preview a chance to handle the key event. if (!fromScriptingAPI && this.isPrintPreview_) { this.sendScriptingMessage_({ type: 'sendKeyEvent', keyEvent: SerializeKeyEvent(e) }); } else { // Show toolbars as a fallback. if (!(e.shiftKey || e.ctrlKey || e.altKey)) this.toolbarManager_.showToolbars(); } }, handleMouseEvent_: function(e) { if (e.type == 'mousemove') this.toolbarManager_.handleMouseMove(e); else if (e.type == 'mouseout') this.toolbarManager_.hideToolbarsForMouseOut(); }, /** * @private * Rotate the plugin clockwise. */ rotateClockwise_: function() { this.plugin_.postMessage({ type: 'rotateClockwise' }); }, /** * @private * Rotate the plugin counter-clockwise. */ rotateCounterClockwise_: function() { this.plugin_.postMessage({ type: 'rotateCounterclockwise' }); }, /** * @private * Set zoom to "fit to page". */ fitToPage_: function() { this.viewport_.fitToPage(); this.toolbarManager_.forceHideTopToolbar(); }, /** * @private * Notify the plugin to print. */ print_: function() { this.plugin_.postMessage({ type: 'print' }); }, /** * @private * Notify the plugin to save. */ save_: function() { this.plugin_.postMessage({ type: 'save' }); }, /** * Fetches the page number corresponding to the given named destination from * the plugin. * @param {string} name The namedDestination to fetch page number from plugin. */ getNamedDestination_: function(name) { this.plugin_.postMessage({ type: 'getNamedDestination', namedDestination: name }); }, /** * @private * Sends a 'documentLoaded' message to the PDFScriptingAPI if the document has * finished loading. */ sendDocumentLoadedMessage_: function() { if (this.loadState_ == LoadState.LOADING) return; this.sendScriptingMessage_({ type: 'documentLoaded', load_state: this.loadState_ }); }, /** * @private * Handle open pdf parameters. This function updates the viewport as per * the parameters mentioned in the url while opening pdf. The order is * important as later actions can override the effects of previous actions. * @param {Object} viewportPosition The initial position of the viewport to be * displayed. */ handleURLParams_: function(viewportPosition) { if (viewportPosition.page != undefined) this.viewport_.goToPage(viewportPosition.page); if (viewportPosition.position) { // Make sure we don't cancel effect of page parameter. this.viewport_.position = { x: this.viewport_.position.x + viewportPosition.position.x, y: this.viewport_.position.y + viewportPosition.position.y }; } if (viewportPosition.zoom) this.viewport_.setZoom(viewportPosition.zoom); }, /** * @private * Update the loading progress of the document in response to a progress * message being received from the plugin. * @param {number} progress the progress as a percentage. */ updateProgress_: function(progress) { if (this.toolbar_) this.toolbar_.loadProgress = progress; if (progress == -1) { // Document load failed. this.errorScreen_.show(); this.sizer_.style.display = 'none'; if (this.passwordScreen_.active) { this.passwordScreen_.deny(); this.passwordScreen_.active = false; } this.loadState_ = LoadState.FAILED; this.sendDocumentLoadedMessage_(); } else if (progress == 100) { // Document load complete. if (this.lastViewportPosition_) this.viewport_.position = this.lastViewportPosition_; this.paramsParser_.getViewportFromUrlParams( this.originalUrl_, this.handleURLParams_.bind(this)); this.loadState_ = LoadState.SUCCESS; this.sendDocumentLoadedMessage_(); while (this.delayedScriptingMessages_.length > 0) this.handleScriptingMessage(this.delayedScriptingMessages_.shift()); this.toolbarManager_.hideToolbarsAfterTimeout(); } }, /** * @private * Load a dictionary of translated strings into the UI. Used as a callback for * chrome.resourcesPrivate. * @param {Object} strings Dictionary of translated strings */ handleStrings_: function(strings) { document.documentElement.dir = strings.textdirection; document.documentElement.lang = strings.language; $('toolbar').strings = strings; $('zoom-toolbar').strings = strings; $('password-screen').strings = strings; $('error-screen').strings = strings; }, /** * @private * An event handler for handling password-submitted events. These are fired * when an event is entered into the password screen. * @param {Object} event a password-submitted event. */ onPasswordSubmitted_: function(event) { this.plugin_.postMessage({ type: 'getPasswordComplete', password: event.detail.password }); }, /** * @private * An event handler for handling message events received from the plugin. * @param {MessageObject} message a message event. */ handlePluginMessage_: function(message) { switch (message.data.type.toString()) { case 'documentDimensions': this.documentDimensions_ = message.data; this.viewport_.setDocumentDimensions(this.documentDimensions_); // If we received the document dimensions, the password was good so we // can dismiss the password screen. if (this.passwordScreen_.active) this.passwordScreen_.accept(); if (this.pageIndicator_) this.pageIndicator_.initialFadeIn(); if (this.toolbar_) { this.toolbar_.docLength = this.documentDimensions_.pageDimensions.length; } break; case 'email': var href = 'mailto:' + message.data.to + '?cc=' + message.data.cc + '&bcc=' + message.data.bcc + '&subject=' + message.data.subject + '&body=' + message.data.body; window.location.href = href; break; case 'getPassword': // If the password screen isn't up, put it up. Otherwise we're // responding to an incorrect password so deny it. if (!this.passwordScreen_.active) this.passwordScreen_.active = true; else this.passwordScreen_.deny(); break; case 'getSelectedTextReply': this.sendScriptingMessage_(message.data); break; case 'goToPage': this.viewport_.goToPage(message.data.page); break; case 'loadProgress': this.updateProgress_(message.data.progress); break; case 'navigate': // If in print preview, always open a new tab. if (this.isPrintPreview_) { this.navigator_.navigate( message.data.url, Navigator.WindowOpenDisposition.NEW_BACKGROUND_TAB); } else { this.navigator_.navigate(message.data.url, message.data.disposition); } break; case 'setScrollPosition': var position = this.viewport_.position; if (message.data.x !== undefined) position.x = message.data.x; if (message.data.y !== undefined) position.y = message.data.y; this.viewport_.position = position; break; case 'cancelStreamUrl': chrome.mimeHandlerPrivate.abortStream(); break; case 'metadata': if (message.data.title) { document.title = message.data.title; } else { document.title = getFilenameFromURL(this.originalUrl_); } this.bookmarks_ = message.data.bookmarks; if (this.toolbar_) { this.toolbar_.docTitle = document.title; this.toolbar_.bookmarks = this.bookmarks; } break; case 'setIsSelecting': this.viewportScroller_.setEnableScrolling(message.data.isSelecting); break; case 'getNamedDestinationReply': this.paramsParser_.onNamedDestinationReceived(message.data.pageNumber); break; case 'formFocusChange': this.isFormFieldFocused_ = message.data.focused; break; } }, /** * @private * A callback that's called before the zoom changes. Notify the plugin to stop * reacting to scroll events while zoom is taking place to avoid flickering. */ beforeZoom_: function() { this.plugin_.postMessage({ type: 'stopScrolling' }); if (this.viewport_.pinchPhase == Viewport.PinchPhase.PINCH_START) { var position = this.viewport_.position; var zoom = this.viewport_.zoom; var pinchPhase = this.viewport_.pinchPhase; this.plugin_.postMessage({ type: 'viewport', zoom: zoom, xOffset: position.x, yOffset: position.y, pinchPhase: pinchPhase }); } }, /** * @private * A callback that's called after the zoom changes. Notify the plugin of the * zoom change and to continue reacting to scroll events. */ afterZoom_: function() { var position = this.viewport_.position; var zoom = this.viewport_.zoom; var pinchVector = this.viewport_.pinchPanVector || {x: 0, y: 0}; var pinchCenter = this.viewport_.pinchCenter || {x: 0, y: 0}; var pinchPhase = this.viewport_.pinchPhase; this.plugin_.postMessage({ type: 'viewport', zoom: zoom, xOffset: position.x, yOffset: position.y, pinchPhase: pinchPhase, pinchX: pinchCenter.x, pinchY: pinchCenter.y, pinchVectorX: pinchVector.x, pinchVectorY: pinchVector.y }); this.zoomManager_.onPdfZoomChange(); }, /** * @private * A callback that's called when an update to a pinch zoom is detected. * @param {!Object} e the pinch event. */ onPinchUpdate_: function(e) { // Throttle number of pinch events to one per frame. if (!this.sentPinchEvent_) { this.sentPinchEvent_ = true; window.requestAnimationFrame(function() { this.sentPinchEvent_ = false; this.viewport_.pinchZoom(e); }.bind(this)); } }, /** * @private * A callback that's called when the end of a pinch zoom is detected. * @param {!Object} e the pinch event. */ onPinchEnd_: function(e) { // Using rAF for pinch end prevents pinch updates scheduled by rAF getting // sent after the pinch end. window.requestAnimationFrame(function() { this.viewport_.pinchZoomEnd(e); }.bind(this)); }, /** * @private * A callback that's called after the viewport changes. */ viewportChanged_: function() { if (!this.documentDimensions_) return; // Offset the toolbar position so that it doesn't move if scrollbars appear. var hasScrollbars = this.viewport_.documentHasScrollbars(); var scrollbarWidth = this.viewport_.scrollbarWidth; var verticalScrollbarWidth = hasScrollbars.vertical ? scrollbarWidth : 0; var horizontalScrollbarWidth = hasScrollbars.horizontal ? scrollbarWidth : 0; // Shift the zoom toolbar to the left by half a scrollbar width. This // gives a compromise: if there is no scrollbar visible then the toolbar // will be half a scrollbar width further left than the spec but if there // is a scrollbar visible it will be half a scrollbar width further right // than the spec. In RTL layout, the zoom toolbar is on the left side, but // the scrollbar is still on the right, so this is not necessary. if (!isRTL()) { this.zoomToolbar_.style.right = -verticalScrollbarWidth + (scrollbarWidth / 2) + 'px'; } // Having a horizontal scrollbar is much rarer so we don't offset the // toolbar from the bottom any more than what the spec says. This means // that when there is a scrollbar visible, it will be a full scrollbar // width closer to the bottom of the screen than usual, but this is ok. this.zoomToolbar_.style.bottom = -horizontalScrollbarWidth + 'px'; // Update the page indicator. var visiblePage = this.viewport_.getMostVisiblePage(); if (this.toolbar_) this.toolbar_.pageNo = visiblePage + 1; // TODO(raymes): Give pageIndicator_ the same API as toolbar_. if (this.pageIndicator_) { this.pageIndicator_.index = visiblePage; if (this.documentDimensions_.pageDimensions.length > 1 && hasScrollbars.vertical) { this.pageIndicator_.style.visibility = 'visible'; } else { this.pageIndicator_.style.visibility = 'hidden'; } } var visiblePageDimensions = this.viewport_.getPageScreenRect(visiblePage); var size = this.viewport_.size; this.sendScriptingMessage_({ type: 'viewport', pageX: visiblePageDimensions.x, pageY: visiblePageDimensions.y, pageWidth: visiblePageDimensions.width, viewportWidth: size.width, viewportHeight: size.height }); }, /** * Handle a scripting message from outside the extension (typically sent by * PDFScriptingAPI in a page containing the extension) to interact with the * plugin. * @param {MessageObject} message the message to handle. */ handleScriptingMessage: function(message) { if (this.parentWindow_ != message.source) { this.parentWindow_ = message.source; this.parentOrigin_ = message.origin; // Ensure that we notify the embedder if the document is loaded. if (this.loadState_ != LoadState.LOADING) this.sendDocumentLoadedMessage_(); } if (this.handlePrintPreviewScriptingMessage_(message)) return; // Delay scripting messages from users of the scripting API until the // document is loaded. This simplifies use of the APIs. if (this.loadState_ != LoadState.SUCCESS) { this.delayedScriptingMessages_.push(message); return; } switch (message.data.type.toString()) { case 'getSelectedText': case 'print': case 'selectAll': this.plugin_.postMessage(message.data); break; } }, /** * @private * Handle scripting messages specific to print preview. * @param {MessageObject} message the message to handle. * @return {boolean} true if the message was handled, false otherwise. */ handlePrintPreviewScriptingMessage_: function(message) { if (!this.isPrintPreview_) return false; switch (message.data.type.toString()) { case 'loadPreviewPage': this.plugin_.postMessage(message.data); return true; case 'resetPrintPreviewMode': this.loadState_ = LoadState.LOADING; if (!this.inPrintPreviewMode_) { this.inPrintPreviewMode_ = true; this.viewport_.fitToPage(); } // Stash the scroll location so that it can be restored when the new // document is loaded. this.lastViewportPosition_ = this.viewport_.position; // TODO(raymes): Disable these properly in the plugin. var printButton = $('print-button'); if (printButton) printButton.parentNode.removeChild(printButton); var saveButton = $('save-button'); if (saveButton) saveButton.parentNode.removeChild(saveButton); this.pageIndicator_.pageLabels = message.data.pageNumbers; this.plugin_.postMessage({ type: 'resetPrintPreviewMode', url: message.data.url, grayscale: message.data.grayscale, // If the PDF isn't modifiable we send 0 as the page count so that no // blank placeholder pages get appended to the PDF. pageCount: (message.data.modifiable ? message.data.pageNumbers.length : 0) }); return true; case 'sendKeyEvent': this.handleKeyEvent_(DeserializeKeyEvent(message.data.keyEvent)); return true; } return false; }, /** * @private * Send a scripting message outside the extension (typically to * PDFScriptingAPI in a page containing the extension). * @param {Object} message the message to send. */ sendScriptingMessage_: function(message) { if (this.parentWindow_ && this.parentOrigin_) { var targetOrigin; // Only send data back to the embedder if it is from the same origin, // unless we're sending it to ourselves (which could happen in the case // of tests). We also allow documentLoaded messages through as this won't // leak important information. if (this.parentOrigin_ == window.location.origin) targetOrigin = this.parentOrigin_; else if (message.type == 'documentLoaded') targetOrigin = '*'; else targetOrigin = this.originalUrl_; this.parentWindow_.postMessage(message, targetOrigin); } }, /** * @type {Viewport} the viewport of the PDF viewer. */ get viewport() { return this.viewport_; }, /** * Each bookmark is an Object containing a: * - title * - page (optional) * - array of children (themselves bookmarks) * @type {Array} the top-level bookmarks of the PDF. */ get bookmarks() { return this.bookmarks_; } }; // Copyright 2015 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. 'use strict'; /** Idle time in ms before the UI is hidden. */ var HIDE_TIMEOUT = 2000; /** Time in ms after force hide before toolbar is shown again. */ var FORCE_HIDE_TIMEOUT = 1000; /** * Velocity required in a mousemove to reveal the UI (pixels/ms). This is * intended to be high enough that a fast flick of the mouse is required to * reach it. */ var SHOW_VELOCITY = 10; /** Distance from the top of the screen required to reveal the toolbars. */ var TOP_TOOLBAR_REVEAL_DISTANCE = 100; /** Distance from the bottom-right of the screen required to reveal toolbars. */ var SIDE_TOOLBAR_REVEAL_DISTANCE_RIGHT = 150; var SIDE_TOOLBAR_REVEAL_DISTANCE_BOTTOM = 250; /** * @param {MouseEvent} e Event to test. * @return {boolean} True if the mouse is close to the top of the screen. */ function isMouseNearTopToolbar(e) { return e.y < TOP_TOOLBAR_REVEAL_DISTANCE; } /** * @param {MouseEvent} e Event to test. * @param {Window} window Window to test against. * @return {boolean} True if the mouse is close to the bottom-right of the * screen. */ function isMouseNearSideToolbar(e, window) { var atSide = e.x > window.innerWidth - SIDE_TOOLBAR_REVEAL_DISTANCE_RIGHT; if (isRTL()) atSide = e.x < SIDE_TOOLBAR_REVEAL_DISTANCE_RIGHT; var atBottom = e.y > window.innerHeight - SIDE_TOOLBAR_REVEAL_DISTANCE_BOTTOM; return atSide && atBottom; } /** * Constructs a Toolbar Manager, responsible for co-ordinating between multiple * toolbar elements. * @constructor * @param {Object} window The window containing the UI. * @param {Object} toolbar The top toolbar element. * @param {Object} zoomToolbar The zoom toolbar element. */ function ToolbarManager(window, toolbar, zoomToolbar) { this.window_ = window; this.toolbar_ = toolbar; this.zoomToolbar_ = zoomToolbar; this.toolbarTimeout_ = null; this.isMouseNearTopToolbar_ = false; this.isMouseNearSideToolbar_ = false; this.sideToolbarAllowedOnly_ = false; this.sideToolbarAllowedOnlyTimer_ = null; this.keyboardNavigationActive = false; this.lastMovementTimestamp = null; this.window_.addEventListener('resize', this.resizeDropdowns_.bind(this)); this.resizeDropdowns_(); } ToolbarManager.prototype = { handleMouseMove: function(e) { this.isMouseNearTopToolbar_ = this.toolbar_ && isMouseNearTopToolbar(e); this.isMouseNearSideToolbar_ = isMouseNearSideToolbar(e, this.window_); this.keyboardNavigationActive = false; var touchInteractionActive = (e.sourceCapabilities && e.sourceCapabilities.firesTouchEvents); // Allow the top toolbar to be shown if the mouse moves away from the side // toolbar (as long as the timeout has elapsed). if (!this.isMouseNearSideToolbar_ && !this.sideToolbarAllowedOnlyTimer_) this.sideToolbarAllowedOnly_ = false; // Allow the top toolbar to be shown if the mouse moves to the top edge. if (this.isMouseNearTopToolbar_) this.sideToolbarAllowedOnly_ = false; // Tapping the screen with toolbars open tries to close them. if (touchInteractionActive && this.zoomToolbar_.isVisible()) { this.hideToolbarsIfAllowed(); return; } // Show the toolbars if the mouse is near the top or bottom-right of the // screen, if the mouse moved fast, or if the touchscreen was tapped. if (this.isMouseNearTopToolbar_ || this.isMouseNearSideToolbar_ || this.isHighVelocityMouseMove_(e) || touchInteractionActive) { if (this.sideToolbarAllowedOnly_) this.zoomToolbar_.show(); else this.showToolbars(); } this.hideToolbarsAfterTimeout(); }, /** * Whether a mousemove event is high enough velocity to reveal the toolbars. * @param {MouseEvent} e Event to test. * @return {boolean} true if the event is a high velocity mousemove, false * otherwise. * @private */ isHighVelocityMouseMove_: function(e) { if (e.type == 'mousemove') { if (this.lastMovementTimestamp == null) { this.lastMovementTimestamp = this.getCurrentTimestamp_(); } else { var movement = Math.sqrt(e.movementX * e.movementX + e.movementY * e.movementY); var newTime = this.getCurrentTimestamp_(); var interval = newTime - this.lastMovementTimestamp; this.lastMovementTimestamp = newTime; if (interval != 0) return movement / interval > SHOW_VELOCITY; } } return false; }, /** * Wrapper around Date.now() to make it easily replaceable for testing. * @return {int} * @private */ getCurrentTimestamp_: function() { return Date.now(); }, /** * Display both UI toolbars. */ showToolbars: function() { if (this.toolbar_) this.toolbar_.show(); this.zoomToolbar_.show(); }, /** * Show toolbars and mark that navigation is being performed with * tab/shift-tab. This disables toolbar hiding until the mouse is moved or * escape is pressed. */ showToolbarsForKeyboardNavigation: function() { this.keyboardNavigationActive = true; this.showToolbars(); }, /** * Hide toolbars after a delay, regardless of the position of the mouse. * Intended to be called when the mouse has moved out of the parent window. */ hideToolbarsForMouseOut: function() { this.isMouseNearTopToolbar_ = false; this.isMouseNearSideToolbar_ = false; this.hideToolbarsAfterTimeout(); }, /** * Check if the toolbars are able to be closed, and close them if they are. * Toolbars may be kept open based on mouse/keyboard activity and active * elements. */ hideToolbarsIfAllowed: function() { if (this.isMouseNearSideToolbar_ || this.isMouseNearTopToolbar_) return; if (this.toolbar_ && this.toolbar_.shouldKeepOpen()) return; if (this.keyboardNavigationActive) return; // Remove focus to make any visible tooltips disappear -- otherwise they'll // still be visible on screen when the toolbar is off screen. if ((this.toolbar_ && document.activeElement == this.toolbar_) || document.activeElement == this.zoomToolbar_) { document.activeElement.blur(); } if (this.toolbar_) this.toolbar_.hide(); this.zoomToolbar_.hide(); }, /** * Hide the toolbar after the HIDE_TIMEOUT has elapsed. */ hideToolbarsAfterTimeout: function() { if (this.toolbarTimeout_) this.window_.clearTimeout(this.toolbarTimeout_); this.toolbarTimeout_ = this.window_.setTimeout( this.hideToolbarsIfAllowed.bind(this), HIDE_TIMEOUT); }, /** * Hide the 'topmost' layer of toolbars. Hides any dropdowns that are open, or * hides the basic toolbars otherwise. */ hideSingleToolbarLayer: function() { if (!this.toolbar_ || !this.toolbar_.hideDropdowns()) { this.keyboardNavigationActive = false; this.hideToolbarsIfAllowed(); } }, /** * Hide the top toolbar and keep it hidden until both: * - The mouse is moved away from the right side of the screen * - 1 second has passed. * * The top toolbar can be immediately re-opened by moving the mouse to the top * of the screen. */ forceHideTopToolbar: function() { if (!this.toolbar_) return; this.toolbar_.hide(); this.sideToolbarAllowedOnly_ = true; this.sideToolbarAllowedOnlyTimer_ = this.window_.setTimeout(function() { this.sideToolbarAllowedOnlyTimer_ = null; }.bind(this), FORCE_HIDE_TIMEOUT); }, /** * Updates the size of toolbar dropdowns based on the positions of the rest of * the UI. * @private */ resizeDropdowns_: function() { if (!this.toolbar_) return; var lowerBound = this.window_.innerHeight - this.zoomToolbar_.clientHeight; this.toolbar_.setDropdownLowerBound(lowerBound); } }; // Copyright 2014 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. /** * Returns the height of the intersection of two rectangles. * @param {Object} rect1 the first rect * @param {Object} rect2 the second rect * @return {number} the height of the intersection of the rects */ function getIntersectionHeight(rect1, rect2) { return Math.max(0, Math.min(rect1.y + rect1.height, rect2.y + rect2.height) - Math.max(rect1.y, rect2.y)); } /** * Makes sure that the scale level doesn't get out of the limits. * @param {number} scale The new scale level. * @return {number} The scale clamped within the limits. */ function clampScale(scale) { return Math.min(5, Math.max(0.25, scale)); } /** * Computes vector between two points. * @param {!Object} p1 The first point. * @param {!Object} p2 The second point. * @return {!Object} The vector. */ function vectorDelta(p1, p2) { return { x: p2.x - p1.x, y: p2.y - p1.y }; } function frameToPluginCoordinate(coordinateInFrame) { var container = $('plugin'); return { x: coordinateInFrame.x - container.getBoundingClientRect().left, y: coordinateInFrame.y - container.getBoundingClientRect().top }; } /** * Create a new viewport. * @constructor * @param {Window} window the window * @param {Object} sizer is the element which represents the size of the * document in the viewport * @param {Function} viewportChangedCallback is run when the viewport changes * @param {Function} beforeZoomCallback is run before a change in zoom * @param {Function} afterZoomCallback is run after a change in zoom * @param {number} scrollbarWidth the width of scrollbars on the page * @param {number} defaultZoom The default zoom level. * @param {number} topToolbarHeight The number of pixels that should initially * be left blank above the document for the toolbar. */ function Viewport(window, sizer, viewportChangedCallback, beforeZoomCallback, afterZoomCallback, scrollbarWidth, defaultZoom, topToolbarHeight) { this.window_ = window; this.sizer_ = sizer; this.viewportChangedCallback_ = viewportChangedCallback; this.beforeZoomCallback_ = beforeZoomCallback; this.afterZoomCallback_ = afterZoomCallback; this.allowedToChangeZoom_ = false; this.internalZoom_ = 1; this.zoomManager_ = new InactiveZoomManager(this, 1); this.documentDimensions_ = null; this.pageDimensions_ = []; this.scrollbarWidth_ = scrollbarWidth; this.fittingType_ = Viewport.FittingType.NONE; this.defaultZoom_ = defaultZoom; this.topToolbarHeight_ = topToolbarHeight; this.prevScale_ = 1; this.pinchPhase_ = Viewport.PinchPhase.PINCH_NONE; this.pinchPanVector_ = null; this.pinchCenter_ = null; this.firstPinchCenterInFrame_ = null; window.addEventListener('scroll', this.updateViewport_.bind(this)); window.addEventListener('resize', this.resize_.bind(this)); } /** * Enumeration of page fitting types. * @enum {string} */ Viewport.FittingType = { NONE: 'none', FIT_TO_PAGE: 'fit-to-page', FIT_TO_WIDTH: 'fit-to-width' }; /** * Enumeration of pinch states. * This should match PinchPhase enum in pdf/out_of_process_instance.h * @enum {number} */ Viewport.PinchPhase = { PINCH_NONE: 0, PINCH_START: 1, PINCH_UPDATE_ZOOM_OUT: 2, PINCH_UPDATE_ZOOM_IN: 3, PINCH_END: 4 }; /** * The increment to scroll a page by in pixels when up/down/left/right arrow * keys are pressed. Usually we just let the browser handle scrolling on the * window when these keys are pressed but in certain cases we need to simulate * these events. */ Viewport.SCROLL_INCREMENT = 40; /** * Predefined zoom factors to be used when zooming in/out. These are in * ascending order. This should match the lists in * components/ui/zoom/page_zoom_constants.h and * chrome/browser/resources/settings/appearance_page/appearance_page.js */ Viewport.ZOOM_FACTORS = [0.25, 1 / 3, 0.5, 2 / 3, 0.75, 0.8, 0.9, 1, 1.1, 1.25, 1.5, 1.75, 2, 2.5, 3, 4, 5]; /** * The minimum and maximum range to be used to clip zoom factor. */ Viewport.ZOOM_FACTOR_RANGE = { min: Viewport.ZOOM_FACTORS[0], max: Viewport.ZOOM_FACTORS[Viewport.ZOOM_FACTORS.length - 1] }; /** * The width of the page shadow around pages in pixels. */ Viewport.PAGE_SHADOW = {top: 3, bottom: 7, left: 5, right: 5}; Viewport.prototype = { /** * Returns the zoomed and rounded document dimensions for the given zoom. * Rounding is necessary when interacting with the renderer which tends to * operate in integral values (for example for determining if scrollbars * should be shown). * @param {number} zoom The zoom to use to compute the scaled dimensions. * @return {Object} A dictionary with scaled 'width'/'height' of the document. * @private */ getZoomedDocumentDimensions_: function(zoom) { if (!this.documentDimensions_) return null; return { width: Math.round(this.documentDimensions_.width * zoom), height: Math.round(this.documentDimensions_.height * zoom) }; }, /** * @private * Returns true if the document needs scrollbars at the given zoom level. * @param {number} zoom compute whether scrollbars are needed at this zoom * @return {Object} with 'horizontal' and 'vertical' keys which map to bool * values indicating if the horizontal and vertical scrollbars are needed * respectively. */ documentNeedsScrollbars_: function(zoom) { var zoomedDimensions = this.getZoomedDocumentDimensions_(zoom); if (!zoomedDimensions) { return { horizontal: false, vertical: false }; } // If scrollbars are required for one direction, expand the document in the // other direction to take the width of the scrollbars into account when // deciding whether the other direction needs scrollbars. if (zoomedDimensions.width > this.window_.innerWidth) zoomedDimensions.height += this.scrollbarWidth_; else if (zoomedDimensions.height > this.window_.innerHeight) zoomedDimensions.width += this.scrollbarWidth_; return { horizontal: zoomedDimensions.width > this.window_.innerWidth, vertical: zoomedDimensions.height + this.topToolbarHeight_ > this.window_.innerHeight }; }, /** * Returns true if the document needs scrollbars at the current zoom level. * @return {Object} with 'x' and 'y' keys which map to bool values * indicating if the horizontal and vertical scrollbars are needed * respectively. */ documentHasScrollbars: function() { return this.documentNeedsScrollbars_(this.zoom); }, /** * @private * Helper function called when the zoomed document size changes. */ contentSizeChanged_: function() { var zoomedDimensions = this.getZoomedDocumentDimensions_(this.zoom); if (zoomedDimensions) { this.sizer_.style.width = zoomedDimensions.width + 'px'; this.sizer_.style.height = zoomedDimensions.height + this.topToolbarHeight_ + 'px'; } }, /** * @private * Called when the viewport should be updated. */ updateViewport_: function() { this.viewportChangedCallback_(); }, /** * @private * Called when the viewport size changes. */ resize_: function() { if (this.fittingType_ == Viewport.FittingType.FIT_TO_PAGE) this.fitToPageInternal_(false); else if (this.fittingType_ == Viewport.FittingType.FIT_TO_WIDTH) this.fitToWidth(); else this.updateViewport_(); }, /** * @type {Object} the scroll position of the viewport. */ get position() { return { x: this.window_.pageXOffset, y: this.window_.pageYOffset - this.topToolbarHeight_ }; }, /** * Scroll the viewport to the specified position. * @type {Object} position the position to scroll to. */ set position(position) { this.window_.scrollTo(position.x, position.y + this.topToolbarHeight_); }, /** * @type {Object} the size of the viewport excluding scrollbars. */ get size() { var needsScrollbars = this.documentNeedsScrollbars_(this.zoom); var scrollbarWidth = needsScrollbars.vertical ? this.scrollbarWidth_ : 0; var scrollbarHeight = needsScrollbars.horizontal ? this.scrollbarWidth_ : 0; return { width: this.window_.innerWidth - scrollbarWidth, height: this.window_.innerHeight - scrollbarHeight }; }, /** * @type {number} the zoom level of the viewport. */ get zoom() { return this.zoomManager_.applyBrowserZoom(this.internalZoom_); }, /** * Set the zoom manager. * @type {ZoomManager} manager the zoom manager to set. */ set zoomManager(manager) { this.zoomManager_ = manager; }, /** * @type {Viewport.PinchPhase} The phase of the current pinch gesture for * the viewport. */ get pinchPhase() { return this.pinchPhase_; }, /** * @type {Object} The panning caused by the current pinch gesture (as * the deltas of the x and y coordinates). */ get pinchPanVector() { return this.pinchPanVector_; }, /** * @type {Object} The coordinates of the center of the current pinch gesture. */ get pinchCenter() { return this.pinchCenter_; }, /** * @private * Used to wrap a function that might perform zooming on the viewport. This is * required so that we can notify the plugin that zooming is in progress * so that while zooming is taking place it can stop reacting to scroll events * from the viewport. This is to avoid flickering. */ mightZoom_: function(f) { this.beforeZoomCallback_(); this.allowedToChangeZoom_ = true; f(); this.allowedToChangeZoom_ = false; this.afterZoomCallback_(); }, /** * @private * Sets the zoom of the viewport. * @param {number} newZoom the zoom level to zoom to. */ setZoomInternal_: function(newZoom) { if (!this.allowedToChangeZoom_) { throw 'Called Viewport.setZoomInternal_ without calling ' + 'Viewport.mightZoom_.'; } // Record the scroll position (relative to the top-left of the window). var currentScrollPos = { x: this.position.x / this.zoom, y: this.position.y / this.zoom }; this.internalZoom_ = newZoom; this.contentSizeChanged_(); // Scroll to the scaled scroll position. this.position = { x: currentScrollPos.x * this.zoom, y: currentScrollPos.y * this.zoom }; }, /** * @private * Sets the zoom of the viewport. * Same as setZoomInternal_ but for pinch zoom we have some more operations. * @param {number} scaleDelta The zoom delta. * @param {!Object} center The pinch center in content coordinates. */ setPinchZoomInternal_: function(scaleDelta, center) { assert(this.allowedToChangeZoom_, 'Called Viewport.setPinchZoomInternal_ without calling ' + 'Viewport.mightZoom_.'); this.internalZoom_ = clampScale(this.internalZoom_ * scaleDelta); var newCenterInContent = this.frameToContent(center); var delta = { x: (newCenterInContent.x - this.oldCenterInContent.x), y: (newCenterInContent.y - this.oldCenterInContent.y) }; // Record the scroll position (relative to the pinch center). var currentScrollPos = { x: this.position.x - delta.x * this.zoom, y: this.position.y - delta.y * this.zoom }; this.contentSizeChanged_(); // Scroll to the scaled scroll position. this.position = { x: currentScrollPos.x, y: currentScrollPos.y }; }, /** * @private * Converts a point from frame to content coordinates. * @param {!Object} framePoint The frame coordinates. * @return {!Object} The content coordinates. */ frameToContent: function(framePoint) { // TODO(mcnee) Add a helper Point class to avoid duplicating operations // on plain {x,y} objects. return { x: (framePoint.x + this.position.x) / this.zoom, y: (framePoint.y + this.position.y) / this.zoom }; }, /** * Sets the zoom to the given zoom level. * @param {number} newZoom the zoom level to zoom to. */ setZoom: function(newZoom) { this.fittingType_ = Viewport.FittingType.NONE; newZoom = Math.max(Viewport.ZOOM_FACTOR_RANGE.min, Math.min(newZoom, Viewport.ZOOM_FACTOR_RANGE.max)); this.mightZoom_(function() { this.setZoomInternal_(newZoom); this.updateViewport_(); }.bind(this)); }, /** * Gets notified of the browser zoom changing seperately from the * internal zoom. * @param {number} oldBrowserZoom the previous value of the browser zoom. */ updateZoomFromBrowserChange: function(oldBrowserZoom) { this.mightZoom_(function() { // Record the scroll position (relative to the top-left of the window). var oldZoom = oldBrowserZoom * this.internalZoom_; var currentScrollPos = { x: this.position.x / oldZoom, y: this.position.y / oldZoom }; this.contentSizeChanged_(); // Scroll to the scaled scroll position. this.position = { x: currentScrollPos.x * this.zoom, y: currentScrollPos.y * this.zoom }; this.updateViewport_(); }.bind(this)); }, /** * @type {number} the width of scrollbars in the viewport in pixels. */ get scrollbarWidth() { return this.scrollbarWidth_; }, /** * @type {Viewport.FittingType} the fitting type the viewport is currently in. */ get fittingType() { return this.fittingType_; }, /** * @private * @param {integer} y the y-coordinate to get the page at. * @return {integer} the index of a page overlapping the given y-coordinate. */ getPageAtY_: function(y) { var min = 0; var max = this.pageDimensions_.length - 1; while (max >= min) { var page = Math.floor(min + ((max - min) / 2)); // There might be a gap between the pages, in which case use the bottom // of the previous page as the top for finding the page. var top = 0; if (page > 0) { top = this.pageDimensions_[page - 1].y + this.pageDimensions_[page - 1].height; } var bottom = this.pageDimensions_[page].y + this.pageDimensions_[page].height; if (top <= y && bottom > y) return page; else if (top > y) max = page - 1; else min = page + 1; } return 0; }, /** * Returns the page with the greatest proportion of its height in the current * viewport. * @return {int} the index of the most visible page. */ getMostVisiblePage: function() { var firstVisiblePage = this.getPageAtY_(this.position.y / this.zoom); if (firstVisiblePage == this.pageDimensions_.length - 1) return firstVisiblePage; var viewportRect = { x: this.position.x / this.zoom, y: this.position.y / this.zoom, width: this.size.width / this.zoom, height: this.size.height / this.zoom }; var firstVisiblePageVisibility = getIntersectionHeight( this.pageDimensions_[firstVisiblePage], viewportRect) / this.pageDimensions_[firstVisiblePage].height; var nextPageVisibility = getIntersectionHeight( this.pageDimensions_[firstVisiblePage + 1], viewportRect) / this.pageDimensions_[firstVisiblePage + 1].height; if (nextPageVisibility > firstVisiblePageVisibility) return firstVisiblePage + 1; return firstVisiblePage; }, /** * @private * Compute the zoom level for fit-to-page or fit-to-width. |pageDimensions| is * the dimensions for a given page and if |widthOnly| is true, it indicates * that fit-to-page zoom should be computed rather than fit-to-page. * @param {Object} pageDimensions the dimensions of a given page * @param {boolean} widthOnly a bool indicating whether fit-to-page or * fit-to-width should be computed. * @return {number} the internal zoom to set */ computeFittingZoom_: function(pageDimensions, widthOnly) { // First compute the zoom without scrollbars. var zoomWidth = this.window_.innerWidth / pageDimensions.width; var zoom; var zoomHeight; if (widthOnly) { zoom = zoomWidth; } else { zoomHeight = this.window_.innerHeight / pageDimensions.height; zoom = Math.min(zoomWidth, zoomHeight); } // Check if there needs to be any scrollbars. var needsScrollbars = this.documentNeedsScrollbars_(zoom); // If the document fits, just return the zoom. if (!needsScrollbars.horizontal && !needsScrollbars.vertical) return zoom; var zoomedDimensions = this.getZoomedDocumentDimensions_(zoom); // Check if adding a scrollbar will result in needing the other scrollbar. var scrollbarWidth = this.scrollbarWidth_; if (needsScrollbars.horizontal && zoomedDimensions.height > this.window_.innerHeight - scrollbarWidth) { needsScrollbars.vertical = true; } if (needsScrollbars.vertical && zoomedDimensions.width > this.window_.innerWidth - scrollbarWidth) { needsScrollbars.horizontal = true; } // Compute available window space. var windowWithScrollbars = { width: this.window_.innerWidth, height: this.window_.innerHeight }; if (needsScrollbars.horizontal) windowWithScrollbars.height -= scrollbarWidth; if (needsScrollbars.vertical) windowWithScrollbars.width -= scrollbarWidth; // Recompute the zoom. zoomWidth = windowWithScrollbars.width / pageDimensions.width; if (widthOnly) { zoom = zoomWidth; } else { zoomHeight = windowWithScrollbars.height / pageDimensions.height; zoom = Math.min(zoomWidth, zoomHeight); } return this.zoomManager_.internalZoomComponent(zoom); }, /** * Zoom the viewport so that the page-width consumes the entire viewport. */ fitToWidth: function() { this.mightZoom_(function() { this.fittingType_ = Viewport.FittingType.FIT_TO_WIDTH; if (!this.documentDimensions_) return; // When computing fit-to-width, the maximum width of a page in the // document is used, which is equal to the size of the document width. this.setZoomInternal_(this.computeFittingZoom_(this.documentDimensions_, true)); var page = this.getMostVisiblePage(); this.updateViewport_(); }.bind(this)); }, /** * @private * Zoom the viewport so that a page consumes the entire viewport. * @param {boolean} scrollToTopOfPage Set to true if the viewport should be * scrolled to the top of the current page. Set to false if the viewport * should remain at the current scroll position. */ fitToPageInternal_: function(scrollToTopOfPage) { this.mightZoom_(function() { this.fittingType_ = Viewport.FittingType.FIT_TO_PAGE; if (!this.documentDimensions_) return; var page = this.getMostVisiblePage(); // Fit to the current page's height and the widest page's width. var dimensions = { width: this.documentDimensions_.width, height: this.pageDimensions_[page].height, }; this.setZoomInternal_(this.computeFittingZoom_(dimensions, false)); if (scrollToTopOfPage) { this.position = { x: 0, y: this.pageDimensions_[page].y * this.zoom }; } this.updateViewport_(); }.bind(this)); }, /** * Zoom the viewport so that a page consumes the entire viewport. Also scrolls * the viewport to the top of the current page. */ fitToPage: function() { this.fitToPageInternal_(true); }, /** * Zoom out to the next predefined zoom level. */ zoomOut: function() { this.mightZoom_(function() { this.fittingType_ = Viewport.FittingType.NONE; var nextZoom = Viewport.ZOOM_FACTORS[0]; for (var i = 0; i < Viewport.ZOOM_FACTORS.length; i++) { if (Viewport.ZOOM_FACTORS[i] < this.internalZoom_) nextZoom = Viewport.ZOOM_FACTORS[i]; } this.setZoomInternal_(nextZoom); this.updateViewport_(); }.bind(this)); }, /** * Zoom in to the next predefined zoom level. */ zoomIn: function() { this.mightZoom_(function() { this.fittingType_ = Viewport.FittingType.NONE; var nextZoom = Viewport.ZOOM_FACTORS[Viewport.ZOOM_FACTORS.length - 1]; for (var i = Viewport.ZOOM_FACTORS.length - 1; i >= 0; i--) { if (Viewport.ZOOM_FACTORS[i] > this.internalZoom_) nextZoom = Viewport.ZOOM_FACTORS[i]; } this.setZoomInternal_(nextZoom); this.updateViewport_(); }.bind(this)); }, /** * Pinch zoom event handler. * @param {!Object} e The pinch event. */ pinchZoom: function(e) { this.mightZoom_(function() { this.pinchPhase_ = e.direction == 'out' ? Viewport.PinchPhase.PINCH_UPDATE_ZOOM_OUT : Viewport.PinchPhase.PINCH_UPDATE_ZOOM_IN; var scaleDelta = e.startScaleRatio / this.prevScale_; this.pinchPanVector_ = vectorDelta(e.center, this.firstPinchCenterInFrame_); var needsScrollbars = this.documentNeedsScrollbars_( this.zoomManager_.applyBrowserZoom( clampScale(this.internalZoom_ * scaleDelta))); this.pinchCenter_ = e.center; // If there's no horizontal scrolling, keep the content centered so the // user can't zoom in on the non-content area. // TODO(mcnee) Investigate other ways of scaling when we don't have // horizontal scrolling. We want to keep the document centered, // but this causes a potentially awkward transition when we start // using the gesture center. if (!needsScrollbars.horizontal) { this.pinchCenter_ = { x: this.window_.innerWidth / 2, y: this.window_.innerHeight / 2 }; } else if (this.keepContentCentered_) { this.oldCenterInContent = this.frameToContent(frameToPluginCoordinate(e.center)); this.keepContentCentered_ = false; } this.setPinchZoomInternal_( scaleDelta, frameToPluginCoordinate(e.center)); this.updateViewport_(); this.prevScale_ = e.startScaleRatio; }.bind(this)); }, pinchZoomStart: function(e) { this.pinchPhase_ = Viewport.PinchPhase.PINCH_START; this.prevScale_ = 1; this.oldCenterInContent = this.frameToContent(frameToPluginCoordinate(e.center)); var needsScrollbars = this.documentNeedsScrollbars_(this.zoom); this.keepContentCentered_ = !needsScrollbars.horizontal; // We keep track of begining of the pinch. // By doing so we will be able to compute the pan distance. this.firstPinchCenterInFrame_ = e.center; }, pinchZoomEnd: function(e) { this.mightZoom_(function() { this.pinchPhase_ = Viewport.PinchPhase.PINCH_END; var scaleDelta = e.startScaleRatio / this.prevScale_; this.pinchCenter_ = e.center; this.setPinchZoomInternal_( scaleDelta, frameToPluginCoordinate(e.center)); this.updateViewport_(); }.bind(this)); this.pinchPhase_ = Viewport.PinchPhase.PINCH_NONE; this.pinchPanVector_ = null; this.pinchCenter_ = null; this.firstPinchCenterInFrame_ = null; }, /** * Go to the given page index. * @param {number} page the index of the page to go to. zero-based. */ goToPage: function(page) { this.mightZoom_(function() { if (this.pageDimensions_.length === 0) return; if (page < 0) page = 0; if (page >= this.pageDimensions_.length) page = this.pageDimensions_.length - 1; var dimensions = this.pageDimensions_[page]; var toolbarOffset = 0; // Unless we're in fit to page mode, scroll above the page by // |this.topToolbarHeight_| so that the toolbar isn't covering it // initially. if (this.fittingType_ != Viewport.FittingType.FIT_TO_PAGE) toolbarOffset = this.topToolbarHeight_; this.position = { x: dimensions.x * this.zoom, y: dimensions.y * this.zoom - toolbarOffset }; this.updateViewport_(); }.bind(this)); }, /** * Set the dimensions of the document. * @param {Object} documentDimensions the dimensions of the document */ setDocumentDimensions: function(documentDimensions) { this.mightZoom_(function() { var initialDimensions = !this.documentDimensions_; this.documentDimensions_ = documentDimensions; this.pageDimensions_ = this.documentDimensions_.pageDimensions; if (initialDimensions) { this.setZoomInternal_( Math.min(this.defaultZoom_, this.computeFittingZoom_(this.documentDimensions_, true))); this.position = { x: 0, y: -this.topToolbarHeight_ }; } this.contentSizeChanged_(); this.resize_(); }.bind(this)); }, /** * Get the coordinates of the page contents (excluding the page shadow) * relative to the screen. * @param {number} page the index of the page to get the rect for. * @return {Object} a rect representing the page in screen coordinates. */ getPageScreenRect: function(page) { if (!this.documentDimensions_) { return { x: 0, y: 0, width: 0, height: 0 }; } if (page >= this.pageDimensions_.length) page = this.pageDimensions_.length - 1; var pageDimensions = this.pageDimensions_[page]; // Compute the page dimensions minus the shadows. var insetDimensions = { x: pageDimensions.x + Viewport.PAGE_SHADOW.left, y: pageDimensions.y + Viewport.PAGE_SHADOW.top, width: pageDimensions.width - Viewport.PAGE_SHADOW.left - Viewport.PAGE_SHADOW.right, height: pageDimensions.height - Viewport.PAGE_SHADOW.top - Viewport.PAGE_SHADOW.bottom }; // Compute the x-coordinate of the page within the document. // TODO(raymes): This should really be set when the PDF plugin passes the // page coordinates, but it isn't yet. var x = (this.documentDimensions_.width - pageDimensions.width) / 2 + Viewport.PAGE_SHADOW.left; // Compute the space on the left of the document if the document fits // completely in the screen. var spaceOnLeft = (this.size.width - this.documentDimensions_.width * this.zoom) / 2; spaceOnLeft = Math.max(spaceOnLeft, 0); return { x: x * this.zoom + spaceOnLeft - this.window_.pageXOffset, y: insetDimensions.y * this.zoom - this.window_.pageYOffset, width: insetDimensions.width * this.zoom, height: insetDimensions.height * this.zoom }; } }; // Copyright 2014 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. 'use strict'; /** * Creates a new OpenPDFParamsParser. This parses the open pdf parameters * passed in the url to set initial viewport settings for opening the pdf. * @param {Object} getNamedDestinationsFunction The function called to fetch * the page number for a named destination. */ function OpenPDFParamsParser(getNamedDestinationsFunction) { this.outstandingRequests_ = []; this.getNamedDestinationsFunction_ = getNamedDestinationsFunction; } OpenPDFParamsParser.prototype = { /** * @private * Parse zoom parameter of open PDF parameters. If this * parameter is passed while opening PDF then PDF should be opened * at the specified zoom level. * @param {number} zoom value. * @param {Object} viewportPosition to store zoom and position value. */ parseZoomParam_: function(paramValue, viewportPosition) { var paramValueSplit = paramValue.split(','); if ((paramValueSplit.length != 1) && (paramValueSplit.length != 3)) return; // User scale of 100 means zoom value of 100% i.e. zoom factor of 1.0. var zoomFactor = parseFloat(paramValueSplit[0]) / 100; if (isNaN(zoomFactor)) return; // Handle #zoom=scale. if (paramValueSplit.length == 1) { viewportPosition['zoom'] = zoomFactor; return; } // Handle #zoom=scale,left,top. var position = {x: parseFloat(paramValueSplit[1]), y: parseFloat(paramValueSplit[2])}; viewportPosition['position'] = position; viewportPosition['zoom'] = zoomFactor; }, /** * Parse the parameters encoded in the fragment of a URL into a dictionary. * @private * @param {string} url to parse * @return {Object} Key-value pairs of URL parameters */ parseUrlParams_: function(url) { var params = {}; var paramIndex = url.search('#'); if (paramIndex == -1) return params; var paramTokens = url.substring(paramIndex + 1).split('&'); if ((paramTokens.length == 1) && (paramTokens[0].search('=') == -1)) { // Handle the case of http://foo.com/bar#NAMEDDEST. This is not // explicitly mentioned except by example in the Adobe // "PDF Open Parameters" document. params['nameddest'] = paramTokens[0]; return params; } for (var i = 0; i < paramTokens.length; ++i) { var keyValueSplit = paramTokens[i].split('='); if (keyValueSplit.length != 2) continue; params[keyValueSplit[0]] = keyValueSplit[1]; } return params; }, /** * Parse PDF url parameters used for controlling the state of UI. These need * to be available when the UI is being initialized, rather than when the PDF * is finished loading. * @param {string} url that needs to be parsed. * @return {Object} parsed url parameters. */ getUiUrlParams: function(url) { var params = this.parseUrlParams_(url); var uiParams = {toolbar: true}; if ('toolbar' in params && params['toolbar'] == 0) uiParams.toolbar = false; return uiParams; }, /** * Parse PDF url parameters. These parameters are mentioned in the url * and specify actions to be performed when opening pdf files. * See http://www.adobe.com/content/dam/Adobe/en/devnet/acrobat/ * pdfs/pdf_open_parameters.pdf for details. * @param {string} url that needs to be parsed. * @param {Function} callback function to be called with viewport info. */ getViewportFromUrlParams: function(url, callback) { var viewportPosition = {}; viewportPosition['url'] = url; var paramsDictionary = this.parseUrlParams_(url); if ('page' in paramsDictionary) { // |pageNumber| is 1-based, but goToPage() take a zero-based page number. var pageNumber = parseInt(paramsDictionary['page']); if (!isNaN(pageNumber) && pageNumber > 0) viewportPosition['page'] = pageNumber - 1; } if ('zoom' in paramsDictionary) this.parseZoomParam_(paramsDictionary['zoom'], viewportPosition); if (viewportPosition.page === undefined && 'nameddest' in paramsDictionary) { this.outstandingRequests_.push({ callback: callback, viewportPosition: viewportPosition }); this.getNamedDestinationsFunction_(paramsDictionary['nameddest']); } else { callback(viewportPosition); } }, /** * This is called when a named destination is received and the page number * corresponding to the request for which a named destination is passed. * @param {number} pageNumber The page corresponding to the named destination * requested. */ onNamedDestinationReceived: function(pageNumber) { var outstandingRequest = this.outstandingRequests_.shift(); if (pageNumber != -1) outstandingRequest.viewportPosition.page = pageNumber; outstandingRequest.callback(outstandingRequest.viewportPosition); }, }; // Copyright 2015 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. 'use strict'; /** * Creates a new NavigatorDelegate for calling browser-specific functions to * do the actual navigating. * @param {number} tabId The tab ID of the PDF viewer or -1 if the viewer is * not displayed in a tab. */ function NavigatorDelegate(tabId) { this.tabId_ = tabId; } /** * Creates a new Navigator for navigating to links inside or outside the PDF. * @param {string} originalUrl The original page URL. * @param {Object} viewport The viewport info of the page. * @param {Object} paramsParser The object for URL parsing. * @param {Object} navigatorDelegate The object with callback functions that * get called when navigation happens in the current tab, a new tab, * and a new window. */ function Navigator(originalUrl, viewport, paramsParser, navigatorDelegate) { this.originalUrl_ = originalUrl; this.viewport_ = viewport; this.paramsParser_ = paramsParser; this.navigatorDelegate_ = navigatorDelegate; } NavigatorDelegate.prototype = { /** * @public * Called when navigation should happen in the current tab. * @param {string} url The url to be opened in the current tab. */ navigateInCurrentTab: function(url) { // When the PDFviewer is inside a browser tab, prefer the tabs API because // it can navigate from one file:// URL to another. if (chrome.tabs && this.tabId_ != -1) chrome.tabs.update(this.tabId_, {url: url}); else window.location.href = url; }, /** * @public * Called when navigation should happen in the new tab. * @param {string} url The url to be opened in the new tab. * @param {boolean} active Indicates if the new tab should be the active tab. */ navigateInNewTab: function(url, active) { // Prefer the tabs API because it guarantees we can just open a new tab. // window.open doesn't have this guarantee. if (chrome.tabs) chrome.tabs.create({url: url, active: active}); else window.open(url); }, /** * @public * Called when navigation should happen in the new window. * @param {string} url The url to be opened in the new window. */ navigateInNewWindow: function(url) { // Prefer the windows API because it guarantees we can just open a new // window. window.open with '_blank' argument doesn't have this guarantee. if (chrome.windows) chrome.windows.create({url: url}); else window.open(url, '_blank'); } }; /** * Represents options when navigating to a new url. C++ counterpart of * the enum is in ui/base/window_open_disposition.h. This enum represents * the only values that are passed from Plugin. * @enum {number} */ Navigator.WindowOpenDisposition = { CURRENT_TAB: 1, NEW_FOREGROUND_TAB: 3, NEW_BACKGROUND_TAB: 4, NEW_WINDOW: 6, SAVE_TO_DISK: 7 }; Navigator.prototype = { /** * @private * Function to navigate to the given URL. This might involve navigating * within the PDF page or opening a new url (in the same tab or a new tab). * @param {string} url The URL to navigate to. * @param {number} disposition The window open disposition when * navigating to the new URL. */ navigate: function(url, disposition) { if (url.length == 0) return; // If |urlFragment| starts with '#', then it's for the same URL with a // different URL fragment. if (url.charAt(0) == '#') { // if '#' is already present in |originalUrl| then remove old fragment // and add new url fragment. var hashIndex = this.originalUrl_.search('#'); if (hashIndex != -1) url = this.originalUrl_.substring(0, hashIndex) + url; else url = this.originalUrl_ + url; } // If there's no scheme, then take a guess at the scheme. if (url.indexOf('://') == -1 && url.indexOf('mailto:') == -1) url = this.guessUrlWithoutScheme_(url); if (!this.isValidUrl_(url)) return; switch (disposition) { case Navigator.WindowOpenDisposition.CURRENT_TAB: this.paramsParser_.getViewportFromUrlParams( url, this.onViewportReceived_.bind(this)); break; case Navigator.WindowOpenDisposition.NEW_BACKGROUND_TAB: this.navigatorDelegate_.navigateInNewTab(url, false); break; case Navigator.WindowOpenDisposition.NEW_FOREGROUND_TAB: this.navigatorDelegate_.navigateInNewTab(url, true); break; case Navigator.WindowOpenDisposition.NEW_WINDOW: this.navigatorDelegate_.navigateInNewWindow(url); break; case Navigator.WindowOpenDisposition.SAVE_TO_DISK: // TODO(jaepark): Alt + left clicking a link in PDF should // download the link. this.paramsParser_.getViewportFromUrlParams( url, this.onViewportReceived_.bind(this)); break; default: break; } }, /** * @private * Called when the viewport position is received. * @param {Object} viewportPosition Dictionary containing the viewport * position. */ onViewportReceived_: function(viewportPosition) { var originalUrl = this.originalUrl_; var hashIndex = originalUrl.search('#'); if (hashIndex != -1) originalUrl = originalUrl.substring(0, hashIndex); var newUrl = viewportPosition.url; hashIndex = newUrl.search('#'); if (hashIndex != -1) newUrl = newUrl.substring(0, hashIndex); var pageNumber = viewportPosition.page; if (pageNumber != undefined && originalUrl == newUrl) this.viewport_.goToPage(pageNumber); else this.navigatorDelegate_.navigateInCurrentTab(viewportPosition.url); }, /** * @private * Checks if the URL starts with a scheme and is not just a scheme. * @param {string} The input URL * @return {boolean} Whether the url is valid. */ isValidUrl_: function(url) { // Make sure |url| starts with a valid scheme. if (!url.startsWith('http://') && !url.startsWith('https://') && !url.startsWith('ftp://') && !url.startsWith('file://') && !url.startsWith('mailto:')) { return false; } // Navigations to file:-URLs are only allowed from file:-URLs. if (url.startsWith('file:') && !this.originalUrl_.startsWith('file:')) return false; // Make sure |url| is not only a scheme. if (url == 'http://' || url == 'https://' || url == 'ftp://' || url == 'file://' || url == 'mailto:') { return false; } return true; }, /** * @private * Attempt to figure out what a URL is when there is no scheme. * @param {string} The input URL * @return {string} The URL with a scheme or the original URL if it is not * possible to determine the scheme. */ guessUrlWithoutScheme_: function(url) { // If the original URL is mailto:, that does not make sense to start with, // and neither does adding |url| to it. // If the original URL is not a valid URL, this cannot make a valid URL. // In both cases, just bail out. if (this.originalUrl_.startsWith('mailto:') || !this.isValidUrl_(this.originalUrl_)) { return url; } // Check for absolute paths. if (url.startsWith('/')) { var schemeEndIndex = this.originalUrl_.indexOf('://'); var firstSlash = this.originalUrl_.indexOf('/', schemeEndIndex + 3); // e.g. http://www.foo.com/bar -> http://www.foo.com var domain = firstSlash != -1 ? this.originalUrl_.substr(0, firstSlash) : this.originalUrl_; return domain + url; } // Check for obvious relative paths. var isRelative = false; if (url.startsWith('.') || url.startsWith('\\')) isRelative = true; // In Adobe Acrobat Reader XI, it looks as though links with less than // 2 dot separators in the domain are considered relative links, and // those with 2 of more are considered http URLs. e.g. // // www.foo.com/bar -> http // foo.com/bar -> relative link if (!isRelative) { var domainSeparatorIndex = url.indexOf('/'); var domainName = domainSeparatorIndex == -1 ? url : url.substr(0, domainSeparatorIndex); var domainDotCount = (domainName.match(/\./g) || []).length; if (domainDotCount < 2) isRelative = true; } if (isRelative) { var slashIndex = this.originalUrl_.lastIndexOf('/'); var path = slashIndex != -1 ? this.originalUrl_.substr(0, slashIndex) : this.originalUrl_; return path + '/' + url; } return 'http://' + url; } }; // Copyright 2015 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. 'use strict'; /** * @private * The period of time in milliseconds to wait between updating the viewport * position by the scroll velocity. */ ViewportScroller.DRAG_TIMER_INTERVAL_MS_ = 100; /** * @private * The maximum drag scroll distance per DRAG_TIMER_INTERVAL in pixels. */ ViewportScroller.MAX_DRAG_SCROLL_DISTANCE_ = 100; /** * Creates a new ViewportScroller. * A ViewportScroller scrolls the page in response to drag selection with the * mouse. * @param {Object} viewport The viewport info of the page. * @param {Object} plugin The PDF plugin element. * @param {Object} window The window containing the viewer. */ function ViewportScroller(viewport, plugin, window) { this.viewport_ = viewport; this.plugin_ = plugin; this.window_ = window; this.mousemoveCallback_ = null; this.timerId_ = null; this.scrollVelocity_ = null; this.lastFrameTime_ = 0; } ViewportScroller.prototype = { /** * @private * Start scrolling the page by |scrollVelocity_| every * |DRAG_TIMER_INTERVAL_MS_|. */ startDragScrollTimer_: function() { if (this.timerId_ === null) { this.timerId_ = this.window_.setInterval(this.dragScrollPage_.bind(this), ViewportScroller.DRAG_TIMER_INTERVAL_MS_); this.lastFrameTime_ = Date.now(); } }, /** * @private * Stops the drag scroll timer if it is active. */ stopDragScrollTimer_: function() { if (this.timerId_ !== null) { this.window_.clearInterval(this.timerId_); this.timerId_ = null; this.lastFrameTime_ = 0; } }, /** * @private * Scrolls the viewport by the current scroll velocity. */ dragScrollPage_: function() { var position = this.viewport_.position; var currentFrameTime = Date.now(); var timeAdjustment = (currentFrameTime - this.lastFrameTime_) / ViewportScroller.DRAG_TIMER_INTERVAL_MS_; position.y += (this.scrollVelocity_.y * timeAdjustment); position.x += (this.scrollVelocity_.x * timeAdjustment); this.viewport_.position = position; this.lastFrameTime_ = currentFrameTime; }, /** * @private * Calculate the velocity to scroll while dragging using the distance of the * cursor outside the viewport. * @param {Object} event The mousemove event. * @return {Object} Object with x and y direction scroll velocity. */ calculateVelocity_: function(event) { var x = Math.min(Math.max(-event.offsetX, event.offsetX - this.plugin_.offsetWidth, 0), ViewportScroller.MAX_DRAG_SCROLL_DISTANCE_) * Math.sign(event.offsetX); var y = Math.min(Math.max(-event.offsetY, event.offsetY - this.plugin_.offsetHeight, 0), ViewportScroller.MAX_DRAG_SCROLL_DISTANCE_) * Math.sign(event.offsetY); return { x: x, y: y }; }, /** * @private * Handles mousemove events. It updates the scroll velocity and starts and * stops timer based on scroll velocity. * @param {Object} event The mousemove event. */ onMousemove_: function(event) { this.scrollVelocity_ = this.calculateVelocity_(event); if (!this.scrollVelocity_.x && !this.scrollVelocity_.y) this.stopDragScrollTimer_(); else if (!this.timerId_) this.startDragScrollTimer_(); }, /** * Sets whether to scroll the viewport when the mouse is outside the * viewport. * @param {boolean} isSelecting Represents selection status. */ setEnableScrolling: function(isSelecting) { if (isSelecting) { if (!this.mousemoveCallback_) this.mousemoveCallback_ = this.onMousemove_.bind(this); this.plugin_.addEventListener('mousemove', this.mousemoveCallback_, false); } else { this.stopDragScrollTimer_(); if (this.mousemoveCallback_) { this.plugin_.removeEventListener('mousemove', this.mousemoveCallback_, false); } } } }; // Copyright 2014 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. /** * Turn a dictionary received from postMessage into a key event. * @param {Object} dict A dictionary representing the key event. * @return {Event} A key event. */ function DeserializeKeyEvent(dict) { var e = document.createEvent('Event'); e.initEvent('keydown'); e.keyCode = dict.keyCode; e.shiftKey = dict.shiftKey; e.ctrlKey = dict.ctrlKey; e.altKey = dict.altKey; e.metaKey = dict.metaKey; e.fromScriptingAPI = true; return e; } /** * Turn a key event into a dictionary which can be sent over postMessage. * @param {Event} event A key event. * @return {Object} A dictionary representing the key event. */ function SerializeKeyEvent(event) { return { keyCode: event.keyCode, shiftKey: event.shiftKey, ctrlKey: event.ctrlKey, altKey: event.altKey, metaKey: event.metaKey }; } /** * An enum containing a value specifying whether the PDF is currently loading, * has finished loading or failed to load. */ var LoadState = { LOADING: 'loading', SUCCESS: 'success', FAILED: 'failed' }; /** * Create a new PDFScriptingAPI. This provides a scripting interface to * the PDF viewer so that it can be customized by things like print preview. * @param {Window} window the window of the page containing the pdf viewer. * @param {Object} plugin the plugin element containing the pdf viewer. */ function PDFScriptingAPI(window, plugin) { this.loadState_ = LoadState.LOADING; this.pendingScriptingMessages_ = []; this.setPlugin(plugin); window.addEventListener('message', function(event) { if (event.origin != 'chrome-extension://mhjfbmdgcfjbbpaeojofohoefgiehjai' && event.origin != 'chrome://print') { console.error('Received message that was not from the extension: ' + event); return; } switch (event.data.type) { case 'viewport': if (this.viewportChangedCallback_) this.viewportChangedCallback_(event.data.pageX, event.data.pageY, event.data.pageWidth, event.data.viewportWidth, event.data.viewportHeight); break; case 'documentLoaded': this.loadState_ = event.data.load_state; if (this.loadCallback_) this.loadCallback_(this.loadState_ == LoadState.SUCCESS); break; case 'getSelectedTextReply': if (this.selectedTextCallback_) { this.selectedTextCallback_(event.data.selectedText); this.selectedTextCallback_ = null; } break; case 'sendKeyEvent': if (this.keyEventCallback_) this.keyEventCallback_(DeserializeKeyEvent(event.data.keyEvent)); break; } }.bind(this), false); } PDFScriptingAPI.prototype = { /** * @private * Send a message to the extension. If messages try to get sent before there * is a plugin element set, then we queue them up and send them later (this * can happen in print preview). * @param {Object} message The message to send. */ sendMessage_: function(message) { if (this.plugin_) this.plugin_.postMessage(message, '*'); else this.pendingScriptingMessages_.push(message); }, /** * Sets the plugin element containing the PDF viewer. The element will usually * be passed into the PDFScriptingAPI constructor but may also be set later. * @param {Object} plugin the plugin element containing the PDF viewer. */ setPlugin: function(plugin) { this.plugin_ = plugin; if (this.plugin_) { // Send a message to ensure the postMessage channel is initialized which // allows us to receive messages. this.sendMessage_({ type: 'initialize' }); // Flush pending messages. while (this.pendingScriptingMessages_.length > 0) this.sendMessage_(this.pendingScriptingMessages_.shift()); } }, /** * Sets the callback which will be run when the PDF viewport changes. * @param {Function} callback the callback to be called. */ setViewportChangedCallback: function(callback) { this.viewportChangedCallback_ = callback; }, /** * Sets the callback which will be run when the PDF document has finished * loading. If the document is already loaded, it will be run immediately. * @param {Function} callback the callback to be called. */ setLoadCallback: function(callback) { this.loadCallback_ = callback; if (this.loadState_ != LoadState.LOADING && this.loadCallback_) this.loadCallback_(this.loadState_ == LoadState.SUCCESS); }, /** * Sets a callback that gets run when a key event is fired in the PDF viewer. * @param {Function} callback the callback to be called with a key event. */ setKeyEventCallback: function(callback) { this.keyEventCallback_ = callback; }, /** * Resets the PDF viewer into print preview mode. * @param {string} url the url of the PDF to load. * @param {boolean} grayscale whether or not to display the PDF in grayscale. * @param {Array} pageNumbers an array of the page numbers. * @param {boolean} modifiable whether or not the document is modifiable. */ resetPrintPreviewMode: function(url, grayscale, pageNumbers, modifiable) { this.loadState_ = LoadState.LOADING; this.sendMessage_({ type: 'resetPrintPreviewMode', url: url, grayscale: grayscale, pageNumbers: pageNumbers, modifiable: modifiable }); }, /** * Load a page into the document while in print preview mode. * @param {string} url the url of the pdf page to load. * @param {number} index the index of the page to load. */ loadPreviewPage: function(url, index) { this.sendMessage_({ type: 'loadPreviewPage', url: url, index: index }); }, /** * Select all the text in the document. May only be called after document * load. */ selectAll: function() { this.sendMessage_({ type: 'selectAll' }); }, /** * Get the selected text in the document. The callback will be called with the * text that is selected. May only be called after document load. * @param {Function} callback a callback to be called with the selected text. * @return {boolean} true if the function is successful, false if there is an * outstanding request for selected text that has not been answered. */ getSelectedText: function(callback) { if (this.selectedTextCallback_) return false; this.selectedTextCallback_ = callback; this.sendMessage_({ type: 'getSelectedText' }); return true; }, /** * Print the document. May only be called after document load. */ print: function() { this.sendMessage_({ type: 'print' }); }, /** * Send a key event to the extension. * @param {Event} keyEvent the key event to send to the extension. */ sendKeyEvent: function(keyEvent) { this.sendMessage_({ type: 'sendKeyEvent', keyEvent: SerializeKeyEvent(keyEvent) }); }, }; /** * Creates a PDF viewer with a scripting interface. This is basically 1) an * iframe which is navigated to the PDF viewer extension and 2) a scripting * interface which provides access to various features of the viewer for use * by print preview and accessibility. * @param {string} src the source URL of the PDF to load initially. * @return {HTMLIFrameElement} the iframe element containing the PDF viewer. */ function PDFCreateOutOfProcessPlugin(src) { var client = new PDFScriptingAPI(window); var iframe = window.document.createElement('iframe'); iframe.setAttribute('src', 'pdf_preview.html?' + src); // Prevent the frame from being tab-focusable. iframe.setAttribute('tabindex', '-1'); iframe.onload = function() { client.setPlugin(iframe.contentWindow); }; // Add the functions to the iframe so that they can be called directly. iframe.setViewportChangedCallback = client.setViewportChangedCallback.bind(client); iframe.setLoadCallback = client.setLoadCallback.bind(client); iframe.setKeyEventCallback = client.setKeyEventCallback.bind(client); iframe.resetPrintPreviewMode = client.resetPrintPreviewMode.bind(client); iframe.loadPreviewPage = client.loadPreviewPage.bind(client); iframe.sendKeyEvent = client.sendKeyEvent.bind(client); return iframe; } // Copyright 2015 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. 'use strict'; /** * Abstract parent of classes that manage updating the browser * with zoom changes and/or updating the viewer's zoom when * the browser zoom changes. */ class ZoomManager { /** * Constructs a ZoomManager. * @param {!Viewport} viewport A Viewport for which to manage zoom. * @param {number} initialZoom The initial browser zoom level. */ constructor(viewport, initialZoom) { if (this.constructor === ZoomManager) { throw new TypeError('Instantiated abstract class: ZoomManager'); } this.viewport_ = viewport; this.browserZoom_ = initialZoom; } /** * Creates the appropriate kind of zoom manager given the zoom behavior. * @param {BrowserApi.ZoomBehavior} zoomBehavior How to manage zoom. * @param {!Viewport} viewport A Viewport for which to manage zoom. * @param {Function} setBrowserZoomFunction A function that sets the browser * zoom to the provided value. * @param {number} initialZoom The initial browser zoom level. */ static create(zoomBehavior, viewport, setBrowserZoomFunction, initialZoom) { switch (zoomBehavior) { case BrowserApi.ZoomBehavior.MANAGE: return new ActiveZoomManager( viewport, setBrowserZoomFunction, initialZoom); case BrowserApi.ZoomBehavior.PROPAGATE_PARENT: return new EmbeddedZoomManager(viewport, initialZoom); default: return new InactiveZoomManager(viewport, initialZoom); } } /** * Invoked when a browser-initiated zoom-level change occurs. * @param {number} newZoom the zoom level to zoom to. */ onBrowserZoomChange(newZoom) {} /** * Invoked when an extension-initiated zoom-level change occurs. */ onPdfZoomChange() {} /** * Combines the internal pdf zoom and the browser zoom to * produce the total zoom level for the viewer. * @param {number} internalZoom the zoom level internal to the viewer. * @return {number} the total zoom level. */ applyBrowserZoom(internalZoom) { return this.browserZoom_ * internalZoom; } /** * Given a zoom level, return the internal zoom level needed to * produce that zoom level. * @param {number} totalZoom the total zoom level. * @return {number} the zoom level internal to the viewer. */ internalZoomComponent(totalZoom) { return totalZoom / this.browserZoom_; } /** * Returns whether two numbers are approximately equal. * @param {number} a The first number. * @param {number} b The second number. */ floatingPointEquals(a, b) { let MIN_ZOOM_DELTA = 0.01; // If the zoom level is close enough to the current zoom level, don't // change it. This avoids us getting into an infinite loop of zoom changes // due to floating point error. return Math.abs(a - b) <= MIN_ZOOM_DELTA; } }; /** * InactiveZoomManager has no control over the browser's zoom * and does not respond to browser zoom changes. */ class InactiveZoomManager extends ZoomManager {}; /** * ActiveZoomManager controls the browser's zoom. */ class ActiveZoomManager extends ZoomManager { /** * Constructs a ActiveZoomManager. * @param {!Viewport} viewport A Viewport for which to manage zoom. * @param {Function} setBrowserZoomFunction A function that sets the browser * zoom to the provided value. * @param {number} initialZoom The initial browser zoom level. */ constructor(viewport, setBrowserZoomFunction, initialZoom) { super(viewport, initialZoom); this.setBrowserZoomFunction_ = setBrowserZoomFunction; this.changingBrowserZoom_ = null; } /** * Invoked when a browser-initiated zoom-level change occurs. * @param {number} newZoom the zoom level to zoom to. */ onBrowserZoomChange(newZoom) { // If we are changing the browser zoom level, ignore any browser zoom level // change events. Either, the change occurred before our update and will be // overwritten, or the change being reported is the change we are making, // which we have already handled. if (this.changingBrowserZoom_) return; if (this.floatingPointEquals(this.browserZoom_, newZoom)) return; this.browserZoom_ = newZoom; this.viewport_.setZoom(newZoom); } /** * Invoked when an extension-initiated zoom-level change occurs. */ onPdfZoomChange() { // If we are already changing the browser zoom level in response to a // previous extension-initiated zoom-level change, ignore this zoom change. // Once the browser zoom level is changed, we check whether the extension's // zoom level matches the most recently sent zoom level. if (this.changingBrowserZoom_) return; let zoom = this.viewport_.zoom; if (this.floatingPointEquals(this.browserZoom_, zoom)) return; this.changingBrowserZoom_ = this.setBrowserZoomFunction_(zoom).then( function() { this.browserZoom_ = zoom; this.changingBrowserZoom_ = null; // The extension's zoom level may have changed while the browser zoom // change was in progress. We call back into onPdfZoomChange to ensure the // browser zoom is up to date. this.onPdfZoomChange(); }.bind(this)); } /** * Combines the internal pdf zoom and the browser zoom to * produce the total zoom level for the viewer. * @param {number} internalZoom the zoom level internal to the viewer. * @return {number} the total zoom level. */ applyBrowserZoom(internalZoom) { // The internal zoom and browser zoom are changed together, so the // browser zoom is already applied. return internalZoom; } /** * Given a zoom level, return the internal zoom level needed to * produce that zoom level. * @param {number} totalZoom the total zoom level. * @return {number} the zoom level internal to the viewer. */ internalZoomComponent(totalZoom) { // The internal zoom and browser zoom are changed together, so the // internal zoom is the total zoom. return totalZoom; } }; /** * This EmbeddedZoomManager responds to changes in the browser zoom, * but does not control the browser zoom. */ class EmbeddedZoomManager extends ZoomManager { /** * Invoked when a browser-initiated zoom-level change occurs. * @param {number} newZoom the new browser zoom level. */ onBrowserZoomChange(newZoom) { let oldZoom = this.browserZoom_; this.browserZoom_ = newZoom; this.viewport_.updateZoomFromBrowserChange(oldZoom); } }; // Copyright 2016 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. 'use strict'; /** * A class that listens for touch events and produces events when these * touches form gestures (e.g. pinching). */ class GestureDetector { /** * Constructs a GestureDetector. * @param {!Element} element The element to monitor for touch gestures. */ constructor(element) { this.element_ = element; this.element_.addEventListener( 'touchstart', this.onTouchStart_.bind(this), { passive: false }); this.element_.addEventListener( 'touchmove', this.onTouch_.bind(this), { passive: true }); this.element_.addEventListener( 'touchend', this.onTouch_.bind(this), { passive: true }); this.element_.addEventListener( 'touchcancel', this.onTouch_.bind(this), { passive: true }); this.pinchStartEvent_ = null; this.lastEvent_ = null; this.listeners_ = new Map([ ['pinchstart', []], ['pinchupdate', []], ['pinchend', []] ]); } /** * Add a |listener| to be notified of |type| events. * @param {string} type The event type to be notified for. * @param {Function} listener The callback. */ addEventListener(type, listener) { if (this.listeners_.has(type)) { this.listeners_.get(type).push(listener); } } /** * Call the relevant listeners with the given |pinchEvent|. * @private * @param {!Object} pinchEvent The event to notify the listeners of. */ notify_(pinchEvent) { let listeners = this.listeners_.get(pinchEvent.type); for (let l of listeners) l(pinchEvent); } /** * The callback for touchstart events on the element. * @private * @param {!TouchEvent} event Touch event on the element. */ onTouchStart_(event) { // We must preventDefault if there is a two finger touch. By doing so // native pinch-zoom does not interfere with our way of handling the event. if (event.touches.length == 2) { event.preventDefault(); this.pinchStartEvent_ = event; this.lastEvent_ = event; this.notify_({ type: 'pinchstart', center: GestureDetector.center_(event) }); } } /** * The callback for touch move, end, and cancel events on the element. * @private * @param {!TouchEvent} event Touch event on the element. */ onTouch_(event) { if (!this.pinchStartEvent_) return; // Check if the pinch ends with the current event. if (event.touches.length < 2 || this.lastEvent_.touches.length !== event.touches.length) { let startScaleRatio = GestureDetector.pinchScaleRatio_( this.lastEvent_, this.pinchStartEvent_); let center = GestureDetector.center_(this.lastEvent_); let endEvent = { type: 'pinchend', startScaleRatio: startScaleRatio, center: center }; this.pinchStartEvent_ = null; this.lastEvent_ = null; this.notify_(endEvent); return; } let scaleRatio = GestureDetector.pinchScaleRatio_(event, this.lastEvent_); let startScaleRatio = GestureDetector.pinchScaleRatio_( event, this.pinchStartEvent_); let center = GestureDetector.center_(event); this.notify_({ type: 'pinchupdate', scaleRatio: scaleRatio, direction: scaleRatio > 1.0 ? 'in' : 'out', startScaleRatio: startScaleRatio, center: center }); this.lastEvent_ = event; } /** * Computes the change in scale between this touch event * and a previous one. * @private * @param {!TouchEvent} event Latest touch event on the element. * @param {!TouchEvent} prevEvent A previous touch event on the element. * @return {?number} The ratio of the scale of this event and the * scale of the previous one. */ static pinchScaleRatio_(event, prevEvent) { let distance1 = GestureDetector.distance_(prevEvent); let distance2 = GestureDetector.distance_(event); return distance1 === 0 ? null : distance2 / distance1; } /** * Computes the distance between fingers. * @private * @param {!TouchEvent} event Touch event with at least 2 touch points. * @return {number} Distance between touch[0] and touch[1]. */ static distance_(event) { let touch1 = event.touches[0]; let touch2 = event.touches[1]; let dx = touch1.clientX - touch2.clientX; let dy = touch1.clientY - touch2.clientY; return Math.sqrt(dx * dx + dy * dy); } /** * Computes the midpoint between fingers. * @private * @param {!TouchEvent} event Touch event with at least 2 touch points. * @return {!Object} Midpoint between touch[0] and touch[1]. */ static center_(event) { let touch1 = event.touches[0]; let touch2 = event.touches[1]; return { x: (touch1.clientX + touch2.clientX) / 2, y: (touch1.clientY + touch2.clientY) / 2 }; } }; // Copyright 2015 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. 'use strict'; /** * Returns a promise that will resolve to the default zoom factor. * @param {!Object} streamInfo The stream object pointing to the data contained * in the PDF. * @return {Promise} A promise that will resolve to the default zoom * factor. */ function lookupDefaultZoom(streamInfo) { // Webviews don't run in tabs so |streamInfo.tabId| is -1 when running within // a webview. if (!chrome.tabs || streamInfo.tabId < 0) return Promise.resolve(1); return new Promise(function(resolve, reject) { chrome.tabs.getZoomSettings(streamInfo.tabId, function(zoomSettings) { resolve(zoomSettings.defaultZoomFactor); }); }); } /** * Returns a promise that will resolve to the initial zoom factor * upon starting the plugin. This may differ from the default zoom * if, for example, the page is zoomed before the plugin is run. * @param {!Object} streamInfo The stream object pointing to the data contained * in the PDF. * @return {Promise} A promise that will resolve to the initial zoom * factor. */ function lookupInitialZoom(streamInfo) { // Webviews don't run in tabs so |streamInfo.tabId| is -1 when running within // a webview. if (!chrome.tabs || streamInfo.tabId < 0) return Promise.resolve(1); return new Promise(function(resolve, reject) { chrome.tabs.getZoom(streamInfo.tabId, resolve); }); } /** * A class providing an interface to the browser. */ class BrowserApi { /** * @constructor * @param {!Object} streamInfo The stream object which points to the data * contained in the PDF. * @param {number} defaultZoom The default browser zoom. * @param {number} initialZoom The initial browser zoom * upon starting the plugin. * @param {BrowserApi.ZoomBehavior} zoomBehavior How to manage zoom. */ constructor(streamInfo, defaultZoom, initialZoom, zoomBehavior) { this.streamInfo_ = streamInfo; this.defaultZoom_ = defaultZoom; this.initialZoom_ = initialZoom; this.zoomBehavior_ = zoomBehavior; } /** * Returns a promise to a BrowserApi. * @param {!Object} streamInfo The stream object pointing to the data * contained in the PDF. * @param {BrowserApi.ZoomBehavior} zoomBehavior How to manage zoom. */ static create(streamInfo, zoomBehavior) { return Promise.all([ lookupDefaultZoom(streamInfo), lookupInitialZoom(streamInfo) ]).then(function(zoomFactors) { return new BrowserApi( streamInfo, zoomFactors[0], zoomFactors[1], zoomBehavior); }); } /** * Returns the stream info pointing to the data contained in the PDF. * @return {Object} The stream info object. */ getStreamInfo() { return this.streamInfo_; } /** * Aborts the stream. */ abortStream() { if (chrome.mimeHandlerPrivate) chrome.mimeHandlerPrivate.abortStream(); } /** * Sets the browser zoom. * @param {number} zoom The zoom factor to send to the browser. * @return {Promise} A promise that will be resolved when the browser zoom * has been updated. */ setZoom(zoom) { if (this.zoomBehavior_ != BrowserApi.ZoomBehavior.MANAGE) return Promise.reject(new Error('Viewer does not manage browser zoom.')); return new Promise(function(resolve, reject) { chrome.tabs.setZoom(this.streamInfo_.tabId, zoom, resolve); }.bind(this)); } /** * Returns the default browser zoom factor. * @return {number} The default browser zoom factor. */ getDefaultZoom() { return this.defaultZoom_; } /** * Returns the initial browser zoom factor. * @return {number} The initial browser zoom factor. */ getInitialZoom() { return this.initialZoom_; } /** * Returns how to manage the zoom. * @return {BrowserApi.ZoomBehavior} How to manage zoom. */ getZoomBehavior() { return this.zoomBehavior_; } /** * Adds an event listener to be notified when the browser zoom changes. * @param {function} listener The listener to be called with the new zoom * factor. */ addZoomEventListener(listener) { if (!(this.zoomBehavior_ == BrowserApi.ZoomBehavior.MANAGE || this.zoomBehavior_ == BrowserApi.ZoomBehavior.PROPAGATE_PARENT)) return; chrome.tabs.onZoomChange.addListener(function(zoomChangeInfo) { if (zoomChangeInfo.tabId != this.streamInfo_.tabId) return; listener(zoomChangeInfo.newZoomFactor); }.bind(this)); } }; /** * Enumeration of ways to manage zoom changes. * @enum {number} */ BrowserApi.ZoomBehavior = { NONE: 0, MANAGE: 1, PROPAGATE_PARENT: 2 }; /** * Creates a BrowserApi for an extension running as a mime handler. * @return {Promise} A promise to a BrowserApi instance constructed * using the mimeHandlerPrivate API. */ function createBrowserApiForMimeHandlerView() { return new Promise(function(resolve, reject) { chrome.mimeHandlerPrivate.getStreamInfo(resolve); }).then(function(streamInfo) { let promises = []; let zoomBehavior = BrowserApi.ZoomBehavior.NONE; if (streamInfo.tabId != -1) { zoomBehavior = streamInfo.embedded ? BrowserApi.ZoomBehavior.PROPAGATE_PARENT : BrowserApi.ZoomBehavior.MANAGE; promises.push(new Promise(function(resolve) { chrome.tabs.get(streamInfo.tabId, resolve); }).then(function(tab) { if (tab) streamInfo.tabUrl = tab.url; })); } if (zoomBehavior == BrowserApi.ZoomBehavior.MANAGE) { promises.push(new Promise(function(resolve) { chrome.tabs.setZoomSettings( streamInfo.tabId, {mode: 'manual', scope: 'per-tab'}, resolve); })); } return Promise.all(promises).then( function() { return BrowserApi.create(streamInfo, zoomBehavior); }); }); } /** * Creates a BrowserApi instance for an extension not running as a mime handler. * @return {Promise} A promise to a BrowserApi instance constructed * from the URL. */ function createBrowserApiForPrintPreview() { let url = window.location.search.substring(1); let streamInfo = { streamUrl: url, originalUrl: url, responseHeaders: {}, embedded: window.parent != window, tabId: -1, }; return new Promise(function(resolve, reject) { if (!chrome.tabs) { resolve(); return; } chrome.tabs.getCurrent(function(tab) { streamInfo.tabId = tab.id; streamInfo.tabUrl = tab.url; resolve(); }); }).then(function() { return BrowserApi.create(streamInfo, BrowserApi.ZoomBehavior.NONE); }); } /** * Returns a promise that will resolve to a BrowserApi instance. * @return {Promise} A promise to a BrowserApi instance for the * current environment. */ function createBrowserApi() { if (location.origin === 'chrome://print') { return createBrowserApiForPrintPreview(); } return createBrowserApiForMimeHandlerView(); } // Copyright 2015 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. // This is to work-around an issue where this extension is not granted // permission to access chrome://resources when iframed for print preview. // See https://crbug.com/444752. /* Copyright 2015 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. */ :root { --iron-icon-height: 20px; --iron-icon-width: 20px; --paper-icon-button: { height: 32px; padding: 6px; width: 32px; }; --paper-icon-button-ink-color: rgb(189, 189, 189); --viewer-icon-ink-color: rgb(189, 189, 189); } /* Copyright 2015 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. */ #item { @apply(--layout-center); @apply(--layout-horizontal); color: rgb(80, 80, 80); cursor: pointer; font-size: 77.8%; height: 30px; position: relative; } #item:hover { background-color: rgb(237, 237, 237); color: rgb(20, 20, 20); } paper-ripple { /* Allowing the ripple to capture pointer events prevents a focus rectangle * for showing up for clicks, while still allowing it with tab-navigation. * This undoes a paper-ripple bugfix aimed at non-Chrome browsers. * TODO(tsergeant): Improve focus in viewer-bookmark so this can be removed * (https://crbug.com/5448190). */ pointer-events: auto; } #title { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } #expand { --iron-icon-height: 16px; --iron-icon-width: 16px; --paper-icon-button-ink-color: var(--paper-grey-900); height: 28px; min-width: 28px; padding: 6px; transition: transform 150ms; width: 28px; } :host-context([dir=rtl]) #expand { transform: rotate(180deg); } :host([children-shown]) #expand { transform: rotate(90deg); } // Copyright 2015 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. (function() { /** Amount that each level of bookmarks is indented by (px). */ var BOOKMARK_INDENT = 20; Polymer({ is: 'viewer-bookmark', properties: { /** * A bookmark object, each containing a: * - title * - page (optional) * - children (an array of bookmarks) */ bookmark: { type: Object, observer: 'bookmarkChanged_' }, depth: { type: Number, observer: 'depthChanged' }, childDepth: Number, childrenShown: { type: Boolean, reflectToAttribute: true, value: false }, keyEventTarget: { type: Object, value: function() { return this.$.item; } } }, behaviors: [ Polymer.IronA11yKeysBehavior ], keyBindings: { 'enter': 'onEnter_', 'space': 'onSpace_' }, bookmarkChanged_: function() { this.$.expand.style.visibility = this.bookmark.children.length > 0 ? 'visible' : 'hidden'; }, depthChanged: function() { this.childDepth = this.depth + 1; this.$.item.style.webkitPaddingStart = (this.depth * BOOKMARK_INDENT) + 'px'; }, onClick: function() { if (this.bookmark.hasOwnProperty('page')) this.fire('change-page', {page: this.bookmark.page}); else if (this.bookmark.hasOwnProperty('uri')) this.fire('navigate', {uri: this.bookmark.uri, newtab: true}); }, onEnter_: function(e) { // Don't allow events which have propagated up from the expand button to // trigger a click. if (e.detail.keyboardEvent.target != this.$.expand) this.onClick(); }, onSpace_: function(e) { // paper-icon-button stops propagation of space events, so there's no need // to check the event source here. this.onClick(); // Prevent default space scroll behavior. e.detail.keyboardEvent.preventDefault(); }, toggleChildren: function(e) { this.childrenShown = !this.childrenShown; e.stopPropagation(); // Prevent the above onClick handler from firing. } }); })(); // Copyright 2015 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. Polymer({ is: 'viewer-bookmarks-content' }); /* Copyright 2015 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. */ .last-item { margin-bottom: 24px; } // Copyright 2014 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. Polymer({ is: 'viewer-error-screen', properties: { reloadFn: { type: Object, value: null, observer: 'reloadFnChanged_' }, strings: Object, }, reloadFnChanged_: function() { // The default margins in paper-dialog don't work well with hiding/showing // the .buttons div. We need to manually manage the bottom margin to get // around this. if (this.reloadFn) this.$['load-failed-message'].classList.remove('last-item'); else this.$['load-failed-message'].classList.add('last-item'); }, show: function() { this.$.dialog.open(); }, reload: function() { if (this.reloadFn) this.reloadFn(); } }); /* Copyright 2013 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. */ :host { -webkit-transition: opacity 400ms ease-in-out; pointer-events: none; position: fixed; right: 0; } #text { background-color: rgba(0, 0, 0, 0.5); border-radius: 5px; color: white; float: left; font-family: sans-serif; font-size: 12px; font-weight: bold; line-height: 48px; text-align: center; text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.8); width: 62px; } #triangle-right { border-bottom: 6px solid transparent; border-left: 8px solid rgba(0, 0, 0, 0.5); border-top: 6px solid transparent; display: inline; float: left; height: 0; margin-top: 18px; width: 0; } // Copyright 2014 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. Polymer({ is: 'viewer-page-indicator', properties: { label: { type: String, value: '1' }, index: { type: Number, observer: 'indexChanged' }, pageLabels: { type: Array, value: null, observer: 'pageLabelsChanged' } }, timerId: undefined, ready: function() { var callback = this.fadeIn.bind(this, 2000); window.addEventListener('scroll', function() { requestAnimationFrame(callback); }); }, initialFadeIn: function() { this.fadeIn(6000); }, fadeIn: function(displayTime) { var percent = window.scrollY / (document.body.scrollHeight - document.documentElement.clientHeight); this.style.top = percent * (document.documentElement.clientHeight - this.offsetHeight) + 'px'; // this.style.opacity = 1; clearTimeout(this.timerId); this.timerId = setTimeout(function() { this.style.opacity = 0; this.timerId = undefined; }.bind(this), displayTime); }, pageLabelsChanged: function() { this.indexChanged(); }, indexChanged: function() { if (this.pageLabels) this.label = this.pageLabels[this.index]; else this.label = String(this.index + 1); } }); /* Copyright 2015 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. */ :host { color: #fff; font-size: 94.4%; } :host ::selection { background: rgba(255, 255, 255, 0.3); } #pageselector { --paper-input-container-underline: { visibility: hidden; }; --paper-input-container-underline-focus: { visibility: hidden; }; display: inline-block; padding: 0; width: 1ch; } #input { -webkit-margin-start: -3px; color: #fff; line-height: 18px; padding: 3px; text-align: end; vertical-align: baseline; } #input:focus, #input:hover { background-color: rgba(0, 0, 0, 0.5); border-radius: 2px; } #slash { padding: 0 3px; } #pagelength-spacer { display: inline-block; text-align: start; } #slash, #pagelength { font-size: 76.5%; } // Copyright 2015 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. Polymer({ is: 'viewer-page-selector', properties: { /** * The number of pages the document contains. */ docLength: { type: Number, value: 1, observer: 'docLengthChanged' }, /** * The current page being viewed (1-based). A change to pageNo is mirrored * immediately to the input field. A change to the input field is not * mirrored back until pageNoCommitted() is called and change-page is fired. */ pageNo: { type: Number, value: 1 }, strings: Object, }, pageNoCommitted: function() { var page = parseInt(this.$.input.value); if (!isNaN(page) && page <= this.docLength && page > 0) this.fire('change-page', {page: page - 1}); else this.$.input.value = this.pageNo; this.$.input.blur(); }, docLengthChanged: function() { var numDigits = this.docLength.toString().length; this.$.pageselector.style.width = numDigits + 'ch'; // Set both sides of the slash to the same width, so that the layout is // exactly centered. this.$['pagelength-spacer'].style.width = numDigits + 'ch'; }, select: function() { this.$.input.select(); }, /** * @return {boolean} True if the selector input field is currently focused. */ isActive: function() { return this.shadowRoot.activeElement == this.$.input; } }); // Copyright 2014 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. Polymer({ is: 'viewer-password-screen', properties: { invalid: Boolean, active: { type: Boolean, value: false, observer: 'activeChanged' }, strings: Object, }, ready: function() { this.activeChanged(); }, accept: function() { this.active = false; }, deny: function() { this.$.password.disabled = false; this.$.submit.disabled = false; this.invalid = true; this.$.password.focus(); this.$.password.select(); }, handleKey: function(e) { if (e.keyCode == 13) this.submit(); }, submit: function() { if (this.$.password.value.length == 0) return; this.$.password.disabled = true; this.$.submit.disabled = true; this.fire('password-submitted', {password: this.$.password.value}); }, activeChanged: function() { if (this.active) { this.$.dialog.open(); this.$.password.focus(); } else { this.$.dialog.close(); } } }); /* Copyright 2015 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. */ :host ::selection { background: rgba(255, 255, 255, 0.3); } /* We introduce a wrapper aligner element to help with laying out the main * toolbar content without changing the bottom-aligned progress bar. */ #aligner { @apply(--layout-horizontal); @apply(--layout-center); padding: 0 16px; width: 100%; } #title { @apply(--layout-flex-5); font-size: 77.8%; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } #pageselector-container { @apply(--layout-flex-1); text-align: center; /* The container resizes according to the width of the toolbar. On small * screens with large numbers of pages, overflow page numbers without * wrapping. */ white-space: nowrap; } #buttons { @apply(--layout-flex-5); -webkit-user-select: none; text-align: end; } paper-icon-button { -webkit-margin-end: 12px; } viewer-toolbar-dropdown { -webkit-margin-end: 4px; } paper-progress { --paper-progress-active-color: var(--google-blue-300); --paper-progress-container-color: transparent; --paper-progress-height: 3px; transition: opacity 150ms; width: 100%; } paper-toolbar { --paper-toolbar-background: rgb(50, 54, 57); --paper-toolbar-height: 48px; @apply(--shadow-elevation-2dp); color: rgb(241, 241, 241); font-size: 1.5em; } .invisible { visibility: hidden; } @media(max-width: 675px) { #bookmarks, #rotate-left { display: none; } #pageselector-container { flex: 2; } } @media(max-width: 450px) { #rotate-right { display: none; } } @media(max-width: 400px) { #buttons, #pageselector-container { display: none; } } // Copyright 2015 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. (function() { Polymer({ is: 'viewer-pdf-toolbar', behaviors: [ Polymer.NeonAnimationRunnerBehavior ], properties: { /** * The current loading progress of the PDF document (0 - 100). */ loadProgress: { type: Number, observer: 'loadProgressChanged' }, /** * The title of the PDF document. */ docTitle: String, /** * The number of the page being viewed (1-based). */ pageNo: Number, /** * Tree of PDF bookmarks (or null if the document has no bookmarks). */ bookmarks: { type: Object, value: null }, /** * The number of pages in the PDF document. */ docLength: Number, /** * Whether the toolbar is opened and visible. */ opened: { type: Boolean, value: true }, strings: Object, animationConfig: { value: function() { return { 'entry': { name: 'transform-animation', node: this, transformFrom: 'translateY(-100%)', transformTo: 'translateY(0%)', timing: { easing: 'cubic-bezier(0, 0, 0.2, 1)', duration: 250 } }, 'exit': { name: 'slide-up-animation', node: this, timing: { easing: 'cubic-bezier(0.4, 0, 1, 1)', duration: 250 } } }; } } }, listeners: { 'neon-animation-finish': '_onAnimationFinished' }, _onAnimationFinished: function() { this.style.transform = this.opened ? 'none' : 'translateY(-100%)'; }, loadProgressChanged: function() { if (this.loadProgress >= 100) { this.$.pageselector.classList.toggle('invisible', false); this.$.buttons.classList.toggle('invisible', false); this.$.progress.style.opacity = 0; } }, hide: function() { if (this.opened) this.toggleVisibility(); }, show: function() { if (!this.opened) { this.toggleVisibility(); } }, toggleVisibility: function() { this.opened = !this.opened; this.cancelAnimation(); this.playAnimation(this.opened ? 'entry' : 'exit'); }, selectPageNumber: function() { this.$.pageselector.select(); }, shouldKeepOpen: function() { return this.$.bookmarks.dropdownOpen || this.loadProgress < 100 || this.$.pageselector.isActive(); }, hideDropdowns: function() { if (this.$.bookmarks.dropdownOpen) { this.$.bookmarks.toggleDropdown(); return true; } return false; }, setDropdownLowerBound: function(lowerBound) { this.$.bookmarks.lowerBound = lowerBound; }, rotateRight: function() { this.fire('rotate-right'); }, download: function() { this.fire('save'); }, print: function() { this.fire('print'); } }); })(); /* Copyright 2015 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. */ :host { text-align: start; } #container { position: absolute; /* Controls the position of the dropdown relative to the right of the screen. * Default is aligned with the right of the toolbar buttons. * TODO(tsergeant): Change the layout of the dropdown so this is not required. */ right: var(--viewer-toolbar-dropdown-right-distance, 36px); } :host-context([dir=rtl]) #container { left: var(--viewer-toolbar-dropdown-right-distance, 36px); right: auto; } paper-material { background-color: rgb(256, 256, 256); border-radius: 4px; overflow-y: hidden; padding-bottom: 2px; width: 260px; } #scroll-container { max-height: 300px; overflow-y: auto; padding: 6px 0 4px 0; } #icon { cursor: pointer; display: inline-block; } :host([dropdown-open]) #icon { background-color: rgb(25, 27, 29); border-radius: 4px; } #arrow { -webkit-margin-start: -12px; -webkit-padding-end: 4px; } h1 { border-bottom: 1px solid rgb(219, 219, 219); color: rgb(33, 33, 33); font-size: 77.8%; font-weight: 500; margin: 0; padding: 14px 28px; } // Copyright 2015 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. (function() { /** * Size of additional padding in the inner scrollable section of the dropdown. */ var DROPDOWN_INNER_PADDING = 12; /** Size of vertical padding on the outer #dropdown element. */ var DROPDOWN_OUTER_PADDING = 2; /** Minimum height of toolbar dropdowns (px). */ var MIN_DROPDOWN_HEIGHT = 200; Polymer({ is: 'viewer-toolbar-dropdown', properties: { /** String to be displayed at the top of the dropdown. */ header: String, /** Icon to display when the dropdown is closed. */ closedIcon: String, /** Icon to display when the dropdown is open. */ openIcon: String, /** True if the dropdown is currently open. */ dropdownOpen: { type: Boolean, reflectToAttribute: true, value: false }, /** Toolbar icon currently being displayed. */ dropdownIcon: { type: String, computed: 'computeIcon_(dropdownOpen, closedIcon, openIcon)' }, /** Lowest vertical point that the dropdown should occupy (px). */ lowerBound: { type: Number, observer: 'lowerBoundChanged_' }, /** * True if the max-height CSS property for the dropdown scroll container * is valid. If false, the height will be updated the next time the * dropdown is visible. */ maxHeightValid_: false, /** Current animation being played, or null if there is none. */ animation_: Object }, computeIcon_: function(dropdownOpen, closedIcon, openIcon) { return dropdownOpen ? openIcon : closedIcon; }, lowerBoundChanged_: function() { this.maxHeightValid_ = false; if (this.dropdownOpen) this.updateMaxHeight(); }, toggleDropdown: function() { this.dropdownOpen = !this.dropdownOpen; if (this.dropdownOpen) { this.$.dropdown.style.display = 'block'; if (!this.maxHeightValid_) this.updateMaxHeight(); } this.cancelAnimation_(); this.playAnimation_(this.dropdownOpen); }, updateMaxHeight: function() { var scrollContainer = this.$['scroll-container']; var height = this.lowerBound - scrollContainer.getBoundingClientRect().top - DROPDOWN_INNER_PADDING; height = Math.max(height, MIN_DROPDOWN_HEIGHT); scrollContainer.style.maxHeight = height + 'px'; this.maxHeightValid_ = true; }, cancelAnimation_: function() { if (this._animation) this._animation.cancel(); }, /** * Start an animation on the dropdown. * @param {boolean} isEntry True to play entry animation, false to play * exit. * @private */ playAnimation_: function(isEntry) { this.animation_ = isEntry ? this.animateEntry_() : this.animateExit_(); this.animation_.onfinish = function() { this.animation_ = null; if (!this.dropdownOpen) this.$.dropdown.style.display = 'none'; }.bind(this); }, animateEntry_: function() { var maxHeight = this.$.dropdown.getBoundingClientRect().height - DROPDOWN_OUTER_PADDING; if (maxHeight < 0) maxHeight = 0; var fade = new KeyframeEffect(this.$.dropdown, [ {opacity: 0}, {opacity: 1} ], {duration: 150, easing: 'cubic-bezier(0, 0, 0.2, 1)'}); var slide = new KeyframeEffect(this.$.dropdown, [ {height: '20px', transform: 'translateY(-10px)'}, {height: maxHeight + 'px', transform: 'translateY(0)'} ], {duration: 250, easing: 'cubic-bezier(0, 0, 0.2, 1)'}); return document.timeline.play(new GroupEffect([fade, slide])); }, animateExit_: function() { return this.$.dropdown.animate([ {transform: 'translateY(0)', opacity: 1}, {transform: 'translateY(-5px)', opacity: 0} ], {duration: 100, easing: 'cubic-bezier(0.4, 0, 1, 1)'}); } }); })(); /* Copyright 2015 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. */ #wrapper { transition: transform 250ms; transition-timing-function: cubic-bezier(0, 0, 0.2, 1); } :host([closed]) #wrapper { /* 132px roughly flips the location of the button across the right edge of the * page. */ transform: translateX(132px); transition-timing-function: cubic-bezier(0.4, 0, 1, 1); } :host-context([dir=rtl]):host([closed]) #wrapper { transform: translateX(-132px); } paper-fab { --paper-fab-keyboard-focus-background: var(--viewer-icon-ink-color); --paper-fab-mini: { height: 36px; padding: 8px; width: 36px; }; @apply(--shadow-elevation-4dp); background-color: rgb(242, 242, 242); color: rgb(96, 96, 96); overflow: visible; } // Copyright 2015 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. Polymer({ is: 'viewer-zoom-button', properties: { /** * Icons to be displayed on the FAB. Multiple icons should be separated with * spaces, and will be cycled through every time the FAB is clicked. */ icons: String, /** * Array version of the list of icons. Polymer does not allow array * properties to be set from HTML, so we must use a string property and * perform the conversion manually. * @private */ icons_: { type: Array, value: [''], computed: 'computeIconsArray_(icons)' }, tooltips: Array, closed: { type: Boolean, reflectToAttribute: true, value: false }, delay: { type: Number, observer: 'delayChanged_' }, /** * Index of the icon currently being displayed. */ activeIndex: { type: Number, value: 0 }, /** * Icon currently being displayed on the FAB. * @private */ visibleIcon_: { type: String, computed: 'computeVisibleIcon_(icons_, activeIndex)' }, visibleTooltip_: { type: String, computed: 'computeVisibleTooltip_(tooltips, activeIndex)' } }, computeIconsArray_: function(icons) { return icons.split(' '); }, computeVisibleIcon_: function(icons, activeIndex) { return icons[activeIndex]; }, computeVisibleTooltip_: function(tooltips, activeIndex) { return tooltips[activeIndex]; }, delayChanged_: function() { this.$.wrapper.style.transitionDelay = this.delay + 'ms'; }, show: function() { this.closed = false; }, hide: function() { this.closed = true; }, fireClick: function() { // We cannot attach an on-click to the entire viewer-zoom-button, as this // will include clicks on the margins. Instead, proxy clicks on the FAB // through. this.fire('fabclick'); this.activeIndex = (this.activeIndex + 1) % this.icons_.length; } }); /* Copyright 2015 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. */ :host { -webkit-user-select: none; bottom: 0; padding: 48px 0; position: fixed; right: 0; z-index: 3; } :host-context([dir=rtl]) { left: 0; right: auto; } #zoom-buttons { position: relative; right: 48px; } :host-context([dir=rtl]) #zoom-buttons { left: 48px; right: auto; } viewer-zoom-button { display: block; } /* A small gap between the zoom in/zoom out buttons. */ #zoom-out-button { margin-top: 10px; } /* A larger gap between the fit button and bottom two buttons. */ #zoom-in-button { margin-top: 24px; } // Copyright 2015 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. (function() { var FIT_TO_PAGE = 0; var FIT_TO_WIDTH = 1; Polymer({ is: 'viewer-zoom-toolbar', properties: { strings: { type: Object, observer: 'updateTooltips_' }, visible_: { type: Boolean, value: true } }, isVisible: function() { return this.visible_; }, /** * @private * Change button tooltips to match any changes to localized strings. */ updateTooltips_: function() { this.$['fit-button'].tooltips = [ this.strings.tooltipFitToPage, this.strings.tooltipFitToWidth ]; this.$['zoom-in-button'].tooltips = [this.strings.tooltipZoomIn]; this.$['zoom-out-button'].tooltips = [this.strings.tooltipZoomOut]; }, /** * Handle clicks of the fit-button. */ fitToggle: function() { if (this.$['fit-button'].activeIndex == FIT_TO_WIDTH) this.fire('fit-to-width'); else this.fire('fit-to-page'); }, /** * Handle the keyboard shortcut equivalent of fit-button clicks. */ fitToggleFromHotKey: function() { this.fitToggle(); // Toggle the button state since there was no mouse click. var button = this.$['fit-button']; if (button.activeIndex == FIT_TO_WIDTH) button.activeIndex = FIT_TO_PAGE; else button.activeIndex = FIT_TO_WIDTH; }, /** * Handle clicks of the zoom-in-button. */ zoomIn: function() { this.fire('zoom-in'); }, /** * Handle clicks of the zoom-out-button. */ zoomOut: function() { this.fire('zoom-out'); }, show: function() { if (!this.visible_) { this.visible_ = true; this.$['fit-button'].show(); this.$['zoom-in-button'].show(); this.$['zoom-out-button'].show(); } }, hide: function() { if (this.visible_) { this.visible_ = false; this.$['fit-button'].hide(); this.$['zoom-in-button'].hide(); this.$['zoom-out-button'].hide(); } }, }); })(); // Copyright 2014 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. /** @fileoverview Various string utility functions */ 'use strict'; /** * Converts a string to an array of bytes. * @param {string} s The string to convert. * @param {(Array|Uint8Array)=} bytes The Array-like object into which to store * the bytes. A new Array will be created if not provided. * @return {(Array|Uint8Array)} An array of bytes representing the string. */ function UTIL_StringToBytes(s, bytes) { bytes = bytes || new Array(s.length); for (var i = 0; i < s.length; ++i) bytes[i] = s.charCodeAt(i); return bytes; } /** * Converts a byte array to a string. * @param {(Uint8Array|Array)} b input byte array. * @return {string} result. */ function UTIL_BytesToString(b) { return String.fromCharCode.apply(null, b); } /** * Converts a byte array to a hex string. * @param {(Uint8Array|Array)} b input byte array. * @return {string} result. */ function UTIL_BytesToHex(b) { if (!b) return '(null)'; var hexchars = '0123456789ABCDEF'; var hexrep = new Array(b.length * 2); for (var i = 0; i < b.length; ++i) { hexrep[i * 2 + 0] = hexchars.charAt((b[i] >> 4) & 15); hexrep[i * 2 + 1] = hexchars.charAt(b[i] & 15); } return hexrep.join(''); } function UTIL_BytesToHexWithSeparator(b, sep) { var hexchars = '0123456789ABCDEF'; var stride = 2 + (sep ? 1 : 0); var hexrep = new Array(b.length * stride); for (var i = 0; i < b.length; ++i) { if (sep) hexrep[i * stride + 0] = sep; hexrep[i * stride + stride - 2] = hexchars.charAt((b[i] >> 4) & 15); hexrep[i * stride + stride - 1] = hexchars.charAt(b[i] & 15); } return (sep ? hexrep.slice(1) : hexrep).join(''); } function UTIL_HexToBytes(h) { var hexchars = '0123456789ABCDEFabcdef'; var res = new Uint8Array(h.length / 2); for (var i = 0; i < h.length; i += 2) { if (hexchars.indexOf(h.substring(i, i + 1)) == -1) break; res[i / 2] = parseInt(h.substring(i, i + 2), 16); } return res; } function UTIL_HexToArray(h) { var hexchars = '0123456789ABCDEFabcdef'; var res = new Array(h.length / 2); for (var i = 0; i < h.length; i += 2) { if (hexchars.indexOf(h.substring(i, i + 1)) == -1) break; res[i / 2] = parseInt(h.substring(i, i + 2), 16); } return res; } function UTIL_equalArrays(a, b) { if (!a || !b) return false; if (a.length != b.length) return false; var accu = 0; for (var i = 0; i < a.length; ++i) accu |= a[i] ^ b[i]; return accu === 0; } function UTIL_ltArrays(a, b) { if (a.length < b.length) return true; if (a.length > b.length) return false; for (var i = 0; i < a.length; ++i) { if (a[i] < b[i]) return true; if (a[i] > b[i]) return false; } return false; } function UTIL_gtArrays(a, b) { return UTIL_ltArrays(b, a); } function UTIL_geArrays(a, b) { return !UTIL_ltArrays(a, b); } function UTIL_unionArrays(a, b) { var obj = {}; for (var i = 0; i < a.length; i++) { obj[a[i]] = a[i]; } for (var i = 0; i < b.length; i++) { obj[b[i]] = b[i]; } var union = []; for (var k in obj) { union.push(obj[k]); } return union; } function UTIL_getRandom(a) { var tmp = new Array(a); var rnd = new Uint8Array(a); window.crypto.getRandomValues(rnd); // Yay! for (var i = 0; i < a; ++i) tmp[i] = rnd[i] & 255; return tmp; } function UTIL_setFavicon(icon) { // Construct a new favion link tag var faviconLink = document.createElement('link'); faviconLink.rel = 'Shortcut Icon'; faviconLink.type = 'image/x-icon'; faviconLink.href = icon; // Remove the old favion, if it exists var head = document.getElementsByTagName('head')[0]; var links = head.getElementsByTagName('link'); for (var i = 0; i < links.length; i++) { var link = links[i]; if (link.type == faviconLink.type && link.rel == faviconLink.rel) { head.removeChild(link); } } // Add in the new one head.appendChild(faviconLink); } // Erase all entries in array function UTIL_clear(a) { if (a instanceof Array) { for (var i = 0; i < a.length; ++i) a[i] = 0; } } // Type tags used for ASN.1 encoding of ECDSA signatures /** @const */ var UTIL_ASN_INT = 0x02; /** @const */ var UTIL_ASN_SEQUENCE = 0x30; /** * Parse SEQ(INT, INT) from ASN1 byte array. * @param {(Uint8Array|Array)} a input to parse from. * @return {{'r': !Array, 's': !Array}|null} */ function UTIL_Asn1SignatureToJson(a) { if (a.length < 6) return null; // Too small to be valid if (a[0] != UTIL_ASN_SEQUENCE) return null; var l = a[1] & 255; if (l & 0x80) return null; // SEQ.size too large if (a.length != 2 + l) return null; // SEQ size does not match input function parseInt(off) { if (a[off] != UTIL_ASN_INT) return null; var l = a[off + 1] & 255; if (l & 0x80) return null; // INT.size too large if (off + 2 + l > a.length) return null; // Out of bounds return a.slice(off + 2, off + 2 + l); } var r = parseInt(2); if (!r) return null; var s = parseInt(2 + 2 + r.length); if (!s) return null; return {'r': r, 's': s}; } /** * Encode a JSON signature {r,s} as an ASN1 SEQ(INT, INT). May modify sig * @param {{'r': (!Array|undefined), 's': !Array}} sig * @return {!Uint8Array} */ function UTIL_JsonSignatureToAsn1(sig) { var rbytes = sig.r; var sbytes = sig.s; // ASN.1 integers are arbitrary length msb first and signed. // sig.r and sig.s are 256 bits msb first but _unsigned_, so we must // prepend a zero byte in case their high bit is set. if (rbytes[0] & 0x80) rbytes.unshift(0); if (sbytes[0] & 0x80) sbytes.unshift(0); var len = 4 + rbytes.length + sbytes.length; var buf = new Uint8Array(2 + len); var i = 0; buf[i++] = UTIL_ASN_SEQUENCE; buf[i++] = len; buf[i++] = UTIL_ASN_INT; buf[i++] = rbytes.length; buf.set(rbytes, i); i += rbytes.length; buf[i++] = UTIL_ASN_INT; buf[i++] = sbytes.length; buf.set(sbytes, i); return buf; } function UTIL_prepend_zero(s, n) { if (s.length == n) return s; var l = s.length; for (var i = 0; i < n - l; ++i) { s = '0' + s; } return s; } // hr:min:sec.milli string function UTIL_time() { var d = new Date(); var m = UTIL_prepend_zero((d.getMonth() + 1).toString(), 2); var t = UTIL_prepend_zero(d.getDate().toString(), 2); var H = UTIL_prepend_zero(d.getHours().toString(), 2); var M = UTIL_prepend_zero(d.getMinutes().toString(), 2); var S = UTIL_prepend_zero(d.getSeconds().toString(), 2); var L = UTIL_prepend_zero((d.getMilliseconds() * 1000).toString(), 6); return m + t + ' ' + H + ':' + M + ':' + S + '.' + L; } var UTIL_events = []; var UTIL_max_events = 500; function UTIL_fmt(s) { var line = UTIL_time() + ': ' + s; if (UTIL_events.push(line) > UTIL_max_events) { // Drop from head. UTIL_events.splice(0, UTIL_events.length - UTIL_max_events); } return line; } // Copyright 2014 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. // WebSafeBase64Escape and Unescape. function B64_encode(bytes, opt_length) { if (!opt_length) opt_length = bytes.length; var b64out = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'; var result = ''; var shift = 0; var accu = 0; var inputIndex = 0; while (opt_length--) { accu <<= 8; accu |= bytes[inputIndex++]; shift += 8; while (shift >= 6) { var i = (accu >> (shift - 6)) & 63; result += b64out.charAt(i); shift -= 6; } } if (shift) { accu <<= 8; shift += 8; var i = (accu >> (shift - 6)) & 63; result += b64out.charAt(i); } return result; } // Normal base64 encode; not websafe, including padding. function base64_encode(bytes, opt_length) { if (!opt_length) opt_length = bytes.length; var b64out = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; var result = ''; var shift = 0; var accu = 0; var inputIndex = 0; while (opt_length--) { accu <<= 8; accu |= bytes[inputIndex++]; shift += 8; while (shift >= 6) { var i = (accu >> (shift - 6)) & 63; result += b64out.charAt(i); shift -= 6; } } if (shift) { accu <<= 8; shift += 8; var i = (accu >> (shift - 6)) & 63; result += b64out.charAt(i); } while (result.length % 4) result += '='; return result; } var B64_inmap = [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 63, 0, 0, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 0, 0, 0, 0, 64, 0, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 0, 0, 0, 0, 0 ]; function B64_decode(string) { var bytes = []; var accu = 0; var shift = 0; for (var i = 0; i < string.length; ++i) { var c = string.charCodeAt(i); if (c < 32 || c > 127 || !B64_inmap[c - 32]) return []; accu <<= 6; accu |= (B64_inmap[c - 32] - 1); shift += 6; if (shift >= 8) { bytes.push((accu >> (shift - 8)) & 255); shift -= 8; } } return bytes; } // Copyright 2014 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. /** * @fileoverview Defines a Closeable interface. */ 'use strict'; /** * A closeable interface. * @interface */ function Closeable() {} /** Closes this object. */ Closeable.prototype.close = function() {}; // Copyright 2014 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. /** * @fileoverview Provides a countdown-based timer interface. */ 'use strict'; /** * A countdown timer. * @interface */ function Countdown() {} /** * Sets a new timeout for this timer. * @param {number} timeoutMillis how long, in milliseconds, the countdown lasts. * @param {Function=} cb called back when the countdown expires. * @return {boolean} whether the timeout could be set. */ Countdown.prototype.setTimeout = function(timeoutMillis, cb) {}; /** Clears this timer's timeout. Timers that are cleared become expired. */ Countdown.prototype.clearTimeout = function() {}; /** * @return {number} how many milliseconds are remaining until the timer expires. */ Countdown.prototype.millisecondsUntilExpired = function() {}; /** @return {boolean} whether the timer has expired. */ Countdown.prototype.expired = function() {}; /** * Constructs a new clone of this timer, while overriding its callback. * @param {Function=} cb callback for new timer. * @return {!Countdown} new clone. */ Countdown.prototype.clone = function(cb) {}; /** * A factory to create countdown timers. * @interface */ function CountdownFactory() {} /** * Creates a new timer. * @param {number} timeoutMillis How long, in milliseconds, the countdown lasts. * @param {function()=} opt_cb Called back when the countdown expires. * @return {!Countdown} The timer. */ CountdownFactory.prototype.createTimer = function(timeoutMillis, opt_cb) {}; // Copyright 2014 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. /** * @fileoverview Provides a countdown-based timer implementation. */ 'use strict'; /** * Constructs a new timer. The timer has a very limited resolution, and does * not attempt to be millisecond accurate. Its intended use is as a * low-precision timer that pauses while debugging. * @param {!SystemTimer} sysTimer The system timer implementation. * @param {number=} timeoutMillis how long, in milliseconds, the countdown * lasts. * @param {Function=} cb called back when the countdown expires. * @constructor * @implements {Countdown} */ function CountdownTimer(sysTimer, timeoutMillis, cb) { /** @private {!SystemTimer} */ this.sysTimer_ = sysTimer; this.remainingMillis = 0; this.setTimeout(timeoutMillis || 0, cb); } /** Timer interval */ CountdownTimer.TIMER_INTERVAL_MILLIS = 200; /** * Sets a new timeout for this timer. Only possible if the timer is not * currently active. * @param {number} timeoutMillis how long, in milliseconds, the countdown lasts. * @param {Function=} cb called back when the countdown expires. * @return {boolean} whether the timeout could be set. */ CountdownTimer.prototype.setTimeout = function(timeoutMillis, cb) { if (this.timeoutId) return false; if (!timeoutMillis || timeoutMillis < 0) return false; this.remainingMillis = timeoutMillis; this.cb = cb; if (this.remainingMillis > CountdownTimer.TIMER_INTERVAL_MILLIS) { this.timeoutId = this.sysTimer_.setInterval(this.timerTick.bind(this), CountdownTimer.TIMER_INTERVAL_MILLIS); } else { // Set a one-shot timer for the last interval. this.timeoutId = this.sysTimer_.setTimeout( this.timerTick.bind(this), this.remainingMillis); } return true; }; /** Clears this timer's timeout. Timers that are cleared become expired. */ CountdownTimer.prototype.clearTimeout = function() { if (this.timeoutId) { this.sysTimer_.clearTimeout(this.timeoutId); this.timeoutId = undefined; } this.remainingMillis = 0; }; /** * @return {number} how many milliseconds are remaining until the timer expires. */ CountdownTimer.prototype.millisecondsUntilExpired = function() { return this.remainingMillis > 0 ? this.remainingMillis : 0; }; /** @return {boolean} whether the timer has expired. */ CountdownTimer.prototype.expired = function() { return this.remainingMillis <= 0; }; /** * Constructs a new clone of this timer, while overriding its callback. * @param {Function=} cb callback for new timer. * @return {!Countdown} new clone. */ CountdownTimer.prototype.clone = function(cb) { return new CountdownTimer(this.sysTimer_, this.remainingMillis, cb); }; /** Timer callback. */ CountdownTimer.prototype.timerTick = function() { this.remainingMillis -= CountdownTimer.TIMER_INTERVAL_MILLIS; if (this.expired()) { this.sysTimer_.clearTimeout(this.timeoutId); this.timeoutId = undefined; if (this.cb) { this.cb(); } } }; /** * A factory for creating CountdownTimers. * @param {!SystemTimer} sysTimer The system timer implementation. * @constructor * @implements {CountdownFactory} */ function CountdownTimerFactory(sysTimer) { /** @private {!SystemTimer} */ this.sysTimer_ = sysTimer; } /** * Creates a new timer. * @param {number} timeoutMillis How long, in milliseconds, the countdown lasts. * @param {function()=} opt_cb Called back when the countdown expires. * @return {!Countdown} The timer. */ CountdownTimerFactory.prototype.createTimer = function(timeoutMillis, opt_cb) { return new CountdownTimer(this.sysTimer_, timeoutMillis, opt_cb); }; /** * Minimum timeout attenuation, below which a response couldn't be reasonably * guaranteed, in seconds. * @const */ var MINIMUM_TIMEOUT_ATTENUATION_SECONDS = 1; /** * @param {number} timeoutSeconds Timeout value in seconds. * @param {number=} opt_attenuationSeconds Attenuation value in seconds. * @return {number} The timeout value, attenuated to ensure a response can be * given before the timeout's expiration. */ function attenuateTimeoutInSeconds(timeoutSeconds, opt_attenuationSeconds) { var attenuationSeconds = opt_attenuationSeconds || MINIMUM_TIMEOUT_ATTENUATION_SECONDS; if (timeoutSeconds < attenuationSeconds) return 0; return timeoutSeconds - attenuationSeconds; } /** * Default request timeout when none is present in the request, in seconds. * @const */ var DEFAULT_REQUEST_TIMEOUT_SECONDS = 30; /** * Gets the timeout value from the request, if any, substituting * opt_defaultTimeoutSeconds or DEFAULT_REQUEST_TIMEOUT_SECONDS if the request * does not contain a timeout value. * @param {Object} request The request containing the timeout. * @param {number=} opt_defaultTimeoutSeconds * @return {number} Timeout value, in seconds. */ function getTimeoutValueFromRequest(request, opt_defaultTimeoutSeconds) { var timeoutValueSeconds; if (request.hasOwnProperty('timeoutSeconds')) { timeoutValueSeconds = request['timeoutSeconds']; } else if (request.hasOwnProperty('timeout')) { timeoutValueSeconds = request['timeout']; } else if (opt_defaultTimeoutSeconds !== undefined) { timeoutValueSeconds = opt_defaultTimeoutSeconds; } else { timeoutValueSeconds = DEFAULT_REQUEST_TIMEOUT_SECONDS; } return timeoutValueSeconds; } /** * Creates a new countdown for the given timeout value, attenuated to ensure a * response is given prior to the countdown's expiration, using the given timer * factory. * @param {CountdownFactory} timerFactory The factory to use. * @param {number} timeoutValueSeconds * @param {number=} opt_attenuationSeconds Attenuation value in seconds. * @return {!Countdown} A countdown timer. */ function createAttenuatedTimer(timerFactory, timeoutValueSeconds, opt_attenuationSeconds) { timeoutValueSeconds = attenuateTimeoutInSeconds(timeoutValueSeconds, opt_attenuationSeconds); return timerFactory.createTimer(timeoutValueSeconds * 1000); } // Copyright 2014 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. // SHA256 in javascript. // // SHA256 { // SHA256(); // void reset(); // void update(byte[] data, opt_length); // byte[32] digest(); // } /** @constructor */ function SHA256() { this._buf = new Array(64); this._W = new Array(64); this._pad = new Array(64); this._k = [ 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2]; this._pad[0] = 0x80; for (var i = 1; i < 64; ++i) this._pad[i] = 0; this.reset(); } /** Reset the hasher */ SHA256.prototype.reset = function() { this._chain = [ 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19]; this._inbuf = 0; this._total = 0; }; /** Hash the next block of 64 bytes * @param {Array} buf A 64 byte buffer */ SHA256.prototype._compress = function(buf) { var W = this._W; var k = this._k; function _rotr(w, r) { return ((w << (32 - r)) | (w >>> r)); }; // get 16 big endian words for (var i = 0; i < 64; i += 4) { var w = (buf[i] << 24) | (buf[i + 1] << 16) | (buf[i + 2] << 8) | (buf[i + 3]); W[i / 4] = w; } // expand to 64 words for (var i = 16; i < 64; ++i) { var s0 = _rotr(W[i - 15], 7) ^ _rotr(W[i - 15], 18) ^ (W[i - 15] >>> 3); var s1 = _rotr(W[i - 2], 17) ^ _rotr(W[i - 2], 19) ^ (W[i - 2] >>> 10); W[i] = (W[i - 16] + s0 + W[i - 7] + s1) & 0xffffffff; } var A = this._chain[0]; var B = this._chain[1]; var C = this._chain[2]; var D = this._chain[3]; var E = this._chain[4]; var F = this._chain[5]; var G = this._chain[6]; var H = this._chain[7]; for (var i = 0; i < 64; ++i) { var S0 = _rotr(A, 2) ^ _rotr(A, 13) ^ _rotr(A, 22); var maj = (A & B) ^ (A & C) ^ (B & C); var t2 = (S0 + maj) & 0xffffffff; var S1 = _rotr(E, 6) ^ _rotr(E, 11) ^ _rotr(E, 25); var ch = (E & F) ^ ((~E) & G); var t1 = (H + S1 + ch + k[i] + W[i]) & 0xffffffff; H = G; G = F; F = E; E = (D + t1) & 0xffffffff; D = C; C = B; B = A; A = (t1 + t2) & 0xffffffff; } this._chain[0] += A; this._chain[1] += B; this._chain[2] += C; this._chain[3] += D; this._chain[4] += E; this._chain[5] += F; this._chain[6] += G; this._chain[7] += H; }; /** Update the hash with additional data * @param {Array|Uint8Array} bytes The data * @param {number=} opt_length How many bytes to hash, if not all */ SHA256.prototype.update = function(bytes, opt_length) { if (!opt_length) opt_length = bytes.length; this._total += opt_length; for (var n = 0; n < opt_length; ++n) { this._buf[this._inbuf++] = bytes[n]; if (this._inbuf == 64) { this._compress(this._buf); this._inbuf = 0; } } }; /** Update the hash with a specified range from a data buffer * @param {Array} bytes The data buffer * @param {number} start Starting index of the range in bytes * @param {number} end End index, will not be included in range */ SHA256.prototype.updateRange = function(bytes, start, end) { this._total += (end - start); for (var n = start; n < end; ++n) { this._buf[this._inbuf++] = bytes[n]; if (this._inbuf == 64) { this._compress(this._buf); this._inbuf = 0; } } }; /** * Optionally update the hash with additional arguments, and return the * resulting hash value. * @param {...*} var_args Data buffers to hash * @return {Array} the SHA256 hash value. */ SHA256.prototype.digest = function(var_args) { for (var i = 0; i < arguments.length; ++i) this.update(arguments[i]); var digest = new Array(32); var totalBits = this._total * 8; // add pad 0x80 0x00* if (this._inbuf < 56) this.update(this._pad, 56 - this._inbuf); else this.update(this._pad, 64 - (this._inbuf - 56)); // add # bits, big endian for (var i = 63; i >= 56; --i) { this._buf[i] = totalBits & 255; totalBits >>>= 8; } this._compress(this._buf); var n = 0; for (var i = 0; i < 8; ++i) for (var j = 24; j >= 0; j -= 8) digest[n++] = (this._chain[i] >> j) & 255; return digest; }; // Copyright 2014 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. /** * @fileoverview Provides an interface representing the browser/extension * system's timer interface. */ 'use strict'; /** * An interface representing the browser/extension system's timer interface. * @interface */ function SystemTimer() {} /** * Sets a single-shot timer. * @param {function()} func Called back when the timer expires. * @param {number} timeoutMillis How long until the timer fires, in * milliseconds. * @return {number} A timeout ID, which can be used to cancel the timer. */ SystemTimer.prototype.setTimeout = function(func, timeoutMillis) {}; /** * Clears a previously set timer. * @param {number} timeoutId The ID of the timer to clear. */ SystemTimer.prototype.clearTimeout = function(timeoutId) {}; /** * Sets a repeating interval timer. * @param {function()} func Called back each time the timer fires. * @param {number} timeoutMillis How long until the timer fires, in * milliseconds. * @return {number} A timeout ID, which can be used to cancel the timer. */ SystemTimer.prototype.setInterval = function(func, timeoutMillis) {}; /** * Clears a previously set interval timer. * @param {number} timeoutId The ID of the timer to clear. */ SystemTimer.prototype.clearInterval = function(timeoutId) {}; // Copyright 2014 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. /** * @fileoverview Implements a low-level gnubby driver based on chrome.hid. */ 'use strict'; /** * Low level gnubby 'driver'. One per physical USB device. * @param {Gnubbies} gnubbies The gnubbies instances this device is enumerated * in. * @param {!chrome.hid.HidConnectInfo} dev The connection to the device. * @param {number} id The device's id. * @constructor * @implements {GnubbyDevice} */ function HidGnubbyDevice(gnubbies, dev, id) { /** @private {Gnubbies} */ this.gnubbies_ = gnubbies; this.dev = dev; this.id = id; this.txqueue = []; this.clients = []; this.lockCID = 0; // channel ID of client holding a lock, if != 0. this.lockMillis = 0; // current lock period. this.lockTID = null; // timer id of lock timeout. this.closing = false; // device to be closed by receive loop. this.updating = false; // device firmware is in final stage of updating. } /** * Namespace for the HidGnubbyDevice implementation. * @const */ HidGnubbyDevice.NAMESPACE = 'hid'; /** Destroys this low-level device instance. */ HidGnubbyDevice.prototype.destroy = function() { if (!this.dev) return; // Already dead. function closeLowLevelDevice(dev) { chrome.hid.disconnect(dev.connectionId, function() { if (chrome.runtime.lastError) { console.warn(UTIL_fmt('Device ' + dev.connectionId + ' couldn\'t be disconnected:')); console.warn(UTIL_fmt(chrome.runtime.lastError.message)); return; } console.log(UTIL_fmt('Device ' + dev.connectionId + ' closed')); }); } this.gnubbies_.removeOpenDevice( {namespace: HidGnubbyDevice.NAMESPACE, device: this.id}); this.closing = true; console.log(UTIL_fmt('HidGnubbyDevice.destroy()')); // Synthesize a close error frame to alert all clients, // some of which might be in read state. // // Use magic CID 0 to address all. this.publishFrame_(new Uint8Array([ 0, 0, 0, 0, // broadcast CID GnubbyDevice.CMD_ERROR, 0, 1, // length GnubbyDevice.GONE]).buffer); // Set all clients to closed status and remove them. while (this.clients.length != 0) { var client = this.clients.shift(); if (client) client.closed = true; } if (this.lockTID) { window.clearTimeout(this.lockTID); this.lockTID = null; } var dev = this.dev; this.dev = null; var reallyCloseDevice = closeLowLevelDevice.bind(null, dev); if (this.destroyHook_) { var p = this.destroyHook_(); if (!p) { reallyCloseDevice(); return; } // When this method returns, a device reference may still be held, until the // promise completes. p.then(reallyCloseDevice); } else { reallyCloseDevice(); } }; /** * Sets a callback that will get called when this device instance is destroyed. * @param {function() : ?Promise} cb Called back when closed. Callback may * yield a promise that resolves when the close hook completes. */ HidGnubbyDevice.prototype.setDestroyHook = function(cb) { this.destroyHook_ = cb; }; /** * Push frame to all clients. * @param {ArrayBuffer} f Data to push * @private */ HidGnubbyDevice.prototype.publishFrame_ = function(f) { var old = this.clients; var remaining = []; var changes = false; for (var i = 0; i < old.length; ++i) { var client = old[i]; if (client.receivedFrame(f)) { // Client still alive; keep on list. remaining.push(client); } else { changes = true; console.log(UTIL_fmt( '[' + Gnubby.hexCid(client.cid) + '] left?')); } } if (changes) this.clients = remaining; }; /** * Register a client for this gnubby. * @param {*} who The client. */ HidGnubbyDevice.prototype.registerClient = function(who) { for (var i = 0; i < this.clients.length; ++i) { if (this.clients[i] === who) return; // Already registered. } this.clients.push(who); if (this.clients.length == 1) { // First client? Kick off read loop. this.readLoop_(); } }; /** * De-register a client. * @param {*} who The client. * @return {number} The number of remaining listeners for this device, or -1 * Returns number of remaining listeners for this device. * if this had no clients to start with. */ HidGnubbyDevice.prototype.deregisterClient = function(who) { var current = this.clients; if (current.length == 0) return -1; this.clients = []; for (var i = 0; i < current.length; ++i) { var client = current[i]; if (client !== who) this.clients.push(client); } return this.clients.length; }; /** * @param {*} who The client. * @return {boolean} Whether this device has who as a client. */ HidGnubbyDevice.prototype.hasClient = function(who) { if (this.clients.length == 0) return false; for (var i = 0; i < this.clients.length; ++i) { if (who === this.clients[i]) return true; } return false; }; /** * Reads all incoming frames and notifies clients of their receipt. * @private */ HidGnubbyDevice.prototype.readLoop_ = function() { //console.log(UTIL_fmt('entering readLoop')); if (!this.dev) return; if (this.closing) { this.destroy(); return; } // No interested listeners, yet we hit readLoop(). // Must be clean-up. We do this here to make sure no transfer is pending. if (!this.clients.length) { this.closing = true; this.destroy(); return; } // firmwareUpdate() sets this.updating when writing the last block before // the signature. We process that reply with the already pending // read transfer but we do not want to start another read transfer for the // signature block, since that request will have no reply. // Instead we will see the device drop and re-appear on the bus. // Current libusb on some platforms gets unhappy when transfer are pending // when that happens. // TODO: revisit once Chrome stabilizes its behavior. if (this.updating) { console.log(UTIL_fmt('device updating. Ending readLoop()')); return; } var self = this; chrome.hid.receive( this.dev.connectionId, function(report_id, data) { if (chrome.runtime.lastError || !data) { console.log(UTIL_fmt('receive got lastError:')); console.log(UTIL_fmt(chrome.runtime.lastError.message)); window.setTimeout(function() { self.destroy(); }, 0); return; } var u8 = new Uint8Array(data); console.log(UTIL_fmt('<' + UTIL_BytesToHex(u8))); self.publishFrame_(data); // Read more. window.setTimeout(function() { self.readLoop_(); }, 0); } ); }; /** * Check whether channel is locked for this request or not. * @param {number} cid Channel id * @param {number} cmd Request command * @return {boolean} true if not locked for this request. * @private */ HidGnubbyDevice.prototype.checkLock_ = function(cid, cmd) { if (this.lockCID) { // We have an active lock. if (this.lockCID != cid) { // Some other channel has active lock. if (cmd != GnubbyDevice.CMD_SYNC && cmd != GnubbyDevice.CMD_INIT) { // Anything but SYNC|INIT gets an immediate busy. var busy = new Uint8Array( [(cid >> 24) & 255, (cid >> 16) & 255, (cid >> 8) & 255, cid & 255, GnubbyDevice.CMD_ERROR, 0, 1, // length GnubbyDevice.BUSY]); // Log the synthetic busy too. console.log(UTIL_fmt('<' + UTIL_BytesToHex(busy))); this.publishFrame_(busy.buffer); return false; } // SYNC|INIT gets to go to the device to flush OS tx/rx queues. // The usb firmware is to alway respond to SYNC/INIT, // regardless of lock status. } } return true; }; /** * Update or grab lock. * @param {number} cid Channel ID * @param {number} cmd Command * @param {number} arg Command argument * @private */ HidGnubbyDevice.prototype.updateLock_ = function(cid, cmd, arg) { if (this.lockCID == 0 || this.lockCID == cid) { // It is this caller's or nobody's lock. if (this.lockTID) { window.clearTimeout(this.lockTID); this.lockTID = null; } if (cmd == GnubbyDevice.CMD_LOCK) { var nseconds = arg; if (nseconds != 0) { this.lockCID = cid; // Set tracking time to be .1 seconds longer than usb device does. this.lockMillis = nseconds * 1000 + 100; } else { // Releasing lock voluntarily. this.lockCID = 0; } } // (re)set the lock timeout if we still hold it. if (this.lockCID) { var self = this; this.lockTID = window.setTimeout( function() { console.warn(UTIL_fmt( 'lock for CID ' + Gnubby.hexCid(cid) + ' expired!')); self.lockTID = null; self.lockCID = 0; }, this.lockMillis); } } }; /** * Queue command to be sent. * If queue was empty, initiate the write. * @param {number} cid The client's channel ID. * @param {number} cmd The command to send. * @param {ArrayBuffer|Uint8Array} data Command arguments */ HidGnubbyDevice.prototype.queueCommand = function(cid, cmd, data) { if (!this.dev) return; if (!this.checkLock_(cid, cmd)) return; var u8 = new Uint8Array(data); var f = new Uint8Array(64); HidGnubbyDevice.setCid_(f, cid); f[4] = cmd; f[5] = (u8.length >> 8); f[6] = (u8.length & 255); var lockArg = (u8.length > 0) ? u8[0] : 0; // Fragment over our 64 byte frames. var n = 7; var seq = 0; for (var i = 0; i < u8.length; ++i) { f[n++] = u8[i]; if (n == f.length) { this.queueFrame_(f.buffer, cid, cmd, lockArg); f = new Uint8Array(64); HidGnubbyDevice.setCid_(f, cid); cmd = f[4] = seq++; n = 5; } } if (n != 5) { this.queueFrame_(f.buffer, cid, cmd, lockArg); } }; /** * Sets the channel id in the frame. * @param {Uint8Array} frame Data frame * @param {number} cid The client's channel ID. * @private */ HidGnubbyDevice.setCid_ = function(frame, cid) { frame[0] = cid >>> 24; frame[1] = cid >>> 16; frame[2] = cid >>> 8; frame[3] = cid; }; /** * Updates the lock, and queues the frame for sending. Also begins sending if * no other writes are outstanding. * @param {ArrayBuffer} frame Data frame * @param {number} cid The client's channel ID. * @param {number} cmd The command to send. * @param {number} arg Command argument * @private */ HidGnubbyDevice.prototype.queueFrame_ = function(frame, cid, cmd, arg) { this.updateLock_(cid, cmd, arg); var wasEmpty = (this.txqueue.length == 0); this.txqueue.push(frame); if (wasEmpty) this.writePump_(); }; /** * Stuff queued frames from txqueue[] to device, one by one. * @private */ HidGnubbyDevice.prototype.writePump_ = function() { if (!this.dev) return; // Ignore. if (this.txqueue.length == 0) return; // Done with current queue. var frame = this.txqueue[0]; var self = this; function transferComplete() { if (chrome.runtime.lastError) { console.log(UTIL_fmt('send got lastError:')); console.log(UTIL_fmt(chrome.runtime.lastError.message)); window.setTimeout(function() { self.destroy(); }, 0); return; } self.txqueue.shift(); // drop sent frame from queue. if (self.txqueue.length != 0) { window.setTimeout(function() { self.writePump_(); }, 0); } }; var u8 = new Uint8Array(frame); // See whether this requires scrubbing before logging. var alternateLog = Gnubby.hasOwnProperty('redactRequestLog') && Gnubby['redactRequestLog'](u8); if (alternateLog) { console.log(UTIL_fmt('>' + alternateLog)); } else { console.log(UTIL_fmt('>' + UTIL_BytesToHex(u8))); } var u8f = new Uint8Array(64); for (var i = 0; i < u8.length; ++i) { u8f[i] = u8[i]; } chrome.hid.send( this.dev.connectionId, 0, // report Id. Must be 0 for our use. u8f.buffer, transferComplete ); }; /** * List of legacy HID devices that do not support the F1D0 usage page as * mandated by the spec, but still need to be supported. * TODO: remove when these devices no longer need to be supported. * @const */ HidGnubbyDevice.HID_VID_PIDS = [ {'vendorId': 4176, 'productId': 512} // Google-specific Yubico HID ]; /** * @param {function(Array)} cb Enumeration callback * @param {GnubbyEnumerationTypes=} opt_type Which type of enumeration to do. */ HidGnubbyDevice.enumerate = function(cb, opt_type) { /** * One pass using getDevices, and one for each of the hardcoded vid/pids. * @const */ var ENUMERATE_PASSES = 1 + HidGnubbyDevice.HID_VID_PIDS.length; var numEnumerated = 0; var allDevs = []; function enumerated(filter, devs) { // Don't double-add a device; it'll just confuse things. // We assume the various calls to getDevices() return from the same // deviceId pool. for (var i = 0; i < devs.length; i++) { var dev = devs[i]; dev.enumeratedBy = filter; // Unfortunately indexOf is not usable, since the two calls produce // different objects. Compare their deviceIds instead. var found = false; for (var j = 0; j < allDevs.length; j++) { if (allDevs[j].deviceId == dev.deviceId) { found = true; allDevs[j].enumeratedBy = filter; break; } } if (!found) { allDevs.push(dev); } } if (++numEnumerated == ENUMERATE_PASSES) { cb(allDevs); } } // Pass 1: usagePage-based enumeration, for FIDO U2F devices. If non-FIDO // devices are asked for, "implement" this pass by providing it the empty // list. (enumerated requires that it's called once per pass.) var f1d0Filter = {usagePage: 0xf1d0}; if (opt_type == GnubbyEnumerationTypes.VID_PID) { enumerated(f1d0Filter, []); } else { chrome.hid.getDevices({filters: [f1d0Filter]}, enumerated.bind(null, f1d0Filter)); } // Pass 2: vid/pid-based enumeration, for legacy devices. If FIDO devices // are asked for, "implement" this pass by providing it the empty list. if (opt_type == GnubbyEnumerationTypes.FIDO_U2F) { enumerated(false, []); } else { for (var i = 0; i < HidGnubbyDevice.HID_VID_PIDS.length; i++) { var vidPid = HidGnubbyDevice.HID_VID_PIDS[i]; chrome.hid.getDevices({filters: [vidPid]}, enumerated.bind(null, vidPid)); } } }; /** * @param {Gnubbies} gnubbies The gnubbies instances this device is enumerated * in. * @param {number} which The index of the device to open. * @param {!chrome.hid.HidDeviceInfo} dev The device to open. * @param {function(number, GnubbyDevice=)} cb Called back with the * result of opening the device. */ HidGnubbyDevice.open = function(gnubbies, which, dev, cb) { chrome.hid.connect(dev.deviceId, function(handle) { if (chrome.runtime.lastError) { console.log(UTIL_fmt('connect got lastError:')); console.log(UTIL_fmt(chrome.runtime.lastError.message)); } if (!handle) { console.warn(UTIL_fmt('failed to connect device. permissions issue?')); cb(-GnubbyDevice.NODEVICE); return; } var nonNullHandle = /** @type {!chrome.hid.HidConnectInfo} */ (handle); var gnubby = new HidGnubbyDevice(gnubbies, nonNullHandle, which); cb(-GnubbyDevice.OK, gnubby); }); }; /** * @param {*} dev A browser API device object * @return {GnubbyDeviceId} A device identifier for the device. */ HidGnubbyDevice.deviceToDeviceId = function(dev) { var hidDev = /** @type {!chrome.hid.HidDeviceInfo} */ (dev); var deviceId = { namespace: HidGnubbyDevice.NAMESPACE, enumeratedBy: hidDev.enumeratedBy, device: hidDev.deviceId }; return deviceId; }; /** * Registers this implementation with gnubbies. * @param {Gnubbies} gnubbies Gnubbies registry */ HidGnubbyDevice.register = function(gnubbies) { var HID_GNUBBY_IMPL = { isSharedAccess: true, enumerate: HidGnubbyDevice.enumerate, deviceToDeviceId: HidGnubbyDevice.deviceToDeviceId, open: HidGnubbyDevice.open }; gnubbies.registerNamespace(HidGnubbyDevice.NAMESPACE, HID_GNUBBY_IMPL); }; // Copyright 2014 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. /** * @fileoverview Implements a low-level gnubby driver based on chrome.usb. */ 'use strict'; /** * Low level gnubby 'driver'. One per physical USB device. * @param {Gnubbies} gnubbies The gnubbies instances this device is enumerated * in. * @param {!chrome.usb.ConnectionHandle} dev The device. * @param {number} id The device's id. * @param {number} inEndpoint The device's in endpoint. * @param {number} outEndpoint The device's out endpoint. * @constructor * @implements {GnubbyDevice} */ function UsbGnubbyDevice(gnubbies, dev, id, inEndpoint, outEndpoint) { /** @private {Gnubbies} */ this.gnubbies_ = gnubbies; this.dev = dev; this.id = id; this.inEndpoint = inEndpoint; this.outEndpoint = outEndpoint; this.txqueue = []; this.clients = []; this.lockCID = 0; // channel ID of client holding a lock, if != 0. this.lockMillis = 0; // current lock period. this.lockTID = null; // timer id of lock timeout. this.closing = false; // device to be closed by receive loop. this.updating = false; // device firmware is in final stage of updating. this.inTransferPending = false; this.outTransferPending = false; } /** * Namespace for the UsbGnubbyDevice implementation. * @const */ UsbGnubbyDevice.NAMESPACE = 'usb'; /** Destroys this low-level device instance. */ UsbGnubbyDevice.prototype.destroy = function() { function closeLowLevelDevice(dev) { chrome.usb.releaseInterface(dev, 0, function() { if (chrome.runtime.lastError) { console.warn(UTIL_fmt('Device ' + dev.handle + ' couldn\'t be released:')); console.warn(UTIL_fmt(chrome.runtime.lastError.message)); return; } console.log(UTIL_fmt('Device ' + dev.handle + ' released')); chrome.usb.closeDevice(dev, function() { if (chrome.runtime.lastError) { console.warn(UTIL_fmt('Device ' + dev.handle + ' couldn\'t be closed:')); console.warn(UTIL_fmt(chrome.runtime.lastError.message)); return; } console.log(UTIL_fmt('Device ' + dev.handle + ' closed')); }); }); } if (!this.dev) return; // Already dead. this.gnubbies_.removeOpenDevice( {namespace: UsbGnubbyDevice.NAMESPACE, device: this.id}); this.closing = true; console.log(UTIL_fmt('UsbGnubbyDevice.destroy()')); // Synthesize a close error frame to alert all clients, // some of which might be in read state. // // Use magic CID 0 to address all. this.publishFrame_(new Uint8Array([ 0, 0, 0, 0, // broadcast CID GnubbyDevice.CMD_ERROR, 0, 1, // length GnubbyDevice.GONE]).buffer); // Set all clients to closed status and remove them. while (this.clients.length != 0) { var client = this.clients.shift(); if (client) client.closed = true; } if (this.lockTID) { window.clearTimeout(this.lockTID); this.lockTID = null; } var dev = this.dev; this.dev = null; var reallyCloseDevice = closeLowLevelDevice.bind(null, dev); if (this.destroyHook_) { var p = this.destroyHook_(); if (!p) { reallyCloseDevice(); return; } p.then(reallyCloseDevice); } else { reallyCloseDevice(); } }; /** * Sets a callback that will get called when this device instance is destroyed. * @param {function() : ?Promise} cb Called back when closed. Callback may * yield a promise that resolves when the close hook completes. */ UsbGnubbyDevice.prototype.setDestroyHook = function(cb) { this.destroyHook_ = cb; }; /** * Push frame to all clients. * @param {ArrayBuffer} f Data frame * @private */ UsbGnubbyDevice.prototype.publishFrame_ = function(f) { var old = this.clients; var remaining = []; var changes = false; for (var i = 0; i < old.length; ++i) { var client = old[i]; if (client.receivedFrame(f)) { // Client still alive; keep on list. remaining.push(client); } else { changes = true; console.log(UTIL_fmt( '[' + Gnubby.hexCid(client.cid) + '] left?')); } } if (changes) this.clients = remaining; }; /** * @return {boolean} whether this device is open and ready to use. * @private */ UsbGnubbyDevice.prototype.readyToUse_ = function() { if (this.closing) return false; if (!this.dev) return false; return true; }; /** * Reads one reply from the low-level device. * @private */ UsbGnubbyDevice.prototype.readOneReply_ = function() { if (!this.readyToUse_()) return; // No point in continuing. if (this.updating) return; // Do not bother waiting for final update reply. var self = this; function inTransferComplete(x) { self.inTransferPending = false; if (!self.readyToUse_()) return; // No point in continuing. if (chrome.runtime.lastError) { console.warn(UTIL_fmt('in bulkTransfer got lastError: ')); console.warn(UTIL_fmt(chrome.runtime.lastError.message)); window.setTimeout(function() { self.destroy(); }, 0); return; } if (x.data) { var u8 = new Uint8Array(x.data); console.log(UTIL_fmt('<' + UTIL_BytesToHex(u8))); self.publishFrame_(x.data); // Write another pending request, if any. window.setTimeout( function() { self.txqueue.shift(); // Drop sent frame from queue. self.writeOneRequest_(); }, 0); } else { console.log(UTIL_fmt('no x.data!')); console.log(x); window.setTimeout(function() { self.destroy(); }, 0); } } if (this.inTransferPending == false) { this.inTransferPending = true; chrome.usb.bulkTransfer( /** @type {!chrome.usb.ConnectionHandle} */(this.dev), { direction: 'in', endpoint: this.inEndpoint, length: 2048 }, inTransferComplete); } else { throw 'inTransferPending!'; } }; /** * Register a client for this gnubby. * @param {*} who The client. */ UsbGnubbyDevice.prototype.registerClient = function(who) { for (var i = 0; i < this.clients.length; ++i) { if (this.clients[i] === who) return; // Already registered. } this.clients.push(who); }; /** * De-register a client. * @param {*} who The client. * @return {number} The number of remaining listeners for this device, or -1 * Returns number of remaining listeners for this device. * if this had no clients to start with. */ UsbGnubbyDevice.prototype.deregisterClient = function(who) { var current = this.clients; if (current.length == 0) return -1; this.clients = []; for (var i = 0; i < current.length; ++i) { var client = current[i]; if (client !== who) this.clients.push(client); } return this.clients.length; }; /** * @param {*} who The client. * @return {boolean} Whether this device has who as a client. */ UsbGnubbyDevice.prototype.hasClient = function(who) { if (this.clients.length == 0) return false; for (var i = 0; i < this.clients.length; ++i) { if (who === this.clients[i]) return true; } return false; }; /** * Stuff queued frames from txqueue[] to device, one by one. * @private */ UsbGnubbyDevice.prototype.writeOneRequest_ = function() { if (!this.readyToUse_()) return; // No point in continuing. if (this.txqueue.length == 0) return; // Nothing to send. var frame = this.txqueue[0]; var self = this; function OutTransferComplete(x) { self.outTransferPending = false; if (!self.readyToUse_()) return; // No point in continuing. if (chrome.runtime.lastError) { console.warn(UTIL_fmt('out bulkTransfer lastError: ')); console.warn(UTIL_fmt(chrome.runtime.lastError.message)); window.setTimeout(function() { self.destroy(); }, 0); return; } window.setTimeout(function() { self.readOneReply_(); }, 0); }; var u8 = new Uint8Array(frame); // See whether this requires scrubbing before logging. var alternateLog = Gnubby.hasOwnProperty('redactRequestLog') && Gnubby['redactRequestLog'](u8); if (alternateLog) { console.log(UTIL_fmt('>' + alternateLog)); } else { console.log(UTIL_fmt('>' + UTIL_BytesToHex(u8))); } if (this.outTransferPending == false) { this.outTransferPending = true; chrome.usb.bulkTransfer( /** @type {!chrome.usb.ConnectionHandle} */(this.dev), { direction: 'out', endpoint: this.outEndpoint, data: frame }, OutTransferComplete); } else { throw 'outTransferPending!'; } }; /** * Check whether channel is locked for this request or not. * @param {number} cid Channel id * @param {number} cmd Command to be sent * @return {boolean} true if not locked for this request. * @private */ UsbGnubbyDevice.prototype.checkLock_ = function(cid, cmd) { if (this.lockCID) { // We have an active lock. if (this.lockCID != cid) { // Some other channel has active lock. if (cmd != GnubbyDevice.CMD_SYNC && cmd != GnubbyDevice.CMD_INIT) { // Anything but SYNC|INIT gets an immediate busy. var busy = new Uint8Array( [(cid >> 24) & 255, (cid >> 16) & 255, (cid >> 8) & 255, cid & 255, GnubbyDevice.CMD_ERROR, 0, 1, // length GnubbyDevice.BUSY]); // Log the synthetic busy too. console.log(UTIL_fmt('<' + UTIL_BytesToHex(busy))); this.publishFrame_(busy.buffer); return false; } // SYNC|INIT get to go to the device to flush OS tx/rx queues. // The usb firmware is to always respond to SYNC|INIT, // regardless of lock status. } } return true; }; /** * Update or grab lock. * @param {number} cid Channel id * @param {number} cmd Command * @param {number} arg Command argument * @private */ UsbGnubbyDevice.prototype.updateLock_ = function(cid, cmd, arg) { if (this.lockCID == 0 || this.lockCID == cid) { // It is this caller's or nobody's lock. if (this.lockTID) { window.clearTimeout(this.lockTID); this.lockTID = null; } if (cmd == GnubbyDevice.CMD_LOCK) { var nseconds = arg; if (nseconds != 0) { this.lockCID = cid; // Set tracking time to be .1 seconds longer than usb device does. this.lockMillis = nseconds * 1000 + 100; } else { // Releasing lock voluntarily. this.lockCID = 0; } } // (re)set the lock timeout if we still hold it. if (this.lockCID) { var self = this; this.lockTID = window.setTimeout( function() { console.warn(UTIL_fmt( 'lock for CID ' + Gnubby.hexCid(cid) + ' expired!')); self.lockTID = null; self.lockCID = 0; }, this.lockMillis); } } }; /** * Queue command to be sent. * If queue was empty, initiate the write. * @param {number} cid The client's channel ID. * @param {number} cmd The command to send. * @param {ArrayBuffer|Uint8Array} data Command argument data */ UsbGnubbyDevice.prototype.queueCommand = function(cid, cmd, data) { if (!this.dev) return; if (!this.checkLock_(cid, cmd)) return; var u8 = new Uint8Array(data); var frame = new Uint8Array(u8.length + 7); frame[0] = cid >>> 24; frame[1] = cid >>> 16; frame[2] = cid >>> 8; frame[3] = cid; frame[4] = cmd; frame[5] = (u8.length >> 8); frame[6] = (u8.length & 255); frame.set(u8, 7); var lockArg = (u8.length > 0) ? u8[0] : 0; this.updateLock_(cid, cmd, lockArg); var wasEmpty = (this.txqueue.length == 0); this.txqueue.push(frame.buffer); if (wasEmpty) this.writeOneRequest_(); }; /** * @const */ UsbGnubbyDevice.WINUSB_VID_PIDS = [ {'vendorId': 4176, 'productId': 529} // Yubico WinUSB ]; /** * @param {function(Array)} cb Enumerate callback * @param {GnubbyEnumerationTypes=} opt_type Which type of enumeration to do. */ UsbGnubbyDevice.enumerate = function(cb, opt_type) { // UsbGnubbyDevices are all non-FIDO devices, so return an empty list if // FIDO is what's wanted. if (opt_type == GnubbyEnumerationTypes.FIDO_U2F) { cb([]); return; } var numEnumerated = 0; var allDevs = []; function enumerated(vidPid, devs) { if (devs) { for (var i = 0; i < devs.length; i++) { devs[i].enumeratedBy = vidPid; } allDevs = allDevs.concat(devs); } if (++numEnumerated == UsbGnubbyDevice.WINUSB_VID_PIDS.length) { cb(allDevs); } } for (var i = 0; i < UsbGnubbyDevice.WINUSB_VID_PIDS.length; i++) { var vidPid = UsbGnubbyDevice.WINUSB_VID_PIDS[i]; chrome.usb.getDevices(vidPid, enumerated.bind(null, vidPid)); } }; /** * @typedef {?{ * address: number, * type: string, * direction: string, * maximumPacketSize: number, * synchronization: (string|undefined), * usage: (string|undefined), * pollingInterval: (number|undefined) * }} * @see http://developer.chrome.com/apps/usb.html#method-listInterfaces */ var InterfaceEndpoint; /** * @typedef {?{ * interfaceNumber: number, * alternateSetting: number, * interfaceClass: number, * interfaceSubclass: number, * interfaceProtocol: number, * description: (string|undefined), * endpoints: !Array * }} * @see http://developer.chrome.com/apps/usb.html#method-listInterfaces */ var InterfaceDescriptor; /** * @param {Gnubbies} gnubbies The gnubbies instances this device is enumerated * in. * @param {number} which The index of the device to open. * @param {!chrome.usb.Device} dev The device to open. * @param {function(number, GnubbyDevice=)} cb Called back with the * result of opening the device. */ UsbGnubbyDevice.open = function(gnubbies, which, dev, cb) { /** @param {chrome.usb.ConnectionHandle=} handle Connection handle */ function deviceOpened(handle) { if (chrome.runtime.lastError) { console.warn(UTIL_fmt('openDevice got lastError:')); console.warn(UTIL_fmt(chrome.runtime.lastError.message)); console.warn(UTIL_fmt('failed to open device. permissions issue?')); cb(-GnubbyDevice.NODEVICE); return; } var nonNullHandle = /** @type {!chrome.usb.ConnectionHandle} */ (handle); chrome.usb.listInterfaces(nonNullHandle, function(descriptors) { var inEndpoint, outEndpoint; for (var i = 0; i < descriptors.length; i++) { var descriptor = /** @type {InterfaceDescriptor} */ (descriptors[i]); for (var j = 0; j < descriptor.endpoints.length; j++) { var endpoint = descriptor.endpoints[j]; if (inEndpoint == undefined && endpoint.type == 'bulk' && endpoint.direction == 'in') { inEndpoint = endpoint.address; } if (outEndpoint == undefined && endpoint.type == 'bulk' && endpoint.direction == 'out') { outEndpoint = endpoint.address; } } } if (inEndpoint == undefined || outEndpoint == undefined) { console.warn(UTIL_fmt('device lacking an endpoint (broken?)')); chrome.usb.closeDevice(nonNullHandle); cb(-GnubbyDevice.NODEVICE); return; } // Try getting it claimed now. chrome.usb.claimInterface(nonNullHandle, 0, function() { if (chrome.runtime.lastError) { console.warn(UTIL_fmt('lastError: ' + chrome.runtime.lastError)); console.log(chrome.runtime.lastError); } var claimed = !chrome.runtime.lastError; if (!claimed) { console.warn(UTIL_fmt('failed to claim interface. busy?')); // Claim failed? Let the callers know and bail out. chrome.usb.closeDevice(nonNullHandle); cb(-GnubbyDevice.BUSY); return; } // Restore the enumeratedBy value, if we had it. if (enumeratedBy) { dev.enumeratedBy = enumeratedBy; } var gnubby = new UsbGnubbyDevice(gnubbies, nonNullHandle, which, inEndpoint, outEndpoint); cb(-GnubbyDevice.OK, gnubby); }); }); } var enumeratedBy = dev.enumeratedBy; if (UsbGnubbyDevice.runningOnCrOS === undefined) { UsbGnubbyDevice.runningOnCrOS = (window.navigator.appVersion.indexOf('; CrOS ') != -1); } // dev contains an enumeratedBy value, which we need to strip prior to // calling Chrome APIs with it. delete dev.enumeratedBy; if (UsbGnubbyDevice.runningOnCrOS) { chrome.usb.requestAccess(dev, 0, function(success) { // Even though the argument to requestAccess is a chrome.usb.Device, the // access request is for access to all devices with the same vid/pid. // Curiously, if the first chrome.usb.requestAccess succeeds, a second // call with a separate device with the same vid/pid fails. Since // chrome.usb.openDevice will fail if a previous access request really // failed, just ignore the outcome of the access request and move along. chrome.usb.openDevice(dev, deviceOpened); }); } else { chrome.usb.openDevice(dev, deviceOpened); } }; /** * @param {*} dev Chrome usb device * @return {GnubbyDeviceId} A device identifier for the device. */ UsbGnubbyDevice.deviceToDeviceId = function(dev) { var usbDev = /** @type {!chrome.usb.Device} */ (dev); var deviceId = { namespace: UsbGnubbyDevice.NAMESPACE, enumeratedBy: dev.enumeratedBy, device: usbDev.device }; return deviceId; }; /** * Registers this implementation with gnubbies. * @param {Gnubbies} gnubbies Gnubbies singleton instance */ UsbGnubbyDevice.register = function(gnubbies) { var USB_GNUBBY_IMPL = { isSharedAccess: false, enumerate: UsbGnubbyDevice.enumerate, deviceToDeviceId: UsbGnubbyDevice.deviceToDeviceId, open: UsbGnubbyDevice.open }; gnubbies.registerNamespace(UsbGnubbyDevice.NAMESPACE, USB_GNUBBY_IMPL); }; // Copyright 2014 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. /** * @fileoverview A class for managing all enumerated gnubby devices. */ 'use strict'; /** * @typedef {{ * vendorId: (number|undefined), * productId: (number|undefined), * usagePage: (number|undefined) * }} */ var GnubbyEnumerationFilter; /** * @typedef {{ * namespace: string, * enumeratedBy: (GnubbyEnumerationFilter|undefined), * device: number * }} */ var GnubbyDeviceId; /** * Ways in which gnubby devices are enumerated. * @const * @enum {number} */ var GnubbyEnumerationTypes = { ANY: 0, VID_PID: 1, FIDO_U2F: 2 }; /** * @typedef {{ * isSharedAccess: boolean, * enumerate: function(function(Array), GnubbyEnumerationTypes=), * deviceToDeviceId: function(*): GnubbyDeviceId, * open: function(Gnubbies, number, *, function(number, GnubbyDevice=)), * cancelOpen: (undefined|function(Gnubbies, number, *)) * }} */ var GnubbyNamespaceImpl; /** * Manager of opened devices. * @constructor */ function Gnubbies() { /** @private {Object} */ this.devs_ = {}; this.pendingEnumerate = []; // clients awaiting an enumerate /** * The distinct namespaces registered in this Gnubbies instance, in order of * registration. * @private {Array} */ this.namespaces_ = []; /** @private {Object} */ this.impl_ = {}; /** @private {Object>} */ this.openDevs_ = {}; /** @private {Object>} */ this.pendingOpens_ = {}; // clients awaiting an open } /** * Registers a new gnubby namespace, i.e. an implementation of the * enumerate/open functions for all devices within a namespace. * @param {string} namespace The namespace of the numerator, e.g. 'usb'. * @param {GnubbyNamespaceImpl} impl The implementation. */ Gnubbies.prototype.registerNamespace = function(namespace, impl) { if (!this.impl_.hasOwnProperty(namespace)) { this.namespaces_.push(namespace); } this.impl_[namespace] = impl; }; /** * @param {GnubbyDeviceId} id The device id. * @return {boolean} Whether the device is a shared access device. */ Gnubbies.prototype.isSharedAccess = function(id) { if (!this.impl_.hasOwnProperty(id.namespace)) return false; return this.impl_[id.namespace].isSharedAccess; }; /** * @param {GnubbyDeviceId} which The device to remove. */ Gnubbies.prototype.removeOpenDevice = function(which) { if (this.openDevs_[which.namespace] && this.openDevs_[which.namespace].hasOwnProperty(which.device)) { delete this.openDevs_[which.namespace][which.device]; } }; /** Close all enumerated devices. */ Gnubbies.prototype.closeAll = function() { if (this.inactivityTimer) { this.inactivityTimer.clearTimeout(); this.inactivityTimer = undefined; } // Close and stop talking to any gnubbies we have enumerated. for (var namespace in this.openDevs_) { for (var dev in this.openDevs_[namespace]) { var deviceId = Number(dev); this.openDevs_[namespace][deviceId].destroy(); } } this.devs_ = {}; this.openDevs_ = {}; }; /** * @param {string} namespace * @return {function(*)} deviceToDeviceId method associated with given namespace * @private */ Gnubbies.prototype.getDeviceToDeviceId_ = function(namespace) { return this.impl_[namespace].deviceToDeviceId; }; /** * @param {function(number, Array)} cb Called back with the * result of enumerating. * @param {GnubbyEnumerationTypes=} opt_type Which type of enumeration to do. */ Gnubbies.prototype.enumerate = function(cb, opt_type) { if (!cb) { cb = function(rc, indexes) { var msg = 'defaultEnumerateCallback(' + rc; if (indexes) { msg += ', ['; for (var i = 0; i < indexes.length; i++) { msg += JSON.stringify(indexes[i]); } msg += ']'; } msg += ')'; console.log(UTIL_fmt(msg)); }; } if (!this.namespaces_.length) { cb(-GnubbyDevice.OK, []); return; } var namespacesEnumerated = 0; var self = this; /** * @param {string} namespace The namespace that was enumerated. * @param {Array} existingDeviceIds Previously enumerated * device IDs (from other namespaces), if any. * @param {Array} devs The devices in the namespace. */ function enumerated(namespace, existingDeviceIds, devs) { namespacesEnumerated++; var lastNamespace = (namespacesEnumerated == self.namespaces_.length); if (chrome.runtime.lastError) { console.warn(UTIL_fmt('lastError: ' + chrome.runtime.lastError)); console.log(chrome.runtime.lastError); devs = []; } console.log(UTIL_fmt('Enumerated ' + devs.length + ' gnubbies')); console.log(devs); var presentDevs = {}; var deviceIds = []; var deviceToDeviceId = self.getDeviceToDeviceId_(namespace); for (var i = 0; i < devs.length; ++i) { var deviceId = deviceToDeviceId(devs[i]); deviceIds.push(deviceId); presentDevs[deviceId.device] = devs[i]; } var toRemove = []; for (var dev in self.openDevs_[namespace]) { if (!presentDevs.hasOwnProperty(dev)) { toRemove.push(dev); } } for (var i = 0; i < toRemove.length; i++) { dev = toRemove[i]; if (self.openDevs_[namespace][dev]) { self.openDevs_[namespace][dev].destroy(); delete self.openDevs_[namespace][dev]; } } self.devs_[namespace] = devs; existingDeviceIds.push.apply(existingDeviceIds, deviceIds); if (lastNamespace) { while (self.pendingEnumerate.length != 0) { var cb = self.pendingEnumerate.shift(); cb(-GnubbyDevice.OK, existingDeviceIds); } } } var deviceIds = []; function makeEnumerateCb(namespace) { return function(devs) { enumerated(namespace, deviceIds, devs); } } this.pendingEnumerate.push(cb); if (this.pendingEnumerate.length == 1) { for (var i = 0; i < this.namespaces_.length; i++) { var namespace = this.namespaces_[i]; var enumerator = this.impl_[namespace].enumerate; enumerator(makeEnumerateCb(namespace), opt_type); } } }; /** * Amount of time past last activity to set the inactivity timer to, in millis. * @const */ Gnubbies.INACTIVITY_TIMEOUT_MARGIN_MILLIS = 30000; /** * Private instance of timers based on window's timer functions. * @const * @private */ Gnubbies.SYS_TIMER_ = new WindowTimer(); /** * @param {number|undefined} opt_timeoutMillis Timeout in milliseconds */ Gnubbies.prototype.resetInactivityTimer = function(opt_timeoutMillis) { var millis = opt_timeoutMillis ? opt_timeoutMillis + Gnubbies.INACTIVITY_TIMEOUT_MARGIN_MILLIS : Gnubbies.INACTIVITY_TIMEOUT_MARGIN_MILLIS; if (!this.inactivityTimer) { this.inactivityTimer = new CountdownTimer( Gnubbies.SYS_TIMER_, millis, this.inactivityTimeout_.bind(this)); } else if (millis > this.inactivityTimer.millisecondsUntilExpired()) { this.inactivityTimer.clearTimeout(); this.inactivityTimer.setTimeout(millis, this.inactivityTimeout_.bind(this)); } }; /** * Called when the inactivity timeout expires. * @private */ Gnubbies.prototype.inactivityTimeout_ = function() { this.inactivityTimer = undefined; for (var namespace in this.openDevs_) { for (var dev in this.openDevs_[namespace]) { var deviceId = Number(dev); console.warn(namespace + ' device ' + deviceId + ' still open after inactivity, closing'); this.openDevs_[namespace][deviceId].destroy(); } } }; /** * Opens and adds a new client of the specified device. * @param {GnubbyDeviceId} which Which device to open. * @param {*} who Client of the device. * @param {function(number, GnubbyDevice=)} cb Called back with the result of * opening the device. */ Gnubbies.prototype.addClient = function(which, who, cb) { this.resetInactivityTimer(); var self = this; function opened(gnubby, who, cb) { if (gnubby.closing) { // Device is closing or already closed. self.removeClient(gnubby, who); if (cb) { cb(-GnubbyDevice.NODEVICE); } } else { gnubby.registerClient(who); if (cb) { cb(-GnubbyDevice.OK, gnubby); } } } function notifyOpenResult(rc) { if (self.pendingOpens_[which.namespace]) { while (self.pendingOpens_[which.namespace][which.device].length != 0) { var client = self.pendingOpens_[which.namespace][which.device].shift(); client.cb(rc); } delete self.pendingOpens_[which.namespace][which.device]; } } var dev = null; var deviceToDeviceId = this.getDeviceToDeviceId_(which.namespace); if (this.devs_[which.namespace]) { for (var i = 0; i < this.devs_[which.namespace].length; i++) { var device = this.devs_[which.namespace][i]; if (deviceToDeviceId(device).device == which.device) { dev = device; break; } } } if (!dev) { // Index out of bounds. Device does not exist in current enumeration. this.removeClient(null, who); if (cb) { cb(-GnubbyDevice.NODEVICE); } return; } function openCb(rc, opt_gnubby) { if (rc) { notifyOpenResult(rc); return; } if (!opt_gnubby) { notifyOpenResult(-GnubbyDevice.NODEVICE); return; } var gnubby = /** @type {!GnubbyDevice} */ (opt_gnubby); if (!self.openDevs_[which.namespace]) { self.openDevs_[which.namespace] = {}; } self.openDevs_[which.namespace][which.device] = gnubby; while (self.pendingOpens_[which.namespace][which.device].length != 0) { var client = self.pendingOpens_[which.namespace][which.device].shift(); opened(gnubby, client.who, client.cb); } delete self.pendingOpens_[which.namespace][which.device]; } if (this.openDevs_[which.namespace] && this.openDevs_[which.namespace].hasOwnProperty(which.device)) { var gnubby = this.openDevs_[which.namespace][which.device]; opened(gnubby, who, cb); } else { var opener = {who: who, cb: cb}; if (!this.pendingOpens_.hasOwnProperty(which.namespace)) { this.pendingOpens_[which.namespace] = {}; } if (this.pendingOpens_[which.namespace].hasOwnProperty(which.device)) { this.pendingOpens_[which.namespace][which.device].push(opener); } else { this.pendingOpens_[which.namespace][which.device] = [opener]; var openImpl = this.impl_[which.namespace].open; openImpl(this, which.device, dev, openCb); } } }; /** * Called to cancel add client operation * @param {GnubbyDeviceId} which Which device to cancel open. */ Gnubbies.prototype.cancelAddClient = function(which) { var dev = null; var deviceToDeviceId = this.getDeviceToDeviceId_(which.namespace); if (this.devs_[which.namespace]) { for (var i = 0; i < this.devs_[which.namespace].length; i++) { var device = this.devs_[which.namespace][i]; if (deviceToDeviceId(device).device == which.device) { dev = device; break; } } } if (!dev) { return; } if (this.pendingOpens_[which.namespace] && this.pendingOpens_[which.namespace][which.device]) { var cancelOpenImpl = this.impl_[which.namespace].cancelOpen; if (cancelOpenImpl) cancelOpenImpl(this, which.device, dev); } }; /** * Removes a client from a low-level gnubby. * @param {GnubbyDevice} whichDev The gnubby. * @param {*} who The client. */ Gnubbies.prototype.removeClient = function(whichDev, who) { console.log(UTIL_fmt('Gnubbies.removeClient()')); this.resetInactivityTimer(); // De-register client from all known devices. for (var namespace in this.openDevs_) { for (var devId in this.openDevs_[namespace]) { var deviceId = Number(devId); if (isNaN(deviceId)) deviceId = devId; var dev = this.openDevs_[namespace][deviceId]; if (dev.hasClient(who)) { if (whichDev && dev != whichDev) { console.warn('Gnubby attached to more than one device!?'); } if (!dev.deregisterClient(who)) { dev.destroy(); } } } } }; // Copyright 2014 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. /** * @fileoverview Provides a client view of a gnubby, aka USB security key. */ 'use strict'; /** * Creates a Gnubby client. There may be more than one simultaneous Gnubby * client of a physical device. This client manages multiplexing access to the * low-level device to maintain the illusion that it is the only client of the * device. * @constructor * @param {number=} opt_busySeconds to retry an exchange upon a BUSY result. */ function Gnubby(opt_busySeconds) { this.dev = null; this.gnubbyInstance = ++Gnubby.gnubbyId_; this.cid = Gnubby.BROADCAST_CID; this.rxframes = []; this.synccnt = 0; this.rxcb = null; this.closed = false; this.commandPending = false; this.notifyOnClose = []; this.busyMillis = (opt_busySeconds ? opt_busySeconds * 1000 : 9500); } /** * Global Gnubby instance counter. * @private {number} */ Gnubby.gnubbyId_ = 0; /** * Sets Gnubby's Gnubbies singleton. * @param {Gnubbies} gnubbies Gnubbies singleton instance */ Gnubby.setGnubbies = function(gnubbies) { /** @private {Gnubbies} */ Gnubby.gnubbies_ = gnubbies; }; /** * Return cid as hex string. * @param {number} cid to convert. * @return {string} hexadecimal string. */ Gnubby.hexCid = function(cid) { var tmp = [(cid >>> 24) & 255, (cid >>> 16) & 255, (cid >>> 8) & 255, (cid >>> 0) & 255]; return UTIL_BytesToHex(tmp); }; /** * Cancels open attempt for this gnubby, if available. */ Gnubby.prototype.cancelOpen = function() { if (this.which) Gnubby.gnubbies_.cancelAddClient(this.which); }; /** * Opens the gnubby with the given index, or the first found gnubby if no * index is specified. * @param {GnubbyDeviceId} which The device to open. If null, the first * gnubby found is opened. * @param {GnubbyEnumerationTypes=} opt_type Which type of device to enumerate. * @param {function(number)|undefined} opt_cb Called with result of opening the * gnubby. * @param {string=} opt_caller Identifier for the caller. */ Gnubby.prototype.open = function(which, opt_type, opt_cb, opt_caller) { var cb = opt_cb ? opt_cb : Gnubby.defaultCallback; if (this.closed) { cb(-GnubbyDevice.NODEVICE); return; } this.closingWhenIdle = false; if (opt_caller) { this.caller_ = opt_caller; } var self = this; function setCid(which) { // Set a default channel ID, in case the caller never sets a better one. self.cid = Gnubby.defaultChannelId_(self.gnubbyInstance, which); } var enumerateRetriesRemaining = 3; function enumerated(rc, devs) { if (!devs.length) rc = -GnubbyDevice.NODEVICE; if (rc) { cb(rc); return; } which = devs[0]; setCid(which); self.which = which; Gnubby.gnubbies_.addClient(which, self, function(rc, device) { if (rc == -GnubbyDevice.NODEVICE && enumerateRetriesRemaining-- > 0) { // We were trying to open the first device, but now it's not there? // Do over. Gnubby.gnubbies_.enumerate(enumerated, opt_type); return; } self.dev = device; if (self.closeHook_) { self.dev.setDestroyHook(self.closeHook_); } cb(rc); }); } if (which) { setCid(which); self.which = which; Gnubby.gnubbies_.addClient(which, self, function(rc, device) { if (!rc) { self.dev = device; if (self.closeHook_) { self.dev.setDestroyHook(self.closeHook_); } } cb(rc); }); } else { Gnubby.gnubbies_.enumerate(enumerated, opt_type); } }; /** * Generates a default channel id value for a gnubby instance that won't * collide within this application, but may when others simultaneously access * the device. * @param {number} gnubbyInstance An instance identifier for a gnubby. * @param {GnubbyDeviceId} which The device identifer for the gnubby device. * @return {number} The channel id. * @private */ Gnubby.defaultChannelId_ = function(gnubbyInstance, which) { var cid = (gnubbyInstance) & 0x00ffffff; cid |= ((which.device + 1) << 24); // For debugging. return cid; }; /** * @return {boolean} Whether this gnubby has any command outstanding. * @private */ Gnubby.prototype.inUse_ = function() { return this.commandPending; }; /** Closes this gnubby. */ Gnubby.prototype.close = function() { this.closed = true; if (this.dev) { console.log(UTIL_fmt('Gnubby.close()')); this.rxframes = []; this.rxcb = null; var dev = this.dev; this.dev = null; var self = this; // Wait a bit in case simpleton client tries open next gnubby. // Without delay, gnubbies would drop all idle devices, before client // gets to the next one. window.setTimeout( function() { Gnubby.gnubbies_.removeClient(dev, self); }, 300); } }; /** * Asks this gnubby to close when it gets a chance. * @param {Function=} cb called back when closed. */ Gnubby.prototype.closeWhenIdle = function(cb) { if (!this.inUse_()) { this.close(); if (cb) cb(); return; } this.closingWhenIdle = true; if (cb) this.notifyOnClose.push(cb); }; /** * Sets a callback that will get called when this gnubby is closed. * @param {function() : ?Promise} cb Called back when closed. Callback * may yield a promise that resolves when the close hook completes. */ Gnubby.prototype.setCloseHook = function(cb) { this.closeHook_ = cb; }; /** * Close and notify every caller that it is now closed. * @private */ Gnubby.prototype.idleClose_ = function() { this.close(); while (this.notifyOnClose.length != 0) { var cb = this.notifyOnClose.shift(); cb(); } }; /** * Notify callback for every frame received. * @param {function()} cb Callback * @private */ Gnubby.prototype.notifyFrame_ = function(cb) { if (this.rxframes.length != 0) { // Already have frames; continue. if (cb) window.setTimeout(cb, 0); } else { this.rxcb = cb; } }; /** * Called by low level driver with a frame. * @param {ArrayBuffer|Uint8Array} frame Data frame * @return {boolean} Whether this client is still interested in receiving * frames from its device. */ Gnubby.prototype.receivedFrame = function(frame) { if (this.closed) return false; // No longer interested. if (!this.checkCID_(frame)) { // Not for me, ignore. return true; } this.rxframes.push(frame); // Callback self in case we were waiting. Once. var cb = this.rxcb; this.rxcb = null; if (cb) window.setTimeout(cb, 0); return true; }; /** * @return {number|undefined} The last read error seen by this device. */ Gnubby.prototype.getLastReadError = function() { return this.lastReadError_; }; /** * @return {ArrayBuffer|Uint8Array} oldest received frame. Throw if none. * @private */ Gnubby.prototype.readFrame_ = function() { if (this.rxframes.length == 0) throw 'rxframes empty!'; var frame = this.rxframes.shift(); return frame; }; /** Poll from rxframes[]. * @param {number} cmd Command * @param {number} timeout timeout in seconds. * @param {?function(...)} cb Callback * @private */ Gnubby.prototype.read_ = function(cmd, timeout, cb) { if (this.closed) { cb(-GnubbyDevice.GONE); return; } if (!this.dev) { cb(-GnubbyDevice.GONE); return; } var tid = null; // timeout timer id. var callback = cb; var self = this; var msg = null; var seqno = 0; var count = 0; /** * Schedule call to cb if not called yet. * @param {number} a Return code. * @param {Object=} b Optional data. */ function schedule_cb(a, b) { self.commandPending = false; if (tid) { // Cancel timeout timer. window.clearTimeout(tid); tid = null; } self.lastReadError_ = /** @private {number|undefined} */ (a); var c = callback; if (c) { callback = null; window.setTimeout(function() { c(a, b); }, 0); } if (self.closingWhenIdle) self.idleClose_(); }; function read_timeout() { if (!callback || !tid) return; // Already done. console.error(UTIL_fmt( '[' + Gnubby.hexCid(self.cid) + '] timeout!')); if (self.dev) { self.dev.destroy(); // Stop pretending this thing works. } tid = null; schedule_cb(-GnubbyDevice.TIMEOUT); }; function cont_frame() { if (!callback || !tid) return; // Already done. var f = new Uint8Array(self.readFrame_()); var rcmd = f[4]; var totalLen = (f[5] << 8) + f[6]; if (rcmd == GnubbyDevice.CMD_ERROR && totalLen == 1) { // Error from device; forward. console.log(UTIL_fmt( '[' + Gnubby.hexCid(self.cid) + '] error frame ' + UTIL_BytesToHex(f))); if (f[7] == GnubbyDevice.GONE) { self.closed = true; } schedule_cb(-f[7]); return; } if ((rcmd & 0x80)) { // Not an CONT frame, ignore. console.log(UTIL_fmt( '[' + Gnubby.hexCid(self.cid) + '] ignoring non-cont frame ' + UTIL_BytesToHex(f))); self.notifyFrame_(cont_frame); return; } var seq = (rcmd & 0x7f); if (seq != seqno++) { console.log(UTIL_fmt( '[' + Gnubby.hexCid(self.cid) + '] bad cont frame ' + UTIL_BytesToHex(f))); schedule_cb(-GnubbyDevice.INVALID_SEQ); return; } // Copy payload. for (var i = 5; i < f.length && count < msg.length; ++i) { msg[count++] = f[i]; } if (count == msg.length) { // Done. schedule_cb(-GnubbyDevice.OK, msg.buffer); } else { // Need more CONT frame(s). self.notifyFrame_(cont_frame); } } function init_frame() { if (!callback || !tid) return; // Already done. var f = new Uint8Array(self.readFrame_()); var rcmd = f[4]; var totalLen = (f[5] << 8) + f[6]; if (rcmd == GnubbyDevice.CMD_ERROR && totalLen == 1) { // Error from device; forward. // Don't log busy frames, they're "normal". if (f[7] != GnubbyDevice.BUSY) { console.log(UTIL_fmt( '[' + Gnubby.hexCid(self.cid) + '] error frame ' + UTIL_BytesToHex(f))); } if (f[7] == GnubbyDevice.GONE) { self.closed = true; } schedule_cb(-f[7]); return; } if (!(rcmd & 0x80)) { // Not an init frame, ignore. console.log(UTIL_fmt( '[' + Gnubby.hexCid(self.cid) + '] ignoring non-init frame ' + UTIL_BytesToHex(f))); self.notifyFrame_(init_frame); return; } if (rcmd != cmd) { // Not expected ack, read more. console.log(UTIL_fmt( '[' + Gnubby.hexCid(self.cid) + '] ignoring non-ack frame ' + UTIL_BytesToHex(f))); self.notifyFrame_(init_frame); return; } // Copy payload. msg = new Uint8Array(totalLen); for (var i = 7; i < f.length && count < msg.length; ++i) { msg[count++] = f[i]; } if (count == msg.length) { // Done. schedule_cb(-GnubbyDevice.OK, msg.buffer); } else { // Need more CONT frame(s). self.notifyFrame_(cont_frame); } } // Start timeout timer. tid = window.setTimeout(read_timeout, 1000.0 * timeout); // Schedule read of first frame. self.notifyFrame_(init_frame); }; /** * @const */ Gnubby.NOTIFICATION_CID = 0; /** * @const */ Gnubby.BROADCAST_CID = (0xff << 24) | (0xff << 16) | (0xff << 8) | 0xff; /** * @param {ArrayBuffer|Uint8Array} frame Data frame * @return {boolean} Whether frame is for my channel. * @private */ Gnubby.prototype.checkCID_ = function(frame) { var f = new Uint8Array(frame); var c = (f[0] << 24) | (f[1] << 16) | (f[2] << 8) | (f[3]); return c === this.cid || c === Gnubby.NOTIFICATION_CID; }; /** * Queue command for sending. * @param {number} cmd The command to send. * @param {ArrayBuffer|Uint8Array} data Command data * @private */ Gnubby.prototype.write_ = function(cmd, data) { if (this.closed) return; if (!this.dev) return; this.commandPending = true; this.dev.queueCommand(this.cid, cmd, data); }; /** * Writes the command, and calls back when the command's reply is received. * @param {number} cmd The command to send. * @param {ArrayBuffer|Uint8Array} data Command data * @param {number} timeout Timeout in seconds. * @param {function(number, ArrayBuffer=)} cb Callback * @private */ Gnubby.prototype.exchange_ = function(cmd, data, timeout, cb) { var busyWait = new CountdownTimer(Gnubby.SYS_TIMER_, this.busyMillis); var self = this; function retryBusy(rc, rc_data) { if (rc == -GnubbyDevice.BUSY && !busyWait.expired()) { if (Gnubby.gnubbies_) { Gnubby.gnubbies_.resetInactivityTimer(timeout * 1000); } self.write_(cmd, data); self.read_(cmd, timeout, retryBusy); } else { busyWait.clearTimeout(); cb(rc, rc_data); } } retryBusy(-GnubbyDevice.BUSY, undefined); // Start work. }; /** * Private instance of timers based on window's timer functions. * @const * @private */ Gnubby.SYS_TIMER_ = new WindowTimer(); /** Default callback for commands. Simply logs to console. * @param {number} rc Result status code * @param {(ArrayBuffer|Uint8Array|Array|null)} data Result data */ Gnubby.defaultCallback = function(rc, data) { var msg = 'defaultCallback(' + rc; if (data) { if (typeof data == 'string') msg += ', ' + data; else msg += ', ' + UTIL_BytesToHex(new Uint8Array(data)); } msg += ')'; console.log(UTIL_fmt(msg)); }; /** * Ensures this device has temporary ownership of the USB device, by: * 1. Using the INIT command to allocate an unique channel id, if one hasn't * been retrieved before, or * 2. Sending a nonce to device, flushing read queue until match. * @param {?function(...)} cb Callback */ Gnubby.prototype.sync = function(cb) { if (!cb) cb = Gnubby.defaultCallback; if (this.closed) { cb(-GnubbyDevice.GONE); return; } var done = false; var trycount = 6; var tid = null; var self = this; function returnValue(rc) { done = true; window.setTimeout(cb.bind(null, rc), 0); if (self.closingWhenIdle) self.idleClose_(); } function callback(rc, opt_frame) { self.commandPending = false; if (tid) { window.clearTimeout(tid); tid = null; } completionAction(rc, opt_frame); } function sendSyncSentinel() { var cmd = GnubbyDevice.CMD_SYNC; var data = new Uint8Array(1); data[0] = ++self.synccnt; self.dev.queueCommand(self.cid, cmd, data.buffer); } function syncSentinelEquals(f) { return (f[4] == GnubbyDevice.CMD_SYNC && (f.length == 7 || /* fw pre-0.2.1 bug: does not echo sentinel */ f[7] == self.synccnt)); } function syncCompletionAction(rc, opt_frame) { if (rc) console.warn(UTIL_fmt('sync failed: ' + rc)); returnValue(rc); } function sendInitSentinel() { var cid = self.cid; // If we do not have a specific CID yet, reset to BROADCAST for init. if (self.cid == Gnubby.defaultChannelId_(self.gnubbyInstance, self.which)) { self.cid = Gnubby.BROADCAST_CID; cid = self.cid; } var cmd = GnubbyDevice.CMD_INIT; self.dev.queueCommand(cid, cmd, nonce); } function initSentinelEquals(f) { return (f[4] == GnubbyDevice.CMD_INIT && f.length >= nonce.length + 7 && UTIL_equalArrays(f.subarray(7, nonce.length + 7), nonce)); } function initCmdUnsupported(rc) { // Different firmwares fail differently on different inputs, so treat any // of the following errors as indicating the INIT command isn't supported. return rc == -GnubbyDevice.INVALID_CMD || rc == -GnubbyDevice.INVALID_PAR || rc == -GnubbyDevice.INVALID_LEN; } function initCompletionAction(rc, opt_frame) { // Actual failures: bail out. if (rc && !initCmdUnsupported(rc)) { console.warn(UTIL_fmt('init failed: ' + rc)); returnValue(rc); } var HEADER_LENGTH = 7; var MIN_LENGTH = HEADER_LENGTH + 4; // 4 bytes for the channel id if (rc || !opt_frame || opt_frame.length < nonce.length + MIN_LENGTH) { // INIT command not supported or is missing the returned channel id: // Pick a random cid to try to prevent collisions on the USB bus. var rnd = UTIL_getRandom(2); self.cid = Gnubby.defaultChannelId_(self.gnubbyInstance, self.which); self.cid ^= (rnd[0] << 16) | (rnd[1] << 8); // Now sync with that cid, to make sure we've got it. setSync(); timeoutLoop(); return; } // Accept the provided cid. var offs = HEADER_LENGTH + nonce.length; self.cid = (opt_frame[offs] << 24) | (opt_frame[offs + 1] << 16) | (opt_frame[offs + 2] << 8) | opt_frame[offs + 3]; returnValue(rc); } function checkSentinel() { var f = new Uint8Array(self.readFrame_()); // Stop on errors and return them. if (f[4] == GnubbyDevice.CMD_ERROR && f[5] == 0 && f[6] == 1) { if (f[7] == GnubbyDevice.BUSY) { // Not spec but some devices do this; retry. sendSentinel(); self.notifyFrame_(checkSentinel); return; } if (f[7] == GnubbyDevice.GONE) { // Device disappeared on us. self.closed = true; } callback(-f[7]); return; } // Eat everything else but expected sentinel reply. if (!sentinelEquals(f)) { // Read more. self.notifyFrame_(checkSentinel); return; } // Done. callback(-GnubbyDevice.OK, f); }; function timeoutLoop() { if (done) return; if (trycount == 0) { // Failed. callback(-GnubbyDevice.TIMEOUT); return; } --trycount; // Try another one. sendSentinel(); self.notifyFrame_(checkSentinel); tid = window.setTimeout(timeoutLoop, 500); }; var sendSentinel; var sentinelEquals; var nonce; var completionAction; function setInit() { sendSentinel = sendInitSentinel; nonce = UTIL_getRandom(8); sentinelEquals = initSentinelEquals; completionAction = initCompletionAction; } function setSync() { sendSentinel = sendSyncSentinel; sentinelEquals = syncSentinelEquals; completionAction = syncCompletionAction; } if (Gnubby.gnubbies_.isSharedAccess(this.which)) { setInit(); } else { setSync(); } timeoutLoop(); }; /** Short timeout value in seconds */ Gnubby.SHORT_TIMEOUT = 1; /** Normal timeout value in seconds */ Gnubby.NORMAL_TIMEOUT = 3; // Max timeout usb firmware has for smartcard response is 30 seconds. // Make our application level tolerance a little longer. /** Maximum timeout in seconds */ Gnubby.MAX_TIMEOUT = 31; /** Blink led * @param {number|ArrayBuffer|Uint8Array} data Command data or number * of seconds to blink * @param {?function(...)} cb Callback */ Gnubby.prototype.blink = function(data, cb) { if (!cb) cb = Gnubby.defaultCallback; if (typeof data == 'number') { var d = new Uint8Array([data]); data = d.buffer; } this.exchange_(GnubbyDevice.CMD_PROMPT, data, Gnubby.NORMAL_TIMEOUT, cb); }; /** Lock the gnubby * @param {number|ArrayBuffer|Uint8Array} data Command data * @param {?function(...)} cb Callback */ Gnubby.prototype.lock = function(data, cb) { if (!cb) cb = Gnubby.defaultCallback; if (typeof data == 'number') { var d = new Uint8Array([data]); data = d.buffer; } this.exchange_(GnubbyDevice.CMD_LOCK, data, Gnubby.NORMAL_TIMEOUT, cb); }; /** Unlock the gnubby * @param {?function(...)} cb Callback */ Gnubby.prototype.unlock = function(cb) { if (!cb) cb = Gnubby.defaultCallback; var data = new Uint8Array([0]); this.exchange_(GnubbyDevice.CMD_LOCK, data.buffer, Gnubby.NORMAL_TIMEOUT, cb); }; /** Request system information data. * @param {?function(...)} cb Callback */ Gnubby.prototype.sysinfo = function(cb) { if (!cb) cb = Gnubby.defaultCallback; this.exchange_(GnubbyDevice.CMD_SYSINFO, new ArrayBuffer(0), Gnubby.NORMAL_TIMEOUT, cb); }; /** Send wink command * @param {?function(...)} cb Callback */ Gnubby.prototype.wink = function(cb) { if (!cb) cb = Gnubby.defaultCallback; this.exchange_(GnubbyDevice.CMD_WINK, new ArrayBuffer(0), Gnubby.NORMAL_TIMEOUT, cb); }; /** Send DFU (Device firmware upgrade) command * @param {ArrayBuffer|Uint8Array} data Command data * @param {?function(...)} cb Callback */ Gnubby.prototype.dfu = function(data, cb) { if (!cb) cb = Gnubby.defaultCallback; this.exchange_(GnubbyDevice.CMD_DFU, data, Gnubby.NORMAL_TIMEOUT, cb); }; /** Ping the gnubby * @param {number|ArrayBuffer|Uint8Array} data Command data * @param {?function(...)} cb Callback */ Gnubby.prototype.ping = function(data, cb) { if (!cb) cb = Gnubby.defaultCallback; if (typeof data == 'number') { var d = new Uint8Array(data); window.crypto.getRandomValues(d); data = d.buffer; } this.exchange_(GnubbyDevice.CMD_PING, data, Gnubby.NORMAL_TIMEOUT, cb); }; /** Send a raw APDU command * @param {ArrayBuffer|Uint8Array} data Command data * @param {?function(...)} cb Callback */ Gnubby.prototype.apdu = function(data, cb) { if (!cb) cb = Gnubby.defaultCallback; this.exchange_(GnubbyDevice.CMD_APDU, data, Gnubby.MAX_TIMEOUT, cb); }; /** Reset gnubby * @param {?function(...)} cb Callback */ Gnubby.prototype.reset = function(cb) { if (!cb) cb = Gnubby.defaultCallback; this.exchange_(GnubbyDevice.CMD_ATR, new ArrayBuffer(0), Gnubby.MAX_TIMEOUT, cb); }; // byte args[3] = [delay-in-ms before disabling interrupts, // delay-in-ms before disabling usb (aka remove), // delay-in-ms before reboot (aka insert)] /** Send usb test command * @param {ArrayBuffer|Uint8Array} args Command data * @param {?function(...)} cb Callback */ Gnubby.prototype.usb_test = function(args, cb) { if (!cb) cb = Gnubby.defaultCallback; var u8 = new Uint8Array(args); this.exchange_(GnubbyDevice.CMD_USB_TEST, u8.buffer, Gnubby.NORMAL_TIMEOUT, cb); }; /** APDU command with reply * @param {ArrayBuffer|Uint8Array} request The request * @param {?function(...)} cb Callback * @param {boolean=} opt_nowink Do not wink */ Gnubby.prototype.apduReply = function(request, cb, opt_nowink) { if (!cb) cb = Gnubby.defaultCallback; var self = this; this.apdu(request, function(rc, data) { if (rc == 0) { var r8 = new Uint8Array(data); if (r8[r8.length - 2] == 0x90 && r8[r8.length - 1] == 0x00) { // strip trailing 9000 var buf = new Uint8Array(r8.subarray(0, r8.length - 2)); cb(-GnubbyDevice.OK, buf.buffer); return; } else { // return non-9000 as rc rc = r8[r8.length - 2] * 256 + r8[r8.length - 1]; // wink gnubby at hand if it needs touching. if (rc == 0x6985 && !opt_nowink) { self.wink(function() { cb(rc); }); return; } } } // Warn on errors other than waiting for touch, wrong data, and // unrecognized command. if (rc != 0x6985 && rc != 0x6a80 && rc != 0x6d00) { console.warn(UTIL_fmt('apduReply_ fail: ' + rc.toString(16))); } cb(rc); }); }; // Copyright 2014 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. /** * @fileoverview Gnubby methods related to U2F support. */ 'use strict'; // Commands and flags of the Gnubby applet /** Enroll */ Gnubby.U2F_ENROLL = 0x01; /** Request signature */ Gnubby.U2F_SIGN = 0x02; /** Request protocol version */ Gnubby.U2F_VERSION = 0x03; /** Request applet version */ Gnubby.APPLET_VERSION = 0x11; // First 3 bytes are applet version. // APDU.P1 flags /** Test of User Presence required */ Gnubby.P1_TUP_REQUIRED = 0x01; /** Consume a Test of User Presence */ Gnubby.P1_TUP_CONSUME = 0x02; /** Test signature only, no TUP. E.g. to check for existing enrollments. */ Gnubby.P1_TUP_TESTONLY = 0x04; /** Attest with device key */ Gnubby.P1_INDIVIDUAL_KEY = 0x80; // Version values /** V1 of the applet. */ Gnubby.U2F_V1 = 'U2F_V1'; /** V2 of the applet. */ Gnubby.U2F_V2 = 'U2F_V2'; /** Perform enrollment * @param {Array|ArrayBuffer|Uint8Array} challenge Enrollment challenge * @param {Array|ArrayBuffer|Uint8Array} appIdHash Hashed application * id * @param {function(...)} cb Result callback * @param {boolean=} opt_individualAttestation Request the individual * attestation cert rather than the batch one. */ Gnubby.prototype.enroll = function(challenge, appIdHash, cb, opt_individualAttestation) { var p1 = Gnubby.P1_TUP_REQUIRED | Gnubby.P1_TUP_CONSUME; if (opt_individualAttestation) { p1 |= Gnubby.P1_INDIVIDUAL_KEY; } var apdu = new Uint8Array( [0x00, Gnubby.U2F_ENROLL, p1, 0x00, 0x00, 0x00, challenge.length + appIdHash.length]); var u8 = new Uint8Array(apdu.length + challenge.length + appIdHash.length + 2); for (var i = 0; i < apdu.length; ++i) u8[i] = apdu[i]; for (var i = 0; i < challenge.length; ++i) u8[i + apdu.length] = challenge[i]; for (var i = 0; i < appIdHash.length; ++i) { u8[i + apdu.length + challenge.length] = appIdHash[i]; } this.apduReply(u8.buffer, cb); }; /** Request signature * @param {Array|ArrayBuffer|Uint8Array} challengeHash Hashed * signature challenge * @param {Array|ArrayBuffer|Uint8Array} appIdHash Hashed application * id * @param {Array|ArrayBuffer|Uint8Array} keyHandle Key handle to use * @param {function(...)} cb Result callback * @param {boolean=} opt_nowink Request signature without winking * (e.g. during enroll) */ Gnubby.prototype.sign = function(challengeHash, appIdHash, keyHandle, cb, opt_nowink) { var self = this; // The sign command's format is ever-so-slightly different between V1 and V2, // so get this gnubby's version prior to sending it. this.version(function(rc, opt_data) { if (rc) { cb(rc); return; } var version = UTIL_BytesToString(new Uint8Array(opt_data || [])); var apduDataLen = challengeHash.length + appIdHash.length + keyHandle.length; if (version != Gnubby.U2F_V1) { // The V2 sign command includes a length byte for the key handle. apduDataLen++; } var apdu = new Uint8Array( [0x00, Gnubby.U2F_SIGN, Gnubby.P1_TUP_REQUIRED | Gnubby.P1_TUP_CONSUME, 0x00, 0x00, 0x00, apduDataLen]); if (opt_nowink) { // A signature request that does not want winking. // These are used during enroll to figure out whether a gnubby was already // enrolled. // Tell applet to not actually produce a signature, even // if already touched. apdu[2] |= Gnubby.P1_TUP_TESTONLY; } var u8 = new Uint8Array(apdu.length + apduDataLen + 2); for (var i = 0; i < apdu.length; ++i) u8[i] = apdu[i]; for (var i = 0; i < challengeHash.length; ++i) u8[i + apdu.length] = challengeHash[i]; for (var i = 0; i < appIdHash.length; ++i) { u8[i + apdu.length + challengeHash.length] = appIdHash[i]; } var keyHandleOffset = apdu.length + challengeHash.length + appIdHash.length; if (version != Gnubby.U2F_V1) { u8[keyHandleOffset++] = keyHandle.length; } for (var i = 0; i < keyHandle.length; ++i) { u8[i + keyHandleOffset] = keyHandle[i]; } self.apduReply(u8.buffer, cb, opt_nowink); }); }; /** Request version information * @param {function(...)} cb Callback */ Gnubby.prototype.version = function(cb) { if (!cb) cb = Gnubby.defaultCallback; if (this.version_) { cb(-GnubbyDevice.OK, this.version_); return; } var self = this; function gotResponse(rc, data) { if (!rc) { self.version_ = data; } cb(rc, data); } var apdu = new Uint8Array([0x00, Gnubby.U2F_VERSION, 0x00, 0x00, 0x00, 0x00, 0x00]); this.apduReply(apdu.buffer, function(rc, data) { if (rc == 0x6d00) { // Command not implemented. Pretend this is v1. var v1 = new Uint8Array(UTIL_StringToBytes(Gnubby.U2F_V1)); self.version_ = v1.buffer; cb(-GnubbyDevice.OK, v1.buffer); return; } if (rc == 0x6700) { // Wrong length. Try with non-ISO 7816-4-conforming layout defined in // earlier U2F drafts. apdu = new Uint8Array([0x00, Gnubby.U2F_VERSION, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]); self.apduReply(apdu.buffer, gotResponse); return; } // Any other response: handle as final result. gotResponse(rc, data); }); }; // Copyright 2014 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. /** * @fileoverview Contains a factory interface for creating and opening gnubbies. */ 'use strict'; /** * A factory for creating and opening gnubbies. * @interface */ function GnubbyFactory() {} /** * Enumerates gnubbies. * @param {function(number, Array)} cb Enumerate callback */ GnubbyFactory.prototype.enumerate = function(cb) { }; /** @typedef {function(number, Gnubby=)} */ var FactoryOpenCallback; /** * Creates a new gnubby object, and opens the gnubby with the given index. * @param {GnubbyDeviceId} which The device to open. * @param {boolean} forEnroll Whether this gnubby is being opened for enrolling. * @param {FactoryOpenCallback} cb Called with result of opening the gnubby. * @param {string=} opt_appIdHash The base64-encoded hash of the app id for * which the gnubby being opened. * @param {string=} opt_logMsgUrl The url to post log messages to. * @param {string=} opt_caller Identifier for the caller. * @return {(function ()|undefined)} Some implementations might return function * that can be used to cancel this pending open operation. Opening device * might take long time or be resource-hungry. */ GnubbyFactory.prototype.openGnubby = function(which, forEnroll, cb, opt_appIdHash, opt_logMsgUrl, opt_caller) { }; /** * Called during enrollment to check whether a gnubby known not to be enrolled * is allowed to enroll in its present state. Upon completion of the check, the * callback is called. * @param {Gnubby} gnubby The not-enrolled gnubby. * @param {string} appIdHash The base64-encoded hash of the app id for which * the gnubby being enrolled. * @param {FactoryOpenCallback} cb Called with the result of the prerequisite * check. (A non-zero status indicates failure.) */ GnubbyFactory.prototype.notEnrolledPrerequisiteCheck = function(gnubby, appIdHash, cb) { }; // Copyright 2014 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. /** * @fileoverview Contains a simple factory for creating and opening Gnubby * instances. */ 'use strict'; /** * @param {Gnubbies} gnubbies Gnubbies singleton instance * @constructor * @implements {GnubbyFactory} */ function UsbGnubbyFactory(gnubbies) { /** @private {Gnubbies} */ this.gnubbies_ = gnubbies; Gnubby.setGnubbies(gnubbies); } /** * Creates a new gnubby object, and opens the gnubby with the given index. * @param {GnubbyDeviceId} which The device to open. * @param {boolean} forEnroll Whether this gnubby is being opened for enrolling. * @param {FactoryOpenCallback} cb Called with result of opening the gnubby. * @param {string=} opt_appIdHash The base64-encoded hash of the app id for * which the gnubby being opened. * @param {string=} opt_logMsgUrl The url to post log messages to. * @param {string=} opt_caller Identifier for the caller. * @return {undefined} no open canceller needed for this type of gnubby * @override */ UsbGnubbyFactory.prototype.openGnubby = function(which, forEnroll, cb, opt_appIdHash, opt_logMsgUrl, opt_caller) { var gnubby = new Gnubby(); gnubby.open(which, GnubbyEnumerationTypes.ANY, function(rc) { if (rc) { cb(rc, gnubby); return; } gnubby.sync(function(rc) { cb(rc, gnubby); }); }, opt_caller); }; /** * Enumerates gnubbies. * @param {function(number, Array)} cb Enumerate callback */ UsbGnubbyFactory.prototype.enumerate = function(cb) { this.gnubbies_.enumerate(cb); }; /** * No-op prerequisite check. * @param {Gnubby} gnubby The not-enrolled gnubby. * @param {string} appIdHash The base64-encoded hash of the app id for which * the gnubby being enrolled. * @param {FactoryOpenCallback} cb Called with the result of the prerequisite * check. (A non-zero status indicates failure.) */ UsbGnubbyFactory.prototype.notEnrolledPrerequisiteCheck = function(gnubby, appIdHash, cb) { cb(DeviceStatusCodes.OK_STATUS, gnubby); }; // Copyright 2014 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. /** * @fileoverview This file defines the status codes returned by the device. */ /** * Status codes returned by the gnubby device. * @const * @enum {number} * @export */ var DeviceStatusCodes = {}; /** * Device operation succeeded. * @const */ DeviceStatusCodes.OK_STATUS = 0; /** * Device operation wrong length status. * @const */ DeviceStatusCodes.WRONG_LENGTH_STATUS = 0x6700; /** * Device operation wait touch status. * @const */ DeviceStatusCodes.WAIT_TOUCH_STATUS = 0x6985; /** * Device operation invalid data status. * @const */ DeviceStatusCodes.INVALID_DATA_STATUS = 0x6984; /** * Device operation wrong data status. * @const */ DeviceStatusCodes.WRONG_DATA_STATUS = 0x6a80; /** * Device operation timeout status. * @const */ DeviceStatusCodes.TIMEOUT_STATUS = -5; /** * Device operation busy status. * @const */ DeviceStatusCodes.BUSY_STATUS = -6; /** * Device removed status. * @const */ DeviceStatusCodes.GONE_STATUS = -8; // Copyright 2014 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. /** * @fileoverview Handles web page requests for gnubby enrollment. */ 'use strict'; /** * Handles a U2F enroll request. * @param {MessageSender} messageSender The message sender. * @param {Object} request The web page's enroll request. * @param {Function} sendResponse Called back with the result of the enroll. * @return {Closeable} A handler object to be closed when the browser channel * closes. */ function handleU2fEnrollRequest(messageSender, request, sendResponse) { var sentResponse = false; var closeable = null; function sendErrorResponse(error) { var response = makeU2fErrorResponse(request, error.errorCode, error.errorMessage); sendResponseOnce(sentResponse, closeable, response, sendResponse); } function sendSuccessResponse(u2fVersion, info, clientData) { var enrollChallenges = request['registerRequests']; var enrollChallenge = findEnrollChallengeOfVersion(enrollChallenges, u2fVersion); if (!enrollChallenge) { sendErrorResponse({errorCode: ErrorCodes.OTHER_ERROR}); return; } var responseData = makeEnrollResponseData(enrollChallenge, u2fVersion, info, clientData); var response = makeU2fSuccessResponse(request, responseData); sendResponseOnce(sentResponse, closeable, response, sendResponse); } function timeout() { sendErrorResponse({errorCode: ErrorCodes.TIMEOUT}); } var sender = createSenderFromMessageSender(messageSender); if (!sender) { sendErrorResponse({errorCode: ErrorCodes.BAD_REQUEST}); return null; } if (sender.origin.indexOf('http://') == 0 && !HTTP_ORIGINS_ALLOWED) { sendErrorResponse({errorCode: ErrorCodes.BAD_REQUEST}); return null; } if (!isValidEnrollRequest(request)) { sendErrorResponse({errorCode: ErrorCodes.BAD_REQUEST}); return null; } var timeoutValueSeconds = getTimeoutValueFromRequest(request); // Attenuate watchdog timeout value less than the enroller's timeout, so the // watchdog only fires after the enroller could reasonably have called back, // not before. var watchdogTimeoutValueSeconds = attenuateTimeoutInSeconds( timeoutValueSeconds, MINIMUM_TIMEOUT_ATTENUATION_SECONDS / 2); var watchdog = new WatchdogRequestHandler(watchdogTimeoutValueSeconds, timeout); var wrappedErrorCb = watchdog.wrapCallback(sendErrorResponse); var wrappedSuccessCb = watchdog.wrapCallback(sendSuccessResponse); var timer = createAttenuatedTimer( FACTORY_REGISTRY.getCountdownFactory(), timeoutValueSeconds); var logMsgUrl = request['logMsgUrl']; var enroller = new Enroller(timer, sender, sendErrorResponse, sendSuccessResponse, logMsgUrl); watchdog.setCloseable(/** @type {!Closeable} */ (enroller)); closeable = watchdog; var registerRequests = request['registerRequests']; var signRequests = getSignRequestsFromEnrollRequest(request); enroller.doEnroll(registerRequests, signRequests, request['appId']); return closeable; } /** * Returns whether the request appears to be a valid enroll request. * @param {Object} request The request. * @return {boolean} Whether the request appears valid. */ function isValidEnrollRequest(request) { if (!request.hasOwnProperty('registerRequests')) return false; var enrollChallenges = request['registerRequests']; if (!enrollChallenges.length) return false; var hasAppId = request.hasOwnProperty('appId'); if (!isValidEnrollChallengeArray(enrollChallenges, !hasAppId)) return false; var signChallenges = getSignChallenges(request); // A missing sign challenge array is ok, in the case the user is not already // enrolled. // A challenge value need not necessarily be supplied with every challenge. var challengeRequired = false; if (signChallenges && !isValidSignChallengeArray(signChallenges, challengeRequired, !hasAppId)) return false; return true; } /** * @typedef {{ * version: (string|undefined), * challenge: string, * appId: string * }} */ var EnrollChallenge; /** * @param {Array} enrollChallenges The enroll challenges to * validate. * @param {boolean} appIdRequired Whether the appId property is required on * each challenge. * @return {boolean} Whether the given array of challenges is a valid enroll * challenges array. */ function isValidEnrollChallengeArray(enrollChallenges, appIdRequired) { var seenVersions = {}; for (var i = 0; i < enrollChallenges.length; i++) { var enrollChallenge = enrollChallenges[i]; var version = enrollChallenge['version']; if (!version) { // Version is implicitly V1 if not specified. version = 'U2F_V1'; } if (version != 'U2F_V1' && version != 'U2F_V2') { return false; } if (seenVersions[version]) { // Each version can appear at most once. return false; } seenVersions[version] = version; if (appIdRequired && !enrollChallenge['appId']) { return false; } if (!enrollChallenge['challenge']) { // The challenge is required. return false; } } return true; } /** * Finds the enroll challenge of the given version in the enroll challlenge * array. * @param {Array} enrollChallenges The enroll challenges to * search. * @param {string} version Version to search for. * @return {?EnrollChallenge} The enroll challenge with the given versions, or * null if it isn't found. */ function findEnrollChallengeOfVersion(enrollChallenges, version) { for (var i = 0; i < enrollChallenges.length; i++) { if (enrollChallenges[i]['version'] == version) { return enrollChallenges[i]; } } return null; } /** * Makes a responseData object for the enroll request with the given parameters. * @param {EnrollChallenge} enrollChallenge The enroll challenge used to * register. * @param {string} u2fVersion Version of gnubby that enrolled. * @param {string} registrationData The registration data. * @param {string=} opt_clientData The client data, if available. * @return {Object} The responseData object. */ function makeEnrollResponseData(enrollChallenge, u2fVersion, registrationData, opt_clientData) { var responseData = {}; responseData['registrationData'] = registrationData; // Echo the used challenge back in the reply. for (var k in enrollChallenge) { responseData[k] = enrollChallenge[k]; } if (u2fVersion == 'U2F_V2') { // For U2F_V2, the challenge sent to the gnubby is modified to be the // hash of the client data. Include the client data. responseData['clientData'] = opt_clientData; } return responseData; } /** * Gets the expanded sign challenges from an enroll request, potentially by * modifying the request to contain a challenge value where one was omitted. * (For enrolling, the server isn't interested in the value of a signature, * only whether the presented key handle is already enrolled.) * @param {Object} request The request. * @return {Array} */ function getSignRequestsFromEnrollRequest(request) { var signChallenges; if (request.hasOwnProperty('registeredKeys')) { signChallenges = request['registeredKeys']; } else { signChallenges = request['signRequests']; } if (signChallenges) { for (var i = 0; i < signChallenges.length; i++) { // Make sure each sign challenge has a challenge value. // The actual value doesn't matter, as long as it's a string. if (!signChallenges[i].hasOwnProperty('challenge')) { signChallenges[i]['challenge'] = ''; } } } return signChallenges; } /** * Creates a new object to track enrolling with a gnubby. * @param {!Countdown} timer Timer for enroll request. * @param {!WebRequestSender} sender The sender of the request. * @param {function(U2fError)} errorCb Called upon enroll failure. * @param {function(string, string, (string|undefined))} successCb Called upon * enroll success with the version of the succeeding gnubby, the enroll * data, and optionally the browser data associated with the enrollment. * @param {string=} opt_logMsgUrl The url to post log messages to. * @constructor */ function Enroller(timer, sender, errorCb, successCb, opt_logMsgUrl) { /** @private {Countdown} */ this.timer_ = timer; /** @private {WebRequestSender} */ this.sender_ = sender; /** @private {function(U2fError)} */ this.errorCb_ = errorCb; /** @private {function(string, string, (string|undefined))} */ this.successCb_ = successCb; /** @private {string|undefined} */ this.logMsgUrl_ = opt_logMsgUrl; /** @private {boolean} */ this.done_ = false; /** @private {Object} */ this.browserData_ = {}; /** @private {Array} */ this.encodedEnrollChallenges_ = []; /** @private {Array} */ this.encodedSignChallenges_ = []; // Allow http appIds for http origins. (Broken, but the caller deserves // what they get.) /** @private {boolean} */ this.allowHttp_ = this.sender_.origin ? this.sender_.origin.indexOf('http://') == 0 : false; /** @private {Closeable} */ this.handler_ = null; } /** * Default timeout value in case the caller never provides a valid timeout. */ Enroller.DEFAULT_TIMEOUT_MILLIS = 30 * 1000; /** * Performs an enroll request with the given enroll and sign challenges. * @param {Array} enrollChallenges A set of enroll challenges. * @param {Array} signChallenges A set of sign challenges for * existing enrollments for this user and appId. * @param {string=} opt_appId The app id for the entire request. */ Enroller.prototype.doEnroll = function(enrollChallenges, signChallenges, opt_appId) { /** @private {Array} */ this.enrollChallenges_ = enrollChallenges; /** @private {Array} */ this.signChallenges_ = signChallenges; /** @private {(string|undefined)} */ this.appId_ = opt_appId; var self = this; getTabIdWhenPossible(this.sender_).then(function() { if (self.done_) return; self.approveOrigin_(); }, function() { self.close(); self.notifyError_({errorCode: ErrorCodes.BAD_REQUEST}); }); }; /** * Ensures the user has approved this origin to use security keys, sending * to the request to the handler if/when the user has done so. * @private */ Enroller.prototype.approveOrigin_ = function() { var self = this; FACTORY_REGISTRY.getApprovedOrigins() .isApprovedOrigin(this.sender_.origin, this.sender_.tabId) .then(function(result) { if (self.done_) return; if (!result) { // Origin not approved: rather than give an explicit indication to // the web page, let a timeout occur. // NOTE: if you are looking at this in a debugger, this line will // always be false since the origin of the debugger is different // than origin of requesting page if (self.timer_.expired()) { self.notifyTimeout_(); return; } var newTimer = self.timer_.clone(self.notifyTimeout_.bind(self)); self.timer_.clearTimeout(); self.timer_ = newTimer; return; } self.sendEnrollRequestToHelper_(); }); }; /** * Notifies the caller of a timeout error. * @private */ Enroller.prototype.notifyTimeout_ = function() { this.notifyError_({errorCode: ErrorCodes.TIMEOUT}); }; /** * Performs an enroll request with this instance's enroll and sign challenges, * by encoding them into a helper request and passing the resulting request to * the factory registry's helper. * @private */ Enroller.prototype.sendEnrollRequestToHelper_ = function() { var encodedEnrollChallenges = this.encodeEnrollChallenges_(this.enrollChallenges_, this.appId_); // If the request didn't contain a sign challenge, provide one. The value // doesn't matter. var defaultSignChallenge = ''; var encodedSignChallenges = encodeSignChallenges(this.signChallenges_, defaultSignChallenge, this.appId_); var request = { type: 'enroll_helper_request', enrollChallenges: encodedEnrollChallenges, signData: encodedSignChallenges, logMsgUrl: this.logMsgUrl_ }; if (!this.timer_.expired()) { request.timeout = this.timer_.millisecondsUntilExpired() / 1000.0; request.timeoutSeconds = this.timer_.millisecondsUntilExpired() / 1000.0; } // Begin fetching/checking the app ids. var enrollAppIds = []; if (this.appId_) { enrollAppIds.push(this.appId_); } for (var i = 0; i < this.enrollChallenges_.length; i++) { if (this.enrollChallenges_[i].hasOwnProperty('appId')) { enrollAppIds.push(this.enrollChallenges_[i]['appId']); } } // Sanity check if (!enrollAppIds.length) { console.warn(UTIL_fmt('empty enroll app ids?')); this.notifyError_({errorCode: ErrorCodes.BAD_REQUEST}); return; } var self = this; this.checkAppIds_(enrollAppIds, function(result) { if (self.done_) return; if (result) { self.handler_ = FACTORY_REGISTRY.getRequestHelper().getHandler(request); if (self.handler_) { var helperComplete = /** @type {function(HelperReply)} */ (self.helperComplete_.bind(self)); self.handler_.run(helperComplete); } else { self.notifyError_({errorCode: ErrorCodes.OTHER_ERROR}); } } else { self.notifyError_({errorCode: ErrorCodes.BAD_REQUEST}); } }); }; /** * Encodes the enroll challenge as an enroll helper challenge. * @param {EnrollChallenge} enrollChallenge The enroll challenge to encode. * @param {string=} opt_appId The app id for the entire request. * @return {EnrollHelperChallenge} The encoded challenge. * @private */ Enroller.encodeEnrollChallenge_ = function(enrollChallenge, opt_appId) { var encodedChallenge = {}; var version; if (enrollChallenge['version']) { version = enrollChallenge['version']; } else { // Version is implicitly V1 if not specified. version = 'U2F_V1'; } encodedChallenge['version'] = version; encodedChallenge['challengeHash'] = enrollChallenge['challenge']; var appId; if (enrollChallenge['appId']) { appId = enrollChallenge['appId']; } else { appId = opt_appId; } if (!appId) { // Sanity check. (Other code should fail if it's not set.) console.warn(UTIL_fmt('No appId?')); } encodedChallenge['appIdHash'] = B64_encode(sha256HashOfString(appId)); return /** @type {EnrollHelperChallenge} */ (encodedChallenge); }; /** * Encodes the given enroll challenges using this enroller's state. * @param {Array} enrollChallenges The enroll challenges. * @param {string=} opt_appId The app id for the entire request. * @return {!Array} The encoded enroll challenges. * @private */ Enroller.prototype.encodeEnrollChallenges_ = function(enrollChallenges, opt_appId) { var challenges = []; for (var i = 0; i < enrollChallenges.length; i++) { var enrollChallenge = enrollChallenges[i]; var version = enrollChallenge.version; if (!version) { // Version is implicitly V1 if not specified. version = 'U2F_V1'; } if (version == 'U2F_V2') { var modifiedChallenge = {}; for (var k in enrollChallenge) { modifiedChallenge[k] = enrollChallenge[k]; } // V2 enroll responses contain signatures over a browser data object, // which we're constructing here. The browser data object contains, among // other things, the server challenge. var serverChallenge = enrollChallenge['challenge']; var browserData = makeEnrollBrowserData( serverChallenge, this.sender_.origin, this.sender_.tlsChannelId); // Replace the challenge with the hash of the browser data. modifiedChallenge['challenge'] = B64_encode(sha256HashOfString(browserData)); this.browserData_[version] = B64_encode(UTIL_StringToBytes(browserData)); challenges.push(Enroller.encodeEnrollChallenge_( /** @type {EnrollChallenge} */ (modifiedChallenge), opt_appId)); } else { challenges.push( Enroller.encodeEnrollChallenge_(enrollChallenge, opt_appId)); } } return challenges; }; /** * Checks the app ids associated with this enroll request, and calls a callback * with the result of the check. * @param {!Array} enrollAppIds The app ids in the enroll challenge * portion of the enroll request. * @param {function(boolean)} cb Called with the result of the check. * @private */ Enroller.prototype.checkAppIds_ = function(enrollAppIds, cb) { var appIds = UTIL_unionArrays(enrollAppIds, getDistinctAppIds(this.signChallenges_)); FACTORY_REGISTRY.getOriginChecker() .canClaimAppIds(this.sender_.origin, appIds) .then(this.originChecked_.bind(this, appIds, cb)); }; /** * Called with the result of checking the origin. When the origin is allowed * to claim the app ids, begins checking whether the app ids also list the * origin. * @param {!Array} appIds The app ids. * @param {function(boolean)} cb Called with the result of the check. * @param {boolean} result Whether the origin could claim the app ids. * @private */ Enroller.prototype.originChecked_ = function(appIds, cb, result) { if (!result) { this.notifyError_({errorCode: ErrorCodes.BAD_REQUEST}); return; } var appIdChecker = FACTORY_REGISTRY.getAppIdCheckerFactory().create(); appIdChecker. checkAppIds( this.timer_.clone(), this.sender_.origin, appIds, this.allowHttp_, this.logMsgUrl_) .then(cb); }; /** Closes this enroller. */ Enroller.prototype.close = function() { if (this.handler_) { this.handler_.close(); this.handler_ = null; } this.done_ = true; }; /** * Notifies the caller with the error. * @param {U2fError} error Error. * @private */ Enroller.prototype.notifyError_ = function(error) { if (this.done_) return; this.close(); this.done_ = true; this.errorCb_(error); }; /** * Notifies the caller of success with the provided response data. * @param {string} u2fVersion Protocol version * @param {string} info Response data * @param {string|undefined} opt_browserData Browser data used * @private */ Enroller.prototype.notifySuccess_ = function(u2fVersion, info, opt_browserData) { if (this.done_) return; this.close(); this.done_ = true; this.successCb_(u2fVersion, info, opt_browserData); }; /** * Called by the helper upon completion. * @param {EnrollHelperReply} reply The result of the enroll request. * @private */ Enroller.prototype.helperComplete_ = function(reply) { if (reply.code) { var reportedError = mapDeviceStatusCodeToU2fError(reply.code); console.log(UTIL_fmt('helper reported ' + reply.code.toString(16) + ', returning ' + reportedError.errorCode)); this.notifyError_(reportedError); } else { console.log(UTIL_fmt('Gnubby enrollment succeeded!!!!!')); var browserData; if (reply.version == 'U2F_V2') { // For U2F_V2, the challenge sent to the gnubby is modified to be the hash // of the browser data. Include the browser data. browserData = this.browserData_[reply.version]; } this.notifySuccess_(/** @type {string} */ (reply.version), /** @type {string} */ (reply.enrollData), browserData); } }; // Copyright 2014 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. /** * @fileoverview Implements an enroll handler using USB gnubbies. */ 'use strict'; /** * @param {!EnrollHelperRequest} request The enroll request. * @constructor * @implements {RequestHandler} */ function UsbEnrollHandler(request) { /** @private {!EnrollHelperRequest} */ this.request_ = request; /** @private {Array} */ this.waitingForTouchGnubbies_ = []; /** @private {boolean} */ this.closed_ = false; /** @private {boolean} */ this.notified_ = false; } /** * Default timeout value in case the caller never provides a valid timeout. * @const */ UsbEnrollHandler.DEFAULT_TIMEOUT_MILLIS = 30 * 1000; /** * @param {RequestHandlerCallback} cb Called back with the result of the * request, and an optional source for the result. * @return {boolean} Whether this handler could be run. */ UsbEnrollHandler.prototype.run = function(cb) { var timeoutMillis = this.request_.timeoutSeconds ? this.request_.timeoutSeconds * 1000 : UsbEnrollHandler.DEFAULT_TIMEOUT_MILLIS; /** @private {Countdown} */ this.timer_ = DEVICE_FACTORY_REGISTRY.getCountdownFactory().createTimer( timeoutMillis); this.enrollChallenges = this.request_.enrollChallenges; /** @private {RequestHandlerCallback} */ this.cb_ = cb; this.signer_ = new MultipleGnubbySigner( true /* forEnroll */, this.signerCompleted_.bind(this), this.signerFoundGnubby_.bind(this), timeoutMillis, this.request_.logMsgUrl); return this.signer_.doSign(this.request_.signData); }; /** Closes this helper. */ UsbEnrollHandler.prototype.close = function() { this.closed_ = true; for (var i = 0; i < this.waitingForTouchGnubbies_.length; i++) { this.waitingForTouchGnubbies_[i].closeWhenIdle(); } this.waitingForTouchGnubbies_ = []; if (this.signer_) { this.signer_.close(); this.signer_ = null; } }; /** * Called when a MultipleGnubbySigner completes its sign request. * @param {boolean} anyPending Whether any gnubbies are pending. * @private */ UsbEnrollHandler.prototype.signerCompleted_ = function(anyPending) { if (!this.anyGnubbiesFound_ || this.anyTimeout_ || anyPending || this.timer_.expired()) { this.notifyError_(DeviceStatusCodes.TIMEOUT_STATUS); } else { // Do nothing: signerFoundGnubby will have been called with each succeeding // gnubby. } }; /** * Called when a MultipleGnubbySigner finds a gnubby that can enroll. * @param {MultipleSignerResult} signResult Signature results * @param {boolean} moreExpected Whether the signer expects to report * results from more gnubbies. * @private */ UsbEnrollHandler.prototype.signerFoundGnubby_ = function(signResult, moreExpected) { if (!signResult.code) { // If the signer reports a gnubby can sign, report this immediately to the // caller, as the gnubby is already enrolled. Map ok to WRONG_DATA, so the // caller knows what to do. this.notifyError_(DeviceStatusCodes.WRONG_DATA_STATUS); } else if (SingleGnubbySigner.signErrorIndicatesInvalidKeyHandle( signResult.code)) { var gnubby = signResult['gnubby']; // A valid helper request contains at least one enroll challenge, so use // the app id hash from the first challenge. var appIdHash = this.request_.enrollChallenges[0].appIdHash; DEVICE_FACTORY_REGISTRY.getGnubbyFactory().notEnrolledPrerequisiteCheck( gnubby, appIdHash, this.gnubbyPrerequisitesChecked_.bind(this)); } else { // Unexpected error in signing? Send this immediately to the caller. this.notifyError_(signResult.code); } }; /** * Called with the result of a gnubby prerequisite check. * @param {number} rc The result of the prerequisite check. * @param {Gnubby=} opt_gnubby The gnubby whose prerequisites were checked. * @private */ UsbEnrollHandler.prototype.gnubbyPrerequisitesChecked_ = function(rc, opt_gnubby) { if (rc || this.timer_.expired()) { // Do nothing: // If the timer is expired, the signerCompleted_ callback will indicate // timeout to the caller. // If there's an error, this gnubby is ineligible, but there's nothing we // can do about that here. return; } // If the callback succeeded, the gnubby is not null. var gnubby = /** @type {Gnubby} */ (opt_gnubby); this.anyGnubbiesFound_ = true; this.waitingForTouchGnubbies_.push(gnubby); this.matchEnrollVersionToGnubby_(gnubby); }; /** * Attempts to match the gnubby's U2F version with an appropriate enroll * challenge. * @param {Gnubby} gnubby Gnubby instance * @private */ UsbEnrollHandler.prototype.matchEnrollVersionToGnubby_ = function(gnubby) { if (!gnubby) { console.warn(UTIL_fmt('no gnubby, WTF?')); return; } gnubby.version(this.gnubbyVersioned_.bind(this, gnubby)); }; /** * Called with the result of a version command. * @param {Gnubby} gnubby Gnubby instance * @param {number} rc result of version command. * @param {ArrayBuffer=} data version. * @private */ UsbEnrollHandler.prototype.gnubbyVersioned_ = function(gnubby, rc, data) { if (rc) { this.removeWrongVersionGnubby_(gnubby); return; } var version = UTIL_BytesToString(new Uint8Array(data || null)); this.tryEnroll_(gnubby, version); }; /** * Drops the gnubby from the list of eligible gnubbies. * @param {Gnubby} gnubby Gnubby instance * @private */ UsbEnrollHandler.prototype.removeWaitingGnubby_ = function(gnubby) { gnubby.closeWhenIdle(); var index = this.waitingForTouchGnubbies_.indexOf(gnubby); if (index >= 0) { this.waitingForTouchGnubbies_.splice(index, 1); } }; /** * Drops the gnubby from the list of eligible gnubbies, as it has the wrong * version. * @param {Gnubby} gnubby Gnubby instance * @private */ UsbEnrollHandler.prototype.removeWrongVersionGnubby_ = function(gnubby) { this.removeWaitingGnubby_(gnubby); if (!this.waitingForTouchGnubbies_.length) { // Whoops, this was the last gnubby. this.anyGnubbiesFound_ = false; if (this.timer_.expired()) { this.notifyError_(DeviceStatusCodes.TIMEOUT_STATUS); } else if (this.signer_) { this.signer_.reScanDevices(); } } }; /** * Attempts enrolling a particular gnubby with a challenge of the appropriate * version. * @param {Gnubby} gnubby Gnubby instance * @param {string} version Protocol version * @private */ UsbEnrollHandler.prototype.tryEnroll_ = function(gnubby, version) { var challenge = this.getChallengeOfVersion_(version); if (!challenge) { this.removeWrongVersionGnubby_(gnubby); return; } var challengeValue = B64_decode(challenge['challengeHash']); var appIdHash = challenge['appIdHash']; var individualAttest = DEVICE_FACTORY_REGISTRY.getIndividualAttestation(). requestIndividualAttestation(appIdHash); gnubby.enroll(challengeValue, B64_decode(appIdHash), this.enrollCallback_.bind(this, gnubby, version), individualAttest); }; /** * Finds the (first) challenge of the given version in this helper's challenges. * @param {string} version Protocol version * @return {Object} challenge, if found, or null if not. * @private */ UsbEnrollHandler.prototype.getChallengeOfVersion_ = function(version) { for (var i = 0; i < this.enrollChallenges.length; i++) { if (this.enrollChallenges[i]['version'] == version) { return this.enrollChallenges[i]; } } return null; }; /** * Called with the result of an enroll request to a gnubby. * @param {Gnubby} gnubby Gnubby instance * @param {string} version Protocol version * @param {number} code Status code * @param {ArrayBuffer=} infoArray Returned data * @private */ UsbEnrollHandler.prototype.enrollCallback_ = function(gnubby, version, code, infoArray) { if (this.notified_) { // Enroll completed after previous success or failure. Disregard. return; } switch (code) { case -GnubbyDevice.GONE: // Close this gnubby. this.removeWaitingGnubby_(gnubby); if (!this.waitingForTouchGnubbies_.length) { // Last enroll attempt is complete and last gnubby is gone. this.anyGnubbiesFound_ = false; if (this.timer_.expired()) { this.notifyError_(DeviceStatusCodes.TIMEOUT_STATUS); } else if (this.signer_) { this.signer_.reScanDevices(); } } break; case DeviceStatusCodes.WAIT_TOUCH_STATUS: case DeviceStatusCodes.BUSY_STATUS: case DeviceStatusCodes.TIMEOUT_STATUS: if (this.timer_.expired()) { // Record that at least one gnubby timed out, to return a timeout status // from the complete callback if no other eligible gnubbies are found. /** @private {boolean} */ this.anyTimeout_ = true; // Close this gnubby. this.removeWaitingGnubby_(gnubby); if (!this.waitingForTouchGnubbies_.length) { // Last enroll attempt is complete: return this error. console.log(UTIL_fmt('timeout (' + code.toString(16) + ') enrolling')); this.notifyError_(DeviceStatusCodes.TIMEOUT_STATUS); } } else { DEVICE_FACTORY_REGISTRY.getCountdownFactory().createTimer( UsbEnrollHandler.ENUMERATE_DELAY_INTERVAL_MILLIS, this.tryEnroll_.bind(this, gnubby, version)); } break; case DeviceStatusCodes.OK_STATUS: var info = B64_encode(new Uint8Array(infoArray || [])); this.notifySuccess_(version, info); break; default: console.log(UTIL_fmt('Failed to enroll gnubby: ' + code)); this.notifyError_(code); break; } }; /** * How long to delay between repeated enroll attempts, in milliseconds. * @const */ UsbEnrollHandler.ENUMERATE_DELAY_INTERVAL_MILLIS = 200; /** * Notifies the callback with an error code. * @param {number} code The error code to report. * @private */ UsbEnrollHandler.prototype.notifyError_ = function(code) { if (this.notified_ || this.closed_) return; this.notified_ = true; this.close(); var reply = { 'type': 'enroll_helper_reply', 'code': code }; this.cb_(reply); }; /** * @param {string} version Protocol version * @param {string} info B64 encoded success data * @private */ UsbEnrollHandler.prototype.notifySuccess_ = function(version, info) { if (this.notified_ || this.closed_) return; this.notified_ = true; this.close(); var reply = { 'type': 'enroll_helper_reply', 'code': DeviceStatusCodes.OK_STATUS, 'version': version, 'enrollData': info }; this.cb_(reply); }; // Copyright 2014 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. /** * @fileoverview Queue of pending requests from an origin. * */ 'use strict'; /** * Represents a queued request. Once given a token, call complete() once the * request is processed (or dropped.) * @interface */ function QueuedRequestToken() {} /** Completes (or cancels) this queued request. */ QueuedRequestToken.prototype.complete = function() {}; /** * @param {!RequestQueue} queue The queue for this request. * @param {number} id An id for this request. * @param {function(QueuedRequestToken)} beginCb Called when work may begin on * this request. * @param {RequestToken} opt_prev Previous request in the same queue. * @param {RequestToken} opt_next Next request in the same queue. * @constructor * @implements {QueuedRequestToken} */ function RequestToken(queue, id, beginCb, opt_prev, opt_next) { /** @private {!RequestQueue} */ this.queue_ = queue; /** @private {number} */ this.id_ = id; /** @type {function(QueuedRequestToken)} */ this.beginCb = beginCb; /** @type {RequestToken} */ this.prev = null; /** @type {RequestToken} */ this.next = null; /** @private {boolean} */ this.completed_ = false; } /** Completes (or cancels) this queued request. */ RequestToken.prototype.complete = function() { if (this.completed_) { // Either the caller called us more than once, or the timer is firing. // Either way, nothing more to do here. return; } this.completed_ = true; this.queue_.complete(this); }; /** @return {boolean} Whether this token has already completed. */ RequestToken.prototype.completed = function() { return this.completed_; }; /** * @param {!SystemTimer} sysTimer A system timer implementation. * @constructor */ function RequestQueue(sysTimer) { /** @private {!SystemTimer} */ this.sysTimer_ = sysTimer; /** @private {RequestToken} */ this.head_ = null; /** @private {RequestToken} */ this.tail_ = null; /** @private {number} */ this.id_ = 0; } /** * Inserts this token into the queue. * @param {RequestToken} token Queue token * @private */ RequestQueue.prototype.insertToken_ = function(token) { console.log(UTIL_fmt('token ' + this.id_ + ' inserted')); if (this.head_ === null) { this.head_ = token; this.tail_ = token; } else { if (!this.tail_) throw 'Non-empty list missing tail'; this.tail_.next = token; token.prev = this.tail_; this.tail_ = token; } }; /** * Removes this token from the queue. * @param {RequestToken} token Queue token * @private */ RequestQueue.prototype.removeToken_ = function(token) { if (token.next) { token.next.prev = token.prev; } if (token.prev) { token.prev.next = token.next; } if (this.head_ === token && this.tail_ === token) { this.head_ = this.tail_ = null; } else { if (this.head_ === token) { this.head_ = token.next; this.head_.prev = null; } if (this.tail_ === token) { this.tail_ = token.prev; this.tail_.next = null; } } token.prev = token.next = null; }; /** * Completes this token's request, and begins the next queued request, if one * exists. * @param {RequestToken} token Queue token */ RequestQueue.prototype.complete = function(token) { console.log(UTIL_fmt('token ' + this.id_ + ' completed')); var next = token.next; this.removeToken_(token); if (next) { next.beginCb(next); } }; /** @return {boolean} Whether this queue is empty. */ RequestQueue.prototype.empty = function() { return this.head_ === null; }; /** * Queues this request, and, if it's the first request, begins work on it. * @param {function(QueuedRequestToken)} beginCb Called when work begins on this * request. * @param {Countdown} timer Countdown timer * @return {QueuedRequestToken} A token for the request. */ RequestQueue.prototype.queueRequest = function(beginCb, timer) { var startNow = this.empty(); var token = new RequestToken(this, ++this.id_, beginCb); // Clone the timer to set a callback on it, which will ensure complete() is // eventually called, even if the caller never gets around to it. timer.clone(token.complete.bind(token)); this.insertToken_(token); if (startNow) { this.sysTimer_.setTimeout(function() { if (!token.completed()) { token.beginCb(token); } }, 0); } return token; }; /** * @param {!SystemTimer} sysTimer A system timer implementation. * @constructor */ function OriginKeyedRequestQueue(sysTimer) { /** @private {!SystemTimer} */ this.sysTimer_ = sysTimer; /** @private {Object} */ this.requests_ = {}; } /** * Queues this request, and, if it's the first request, begins work on it. * @param {string} appId Application Id * @param {string} origin Request origin * @param {function(QueuedRequestToken)} beginCb Called when work begins on this * request. * @param {Countdown} timer Countdown timer * @return {QueuedRequestToken} A token for the request. */ OriginKeyedRequestQueue.prototype.queueRequest = function(appId, origin, beginCb, timer) { var key = appId + ' ' + origin; if (!this.requests_.hasOwnProperty(key)) { this.requests_[key] = new RequestQueue(this.sysTimer_); } var queue = this.requests_[key]; return queue.queueRequest(beginCb, timer); }; // Copyright 2014 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. /** * @fileoverview Handles web page requests for gnubby sign requests. * */ 'use strict'; var gnubbySignRequestQueue; function initRequestQueue() { gnubbySignRequestQueue = new OriginKeyedRequestQueue( FACTORY_REGISTRY.getSystemTimer()); } /** * Handles a U2F sign request. * @param {MessageSender} messageSender The message sender. * @param {Object} request The web page's sign request. * @param {Function} sendResponse Called back with the result of the sign. * @return {Closeable} Request handler that should be closed when the browser * message channel is closed. */ function handleU2fSignRequest(messageSender, request, sendResponse) { var sentResponse = false; var queuedSignRequest; function sendErrorResponse(error) { sendResponseOnce(sentResponse, queuedSignRequest, makeU2fErrorResponse(request, error.errorCode, error.errorMessage), sendResponse); } function sendSuccessResponse(challenge, info, browserData) { var responseData = makeU2fSignResponseDataFromChallenge(challenge); addSignatureAndBrowserDataToResponseData(responseData, info, browserData, 'clientData'); var response = makeU2fSuccessResponse(request, responseData); sendResponseOnce(sentResponse, queuedSignRequest, response, sendResponse); } var sender = createSenderFromMessageSender(messageSender); if (!sender) { sendErrorResponse({errorCode: ErrorCodes.BAD_REQUEST}); return null; } if (sender.origin.indexOf('http://') == 0 && !HTTP_ORIGINS_ALLOWED) { sendErrorResponse({errorCode: ErrorCodes.BAD_REQUEST}); return null; } queuedSignRequest = validateAndEnqueueSignRequest( sender, request, sendErrorResponse, sendSuccessResponse); return queuedSignRequest; } /** * Creates a base U2F responseData object from the server challenge. * @param {SignChallenge} challenge The server challenge. * @return {Object} The responseData object. */ function makeU2fSignResponseDataFromChallenge(challenge) { var responseData = { 'keyHandle': challenge['keyHandle'] }; return responseData; } /** * Adds the browser data and signature values to a responseData object. * @param {Object} responseData The "base" responseData object. * @param {string} signatureData The signature data. * @param {string} browserData The browser data generated from the challenge. * @param {string} browserDataName The name of the browser data key in the * responseData object. */ function addSignatureAndBrowserDataToResponseData(responseData, signatureData, browserData, browserDataName) { responseData[browserDataName] = B64_encode(UTIL_StringToBytes(browserData)); responseData['signatureData'] = signatureData; } /** * Validates a sign request using the given sign challenges name, and, if valid, * enqueues the sign request for eventual processing. * @param {WebRequestSender} sender The sender of the message. * @param {Object} request The web page's sign request. * @param {function(U2fError)} errorCb Error callback. * @param {function(SignChallenge, string, string)} successCb Success callback. * @return {Closeable} Request handler that should be closed when the browser * message channel is closed. */ function validateAndEnqueueSignRequest(sender, request, errorCb, successCb) { function timeout() { errorCb({errorCode: ErrorCodes.TIMEOUT}); } if (!isValidSignRequest(request)) { errorCb({errorCode: ErrorCodes.BAD_REQUEST}); return null; } // The typecast is necessary because getSignChallenges can return undefined. // On the other hand, a valid sign request can't contain an undefined sign // challenge list, so the typecast is safe. var signChallenges = /** @type {!Array} */ ( getSignChallenges(request)); var appId; if (request['appId']) { appId = request['appId']; } else if (signChallenges.length) { appId = signChallenges[0]['appId']; } // Sanity check if (!appId) { console.warn(UTIL_fmt('empty sign appId?')); errorCb({errorCode: ErrorCodes.BAD_REQUEST}); return null; } var timeoutValueSeconds = getTimeoutValueFromRequest(request); // Attenuate watchdog timeout value less than the signer's timeout, so the // watchdog only fires after the signer could reasonably have called back, // not before. timeoutValueSeconds = attenuateTimeoutInSeconds(timeoutValueSeconds, MINIMUM_TIMEOUT_ATTENUATION_SECONDS / 2); var watchdog = new WatchdogRequestHandler(timeoutValueSeconds, timeout); var wrappedErrorCb = watchdog.wrapCallback(errorCb); var wrappedSuccessCb = watchdog.wrapCallback(successCb); var timer = createAttenuatedTimer( FACTORY_REGISTRY.getCountdownFactory(), timeoutValueSeconds); var logMsgUrl = request['logMsgUrl']; // Queue sign requests from the same origin, to protect against simultaneous // sign-out on many tabs resulting in repeated sign-in requests. var queuedSignRequest = new QueuedSignRequest(signChallenges, timer, sender, wrappedErrorCb, wrappedSuccessCb, request['challenge'], appId, logMsgUrl); if (!gnubbySignRequestQueue) { initRequestQueue(); } var requestToken = gnubbySignRequestQueue.queueRequest(appId, sender.origin, queuedSignRequest.begin.bind(queuedSignRequest), timer); queuedSignRequest.setToken(requestToken); watchdog.setCloseable(queuedSignRequest); return watchdog; } /** * Returns whether the request appears to be a valid sign request. * @param {Object} request The request. * @return {boolean} Whether the request appears valid. */ function isValidSignRequest(request) { var signChallenges = getSignChallenges(request); if (!signChallenges) { return false; } var hasDefaultChallenge = request.hasOwnProperty('challenge'); var hasAppId = request.hasOwnProperty('appId'); // If the sign challenge array is empty, the global appId is required. if (!hasAppId && (!signChallenges || !signChallenges.length)) { return false; } return isValidSignChallengeArray(signChallenges, !hasDefaultChallenge, !hasAppId); } /** * Adapter class representing a queued sign request. * @param {!Array} signChallenges The sign challenges. * @param {Countdown} timer Timeout timer * @param {WebRequestSender} sender Message sender. * @param {function(U2fError)} errorCb Error callback * @param {function(SignChallenge, string, string)} successCb Success callback * @param {string|undefined} opt_defaultChallenge A default sign challenge * value, if a request does not provide one. * @param {string|undefined} opt_appId The app id for the entire request. * @param {string|undefined} opt_logMsgUrl Url to post log messages to * @constructor * @implements {Closeable} */ function QueuedSignRequest(signChallenges, timer, sender, errorCb, successCb, opt_defaultChallenge, opt_appId, opt_logMsgUrl) { /** @private {!Array} */ this.signChallenges_ = signChallenges; /** @private {Countdown} */ this.timer_ = timer.clone(this.close.bind(this)); /** @private {WebRequestSender} */ this.sender_ = sender; /** @private {function(U2fError)} */ this.errorCb_ = errorCb; /** @private {function(SignChallenge, string, string)} */ this.successCb_ = successCb; /** @private {string|undefined} */ this.defaultChallenge_ = opt_defaultChallenge; /** @private {string|undefined} */ this.appId_ = opt_appId; /** @private {string|undefined} */ this.logMsgUrl_ = opt_logMsgUrl; /** @private {boolean} */ this.begun_ = false; /** @private {boolean} */ this.closed_ = false; } /** Closes this sign request. */ QueuedSignRequest.prototype.close = function() { if (this.closed_) return; var hadBegunSigning = false; if (this.begun_ && this.signer_) { this.signer_.close(); hadBegunSigning = true; } if (this.token_) { if (hadBegunSigning) { console.log(UTIL_fmt('closing in-progress request')); } else { console.log(UTIL_fmt('closing timed-out request before processing')); } this.token_.complete(); } this.closed_ = true; }; /** * @param {QueuedRequestToken} token Token for this sign request. */ QueuedSignRequest.prototype.setToken = function(token) { /** @private {QueuedRequestToken} */ this.token_ = token; }; /** * Called when this sign request may begin work. * @param {QueuedRequestToken} token Token for this sign request. */ QueuedSignRequest.prototype.begin = function(token) { if (this.timer_.expired()) { console.log(UTIL_fmt('Queued request begun after timeout')); this.close(); this.errorCb_({errorCode: ErrorCodes.TIMEOUT}); return; } this.begun_ = true; this.setToken(token); this.signer_ = new Signer(this.timer_, this.sender_, this.signerFailed_.bind(this), this.signerSucceeded_.bind(this), this.logMsgUrl_); if (!this.signer_.setChallenges(this.signChallenges_, this.defaultChallenge_, this.appId_)) { token.complete(); this.errorCb_({errorCode: ErrorCodes.BAD_REQUEST}); } // Signer now has responsibility for maintaining timeout. this.timer_.clearTimeout(); }; /** * Called when this request's signer fails. * @param {U2fError} error The failure reported by the signer. * @private */ QueuedSignRequest.prototype.signerFailed_ = function(error) { this.token_.complete(); this.errorCb_(error); }; /** * Called when this request's signer succeeds. * @param {SignChallenge} challenge The challenge that was signed. * @param {string} info The sign result. * @param {string} browserData Browser data JSON * @private */ QueuedSignRequest.prototype.signerSucceeded_ = function(challenge, info, browserData) { this.token_.complete(); this.successCb_(challenge, info, browserData); }; /** * Creates an object to track signing with a gnubby. * @param {Countdown} timer Timer for sign request. * @param {WebRequestSender} sender The message sender. * @param {function(U2fError)} errorCb Called when the sign operation fails. * @param {function(SignChallenge, string, string)} successCb Called when the * sign operation succeeds. * @param {string=} opt_logMsgUrl The url to post log messages to. * @constructor */ function Signer(timer, sender, errorCb, successCb, opt_logMsgUrl) { /** @private {Countdown} */ this.timer_ = timer.clone(); /** @private {WebRequestSender} */ this.sender_ = sender; /** @private {function(U2fError)} */ this.errorCb_ = errorCb; /** @private {function(SignChallenge, string, string)} */ this.successCb_ = successCb; /** @private {string|undefined} */ this.logMsgUrl_ = opt_logMsgUrl; /** @private {boolean} */ this.challengesSet_ = false; /** @private {boolean} */ this.done_ = false; /** @private {Object} */ this.browserData_ = {}; /** @private {Object} */ this.serverChallenges_ = {}; // Allow http appIds for http origins. (Broken, but the caller deserves // what they get.) /** @private {boolean} */ this.allowHttp_ = this.sender_.origin ? this.sender_.origin.indexOf('http://') == 0 : false; /** @private {Closeable} */ this.handler_ = null; } /** * Sets the challenges to be signed. * @param {Array} signChallenges The challenges to set. * @param {string=} opt_defaultChallenge A default sign challenge * value, if a request does not provide one. * @param {string=} opt_appId The app id for the entire request. * @return {boolean} Whether the challenges could be set. */ Signer.prototype.setChallenges = function(signChallenges, opt_defaultChallenge, opt_appId) { if (this.challengesSet_ || this.done_) return false; if (this.timer_.expired()) { this.notifyError_({errorCode: ErrorCodes.TIMEOUT}); return true; } /** @private {Array} */ this.signChallenges_ = signChallenges; /** @private {string|undefined} */ this.defaultChallenge_ = opt_defaultChallenge; /** @private {string|undefined} */ this.appId_ = opt_appId; /** @private {boolean} */ this.challengesSet_ = true; this.checkAppIds_(); return true; }; /** * Checks the app ids of incoming requests. * @private */ Signer.prototype.checkAppIds_ = function() { var appIds = getDistinctAppIds(this.signChallenges_); if (this.appId_) { appIds = UTIL_unionArrays([this.appId_], appIds); } if (!appIds || !appIds.length) { var error = { errorCode: ErrorCodes.BAD_REQUEST, errorMessage: 'missing appId' }; this.notifyError_(error); return; } FACTORY_REGISTRY.getOriginChecker() .canClaimAppIds(this.sender_.origin, appIds) .then(this.originChecked_.bind(this, appIds)); }; /** * Called with the result of checking the origin. When the origin is allowed * to claim the app ids, begins checking whether the app ids also list the * origin. * @param {!Array} appIds The app ids. * @param {boolean} result Whether the origin could claim the app ids. * @private */ Signer.prototype.originChecked_ = function(appIds, result) { if (!result) { var error = { errorCode: ErrorCodes.BAD_REQUEST, errorMessage: 'bad appId' }; this.notifyError_(error); return; } var appIdChecker = FACTORY_REGISTRY.getAppIdCheckerFactory().create(); appIdChecker. checkAppIds( this.timer_.clone(), this.sender_.origin, /** @type {!Array} */ (appIds), this.allowHttp_, this.logMsgUrl_) .then(this.appIdChecked_.bind(this)); }; /** * Called with the result of checking app ids. When the app ids are valid, * adds the sign challenges to those being signed. * @param {boolean} result Whether the app ids are valid. * @private */ Signer.prototype.appIdChecked_ = function(result) { if (!result) { var error = { errorCode: ErrorCodes.BAD_REQUEST, errorMessage: 'bad appId' }; this.notifyError_(error); return; } if (!this.doSign_()) { this.notifyError_({errorCode: ErrorCodes.BAD_REQUEST}); return; } }; /** * Begins signing this signer's challenges. * @return {boolean} Whether the challenge could be added. * @private */ Signer.prototype.doSign_ = function() { // Create the browser data for each challenge. for (var i = 0; i < this.signChallenges_.length; i++) { var challenge = this.signChallenges_[i]; var serverChallenge; if (challenge.hasOwnProperty('challenge')) { serverChallenge = challenge['challenge']; } else { serverChallenge = this.defaultChallenge_; } if (!serverChallenge) { console.warn(UTIL_fmt('challenge missing')); return false; } var keyHandle = challenge['keyHandle']; var browserData = makeSignBrowserData(serverChallenge, this.sender_.origin, this.sender_.tlsChannelId); this.browserData_[keyHandle] = browserData; this.serverChallenges_[keyHandle] = challenge; } var encodedChallenges = encodeSignChallenges(this.signChallenges_, this.defaultChallenge_, this.appId_, this.getChallengeHash_.bind(this)); var timeoutSeconds = this.timer_.millisecondsUntilExpired() / 1000.0; var request = makeSignHelperRequest(encodedChallenges, timeoutSeconds, this.logMsgUrl_); this.handler_ = FACTORY_REGISTRY.getRequestHelper() .getHandler(/** @type {HelperRequest} */ (request)); if (!this.handler_) return false; return this.handler_.run(this.helperComplete_.bind(this)); }; /** * @param {string} keyHandle The key handle used with the challenge. * @param {string} challenge The challenge. * @return {string} The hashed challenge associated with the key * handle/challenge pair. * @private */ Signer.prototype.getChallengeHash_ = function(keyHandle, challenge) { return B64_encode(sha256HashOfString(this.browserData_[keyHandle])); }; /** Closes this signer. */ Signer.prototype.close = function() { this.close_(); }; /** * Closes this signer, and optionally notifies the caller of error. * @param {boolean=} opt_notifying When true, this method is being called in the * process of notifying the caller of an existing status. When false, * the caller is notified with a default error value, ErrorCodes.TIMEOUT. * @private */ Signer.prototype.close_ = function(opt_notifying) { if (this.handler_) { this.handler_.close(); this.handler_ = null; } this.timer_.clearTimeout(); if (!opt_notifying) { this.notifyError_({errorCode: ErrorCodes.TIMEOUT}); } }; /** * Notifies the caller of error. * @param {U2fError} error Error. * @private */ Signer.prototype.notifyError_ = function(error) { if (this.done_) return; this.done_ = true; this.close_(true); this.errorCb_(error); }; /** * Notifies the caller of success. * @param {SignChallenge} challenge The challenge that was signed. * @param {string} info The sign result. * @param {string} browserData Browser data JSON * @private */ Signer.prototype.notifySuccess_ = function(challenge, info, browserData) { if (this.done_) return; this.done_ = true; this.close_(true); this.successCb_(challenge, info, browserData); }; /** * Called by the helper upon completion. * @param {HelperReply} helperReply The result of the sign request. * @param {string=} opt_source The source of the sign result. * @private */ Signer.prototype.helperComplete_ = function(helperReply, opt_source) { if (helperReply.type != 'sign_helper_reply') { this.notifyError_({errorCode: ErrorCodes.OTHER_ERROR}); return; } var reply = /** @type {SignHelperReply} */ (helperReply); if (reply.code) { var reportedError = mapDeviceStatusCodeToU2fError(reply.code); console.log(UTIL_fmt('helper reported ' + reply.code.toString(16) + ', returning ' + reportedError.errorCode)); this.notifyError_(reportedError); } else { if (this.logMsgUrl_ && opt_source) { var logMsg = 'signed&source=' + opt_source; logMessage(logMsg, this.logMsgUrl_); } var key = reply.responseData['keyHandle']; var browserData = this.browserData_[key]; // Notify with server-provided challenge, not the encoded one: the // server-provided challenge contains additional fields it relies on. var serverChallenge = this.serverChallenges_[key]; this.notifySuccess_(serverChallenge, reply.responseData.signatureData, browserData); } }; // Copyright 2014 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. /** * @fileoverview A single gnubby signer wraps the process of opening a gnubby, * signing each challenge in an array of challenges until a success condition * is satisfied, and finally yielding the gnubby upon success. * */ 'use strict'; /** * @typedef {{ * code: number, * gnubby: (Gnubby|undefined), * challenge: (SignHelperChallenge|undefined), * info: (ArrayBuffer|undefined) * }} */ var SingleSignerResult; /** * Creates a new sign handler with a gnubby. This handler will perform a sign * operation using each challenge in an array of challenges until its success * condition is satisified, or an error or timeout occurs. The success condition * is defined differently depending whether this signer is used for enrolling * or for signing: * * For enroll, success is defined as each challenge yielding wrong data. This * means this gnubby is not currently enrolled for any of the appIds in any * challenge. * * For sign, success is defined as any challenge yielding ok. * * The complete callback is called only when the signer reaches success or * failure, i.e. when there is no need for this signer to continue trying new * challenges. * * @param {GnubbyDeviceId} gnubbyId Which gnubby to open. * @param {boolean} forEnroll Whether this signer is signing for an attempted * enroll operation. * @param {function(SingleSignerResult)} * completeCb Called when this signer completes, i.e. no further results are * possible. * @param {Countdown} timer An advisory timer, beyond whose expiration the * signer will not attempt any new operations, assuming the caller is no * longer interested in the outcome. * @param {string=} opt_logMsgUrl A URL to post log messages to. * @constructor */ function SingleGnubbySigner(gnubbyId, forEnroll, completeCb, timer, opt_logMsgUrl) { /** @private {GnubbyDeviceId} */ this.gnubbyId_ = gnubbyId; /** @private {SingleGnubbySigner.State} */ this.state_ = SingleGnubbySigner.State.INIT; /** @private {boolean} */ this.forEnroll_ = forEnroll; /** @private {function(SingleSignerResult)} */ this.completeCb_ = completeCb; /** @private {Countdown} */ this.timer_ = timer; /** @private {string|undefined} */ this.logMsgUrl_ = opt_logMsgUrl; /** @private {!Array} */ this.challenges_ = []; /** @private {number} */ this.challengeIndex_ = 0; /** @private {boolean} */ this.challengesSet_ = false; /** @private {!Object} */ this.cachedError_ = []; /** @private {(function()|undefined)} */ this.openCanceller_; } /** @enum {number} */ SingleGnubbySigner.State = { /** Initial state. */ INIT: 0, /** The signer is attempting to open a gnubby. */ OPENING: 1, /** The signer's gnubby opened, but is busy. */ BUSY: 2, /** The signer has an open gnubby, but no challenges to sign. */ IDLE: 3, /** The signer is currently signing a challenge. */ SIGNING: 4, /** The signer got a final outcome. */ COMPLETE: 5, /** The signer is closing its gnubby. */ CLOSING: 6, /** The signer is closed. */ CLOSED: 7 }; /** * @return {GnubbyDeviceId} This device id of the gnubby for this signer. */ SingleGnubbySigner.prototype.getDeviceId = function() { return this.gnubbyId_; }; /** * Closes this signer's gnubby, if it's held. */ SingleGnubbySigner.prototype.close = function() { if (this.state_ == SingleGnubbySigner.State.OPENING) { if (this.openCanceller_) this.openCanceller_(); } if (!this.gnubby_) return; this.state_ = SingleGnubbySigner.State.CLOSING; this.gnubby_.closeWhenIdle(this.closed_.bind(this)); }; /** * Called when this signer's gnubby is closed. * @private */ SingleGnubbySigner.prototype.closed_ = function() { this.gnubby_ = null; this.state_ = SingleGnubbySigner.State.CLOSED; }; /** * Begins signing the given challenges. * @param {Array} challenges The challenges to sign. * @return {boolean} Whether the challenges were accepted. */ SingleGnubbySigner.prototype.doSign = function(challenges) { if (this.challengesSet_) { // Can't add new challenges once they've been set. return false; } if (challenges) { console.log(this.gnubby_); console.log(UTIL_fmt('adding ' + challenges.length + ' challenges')); for (var i = 0; i < challenges.length; i++) { this.challenges_.push(challenges[i]); } } this.challengesSet_ = true; switch (this.state_) { case SingleGnubbySigner.State.INIT: this.open_(); break; case SingleGnubbySigner.State.OPENING: // The open has already commenced, so accept the challenges, but don't do // anything. break; case SingleGnubbySigner.State.IDLE: if (this.challengeIndex_ < challenges.length) { // Challenges set: start signing. this.doSign_(this.challengeIndex_); } else { // An empty list of challenges can be set during enroll, when the user // has no existing enrolled gnubbies. It's unexpected during sign, but // returning WRONG_DATA satisfies the caller in either case. var self = this; window.setTimeout(function() { self.goToError_(DeviceStatusCodes.WRONG_DATA_STATUS); }, 0); } break; case SingleGnubbySigner.State.SIGNING: // Already signing, so don't kick off a new sign, but accept the added // challenges. break; default: return false; } return true; }; /** * Attempts to open this signer's gnubby, if it's not already open. * @private */ SingleGnubbySigner.prototype.open_ = function() { var appIdHash; if (this.challenges_.length) { // Assume the first challenge's appId is representative of all of them. appIdHash = B64_encode(this.challenges_[0].appIdHash); } if (this.state_ == SingleGnubbySigner.State.INIT) { this.state_ = SingleGnubbySigner.State.OPENING; this.openCanceller_ = DEVICE_FACTORY_REGISTRY.getGnubbyFactory().openGnubby( this.gnubbyId_, this.forEnroll_, this.openCallback_.bind(this), appIdHash, this.logMsgUrl_, 'singlesigner.js:SingleGnubbySigner.prototype.open_'); } }; /** * How long to delay retrying a failed open. */ SingleGnubbySigner.OPEN_DELAY_MILLIS = 200; /** * How long to delay retrying a sign requiring touch. */ SingleGnubbySigner.SIGN_DELAY_MILLIS = 200; /** * @param {number} rc The result of the open operation. * @param {Gnubby=} gnubby The opened gnubby, if open was successful (or busy). * @private */ SingleGnubbySigner.prototype.openCallback_ = function(rc, gnubby) { if (this.state_ != SingleGnubbySigner.State.OPENING && this.state_ != SingleGnubbySigner.State.BUSY) { // Open completed after close, perhaps? Ignore. return; } switch (rc) { case DeviceStatusCodes.OK_STATUS: if (!gnubby) { console.warn(UTIL_fmt('open succeeded but gnubby is null, WTF?')); } else { this.gnubby_ = gnubby; this.gnubby_.version(this.versionCallback_.bind(this)); } break; case DeviceStatusCodes.BUSY_STATUS: this.gnubby_ = gnubby; this.state_ = SingleGnubbySigner.State.BUSY; // If there's still time, retry the open. if (!this.timer_ || !this.timer_.expired()) { var self = this; window.setTimeout(function() { if (self.gnubby_) { this.openCanceller_ = DEVICE_FACTORY_REGISTRY .getGnubbyFactory().openGnubby( self.gnubbyId_, self.forEnroll_, self.openCallback_.bind(self), self.logMsgUrl_, 'singlesigner.js:SingleGnubbySigner.prototype.openCallback_'); } }, SingleGnubbySigner.OPEN_DELAY_MILLIS); } else { this.goToError_(DeviceStatusCodes.BUSY_STATUS); } break; default: // TODO: This won't be confused with success, but should it be // part of the same namespace as the other error codes, which are // always in DeviceStatusCodes.*? this.goToError_(rc, true); } }; /** * Called with the result of a version command. * @param {number} rc Result of version command. * @param {ArrayBuffer=} opt_data Version. * @private */ SingleGnubbySigner.prototype.versionCallback_ = function(rc, opt_data) { if (rc == DeviceStatusCodes.BUSY_STATUS) { if (this.timer_ && this.timer_.expired()) { this.goToError_(DeviceStatusCodes.TIMEOUT_STATUS); return; } // There's still time: resync and retry. var self = this; this.gnubby_.sync(function(code) { if (code) { self.goToError_(code, true); return; } self.gnubby_.version(self.versionCallback_.bind(self)); }); return; } if (rc) { this.goToError_(rc, true); return; } this.state_ = SingleGnubbySigner.State.IDLE; this.version_ = UTIL_BytesToString(new Uint8Array(opt_data || [])); this.doSign_(this.challengeIndex_); }; /** * @param {number} challengeIndex Index of challenge to sign * @private */ SingleGnubbySigner.prototype.doSign_ = function(challengeIndex) { if (!this.gnubby_) { // Already closed? Nothing to do. return; } if (this.timer_ && this.timer_.expired()) { // If the timer is expired, that means we never got a success response. // We could have gotten wrong data on a partial set of challenges, but this // means we don't yet know the final outcome. In any event, we don't yet // know the final outcome: return timeout. this.goToError_(DeviceStatusCodes.TIMEOUT_STATUS); return; } if (!this.challengesSet_) { this.state_ = SingleGnubbySigner.State.IDLE; return; } this.state_ = SingleGnubbySigner.State.SIGNING; if (challengeIndex >= this.challenges_.length) { this.signCallback_(challengeIndex, DeviceStatusCodes.WRONG_DATA_STATUS); return; } var challenge = this.challenges_[challengeIndex]; var challengeHash = challenge.challengeHash; var appIdHash = challenge.appIdHash; var keyHandle = challenge.keyHandle; if (this.cachedError_.hasOwnProperty(keyHandle)) { // Cache hit: return wrong data again. this.signCallback_(challengeIndex, this.cachedError_[keyHandle]); } else if (challenge.version && challenge.version != this.version_) { // Sign challenge for a different version of gnubby: return wrong data. this.signCallback_(challengeIndex, DeviceStatusCodes.WRONG_DATA_STATUS); } else { var nowink = false; this.gnubby_.sign(challengeHash, appIdHash, keyHandle, this.signCallback_.bind(this, challengeIndex), nowink); } }; /** * @param {number} code The result of a sign operation. * @return {boolean} Whether the error indicates the key handle is invalid * for this gnubby. */ SingleGnubbySigner.signErrorIndicatesInvalidKeyHandle = function(code) { return (code == DeviceStatusCodes.WRONG_DATA_STATUS || code == DeviceStatusCodes.WRONG_LENGTH_STATUS || code == DeviceStatusCodes.INVALID_DATA_STATUS); }; /** * Called with the result of a single sign operation. * @param {number} challengeIndex the index of the challenge just attempted * @param {number} code the result of the sign operation * @param {ArrayBuffer=} opt_info Optional result data * @private */ SingleGnubbySigner.prototype.signCallback_ = function(challengeIndex, code, opt_info) { console.log(UTIL_fmt('gnubby ' + JSON.stringify(this.gnubbyId_) + ', challenge ' + challengeIndex + ' yielded ' + code.toString(16))); if (this.state_ != SingleGnubbySigner.State.SIGNING) { console.log(UTIL_fmt('already done!')); // We're done, the caller's no longer interested. return; } // Cache certain idempotent errors, re-asking the gnubby to sign it // won't produce different results. if (SingleGnubbySigner.signErrorIndicatesInvalidKeyHandle(code)) { if (challengeIndex < this.challenges_.length) { var challenge = this.challenges_[challengeIndex]; if (!this.cachedError_.hasOwnProperty(challenge.keyHandle)) { this.cachedError_[challenge.keyHandle] = code; } } } var self = this; switch (code) { case DeviceStatusCodes.GONE_STATUS: this.goToError_(code); break; case DeviceStatusCodes.TIMEOUT_STATUS: this.gnubby_.sync(this.synced_.bind(this)); break; case DeviceStatusCodes.BUSY_STATUS: this.doSign_(this.challengeIndex_); break; case DeviceStatusCodes.OK_STATUS: // Lower bound on the minimum length, signature length can vary. var MIN_SIGNATURE_LENGTH = 7; if (!opt_info || opt_info.byteLength < MIN_SIGNATURE_LENGTH) { console.error(UTIL_fmt('Got short response to sign request (' + (opt_info ? opt_info.byteLength : 0) + ' bytes), WTF?')); } if (this.forEnroll_) { this.goToError_(code); } else { this.goToSuccess_(code, this.challenges_[challengeIndex], opt_info); } break; case DeviceStatusCodes.WAIT_TOUCH_STATUS: window.setTimeout(function() { self.doSign_(self.challengeIndex_); }, SingleGnubbySigner.SIGN_DELAY_MILLIS); break; case DeviceStatusCodes.WRONG_DATA_STATUS: case DeviceStatusCodes.WRONG_LENGTH_STATUS: case DeviceStatusCodes.INVALID_DATA_STATUS: if (this.challengeIndex_ < this.challenges_.length - 1) { this.doSign_(++this.challengeIndex_); } else if (this.forEnroll_) { this.goToSuccess_(code); } else { this.goToError_(code); } break; default: if (this.forEnroll_) { this.goToError_(code, true); } else if (this.challengeIndex_ < this.challenges_.length - 1) { this.doSign_(++this.challengeIndex_); } else { this.goToError_(code, true); } } }; /** * Called with the response of a sync command, called when a sign yields a * timeout to reassert control over the gnubby. * @param {number} code Error code * @private */ SingleGnubbySigner.prototype.synced_ = function(code) { if (code) { this.goToError_(code, true); return; } this.doSign_(this.challengeIndex_); }; /** * Switches to the error state, and notifies caller. * @param {number} code Error code * @param {boolean=} opt_warn Whether to warn in the console about the error. * @private */ SingleGnubbySigner.prototype.goToError_ = function(code, opt_warn) { this.state_ = SingleGnubbySigner.State.COMPLETE; var logFn = opt_warn ? console.warn.bind(console) : console.log.bind(console); logFn(UTIL_fmt('failed (' + code.toString(16) + ')')); var result = { code: code }; if (!this.forEnroll_ && SingleGnubbySigner.signErrorIndicatesInvalidKeyHandle(code)) { // When a device yields an idempotent bad key handle error to all sign // challenges, and this is a sign request, we don't want to yield to the // web page that it's not enrolled just yet: we want the user to tap the // device first. We'll report the gnubby to the caller and let it close it // instead of closing it here. result.gnubby = this.gnubby_; } else { // Since this gnubby can no longer produce a useful result, go ahead and // close it. this.close(); } this.completeCb_(result); }; /** * Switches to the success state, and notifies caller. * @param {number} code Status code * @param {SignHelperChallenge=} opt_challenge The challenge signed * @param {ArrayBuffer=} opt_info Optional result data * @private */ SingleGnubbySigner.prototype.goToSuccess_ = function(code, opt_challenge, opt_info) { this.state_ = SingleGnubbySigner.State.COMPLETE; console.log(UTIL_fmt('success (' + code.toString(16) + ')')); var result = { code: code, gnubby: this.gnubby_ }; if (opt_challenge || opt_info) { if (opt_challenge) { result['challenge'] = opt_challenge; } if (opt_info) { result['info'] = opt_info; } } this.completeCb_(result); // this.gnubby_ is now owned by completeCb_. this.gnubby_ = null; }; // Copyright 2014 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. /** * @fileoverview A multiple gnubby signer wraps the process of opening a number * of gnubbies, signing each challenge in an array of challenges until a * success condition is satisfied, and yielding each succeeding gnubby. * */ 'use strict'; /** * @typedef {{ * code: number, * gnubbyId: GnubbyDeviceId, * challenge: (SignHelperChallenge|undefined), * info: (ArrayBuffer|undefined) * }} */ var MultipleSignerResult; /** * Creates a new sign handler that manages signing with all the available * gnubbies. * @param {boolean} forEnroll Whether this signer is signing for an attempted * enroll operation. * @param {function(boolean)} allCompleteCb Called when this signer completes * sign attempts, i.e. no further results will be produced. The parameter * indicates whether any gnubbies are present that have not yet produced a * final result. * @param {function(MultipleSignerResult, boolean)} gnubbyCompleteCb * Called with each gnubby/challenge that yields a final result, along with * whether this signer expects to produce more results. The boolean is a * hint rather than a promise: it's possible for this signer to produce * further results after saying it doesn't expect more, or to fail to * produce further results after saying it does. * @param {number} timeoutMillis A timeout value, beyond whose expiration the * signer will not attempt any new operations, assuming the caller is no * longer interested in the outcome. * @param {string=} opt_logMsgUrl A URL to post log messages to. * @constructor */ function MultipleGnubbySigner(forEnroll, allCompleteCb, gnubbyCompleteCb, timeoutMillis, opt_logMsgUrl) { /** @private {boolean} */ this.forEnroll_ = forEnroll; /** @private {function(boolean)} */ this.allCompleteCb_ = allCompleteCb; /** @private {function(MultipleSignerResult, boolean)} */ this.gnubbyCompleteCb_ = gnubbyCompleteCb; /** @private {string|undefined} */ this.logMsgUrl_ = opt_logMsgUrl; /** @private {Array} */ this.challenges_ = []; /** @private {boolean} */ this.challengesSet_ = false; /** @private {boolean} */ this.complete_ = false; /** @private {number} */ this.numComplete_ = 0; /** @private {!Object} */ this.gnubbies_ = {}; /** @private {Countdown} */ this.timer_ = DEVICE_FACTORY_REGISTRY.getCountdownFactory() .createTimer(timeoutMillis); /** @private {Countdown} */ this.reenumerateTimer_ = DEVICE_FACTORY_REGISTRY.getCountdownFactory() .createTimer(timeoutMillis); } /** * @typedef {{ * index: string, * signer: SingleGnubbySigner, * stillGoing: boolean, * errorStatus: number * }} */ var GnubbyTracker; /** * Closes this signer's gnubbies, if any are open. */ MultipleGnubbySigner.prototype.close = function() { for (var k in this.gnubbies_) { this.gnubbies_[k].signer.close(); } this.reenumerateTimer_.clearTimeout(); this.timer_.clearTimeout(); if (this.reenumerateIntervalTimer_) { this.reenumerateIntervalTimer_.clearTimeout(); } }; /** * Begins signing the given challenges. * @param {Array} challenges The challenges to sign. * @return {boolean} whether the challenges were successfully added. */ MultipleGnubbySigner.prototype.doSign = function(challenges) { if (this.challengesSet_) { // Can't add new challenges once they're finalized. return false; } if (challenges) { for (var i = 0; i < challenges.length; i++) { var decodedChallenge = {}; var challenge = challenges[i]; decodedChallenge['challengeHash'] = B64_decode(challenge['challengeHash']); decodedChallenge['appIdHash'] = B64_decode(challenge['appIdHash']); decodedChallenge['keyHandle'] = B64_decode(challenge['keyHandle']); if (challenge['version']) { decodedChallenge['version'] = challenge['version']; } this.challenges_.push(decodedChallenge); } } this.challengesSet_ = true; this.enumerateGnubbies_(); return true; }; /** * Signals this signer to rescan for gnubbies. Useful when the caller has * knowledge that the last device has been removed, and can notify this class * before it will discover it on its own. */ MultipleGnubbySigner.prototype.reScanDevices = function() { if (this.reenumerateIntervalTimer_) { this.reenumerateIntervalTimer_.clearTimeout(); } this.maybeReEnumerateGnubbies_(true); }; /** * Enumerates gnubbies. * @private */ MultipleGnubbySigner.prototype.enumerateGnubbies_ = function() { DEVICE_FACTORY_REGISTRY.getGnubbyFactory().enumerate( this.enumerateCallback_.bind(this)); }; /** * Called with the result of enumerating gnubbies. * @param {number} rc The return code from enumerating. * @param {Array} ids The gnubbies enumerated. * @private */ MultipleGnubbySigner.prototype.enumerateCallback_ = function(rc, ids) { if (this.complete_) { return; } if (rc || !ids || !ids.length) { this.maybeReEnumerateGnubbies_(true); return; } for (var i = 0; i < ids.length; i++) { this.addGnubby_(ids[i]); } this.maybeReEnumerateGnubbies_(false); }; /** * How frequently to reenumerate gnubbies when none are found, in milliseconds. * @const */ MultipleGnubbySigner.ACTIVE_REENUMERATE_INTERVAL_MILLIS = 200; /** * How frequently to reenumerate gnubbies when some are found, in milliseconds. * @const */ MultipleGnubbySigner.PASSIVE_REENUMERATE_INTERVAL_MILLIS = 3000; /** * Reenumerates gnubbies if there's still time. * @param {boolean} activeScan Whether to poll more aggressively, e.g. if * there are no devices present. * @private */ MultipleGnubbySigner.prototype.maybeReEnumerateGnubbies_ = function(activeScan) { if (this.reenumerateTimer_.expired()) { // If the timer is expired, call timeout_ if there aren't any still-running // gnubbies. (If there are some still running, the last will call timeout_ // itself.) if (!this.anyPending_()) { this.timeout_(false); } return; } // Reenumerate more aggressively if there are no gnubbies present than if // there are any. var reenumerateTimeoutMillis; if (activeScan) { reenumerateTimeoutMillis = MultipleGnubbySigner.ACTIVE_REENUMERATE_INTERVAL_MILLIS; } else { reenumerateTimeoutMillis = MultipleGnubbySigner.PASSIVE_REENUMERATE_INTERVAL_MILLIS; } if (reenumerateTimeoutMillis > this.reenumerateTimer_.millisecondsUntilExpired()) { reenumerateTimeoutMillis = this.reenumerateTimer_.millisecondsUntilExpired(); } /** @private {Countdown} */ this.reenumerateIntervalTimer_ = DEVICE_FACTORY_REGISTRY.getCountdownFactory().createTimer( reenumerateTimeoutMillis, this.enumerateGnubbies_.bind(this)); }; /** * Adds a new gnubby to this signer's list of gnubbies. (Only possible while * this signer is still signing: without this restriction, the completed * callback could be called more than once, in violation of its contract.) * If this signer has challenges to sign, begins signing on the new gnubby with * them. * @param {GnubbyDeviceId} gnubbyId The id of the gnubby to add. * @return {boolean} Whether the gnubby was added successfully. * @private */ MultipleGnubbySigner.prototype.addGnubby_ = function(gnubbyId) { var index = JSON.stringify(gnubbyId); if (this.gnubbies_.hasOwnProperty(index)) { // Can't add the same gnubby twice. return false; } var tracker = { index: index, errorStatus: 0, stillGoing: false, signer: null }; tracker.signer = new SingleGnubbySigner( gnubbyId, this.forEnroll_, this.signCompletedCallback_.bind(this, tracker), this.timer_.clone(), this.logMsgUrl_); this.gnubbies_[index] = tracker; this.gnubbies_[index].stillGoing = tracker.signer.doSign(this.challenges_); if (!this.gnubbies_[index].errorStatus) { this.gnubbies_[index].errorStatus = 0; } return true; }; /** * Called by a SingleGnubbySigner upon completion. * @param {GnubbyTracker} tracker The tracker object of the gnubby whose result * this is. * @param {SingleSignerResult} result The result of the sign operation. * @private */ MultipleGnubbySigner.prototype.signCompletedCallback_ = function(tracker, result) { console.log( UTIL_fmt((result.code ? 'failure.' : 'success!') + ' gnubby ' + tracker.index + ' got code ' + result.code.toString(16))); if (!tracker.stillGoing) { console.log(UTIL_fmt('gnubby ' + tracker.index + ' no longer running!')); // Shouldn't ever happen? Disregard. return; } tracker.stillGoing = false; tracker.errorStatus = result.code; var moreExpected = this.tallyCompletedGnubby_(); switch (result.code) { case DeviceStatusCodes.GONE_STATUS: // Squelch removed gnubbies: the caller can't act on them. But if this // was the last one, speed up reenumerating. if (!moreExpected) { this.maybeReEnumerateGnubbies_(true); } break; default: // Report any other results directly to the caller. this.notifyGnubbyComplete_(tracker, result, moreExpected); break; } if (!moreExpected && this.timer_.expired()) { this.timeout_(false); } }; /** * Counts another gnubby has having completed, and returns whether more results * are expected. * @return {boolean} Whether more gnubbies are still running. * @private */ MultipleGnubbySigner.prototype.tallyCompletedGnubby_ = function() { this.numComplete_++; return this.anyPending_(); }; /** * @return {boolean} Whether more gnubbies are still running. * @private */ MultipleGnubbySigner.prototype.anyPending_ = function() { return this.numComplete_ < Object.keys(this.gnubbies_).length; }; /** * Called upon timeout. * @param {boolean} anyPending Whether any gnubbies are awaiting results. * @private */ MultipleGnubbySigner.prototype.timeout_ = function(anyPending) { if (this.complete_) return; this.complete_ = true; // Defer notifying the caller that all are complete, in case the caller is // doing work in response to a gnubbyFound callback and has an inconsistent // view of the state of this signer. var self = this; window.setTimeout(function() { self.allCompleteCb_(anyPending); }, 0); }; /** * @param {GnubbyTracker} tracker The tracker object of the gnubby whose result * this is. * @param {SingleSignerResult} result Result object. * @param {boolean} moreExpected Whether more gnubbies may still produce an * outcome. * @private */ MultipleGnubbySigner.prototype.notifyGnubbyComplete_ = function(tracker, result, moreExpected) { console.log(UTIL_fmt('gnubby ' + tracker.index + ' complete (' + result.code.toString(16) + ')')); var signResult = { 'code': result.code, 'gnubby': result.gnubby, 'gnubbyId': tracker.signer.getDeviceId() }; if (result['challenge']) signResult['challenge'] = result['challenge']; if (result['info']) signResult['info'] = result['info']; this.gnubbyCompleteCb_(signResult, moreExpected); }; // Copyright 2014 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. /** * @fileoverview Implements a sign handler using USB gnubbies. */ 'use strict'; var CORRUPT_sign = false; /** * @param {!SignHelperRequest} request The sign request. * @constructor * @implements {RequestHandler} */ function UsbSignHandler(request) { /** @private {!SignHelperRequest} */ this.request_ = request; /** @private {boolean} */ this.notified_ = false; /** @private {boolean} */ this.anyGnubbiesFound_ = false; /** @private {!Array} */ this.notEnrolledGnubbies_ = []; } /** * Default timeout value in case the caller never provides a valid timeout. * @const */ UsbSignHandler.DEFAULT_TIMEOUT_MILLIS = 30 * 1000; /** * Attempts to run this handler's request. * @param {RequestHandlerCallback} cb Called with the result of the request and * an optional source for the sign result. * @return {boolean} whether this set of challenges was accepted. */ UsbSignHandler.prototype.run = function(cb) { if (this.cb_) { // Can only handle one request. return false; } /** @private {RequestHandlerCallback} */ this.cb_ = cb; if (!this.request_.signData || !this.request_.signData.length) { // Fail a sign request with an empty set of challenges. return false; } var timeoutMillis = this.request_.timeoutSeconds ? this.request_.timeoutSeconds * 1000 : UsbSignHandler.DEFAULT_TIMEOUT_MILLIS; /** @private {MultipleGnubbySigner} */ this.signer_ = new MultipleGnubbySigner( false /* forEnroll */, this.signerCompleted_.bind(this), this.signerFoundGnubby_.bind(this), timeoutMillis, this.request_.logMsgUrl); return this.signer_.doSign(this.request_.signData); }; /** * Called when a MultipleGnubbySigner completes. * @param {boolean} anyPending Whether any gnubbies are pending. * @private */ UsbSignHandler.prototype.signerCompleted_ = function(anyPending) { if (!this.anyGnubbiesFound_ || anyPending) { this.notifyError_(DeviceStatusCodes.TIMEOUT_STATUS); } else if (this.signerError_ !== undefined) { this.notifyError_(this.signerError_); } else { // Do nothing: signerFoundGnubby_ will have returned results from other // gnubbies. } }; /** * Called when a MultipleGnubbySigner finds a gnubby that has completed signing * its challenges. * @param {MultipleSignerResult} signResult Signer result object * @param {boolean} moreExpected Whether the signer expects to produce more * results. * @private */ UsbSignHandler.prototype.signerFoundGnubby_ = function(signResult, moreExpected) { this.anyGnubbiesFound_ = true; if (!signResult.code) { var gnubby = signResult['gnubby']; var challenge = signResult['challenge']; var info = new Uint8Array(signResult['info']); this.notifySuccess_(gnubby, challenge, info); } else if (SingleGnubbySigner.signErrorIndicatesInvalidKeyHandle( signResult.code)) { var gnubby = signResult['gnubby']; this.notEnrolledGnubbies_.push(gnubby); this.sendBogusEnroll_(gnubby); } else if (!moreExpected) { // If the signer doesn't expect more results, return the error directly to // the caller. this.notifyError_(signResult.code); } else { // Record the last error, to report from the complete callback if no other // eligible gnubbies are found. /** @private {number} */ this.signerError_ = signResult.code; } }; /** @const */ UsbSignHandler.BOGUS_APP_ID_HASH = [ 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41 ]; /** @const */ UsbSignHandler.BOGUS_CHALLENGE_V1 = [ 0x04, 0xA2, 0x24, 0x7D, 0x5C, 0x0B, 0x76, 0xF1, 0xDC, 0xCD, 0x44, 0xAF, 0x91, 0x9A, 0xA2, 0x3F, 0x3F, 0xBA, 0x65, 0x9F, 0x06, 0x78, 0x82, 0xFB, 0x93, 0x4B, 0xBF, 0x86, 0x55, 0x95, 0x66, 0x46, 0x76, 0x90, 0xDC, 0xE1, 0xE8, 0x6C, 0x86, 0x86, 0xC3, 0x03, 0x4E, 0x65, 0x52, 0x4C, 0x32, 0x6F, 0xB6, 0x44, 0x0D, 0x50, 0xF9, 0x16, 0xC0, 0xA3, 0xDA, 0x31, 0x4B, 0xD3, 0x3F, 0x94, 0xA5, 0xF1, 0xD3 ]; /** @const */ UsbSignHandler.BOGUS_CHALLENGE_V2 = [ 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42 ]; /** * Sends a bogus enroll command to the not-enrolled gnubby, to force the user * to tap the gnubby before revealing its state to the caller. * @param {Gnubby} gnubby The gnubby to "enroll" on. * @private */ UsbSignHandler.prototype.sendBogusEnroll_ = function(gnubby) { var self = this; gnubby.version(function(rc, opt_data) { if (rc) { self.notifyError_(rc); return; } var enrollChallenge; var version = UTIL_BytesToString(new Uint8Array(opt_data || [])); switch (version) { case Gnubby.U2F_V1: enrollChallenge = UsbSignHandler.BOGUS_CHALLENGE_V1; break; case Gnubby.U2F_V2: enrollChallenge = UsbSignHandler.BOGUS_CHALLENGE_V2; break; default: self.notifyError_(DeviceStatusCodes.INVALID_DATA_STATUS); } gnubby.enroll( /** @type {Array} */ (enrollChallenge), UsbSignHandler.BOGUS_APP_ID_HASH, self.enrollCallback_.bind(self, gnubby)); }); }; /** * Called with the result of the (bogus, tap capturing) enroll command. * @param {Gnubby} gnubby The gnubby "enrolled". * @param {number} code The result of the enroll command. * @param {ArrayBuffer=} infoArray Returned data. * @private */ UsbSignHandler.prototype.enrollCallback_ = function(gnubby, code, infoArray) { if (this.notified_) return; switch (code) { case DeviceStatusCodes.WAIT_TOUCH_STATUS: this.sendBogusEnroll_(gnubby); return; case DeviceStatusCodes.OK_STATUS: // Got a successful enroll => user tapped gnubby. // Send a WRONG_DATA_STATUS finally. (The gnubby is implicitly closed // by notifyError_.) this.notifyError_(DeviceStatusCodes.WRONG_DATA_STATUS); return; } }; /** * Reports the result of a successful sign operation. * @param {Gnubby} gnubby Gnubby instance * @param {SignHelperChallenge} challenge Challenge signed * @param {Uint8Array} info Result data * @private */ UsbSignHandler.prototype.notifySuccess_ = function(gnubby, challenge, info) { if (this.notified_) return; this.notified_ = true; gnubby.closeWhenIdle(); this.close(); if (CORRUPT_sign) { CORRUPT_sign = false; info[info.length - 1] = info[info.length - 1] ^ 0xff; } var responseData = { 'appIdHash': B64_encode(challenge['appIdHash']), 'challengeHash': B64_encode(challenge['challengeHash']), 'keyHandle': B64_encode(challenge['keyHandle']), 'signatureData': B64_encode(info) }; var reply = { 'type': 'sign_helper_reply', 'code': DeviceStatusCodes.OK_STATUS, 'responseData': responseData }; this.cb_(reply, 'USB'); }; /** * Reports error to the caller. * @param {number} code error to report * @private */ UsbSignHandler.prototype.notifyError_ = function(code) { if (this.notified_) return; this.notified_ = true; this.close(); var reply = { 'type': 'sign_helper_reply', 'code': code }; this.cb_(reply); }; /** * Closes the MultipleGnubbySigner, if any. */ UsbSignHandler.prototype.close = function() { while (this.notEnrolledGnubbies_.length != 0) { var gnubby = this.notEnrolledGnubbies_.shift(); gnubby.closeWhenIdle(); } if (this.signer_) { this.signer_.close(); this.signer_ = null; } }; // Copyright 2014 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. /** * @fileoverview Does common handling for requests coming from web pages and * routes them to the provided handler. */ /** * FIDO U2F Javascript API Version * @const * @type {number} */ var JS_API_VERSION = 1.1; /** * Gets the scheme + origin from a web url. * @param {string} url Input url * @return {?string} Scheme and origin part if url parses */ function getOriginFromUrl(url) { var re = new RegExp('^(https?://)[^/]*/?'); var originarray = re.exec(url); if (originarray == null) return originarray; var origin = originarray[0]; while (origin.charAt(origin.length - 1) == '/') { origin = origin.substring(0, origin.length - 1); } if (origin == 'http:' || origin == 'https:') return null; return origin; } /** * Returns whether the registered key appears to be valid. * @param {Object} registeredKey The registered key object. * @param {boolean} appIdRequired Whether the appId property is required on * each challenge. * @return {boolean} Whether the object appears valid. */ function isValidRegisteredKey(registeredKey, appIdRequired) { if (appIdRequired && !registeredKey.hasOwnProperty('appId')) { return false; } if (!registeredKey.hasOwnProperty('keyHandle')) return false; if (registeredKey['version']) { if (registeredKey['version'] != 'U2F_V1' && registeredKey['version'] != 'U2F_V2') { return false; } } return true; } /** * Returns whether the array of registered keys appears to be valid. * @param {Array} registeredKeys The array of registered keys. * @param {boolean} appIdRequired Whether the appId property is required on * each challenge. * @return {boolean} Whether the array appears valid. */ function isValidRegisteredKeyArray(registeredKeys, appIdRequired) { return registeredKeys.every(function(key) { return isValidRegisteredKey(key, appIdRequired); }); } /** * Gets the sign challenges from the request. The sign challenges may be the * U2F 1.0 variant, signRequests, or the U2F 1.1 version, registeredKeys. * @param {Object} request The request. * @return {!Array|undefined} The sign challenges, if found. */ function getSignChallenges(request) { if (!request) { return undefined; } var signChallenges; if (request.hasOwnProperty('signRequests')) { signChallenges = request['signRequests']; } else if (request.hasOwnProperty('registeredKeys')) { signChallenges = request['registeredKeys']; } return signChallenges; } /** * Returns whether the array of SignChallenges appears to be valid. * @param {Array} signChallenges The array of sign challenges. * @param {boolean} challengeValueRequired Whether each challenge object * requires a challenge value. * @param {boolean} appIdRequired Whether the appId property is required on * each challenge. * @return {boolean} Whether the array appears valid. */ function isValidSignChallengeArray(signChallenges, challengeValueRequired, appIdRequired) { for (var i = 0; i < signChallenges.length; i++) { var incomingChallenge = signChallenges[i]; if (challengeValueRequired && !incomingChallenge.hasOwnProperty('challenge')) return false; if (!isValidRegisteredKey(incomingChallenge, appIdRequired)) { return false; } } return true; } /** * @param {Object} request Request object * @param {MessageSender} sender Sender frame * @param {Function} sendResponse Response callback * @return {?Closeable} Optional handler object that should be closed when port * closes */ function handleWebPageRequest(request, sender, sendResponse) { switch (request.type) { case MessageTypes.U2F_REGISTER_REQUEST: return handleU2fEnrollRequest(sender, request, sendResponse); case MessageTypes.U2F_SIGN_REQUEST: return handleU2fSignRequest(sender, request, sendResponse); case MessageTypes.U2F_GET_API_VERSION_REQUEST: sendResponse( makeU2fGetApiVersionResponse(request, JS_API_VERSION, MessageTypes.U2F_GET_API_VERSION_RESPONSE)); return null; default: sendResponse( makeU2fErrorResponse(request, ErrorCodes.BAD_REQUEST, undefined, MessageTypes.U2F_REGISTER_RESPONSE)); return null; } } /** * Makes a response to a request. * @param {Object} request The request to make a response to. * @param {string} responseSuffix How to name the response's type. * @param {string=} opt_defaultType The default response type, if none is * present in the request. * @return {Object} The response object. */ function makeResponseForRequest(request, responseSuffix, opt_defaultType) { var type; if (request && request.type) { type = request.type.replace(/_request$/, responseSuffix); } else { type = opt_defaultType; } var reply = { 'type': type }; if (request && request.requestId) { reply.requestId = request.requestId; } return reply; } /** * Makes a response to a U2F request with an error code. * @param {Object} request The request to make a response to. * @param {ErrorCodes} code The error code to return. * @param {string=} opt_detail An error detail string. * @param {string=} opt_defaultType The default response type, if none is * present in the request. * @return {Object} The U2F error. */ function makeU2fErrorResponse(request, code, opt_detail, opt_defaultType) { var reply = makeResponseForRequest(request, '_response', opt_defaultType); var error = {'errorCode': code}; if (opt_detail) { error['errorMessage'] = opt_detail; } reply['responseData'] = error; return reply; } /** * Makes a success response to a web request with a responseData object. * @param {Object} request The request to make a response to. * @param {Object} responseData The response data. * @return {Object} The web error. */ function makeU2fSuccessResponse(request, responseData) { var reply = makeResponseForRequest(request, '_response'); reply['responseData'] = responseData; return reply; } /** * Maps a helper's error code from the DeviceStatusCodes namespace to a * U2fError. * @param {number} code Error code from DeviceStatusCodes namespace. * @return {U2fError} An error. */ function mapDeviceStatusCodeToU2fError(code) { switch (code) { case DeviceStatusCodes.WRONG_DATA_STATUS: return {errorCode: ErrorCodes.DEVICE_INELIGIBLE}; case DeviceStatusCodes.TIMEOUT_STATUS: case DeviceStatusCodes.WAIT_TOUCH_STATUS: return {errorCode: ErrorCodes.TIMEOUT}; default: var reportedError = { errorCode: ErrorCodes.OTHER_ERROR, errorMessage: 'device status code: ' + code.toString(16) }; return reportedError; } } /** * Sends a response, using the given sentinel to ensure at most one response is * sent. Also closes the closeable, if it's given. * @param {boolean} sentResponse Whether a response has already been sent. * @param {?Closeable} closeable A thing to close. * @param {*} response The response to send. * @param {Function} sendResponse A function to send the response. */ function sendResponseOnce(sentResponse, closeable, response, sendResponse) { if (closeable) { closeable.close(); } if (!sentResponse) { sentResponse = true; try { // If the page has gone away or the connection has otherwise gone, // sendResponse fails. sendResponse(response); } catch (exception) { console.warn('sendResponse failed: ' + exception); } } else { console.warn(UTIL_fmt('Tried to reply more than once!')); } } /** * @param {!string} string Input string * @return {Array} SHA256 hash value of string. */ function sha256HashOfString(string) { var s = new SHA256(); s.update(UTIL_StringToBytes(string)); return s.digest(); } var UNUSED_CID_PUBKEY_VALUE = 'unused'; /** * Normalizes the TLS channel ID value: * 1. Converts semantically empty values (undefined, null, 0) to the empty * string. * 2. Converts valid JSON strings to a JS object. * 3. Otherwise, returns the input value unmodified. * @param {Object|string|undefined} opt_tlsChannelId TLS Channel id * @return {Object|string} The normalized TLS channel ID value. */ function tlsChannelIdValue(opt_tlsChannelId) { if (!opt_tlsChannelId) { // Case 1: Always set some value for TLS channel ID, even if it's the empty // string: this browser definitely supports them. return UNUSED_CID_PUBKEY_VALUE; } if (typeof opt_tlsChannelId === 'string') { try { var obj = JSON.parse(opt_tlsChannelId); if (!obj) { // Case 1: The string value 'null' parses as the Javascript object null, // so return an empty string: the browser definitely supports TLS // channel id. return UNUSED_CID_PUBKEY_VALUE; } // Case 2: return the value as a JS object. return /** @type {Object} */ (obj); } catch (e) { console.warn('Unparseable TLS channel ID value ' + opt_tlsChannelId); // Case 3: return the value unmodified. } } return opt_tlsChannelId; } /** * Creates a browser data object with the given values. * @param {!string} type A string representing the "type" of this browser data * object. * @param {!string} serverChallenge The server's challenge, as a base64- * encoded string. * @param {!string} origin The server's origin, as seen by the browser. * @param {Object|string|undefined} opt_tlsChannelId TLS Channel Id * @return {string} A string representation of the browser data object. */ function makeBrowserData(type, serverChallenge, origin, opt_tlsChannelId) { var browserData = { 'typ' : type, 'challenge' : serverChallenge, 'origin' : origin }; if (BROWSER_SUPPORTS_TLS_CHANNEL_ID) { browserData['cid_pubkey'] = tlsChannelIdValue(opt_tlsChannelId); } return JSON.stringify(browserData); } /** * Creates a browser data object for an enroll request with the given values. * @param {!string} serverChallenge The server's challenge, as a base64- * encoded string. * @param {!string} origin The server's origin, as seen by the browser. * @param {Object|string|undefined} opt_tlsChannelId TLS Channel Id * @return {string} A string representation of the browser data object. */ function makeEnrollBrowserData(serverChallenge, origin, opt_tlsChannelId) { return makeBrowserData( 'navigator.id.finishEnrollment', serverChallenge, origin, opt_tlsChannelId); } /** * Creates a browser data object for a sign request with the given values. * @param {!string} serverChallenge The server's challenge, as a base64- * encoded string. * @param {!string} origin The server's origin, as seen by the browser. * @param {Object|string|undefined} opt_tlsChannelId TLS Channel Id * @return {string} A string representation of the browser data object. */ function makeSignBrowserData(serverChallenge, origin, opt_tlsChannelId) { return makeBrowserData( 'navigator.id.getAssertion', serverChallenge, origin, opt_tlsChannelId); } /** * Makes a response to a U2F request with an error code. * @param {Object} request The request to make a response to. * @param {number=} version The JS API version to return. * @param {string=} opt_defaultType The default response type, if none is * present in the request. * @return {Object} The GetJsApiVersionResponse. */ function makeU2fGetApiVersionResponse(request, version, opt_defaultType) { var reply = makeResponseForRequest(request, '_response', opt_defaultType); var data = {'js_api_version': version}; reply['responseData'] = data; return reply; } /** * Encodes the sign data as an array of sign helper challenges. * @param {Array} signChallenges The sign challenges to encode. * @param {string|undefined} opt_defaultChallenge A default sign challenge * value, if a request does not provide one. * @param {string=} opt_defaultAppId The app id to use for each challenge, if * the challenge contains none. * @param {function(string, string): string=} opt_challengeHashFunction * A function that produces, from a key handle and a raw challenge, a hash * of the raw challenge. If none is provided, a default hash function is * used. * @return {!Array} The sign challenges, encoded. */ function encodeSignChallenges(signChallenges, opt_defaultChallenge, opt_defaultAppId, opt_challengeHashFunction) { function encodedSha256(keyHandle, challenge) { return B64_encode(sha256HashOfString(challenge)); } var challengeHashFn = opt_challengeHashFunction || encodedSha256; var encodedSignChallenges = []; if (signChallenges) { for (var i = 0; i < signChallenges.length; i++) { var challenge = signChallenges[i]; var keyHandle = challenge['keyHandle']; var challengeValue; if (challenge.hasOwnProperty('challenge')) { challengeValue = challenge['challenge']; } else { challengeValue = opt_defaultChallenge; } var challengeHash = challengeHashFn(keyHandle, challengeValue); var appId; if (challenge.hasOwnProperty('appId')) { appId = challenge['appId']; } else { appId = opt_defaultAppId; } var encodedChallenge = { 'challengeHash': challengeHash, 'appIdHash': B64_encode(sha256HashOfString(appId)), 'keyHandle': keyHandle, 'version': (challenge['version'] || 'U2F_V1') }; encodedSignChallenges.push(encodedChallenge); } } return encodedSignChallenges; } /** * Makes a sign helper request from an array of challenges. * @param {Array} challenges The sign challenges. * @param {number=} opt_timeoutSeconds Timeout value. * @param {string=} opt_logMsgUrl URL to log to. * @return {SignHelperRequest} The sign helper request. */ function makeSignHelperRequest(challenges, opt_timeoutSeconds, opt_logMsgUrl) { var request = { 'type': 'sign_helper_request', 'signData': challenges, 'timeout': opt_timeoutSeconds || 0, 'timeoutSeconds': opt_timeoutSeconds || 0 }; if (opt_logMsgUrl !== undefined) { request.logMsgUrl = opt_logMsgUrl; } return request; } // Copyright 2014 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. /** * @fileoverview Implements a check whether an app id lists an origin. */ 'use strict'; /** * Parses the text as JSON and returns it as an array of strings. * @param {string} text Input JSON * @return {!Array} Array of origins */ function getOriginsFromJson(text) { try { var urls, i; var appIdData = JSON.parse(text); var trustedFacets = appIdData['trustedFacets']; if (trustedFacets) { var versionBlock; for (i = 0; versionBlock = trustedFacets[i]; i++) { if (versionBlock['version'] && versionBlock['version']['major'] == 1 && versionBlock['version']['minor'] == 0) { urls = versionBlock['ids']; break; } } } if (typeof urls == 'undefined') { throw Error('Could not find trustedFacets for version 1.0'); } var origins = {}; var url; for (i = 0; url = urls[i]; i++) { var origin = getOriginFromUrl(url); if (origin) { origins[origin] = origin; } } return Object.keys(origins); } catch (e) { console.error(UTIL_fmt('could not parse ' + text)); return []; } } /** * Retrieves a set of distinct app ids from the sign challenges. * @param {Array=} signChallenges Input sign challenges. * @return {Array} array of distinct app ids. */ function getDistinctAppIds(signChallenges) { if (!signChallenges) { return []; } var appIds = {}; for (var i = 0, request; request = signChallenges[i]; i++) { var appId = request['appId']; if (appId) { appIds[appId] = appId; } } return Object.keys(appIds); } /** * An object that checks one or more appIds' contents against an origin. * @interface */ function AppIdChecker() {} /** * Checks whether the given origin is allowed by all of the given appIds. * @param {!Countdown} timer A timer by which to resolve all provided app ids. * @param {string} origin The origin to check. * @param {!Array} appIds The app ids to check. * @param {boolean} allowHttp Whether to allow http:// URLs. * @param {string=} opt_logMsgUrl A log message URL. * @return {Promise} A promise for the result of the check */ AppIdChecker.prototype.checkAppIds = function(timer, origin, appIds, allowHttp, opt_logMsgUrl) {}; /** * An interface to create an AppIdChecker. * @interface */ function AppIdCheckerFactory() {} /** * @return {!AppIdChecker} A new AppIdChecker. */ AppIdCheckerFactory.prototype.create = function() {}; /** * Provides an object to track checking a list of appIds. * @param {!TextFetcher} fetcher A URL fetcher. * @constructor * @implements AppIdChecker */ function XhrAppIdChecker(fetcher) { /** @private {!TextFetcher} */ this.fetcher_ = fetcher; } /** * Checks whether all the app ids provided can be asserted by the given origin. * @param {!Countdown} timer A timer by which to resolve all provided app ids. * @param {string} origin The origin to check. * @param {!Array} appIds The app ids to check. * @param {boolean} allowHttp Whether to allow http:// URLs. * @param {string=} opt_logMsgUrl A log message URL. * @return {Promise} A promise for the result of the check */ XhrAppIdChecker.prototype.checkAppIds = function(timer, origin, appIds, allowHttp, opt_logMsgUrl) { if (this.timer_) { // Can't use the same object to check appIds more than once. return Promise.resolve(false); } /** @private {!Countdown} */ this.timer_ = timer; /** @private {string} */ this.origin_ = origin; var appIdsMap = {}; if (appIds) { for (var i = 0; i < appIds.length; i++) { appIdsMap[appIds[i]] = appIds[i]; } } /** @private {Array} */ this.distinctAppIds_ = Object.keys(appIdsMap); /** @private {boolean} */ this.allowHttp_ = allowHttp; /** @private {string|undefined} */ this.logMsgUrl_ = opt_logMsgUrl; if (!this.distinctAppIds_.length) return Promise.resolve(false); if (this.allAppIdsEqualOrigin_()) { // Trivially allowed. return Promise.resolve(true); } else { var self = this; // Begin checking remaining app ids. var appIdChecks = self.distinctAppIds_.map(self.checkAppId_.bind(self)); return Promise.all(appIdChecks).then(function(results) { return results.every(function(result) { return result; }); }); } }; /** * Checks if a single appId can be asserted by the given origin. * @param {string} appId The appId to check * @return {Promise} A promise for the result of the check * @private */ XhrAppIdChecker.prototype.checkAppId_ = function(appId) { if (appId == this.origin_) { // Trivially allowed return Promise.resolve(true); } var p = this.fetchAllowedOriginsForAppId_(appId); var self = this; return p.then(function(allowedOrigins) { if (allowedOrigins.indexOf(self.origin_) == -1) { console.warn(UTIL_fmt('Origin ' + self.origin_ + ' not allowed by app id ' + appId)); return false; } return true; }); }; /** * @return {boolean} Whether all the app ids being checked are equal to the * calling origin. * @private */ XhrAppIdChecker.prototype.allAppIdsEqualOrigin_ = function() { var self = this; return this.distinctAppIds_.every(function(appId) { return appId == self.origin_; }); }; /** * Fetches the allowed origins for an appId. * @param {string} appId Application id * @return {Promise>} A promise for a list of allowed origins * for appId * @private */ XhrAppIdChecker.prototype.fetchAllowedOriginsForAppId_ = function(appId) { if (!appId) { return Promise.resolve([]); } if (appId.indexOf('http://') == 0 && !this.allowHttp_) { console.log(UTIL_fmt('http app ids disallowed, ' + appId + ' requested')); return Promise.resolve([]); } var origin = getOriginFromUrl(appId); if (!origin) { return Promise.resolve([]); } var p = this.fetcher_.fetch(appId); var self = this; return p.then(getOriginsFromJson, function(rc_) { var rc = /** @type {number} */(rc_); console.log(UTIL_fmt('fetching ' + appId + ' failed: ' + rc)); if (!(rc >= 400 && rc < 500) && !self.timer_.expired()) { // Retry return self.fetchAllowedOriginsForAppId_(appId); } return []; }); }; /** * A factory to create an XhrAppIdChecker. * @implements AppIdCheckerFactory * @param {!TextFetcher} fetcher * @constructor */ function XhrAppIdCheckerFactory(fetcher) { /** @private {!TextFetcher} */ this.fetcher_ = fetcher; } /** * @return {!AppIdChecker} A new AppIdChecker. */ XhrAppIdCheckerFactory.prototype.create = function() { return new XhrAppIdChecker(this.fetcher_); }; // Copyright 2014 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. /** * @fileoverview Implements a helper using USB gnubbies. */ 'use strict'; /** * @constructor * @extends {GenericRequestHelper} */ function UsbHelper() { GenericRequestHelper.apply(this, arguments); var self = this; this.registerHandlerFactory('enroll_helper_request', function(request) { return new UsbEnrollHandler(/** @type {EnrollHelperRequest} */ (request)); }); this.registerHandlerFactory('sign_helper_request', function(request) { return new UsbSignHandler(/** @type {SignHelperRequest} */ (request)); }); } inherits(UsbHelper, GenericRequestHelper); // Copyright 2014 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. /** * @fileoverview Implements a simple XmlHttpRequest-based text document * fetcher. * */ 'use strict'; /** * A fetcher of text files. * @interface */ function TextFetcher() {} /** * @param {string} url The URL to fetch. * @param {string?} opt_method The HTTP method to use (default GET) * @param {string?} opt_body The request body * @return {!Promise} A promise for the fetched text. In case of an * error, this promise is rejected with an HTTP status code. */ TextFetcher.prototype.fetch = function(url, opt_method, opt_body) {}; /** * @constructor * @implements {TextFetcher} */ function XhrTextFetcher() { } /** * @param {string} url The URL to fetch. * @param {string?} opt_method The HTTP method to use (default GET) * @param {string?} opt_body The request body * @return {!Promise} A promise for the fetched text. In case of an * error, this promise is rejected with an HTTP status code. */ XhrTextFetcher.prototype.fetch = function(url, opt_method, opt_body) { return new Promise(function(resolve, reject) { var xhr = new XMLHttpRequest(); var method = opt_method || 'GET'; xhr.open(method, url, true); xhr.onloadend = function() { if (xhr.status != 200) { reject(xhr.status); return; } resolve(xhr.responseText); }; xhr.onerror = function() { // Treat any network-level errors as though the page didn't exist. reject(404); }; if (opt_body) xhr.send(opt_body); else xhr.send(); }); }; // Copyright 2014 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. /** * @fileoverview Provides a "bottom half" helper to assist with raw requests. * This fills the same role as the Authenticator-Specific Module component of * U2F documents, although the API is different. */ 'use strict'; /** * @typedef {{ * type: string, * timeout: number * }} */ var HelperRequest; /** * @typedef {{ * type: string, * code: (number|undefined) * }} */ var HelperReply; /** * A helper to process requests. * @interface */ function RequestHelper() {} /** * Gets a handler for a request. * @param {HelperRequest} request The request to handle. * @return {RequestHandler} A handler for the request. */ RequestHelper.prototype.getHandler = function(request) {}; /** * A handler to track an outstanding request. * @extends {Closeable} * @interface */ function RequestHandler() {} /** @typedef {function(HelperReply, string=)} */ var RequestHandlerCallback; /** * @param {RequestHandlerCallback} cb Called with the result of the request, * and an optional source for the result. * @return {boolean} Whether this handler could be run. */ RequestHandler.prototype.run = function(cb) {}; /** Closes this handler. */ RequestHandler.prototype.close = function() {}; /** * Makes a response to a helper request with an error code. * @param {HelperRequest} request The request to make a response to. * @param {DeviceStatusCodes} code The error code to return. * @param {string=} opt_defaultType The default response type, if none is * present in the request. * @return {HelperReply} The helper error response. */ function makeHelperErrorResponse(request, code, opt_defaultType) { var type; if (request && request.type) { type = request.type.replace(/_request$/, '_reply'); } else { type = opt_defaultType || 'unknown_type_reply'; } var reply = { 'type': type, 'code': /** @type {number} */ (code) }; return reply; } // Copyright 2014 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. /** * @fileoverview U2F message types. */ 'use strict'; /** * Message types for messsages to/from the extension * @const * @enum {string} */ var MessageTypes = { U2F_REGISTER_REQUEST: 'u2f_register_request', U2F_SIGN_REQUEST: 'u2f_sign_request', U2F_REGISTER_RESPONSE: 'u2f_register_response', U2F_SIGN_RESPONSE: 'u2f_sign_response', U2F_GET_API_VERSION_REQUEST: 'u2f_get_api_version_request', U2F_GET_API_VERSION_RESPONSE: 'u2f_get_api_version_response' }; // Copyright 2014 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. /** * @fileoverview Provides a partial copy of goog.inherits, so inheritance works * even in the absence of Closure. */ 'use strict'; // A partial copy of goog.inherits, so inheritance works even in the absence of // Closure. function inherits(childCtor, parentCtor) { /** @constructor */ function tempCtor() { } tempCtor.prototype = parentCtor.prototype; childCtor.prototype = new tempCtor; childCtor.prototype.constructor = childCtor; } // Copyright 2014 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. /** * @fileoverview Interface for representing a low-level gnubby device. */ 'use strict'; /** * Low level gnubby 'driver'. One per physical USB device. * @interface */ function GnubbyDevice() {} // Commands of the USB interface. /** Echo data through local processor only */ GnubbyDevice.CMD_PING = 0x81; /** Perform reset action and read ATR string */ GnubbyDevice.CMD_ATR = 0x82; /** Send raw APDU */ GnubbyDevice.CMD_APDU = 0x83; /** Send lock channel command */ GnubbyDevice.CMD_LOCK = 0x84; /** Obtain system information record */ GnubbyDevice.CMD_SYSINFO = 0x85; /** Obtain an unused channel ID */ GnubbyDevice.CMD_INIT = 0x86; /** Control prompt flashing */ GnubbyDevice.CMD_PROMPT = 0x87; /** Send device identification wink */ GnubbyDevice.CMD_WINK = 0x88; /** BLE UID read/set */ GnubbyDevice.CMD_BLE_UID = 0xb5; /** USB test */ GnubbyDevice.CMD_USB_TEST = 0xb9; /** Device Firmware Upgrade */ GnubbyDevice.CMD_DFU = 0xba; /** Protocol resync command */ GnubbyDevice.CMD_SYNC = 0xbc; /** Error response */ GnubbyDevice.CMD_ERROR = 0xbf; // Low-level error codes. /** No error */ GnubbyDevice.OK = 0; /** Invalid command */ GnubbyDevice.INVALID_CMD = 1; /** Invalid parameter */ GnubbyDevice.INVALID_PAR = 2; /** Invalid message length */ GnubbyDevice.INVALID_LEN = 3; /** Invalid message sequencing */ GnubbyDevice.INVALID_SEQ = 4; /** Message has timed out */ GnubbyDevice.TIMEOUT = 5; /** Channel is busy */ GnubbyDevice.BUSY = 6; /** Access denied */ GnubbyDevice.ACCESS_DENIED = 7; /** Device is gone */ GnubbyDevice.GONE = 8; /** Verification error */ GnubbyDevice.VERIFY_ERROR = 9; /** Command requires channel lock */ GnubbyDevice.LOCK_REQUIRED = 10; /** Sync error */ GnubbyDevice.SYNC_FAIL = 11; /** Other unspecified error */ GnubbyDevice.OTHER = 127; // Remote helper errors. /** Not a remote helper */ GnubbyDevice.NOTREMOTE = 263; /** Could not reach remote endpoint */ GnubbyDevice.COULDNOTDIAL = 264; // chrome.usb-related errors. /** No device */ GnubbyDevice.NODEVICE = 512; /** More than one device */ GnubbyDevice.TOOMANY = 513; /** Permission denied */ GnubbyDevice.NOPERMISSION = 666; /** Destroys this low-level device instance. */ GnubbyDevice.prototype.destroy = function() {}; /** * Sets a callback that will get called when this device instance is destroyed. * @param {function() : ?Promise} cb Called back when closed. Callback may * yield a promise that resolves when the close hook completes. */ GnubbyDevice.prototype.setDestroyHook = function(cb) {}; /** * Register a client for this gnubby. * @param {*} who The client. */ GnubbyDevice.prototype.registerClient = function(who) {}; /** * De-register a client. * @param {*} who The client. * @return {number} The number of remaining listeners for this device, or -1 * if this had no clients to start with. */ GnubbyDevice.prototype.deregisterClient = function(who) {}; /** * @param {*} who The client. * @return {boolean} Whether this device has who as a client. */ GnubbyDevice.prototype.hasClient = function(who) {}; /** * Queue command to be sent. * If queue was empty, initiate the write. * @param {number} cid The client's channel ID. * @param {number} cmd The command to send. * @param {ArrayBuffer|Uint8Array} data Command data */ GnubbyDevice.prototype.queueCommand = function(cid, cmd, data) {}; /** * @typedef {{ * vendorId: number, * productId: number * }} */ var UsbDeviceSpec; // Copyright 2014 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. /** * @fileoverview Implements a "generic" RequestHelper that provides a default * response to unknown requests, and supports registering handlers for known * requests. */ 'use strict'; /** * @typedef {function(HelperRequest): RequestHandler} */ var RequestHandlerFactory; /** * Implements a "generic" RequestHelper that provides a default * response to unknown requests, and supports registering handlers for known * @constructor * @implements {RequestHelper} */ function GenericRequestHelper() { /** @private {Object} */ this.handlerFactories_ = {}; } /** * Gets a handler for a request. * @param {HelperRequest} request The request to handle. * @return {RequestHandler} A handler for the request. */ GenericRequestHelper.prototype.getHandler = function(request) { if (this.handlerFactories_.hasOwnProperty(request.type)) { return this.handlerFactories_[request.type](request); } return null; }; /** * Registers a handler factory for a given type. * @param {string} type The request type. * @param {RequestHandlerFactory} factory A factory that can produce a handler * for a request of a given type. */ GenericRequestHelper.prototype.registerHandlerFactory = function(type, factory) { this.handlerFactories_[type] = factory; }; // Copyright 2014 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. /** * @fileoverview Class providing common dependencies for the extension's * top half. */ 'use strict'; /** * @param {!AppIdCheckerFactory} appIdCheckerFactory An appId checker factory. * @param {!ApprovedOrigins} approvedOrigins An origin approval implementation. * @param {!CountdownFactory} countdownFactory A countdown timer factory. * @param {!OriginChecker} originChecker An origin checker. * @param {!RequestHelper} requestHelper A request helper. * @param {!SystemTimer} sysTimer A system timer implementation. * @param {!TextFetcher} textFetcher A text fetcher. * @constructor */ function FactoryRegistry(appIdCheckerFactory, approvedOrigins, countdownFactory, originChecker, requestHelper, sysTimer, textFetcher) { /** @private {!AppIdCheckerFactory} */ this.appIdCheckerFactory_ = appIdCheckerFactory; /** @private {!ApprovedOrigins} */ this.approvedOrigins_ = approvedOrigins; /** @private {!CountdownFactory} */ this.countdownFactory_ = countdownFactory; /** @private {!OriginChecker} */ this.originChecker_ = originChecker; /** @private {!RequestHelper} */ this.requestHelper_ = requestHelper; /** @private {!SystemTimer} */ this.sysTimer_ = sysTimer; /** @private {!TextFetcher} */ this.textFetcher_ = textFetcher; } /** @return {!AppIdCheckerFactory} An appId checker factory. */ FactoryRegistry.prototype.getAppIdCheckerFactory = function() { return this.appIdCheckerFactory_; }; /** @return {!ApprovedOrigins} An origin approval implementation. */ FactoryRegistry.prototype.getApprovedOrigins = function() { return this.approvedOrigins_; }; /** @return {!CountdownFactory} A countdown factory. */ FactoryRegistry.prototype.getCountdownFactory = function() { return this.countdownFactory_; }; /** @return {!OriginChecker} An origin checker. */ FactoryRegistry.prototype.getOriginChecker = function() { return this.originChecker_; }; /** @return {!RequestHelper} A request helper. */ FactoryRegistry.prototype.getRequestHelper = function() { return this.requestHelper_; }; /** @return {!SystemTimer} A system timer implementation. */ FactoryRegistry.prototype.getSystemTimer = function() { return this.sysTimer_; }; /** @return {!TextFetcher} A text fetcher. */ FactoryRegistry.prototype.getTextFetcher = function() { return this.textFetcher_; }; // Copyright 2014 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. /** * @fileoverview Errors reported by top-level request handlers. */ 'use strict'; /** * Response status codes * @const * @enum {number} */ var ErrorCodes = { 'OK': 0, 'OTHER_ERROR': 1, 'BAD_REQUEST': 2, 'CONFIGURATION_UNSUPPORTED': 3, 'DEVICE_INELIGIBLE': 4, 'TIMEOUT': 5 }; /** * An error object for responses * @typedef {{ * errorCode: ErrorCodes, * errorMessage: (?string|undefined) * }} */ var U2fError; // Copyright 2014 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. /** * @fileoverview Class providing common dependencies for the extension's * bottom half. */ 'use strict'; /** * @param {!GnubbyFactory} gnubbyFactory A Gnubby factory. * @param {!CountdownFactory} countdownFactory A countdown timer factory. * @param {!IndividualAttestation} individualAttestation An individual * attestation implementation. * @constructor */ function DeviceFactoryRegistry(gnubbyFactory, countdownFactory, individualAttestation) { /** @private {!GnubbyFactory} */ this.gnubbyFactory_ = gnubbyFactory; /** @private {!CountdownFactory} */ this.countdownFactory_ = countdownFactory; /** @private {!IndividualAttestation} */ this.individualAttestation_ = individualAttestation; } /** @return {!GnubbyFactory} A Gnubby factory. */ DeviceFactoryRegistry.prototype.getGnubbyFactory = function() { return this.gnubbyFactory_; }; /** @return {!CountdownFactory} A countdown factory. */ DeviceFactoryRegistry.prototype.getCountdownFactory = function() { return this.countdownFactory_; }; /** @return {!IndividualAttestation} An individual attestation implementation. */ DeviceFactoryRegistry.prototype.getIndividualAttestation = function() { return this.individualAttestation_; }; // Copyright 2014 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. /** * @fileoverview Implements a check whether an origin is allowed to assert an * app id. * */ 'use strict'; /** * Implements half of the app id policy: whether an origin is allowed to claim * an app id. For checking whether the app id also lists the origin, * @see AppIdChecker. * @interface */ function OriginChecker() {} /** * Checks whether the origin is allowed to claim the app ids. * @param {string} origin The origin claiming the app id. * @param {!Array} appIds The app ids being claimed. * @return {Promise} A promise for the result of the check. */ OriginChecker.prototype.canClaimAppIds = function(origin, appIds) {}; // Copyright 2014 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. /** * @fileoverview Provides an interface to determine whether to request the * individual attestation certificate during enrollment. */ 'use strict'; /** * Interface to determine whether to request the individual attestation * certificate during enrollment. * @interface */ function IndividualAttestation() {} /** * @param {string} appIdHash The app id hash. * @return {boolean} Whether to request the individual attestation certificate * for this app id. */ IndividualAttestation.prototype.requestIndividualAttestation = function(appIdHash) {}; // Copyright 2014 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. /** * @fileoverview Provides a Google corp implementation of IndividualAttestation. */ 'use strict'; /** * Google corp implementation of IndividualAttestation that requests * individual certificates for corp accounts. * @constructor * @implements IndividualAttestation */ function GoogleCorpIndividualAttestation() {} /** * @param {string} appIdHash The app id hash. * @return {boolean} Whether to request the individual attestation certificate * for this app id. */ GoogleCorpIndividualAttestation.prototype.requestIndividualAttestation = function(appIdHash) { return appIdHash == GoogleCorpIndividualAttestation.GOOGLE_CORP_APP_ID_HASH; }; /** * App ID used by Google employee accounts. * @const */ GoogleCorpIndividualAttestation.GOOGLE_CORP_APP_ID = 'https://www.gstatic.com/securitykey/a/google.com/origins.json'; /** * Hash of the app ID used by Google employee accounts. * @const */ GoogleCorpIndividualAttestation.GOOGLE_CORP_APP_ID_HASH = B64_encode(sha256HashOfString( GoogleCorpIndividualAttestation.GOOGLE_CORP_APP_ID)); // Copyright 2014 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. /** * @fileoverview Provides an interface to check whether the user has approved * an origin to use security keys. * */ 'use strict'; /** * Allows the caller to check whether the user has approved the use of * security keys from an origin. * @interface */ function ApprovedOrigins() {} /** * Checks whether the origin is approved to use security keys. (If not, an * approval prompt may be shown.) * @param {string} origin The origin to approve. * @param {number=} opt_tabId A tab id to display approval prompt in, if * necessary. * @return {Promise} A promise for the result of the check. */ ApprovedOrigins.prototype.isApprovedOrigin = function(origin, opt_tabId) {}; // Copyright 2014 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. /** * @fileoverview Provides a representation of a web request sender, and * utility functions for creating them. */ 'use strict'; /** * @typedef {{ * origin: string, * tlsChannelId: (string|undefined), * tabId: (number|undefined) * }} */ var WebRequestSender; /** * Creates an object representing the sender's origin, and, if available, * tab. * @param {MessageSender} messageSender The message sender. * @return {?WebRequestSender} The sender's origin and tab, or null if the * sender is invalid. */ function createSenderFromMessageSender(messageSender) { var origin = getOriginFromUrl(/** @type {string} */ (messageSender.url)); if (!origin) { return null; } var sender = { origin: origin }; if (messageSender.tlsChannelId) { sender.tlsChannelId = messageSender.tlsChannelId; } if (messageSender.tab) { sender.tabId = messageSender.tab.id; } return sender; } /** * Checks whether the given tab could have sent a message from the given * origin. * @param {Tab} tab The tab to match * @param {string} origin The origin to check. * @return {Promise} A promise resolved with the tab id if it the tab could, * have sent the request, and rejected if it can't. */ function tabMatchesOrigin(tab, origin) { // If the tab's origin matches, trust that the request came from this tab. if (getOriginFromUrl(tab.url) == origin) { return Promise.resolve(tab.id); } return Promise.reject(false); } /** * Attempts to ensure that the tabId of the sender is set, using chrome.tabs * when available. * @param {WebRequestSender} sender The request sender. * @return {Promise} A promise resolved once the tabId retrieval is done. * The promise is rejected if the tabId is untrustworthy, e.g. if the * user rapidly switched tabs. */ function getTabIdWhenPossible(sender) { if (sender.tabId) { // Already got it? Done. return Promise.resolve(true); } else if (!chrome.tabs) { // Can't get it? Done. (This happens to packaged apps, which can't access // chrome.tabs.) return Promise.resolve(true); } else { return new Promise(function(resolve, reject) { chrome.tabs.query({active: true, lastFocusedWindow: true}, function(tabs) { if (!tabs.length) { // Safety check. reject(false); return; } var tab = tabs[0]; tabMatchesOrigin(tab, sender.origin).then(function(tabId) { sender.tabId = tabId; resolve(true); }, function() { // Didn't match? Check if the debugger is open. if (tab.url.indexOf('chrome-devtools://') != 0) { reject(false); return; } // Debugger active: find first tab with the sender's origin. chrome.tabs.query({active: true}, function(tabs) { if (!tabs.length) { // Safety check. reject(false); return; } var numRejected = 0; for (var i = 0; i < tabs.length; i++) { tab = tabs[i]; tabMatchesOrigin(tab, sender.origin).then(function(tabId) { sender.tabId = tabId; resolve(true); }, function() { if (++numRejected >= tabs.length) { // None matches: reject. reject(false); } }); } }); }); }); }); } } /** * Checks whether the given tab is in the foreground, i.e. is the active tab * of the focused window. * @param {number} tabId The tab id to check. * @return {Promise} A promise for the result of the check. */ function tabInForeground(tabId) { return new Promise(function(resolve, reject) { if (!chrome.tabs || !chrome.tabs.get) { reject(); return; } if (!chrome.windows || !chrome.windows.get) { reject(); return; } chrome.tabs.get(tabId, function(tab) { if (chrome.runtime.lastError) { resolve(false); return; } if (!tab.active) { resolve(false); return; } chrome.windows.get(tab.windowId, function(aWindow) { resolve(aWindow && aWindow.focused); }); }); }); } // Copyright 2014 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. /** * @fileoverview Provides an implementation of the SystemTimer interface based * on window's timer methods. */ 'use strict'; /** * Creates an implementation of the SystemTimer interface based on window's * timer methods. * @constructor * @implements {SystemTimer} */ function WindowTimer() { } /** * Sets a single-shot timer. * @param {function()} func Called back when the timer expires. * @param {number} timeoutMillis How long until the timer fires, in * milliseconds. * @return {number} A timeout ID, which can be used to cancel the timer. */ WindowTimer.prototype.setTimeout = function(func, timeoutMillis) { return window.setTimeout(func, timeoutMillis); }; /** * Clears a previously set timer. * @param {number} timeoutId The ID of the timer to clear. */ WindowTimer.prototype.clearTimeout = function(timeoutId) { window.clearTimeout(timeoutId); }; /** * Sets a repeating interval timer. * @param {function()} func Called back each time the timer fires. * @param {number} timeoutMillis How long until the timer fires, in * milliseconds. * @return {number} A timeout ID, which can be used to cancel the timer. */ WindowTimer.prototype.setInterval = function(func, timeoutMillis) { return window.setInterval(func, timeoutMillis); }; /** * Clears a previously set interval timer. * @param {number} timeoutId The ID of the timer to clear. */ WindowTimer.prototype.clearInterval = function(timeoutId) { window.clearInterval(timeoutId); }; // Copyright 2014 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. /** * @fileoverview Provides a watchdog around a collection of callback functions. */ 'use strict'; /** * Creates a watchdog around a collection of callback functions, * ensuring at least one of them is called before the timeout expires. * If a timeout function is provided, calls the timeout function upon timeout * expiration if none of the callback functions has been called. * @param {number} timeoutValueSeconds Timeout value, in seconds. * @param {function()=} opt_timeoutCb Callback function to call on timeout. * @constructor * @implements {Closeable} */ function WatchdogRequestHandler(timeoutValueSeconds, opt_timeoutCb) { /** @private {number} */ this.timeoutValueSeconds_ = timeoutValueSeconds; /** @private {function()|undefined} */ this.timeoutCb_ = opt_timeoutCb; /** @private {boolean} */ this.calledBack_ = false; /** @private {Countdown} */ this.timer_ = FACTORY_REGISTRY.getCountdownFactory().createTimer( this.timeoutValueSeconds_ * 1000, this.timeout_.bind(this)); /** @private {Closeable|undefined} */ this.closeable_ = undefined; /** @private {boolean} */ this.closed_ = false; } /** * Wraps a callback function, such that the fact that the callback function * was or was not called gets tracked by this watchdog object. * @param {function(...?)} cb The callback function to wrap. * @return {function(...?)} A wrapped callback function. */ WatchdogRequestHandler.prototype.wrapCallback = function(cb) { return this.wrappedCallback_.bind(this, cb); }; /** Closes this watchdog. */ WatchdogRequestHandler.prototype.close = function() { this.closed_ = true; this.timer_.clearTimeout(); if (this.closeable_) { this.closeable_.close(); this.closeable_ = undefined; } }; /** * Sets this watchdog's closeable. * @param {!Closeable} closeable The closeable. */ WatchdogRequestHandler.prototype.setCloseable = function(closeable) { this.closeable_ = closeable; }; /** * Called back when the watchdog expires. * @private */ WatchdogRequestHandler.prototype.timeout_ = function() { if (!this.calledBack_ && !this.closed_) { var logMsg = 'Not called back within ' + this.timeoutValueSeconds_ + ' second timeout'; if (this.timeoutCb_) { logMsg += ', calling default callback'; console.warn(UTIL_fmt(logMsg)); this.timeoutCb_(); } else { console.warn(UTIL_fmt(logMsg)); } } }; /** * Wrapped callback function. * @param {function(...?)} cb The callback function to call. * @param {...?} var_args The callback function's arguments. * @private */ WatchdogRequestHandler.prototype.wrappedCallback_ = function(cb, var_args) { if (!this.closed_) { this.calledBack_ = true; this.timer_.clearTimeout(); var originalArgs = Array.prototype.slice.call(arguments, 1); cb.apply(null, originalArgs); } }; // Copyright 2014 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. /** * @fileoverview Logging related utility functions. */ /** Posts the log message to the log url. * @param {string} logMsg the log message to post. * @param {string=} opt_logMsgUrl the url to post log messages to. */ function logMessage(logMsg, opt_logMsgUrl) { console.log(UTIL_fmt('logMessage("' + logMsg + '")')); if (!opt_logMsgUrl) { return; } var audio = new Audio(); audio.src = opt_logMsgUrl + logMsg; } // Copyright 2014 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. /** * @fileoverview Provides an implementation of approved origins that relies * on the chrome.cryptotokenPrivate.requestPermission API. * (and only) allows google.com to use security keys. * */ 'use strict'; /** * Allows the caller to check whether the user has approved the use of * security keys from an origin. * @constructor * @implements {ApprovedOrigins} */ function CryptoTokenApprovedOrigin() {} /** * Checks whether the origin is approved to use security keys. (If not, an * approval prompt may be shown.) * @param {string} origin The origin to approve. * @param {number=} opt_tabId A tab id to display approval prompt in. * For this implementation, the tabId is always necessary, even though * the type allows undefined. * @return {Promise} A promise for the result of the check. */ CryptoTokenApprovedOrigin.prototype.isApprovedOrigin = function(origin, opt_tabId) { return new Promise(function(resolve, reject) { if (opt_tabId === undefined) { resolve(false); return; } var tabId = /** @type {number} */ (opt_tabId); tabInForeground(tabId).then(function(result) { if (!result) { resolve(false); return; } if (!chrome.tabs || !chrome.tabs.get) { reject(); return; } chrome.tabs.get(tabId, function(tab) { if (chrome.runtime.lastError) { resolve(false); return; } var tabOrigin = getOriginFromUrl(tab.url); resolve(tabOrigin == origin); }); }); }); }; // Copyright 2015 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. /** * @fileoverview Implements a check whether an origin is allowed to assert an * app id based on whether they share the same effective TLD + 1. * */ 'use strict'; /** * Implements half of the app id policy: whether an origin is allowed to claim * an app id. For checking whether the app id also lists the origin, * @see AppIdChecker. * @implements OriginChecker * @constructor */ function CryptoTokenOriginChecker() { } /** * Checks whether the origin is allowed to claim the app ids. * @param {string} origin The origin claiming the app id. * @param {!Array} appIds The app ids being claimed. * @return {Promise} A promise for the result of the check. */ CryptoTokenOriginChecker.prototype.canClaimAppIds = function(origin, appIds) { var appIdChecks = appIds.map(this.checkAppId_.bind(this, origin)); return Promise.all(appIdChecks).then(function(results) { return results.every(function(result) { return result; }); }); }; /** * Checks if a single appId can be asserted by the given origin. * @param {string} origin The origin. * @param {string} appId The appId to check * @return {Promise} A promise for the result of the check * @private */ CryptoTokenOriginChecker.prototype.checkAppId_ = function(origin, appId) { return new Promise(function(resolve, reject) { if (!chrome.cryptotokenPrivate) { reject(); return; } chrome.cryptotokenPrivate.canOriginAssertAppId(origin, appId, resolve); }); }; // Copyright 2014 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. /** * @fileoverview CryptoToken background page */ 'use strict'; /** @const */ var BROWSER_SUPPORTS_TLS_CHANNEL_ID = true; /** @const */ var HTTP_ORIGINS_ALLOWED = false; /** @const */ var LOG_SAVER_EXTENSION_ID = 'fjajfjhkeibgmiggdfehjplbhmfkialk'; // Singleton tracking available devices. var gnubbies = new Gnubbies(); HidGnubbyDevice.register(gnubbies); UsbGnubbyDevice.register(gnubbies); var FACTORY_REGISTRY = (function() { var windowTimer = new WindowTimer(); var xhrTextFetcher = new XhrTextFetcher(); return new FactoryRegistry( new XhrAppIdCheckerFactory(xhrTextFetcher), new CryptoTokenApprovedOrigin(), new CountdownTimerFactory(windowTimer), new CryptoTokenOriginChecker(), new UsbHelper(), windowTimer, xhrTextFetcher); })(); var DEVICE_FACTORY_REGISTRY = new DeviceFactoryRegistry( new UsbGnubbyFactory(gnubbies), FACTORY_REGISTRY.getCountdownFactory(), new GoogleCorpIndividualAttestation()); /** * @param {*} request The received request * @return {boolean} Whether the request is a register/enroll request. */ function isRegisterRequest(request) { if (!request) { return false; } switch (request.type) { case MessageTypes.U2F_REGISTER_REQUEST: return true; default: return false; } } /** * Default response callback to deliver a response to a request. * @param {*} request The received request. * @param {function(*): void} sendResponse A callback that delivers a response. * @param {*} response The response to return. */ function defaultResponseCallback(request, sendResponse, response) { response['requestId'] = request['requestId']; try { sendResponse(response); } catch (e) { console.warn(UTIL_fmt('caught: ' + e.message)); } } /** * Response callback that delivers a response to a request only when the * sender is a foreground tab. * @param {*} request The received request. * @param {!MessageSender} sender The message sender. * @param {function(*): void} sendResponse A callback that delivers a response. * @param {*} response The response to return. */ function sendResponseToActiveTabOnly(request, sender, sendResponse, response) { tabInForeground(sender.tab.id).then(function(result) { // If the tab is no longer in the foreground, drop the result: the user // is no longer interacting with the tab that originated the request. if (result) { defaultResponseCallback(request, sendResponse, response); } }); } /** * Common handler for messages received from chrome.runtime.sendMessage and * chrome.runtime.connect + postMessage. * @param {*} request The received request * @param {!MessageSender} sender The message sender * @param {function(*): void} sendResponse A callback that delivers a response * @return {Closeable} A Closeable request handler. */ function messageHandler(request, sender, sendResponse) { var responseCallback; if (isRegisterRequest(request)) { responseCallback = sendResponseToActiveTabOnly.bind(null, request, sender, sendResponse); } else { responseCallback = defaultResponseCallback.bind(null, request, sendResponse); } var closeable = handleWebPageRequest(/** @type {Object} */(request), sender, responseCallback); return closeable; } /** * Listen to individual messages sent from (whitelisted) webpages via * chrome.runtime.sendMessage * @param {*} request The received request * @param {!MessageSender} sender The message sender * @param {function(*): void} sendResponse A callback that delivers a response * @return {boolean} */ function messageHandlerExternal(request, sender, sendResponse) { if (sender.id && sender.id === LOG_SAVER_EXTENSION_ID) { return handleLogSaverMessage(request); } messageHandler(request, sender, sendResponse); return true; // Tell Chrome not to destroy sendResponse yet } chrome.runtime.onMessageExternal.addListener(messageHandlerExternal); // Listen to direct connection events, and wire up a message handler on the port chrome.runtime.onConnectExternal.addListener(function(port) { function sendResponse(response) { port.postMessage(response); } var closeable; port.onMessage.addListener(function(request) { var sender = /** @type {!MessageSender} */ (port.sender); closeable = messageHandler(request, sender, sendResponse); }); port.onDisconnect.addListener(function() { if (closeable) { closeable.close(); } }); }); /** * Handles messages from the log-saver app. Temporarily replaces UTIL_fmt with * a wrapper that also sends formatted messages to the app. * @param {*} request The message received from the app * @return {boolean} Used as chrome.runtime.onMessage handler return value */ function handleLogSaverMessage(request) { if (request === 'start') { if (originalUtilFmt_) { // We're already sending return false; } originalUtilFmt_ = UTIL_fmt; UTIL_fmt = function(s) { var line = originalUtilFmt_(s); chrome.runtime.sendMessage(LOG_SAVER_EXTENSION_ID, line); return line; }; } else if (request === 'stop') { if (originalUtilFmt_) { UTIL_fmt = originalUtilFmt_; originalUtilFmt_ = null; } } return false; } /** @private */ var originalUtilFmt_ = null;













// Copyright 2014 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. // This features file defines extension APIs implemented under src/extensions. // See chrome/common/extensions/api/_features.md to understand this file, as // well as feature.h, simple_feature.h, and base_feature_provider.h. // // Note that specifying "web_page", "blessed_web_page", or "all" as a context // type will require manually updating chrome/renderer/resources/dispatcher.cc. { "alarms": { "dependencies": ["permission:alarms"], "contexts": ["blessed_extension"] }, "app.runtime": [{ "channel": "stable", "contexts": ["blessed_extension"], "extension_types": ["platform_app"], "noparent": true }, { "channel": "stable", "component_extensions_auto_granted": false, "contexts": ["blessed_extension"], "extension_types": ["extension"], "noparent": true, "whitelist": [ "2FC374607C2DF285634B67C64A2E356C607091C3", // Quickoffice "3727DD3E564B6055387425027AD74C58784ACC15", // Quickoffice internal "12E618C3C6E97495AAECF2AC12DEB082353241C6" // QO component extension ] }], "app.window": [{ "channel": "stable", "contexts": ["blessed_extension"], "extension_types": ["platform_app"], "noparent": true }, { "channel": "stable", "contexts": ["blessed_extension"], "extension_types": ["extension"], "noparent": true, "component_extensions_auto_granted": false, "whitelist": [ "B9EF10DDFEA11EF77873CC5009809E5037FC4C7A", // Google input tools "06BE211D5F014BAB34BC22D9DDA09C63A81D828E", // Official xkb extension "F94EE6AB36D6C6588670B2B01EB65212D9C64E33" // Open source xkb extension ] }], "app.currentWindowInternal": { "noparent": true, "internal": true, "channel": "stable", "contexts": ["blessed_extension"] }, "app.currentWindowInternal.setShape": { "dependencies": ["permission:app.window.shape"], "contexts": ["blessed_extension"] }, // The API for the *embedder* of appview. Appview has both an embedder and // guest API, which are different. "appViewEmbedderInternal": { "internal": true, "contexts": ["blessed_extension"], "dependencies": ["permission:appview"] }, // Note that exposing this doesn't necessarily expose AppView, // appViewEmbedderInternal is required for that. // See http://crbug.com/437891. "appViewGuestInternal": { "internal": true, "channel": "stable", "contexts": ["blessed_extension"] }, "audio": { "dependencies": ["permission:audio"], "contexts": ["blessed_extension"] }, "bluetooth": [{ "dependencies": ["manifest:bluetooth"], "contexts": ["blessed_extension"] }, { "channel": "stable", "contexts": ["webui"], "matches": [ "chrome://bluetooth-pairing/*", "chrome://md-settings/*", "chrome://settings/*", "chrome://settings-frame/*" ] }], "bluetoothLowEnergy": { "dependencies": ["manifest:bluetooth"], "contexts": ["blessed_extension"], "platforms": ["chromeos", "linux"] }, "bluetoothPrivate": [{ "dependencies": ["permission:bluetoothPrivate"], "contexts": ["blessed_extension"] }, { "channel": "stable", "contexts": ["webui"], "matches": [ "chrome://bluetooth-pairing/*", "chrome://md-settings/*", "chrome://settings/*", "chrome://settings-frame/*" ] }], "bluetoothSocket": { "dependencies": ["manifest:bluetooth"], "contexts": ["blessed_extension"] }, "clipboard": { "dependencies": ["permission:clipboard"], "contexts": ["blessed_extension"] }, "clipboard.onClipboardDataChanged": { "dependencies": ["permission:clipboardRead"] }, "clipboard.setImageData": { "dependencies": ["permission:clipboardWrite"] }, "declarativeWebRequest": { "dependencies": ["permission:declarativeWebRequest"], "contexts": ["blessed_extension"] }, "diagnostics": { "dependencies": ["permission:diagnostics"], "extension_types": ["platform_app"], "contexts": ["blessed_extension"] }, "displaySource": { "dependencies": ["permission:displaySource"], "contexts": ["blessed_extension"] }, "dns": { "dependencies": ["permission:dns"], "contexts": ["blessed_extension"] }, "documentScan": { "dependencies": ["permission:documentScan"], "contexts": ["blessed_extension"] }, // This is not a real API, only here for documentation purposes. // See http://crbug.com/275944 for background. "extensionTypes": { "internal": true, "channel": "stable", "extension_types": ["extension", "legacy_packaged_app", "platform_app"], "contexts": ["blessed_extension"] }, "extensionViewInternal": [ { "internal": true, "contexts": ["blessed_extension"], "dependencies": ["permission:extensionview"] }, { "internal": true, "channel": "stable", "contexts": ["webui"], "matches": [ "chrome://cast/*", "chrome://media-router/*" ] } ], "events": { "internal": true, "channel": "stable", "extension_types": ["platform_app", "extension"], "contexts": "all", "matches": [""] }, "guestViewInternal": [ { "internal": true, "channel": "stable", "contexts": ["blessed_extension"] }, { "internal": true, "channel": "trunk", "contexts": ["webui"], "matches": [ "chrome://cast/*", "chrome://extensions-frame/*", "chrome://extensions/*", "chrome://chrome-signin/*", "chrome://media-router/*", "chrome://oobe/*" ] } ], "hid": { "dependencies": ["permission:hid"], "contexts": ["blessed_extension"] }, "hid.getUserSelectedDevices": { "contexts": ["blessed_extension"], "channel": "dev", "dependencies": ["permission:hid"] }, "idle": { "dependencies": ["permission:idle"], "contexts": ["blessed_extension"] }, "management": [{ "dependencies": ["permission:management"], "contexts": ["blessed_extension"], "default_parent": true }, { "channel": "stable", "contexts": ["webui"], "matches": [ "chrome://extensions/*", "chrome://extensions-frame/*", "chrome://chrome/extensions/*", "chrome://md-settings/*", "chrome://settings/*", "chrome://settings-frame/*" ] }], "management.getPermissionWarningsByManifest": { "dependencies": [], "channel": "stable", "extension_types": ["extension", "legacy_packaged_app", "platform_app"] }, "management.getSelf": { "dependencies": [], "channel": "stable", "extension_types": ["extension", "legacy_packaged_app", "platform_app"] }, "management.uninstallSelf": { "dependencies": [], "channel": "stable", "extension_types": ["extension", "legacy_packaged_app", "platform_app"] }, "metricsPrivate": [{ "dependencies": ["permission:metricsPrivate"], "contexts": ["blessed_extension"], "default_parent": true }, { "channel": "trunk", "contexts": ["webui"], "matches": [ "chrome://md-settings/*", "chrome://settings/*", "chrome://settings-frame/*" ] }], "metricsPrivate.getIsCrashReportingEnabled": { "whitelist": [ // This function inherits the extension restrictions of metricsPrivate, // but also requires whitelisting. New uses of this function should get // /tools/metrics/OWNERS approval of the usage before adding entries // below. See crbug.com/374199. "2FC374607C2DF285634B67C64A2E356C607091C3", // Quickoffice "3727DD3E564B6055387425027AD74C58784ACC15", // Quickoffice internal "12E618C3C6E97495AAECF2AC12DEB082353241C6", // QO component extension "3727DD3E564B6055387425027AD74C58784ACC15", // Editor "C41AD9DCD670210295614257EF8C9945AD68D86E", // Google Now // TODO(michaelpg): Determine whether these three extensions (D5736E4, // D57DE39, 3F65507) require this feature: crbug.com/652433. "D5736E4B5CF695CB93A2FB57E4FDC6E5AFAB6FE2", // http://crbug.com/312900. "D57DE394F36DC1C3220E7604C575D29C51A6C495", // http://crbug.com/319444. "3F65507A3B39259B38C8173C6FFA3D12DF64CCE9", // http://crbug.com/371562. "D7986543275120831B39EF28D1327552FC343960", // http://crbug.com/378067 "A291B26E088FA6BA53FFD72F0916F06EBA7C585A", // http://crbug.com/378067 "62CCAAD339E6451BBF97C4BBDF758E934A05AD0B", // Hotword triggering "07BD6A765FFC289FF755D7CAB2893A40EC337FEC", // http://crbug.com/456214 "896B85CC7E913E11C34892C1425A093C0701D386", // http://crbug.com/456214 "11A01C82EF355E674E4F9728A801F5C3CB40D83F", // http://crbug.com/456214 "F410C88469990EE7947450311D24B8AF2ADB2595", // http://crbug.com/456214 "226CF815E39A363090A1E547D53063472B8279FA", // http://crbug.com/574889 "C17CD9E6868D7B9C67926E0EC612EA25C768418F", // http://crbug.com/504940 "A45DABDB47A31CC812E5490AB748C7D05E2D32E9", // http://crbug.com/504940 "BFEE2E3B80BE21A645E63E9346DFC383E7CB3BDA", // http://crbug.com/504940 "63ED55E43214C211F82122ED56407FF1A807F2A3", // http://crbug.com/504940 // TODO (ntang) Remove the following 2 hashes by 12/31/2017. "B620CF4203315F9F2E046EDED22C7571A935958D", // http://crbug.com/510270 "B206D8716769728278D2D300349C6CB7D7DE2EF9", // http://crbug.com/510270 "2B6C6A4A5940017146F3E58B7F90116206E84685", // http://crbug.com/642141 "B6C2EFAB3EC3BF6EF03701408B6B09A67B2D0069", // http://crbug.com/642141 "96FF2FFA5C9173C76D47184B3E86D267B37781DE", // http://crbug.com/642141 "0136FCB13DB29FD5CD442F56E59E53B61F1DF96F" // http://crbug.com/642141 ] }, "mimeHandlerPrivate": { "dependencies": ["manifest:mime_types_handler"], "contexts": ["blessed_extension"] }, "mojoPrivate": { "contexts": ["blessed_extension"], "channel": "stable", "extension_types": ["platform_app", "extension"], "whitelist": [ "226CF815E39A363090A1E547D53063472B8279FA", // http://crbug.com/574889 "C17CD9E6868D7B9C67926E0EC612EA25C768418F", // http://crbug.com/448569 "A45DABDB47A31CC812E5490AB748C7D05E2D32E9", "BFEE2E3B80BE21A645E63E9346DFC383E7CB3BDA", "63ED55E43214C211F82122ED56407FF1A807F2A3" ] }, "networking.config": { "dependencies": ["permission:networking.config"], "contexts": ["blessed_extension"] }, "networking.onc": { "dependencies": ["permission:networking.onc"], "contexts": ["blessed_extension"], "source": "networkingPrivate" }, "networkingPrivate": [{ "dependencies": ["permission:networkingPrivate"], "contexts": ["blessed_extension"], // TODO(tbarzic): networkingPrivate is being renamed to networking.onc. // The goal is to eventually remove networkingPrivate API in favour of // networking.onc, but until current usages are migrated to the new // name, use API aliasing to expose the API under both names. // (http://crbug.com/672186). "alias": "networking.onc" }, { "channel": "stable", "contexts": ["webui"], "matches": [ "chrome://md-settings/*", "chrome://network/*", "chrome://oobe/*", "chrome://settings/*", "chrome://settings-frame/*" ] }], "power": { "dependencies": ["permission:power"], "contexts": ["blessed_extension"] }, "printerProvider": { "dependencies": ["permission:printerProvider"], "contexts": ["blessed_extension"] }, "printerProviderInternal": { "internal": true, "dependencies": ["permission:printerProvider"], "contexts": ["blessed_extension"] }, "runtime": { "channel": "stable", "extension_types": ["extension", "legacy_packaged_app", "platform_app"], "contexts": ["blessed_extension"] }, "runtime.getManifest": { "contexts": ["blessed_extension", "unblessed_extension", "content_script"] }, "runtime.connect": { // Everything except WebUI. "contexts": [ "blessed_web_page", "content_script", "blessed_extension", "unblessed_extension", "web_page" ], "matches": [""] }, "runtime.connectNative": { "dependencies": ["permission:nativeMessaging"], "contexts": ["blessed_extension"] }, "runtime.getURL": { "contexts": ["blessed_extension", "unblessed_extension", "content_script"] }, "runtime.id": { "contexts": ["blessed_extension", "unblessed_extension", "content_script"] }, "runtime.lastError": { "contexts": "all", "extension_types": "all", "matches": [""] }, "runtime.onConnect": { "contexts": ["blessed_extension", "unblessed_extension", "content_script"] }, "runtime.onMessage": { "contexts": ["blessed_extension", "unblessed_extension", "content_script"] }, "runtime.sendMessage": { // Everything except WebUI. "contexts": [ "blessed_web_page", "content_script", "blessed_extension", "unblessed_extension", "web_page" ], "matches": [""] }, "runtime.sendNativeMessage": { "dependencies": ["permission:nativeMessaging"], "contexts": ["blessed_extension"] }, "serial": { "dependencies": ["permission:serial"], "contexts": ["blessed_extension"] }, "socket": { "dependencies": ["permission:socket"], "contexts": ["blessed_extension"] }, "sockets.tcp": { "dependencies": ["manifest:sockets"], "contexts": ["blessed_extension"] }, "sockets.tcpServer": { "dependencies": ["manifest:sockets"], "contexts": ["blessed_extension"] }, "sockets.udp": { "dependencies": ["manifest:sockets"], "contexts": ["blessed_extension"] }, "storage": { "dependencies": ["permission:storage"], "contexts": ["blessed_extension", "unblessed_extension", "content_script"] }, "system.cpu": { "dependencies": ["permission:system.cpu"], "contexts": ["blessed_extension"] }, "system.display": [{ "dependencies": ["permission:system.display"], "contexts": ["blessed_extension"] }, { "channel": "stable", "contexts": ["webui"], "matches": [ "chrome://md-settings/*", "chrome://settings/*", "chrome://settings-frame/*" ] }], "system.memory": { "dependencies": ["permission:system.memory"], "contexts": ["blessed_extension"] }, "system.network": { "dependencies": ["permission:system.network"], "contexts": ["blessed_extension"] }, "system.storage": { "dependencies": ["permission:system.storage"], "contexts": ["blessed_extension"] }, "system.storage.getAvailableCapacity": { "channel": "dev" }, "test": [{ "channel": "stable", "extension_types": "all", // Everything except web pages and WebUI. WebUI is declared in a separate // rule to keep the "matches" property isolated. "contexts": [ "blessed_extension", "blessed_web_page", "content_script", "extension_service_worker", "unblessed_extension" ] }, { "channel": "stable", "contexts": ["webui"], "matches": [ "chrome://extensions/*", "chrome://extensions-frame/*", "chrome://chrome/extensions/*" ] }], "types": { "internal": true, "channel": "stable", "extension_types": ["extension", "legacy_packaged_app", "platform_app"], "contexts": ["blessed_extension"] }, "types.private": { // preferencesPrivate is the only API that uses types.private. // If any other APIs need it then they'll need to be added in // separate rules. "dependencies": ["permission:preferencesPrivate"], "contexts": ["blessed_extension"] }, "usb": { "dependencies": ["permission:usb"], "contexts": ["blessed_extension"] }, "vpnProvider": { "dependencies": ["permission:vpnProvider"], "contexts": ["blessed_extension"] }, "webRequest": { "dependencies": ["permission:webRequest"], "contexts": ["blessed_extension"] }, "webRequestInternal": [{ "internal": true, "channel": "stable", "contexts": ["blessed_extension"] }, { // webview uses webRequestInternal API. "channel": "stable", "internal": true, "contexts": ["webui"], "matches": [ "chrome://chrome-signin/*", "chrome://media-router/*", "chrome://oobe/*" ] }], "webViewInternal": [{ "internal": true, "dependencies": ["permission:webview"], "contexts": ["blessed_extension"] }, { "internal": true, "channel": "dev", "contexts": ["webui"], "matches": [ "chrome://chrome-signin/*", "chrome://media-router/*", "chrome://oobe/*" ] }], "webViewRequest": [{ "dependencies": ["permission:webview"], "contexts": ["blessed_extension"] }, { "channel": "stable", "contexts": ["webui"], "matches": [ "chrome://chrome-signin/*", "chrome://media-router/*", "chrome://oobe/*" ] }] } // Copyright 2014 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. var DocumentNatives = requireNative('document_natives'); var GuestViewContainer = require('guestViewContainer').GuestViewContainer; var IdGenerator = requireNative('id_generator'); function AppViewImpl(appviewElement) { GuestViewContainer.call(this, appviewElement, 'appview'); this.app = ''; this.data = ''; } AppViewImpl.prototype.__proto__ = GuestViewContainer.prototype; AppViewImpl.VIEW_TYPE = 'AppView'; // Add extra functionality to |this.element|. AppViewImpl.setupElement = function(proto) { var apiMethods = [ 'connect' ]; // Forward proto.foo* method calls to AppViewImpl.foo*. GuestViewContainer.forwardApiMethods(proto, apiMethods); } AppViewImpl.prototype.getErrorNode = function() { if (!this.errorNode) { this.errorNode = document.createElement('div'); this.errorNode.innerText = 'Unable to connect to app.'; this.errorNode.style.position = 'absolute'; this.errorNode.style.left = '0px'; this.errorNode.style.top = '0px'; this.errorNode.style.width = '100%'; this.errorNode.style.height = '100%'; this.element.shadowRoot.appendChild(this.errorNode); } return this.errorNode; }; AppViewImpl.prototype.buildContainerParams = function() { return { 'appId': this.app, 'data': this.data || {} }; }; AppViewImpl.prototype.connect = function(app, data, callback) { if (!this.elementAttached) { if (callback) { callback(false); } return; } this.app = app; this.data = data; this.guest.destroy(); this.guest.create(this.buildParams(), function() { if (!this.guest.getId()) { var errorMsg = 'Unable to connect to app "' + app + '".'; window.console.warn(errorMsg); this.getErrorNode().innerText = errorMsg; if (callback) { callback(false); } return; } this.attachWindow$(); if (callback) { callback(true); } }.bind(this)); }; GuestViewContainer.registerElement(AppViewImpl); // Copyright 2014 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. define('async_waiter', [ 'mojo/public/js/support', ], function(supportModule) { /** * @module async_waiter */ /** * @callback module:async_waiter.AsyncWaiter.Callback * @param {number} result The result of waiting. */ /** * A waiter that waits for a handle to be ready for either reading or writing. * @param {!MojoHandle} handle The handle to wait on. * @param {number} signals The signals to wait for handle to be ready for. * @param {module:async_waiter.AsyncWaiter.Callback} callback The callback to * call when handle is ready. * @constructor * @alias module:async_waiter.AsyncWaiter */ function AsyncWaiter(handle, signals, callback) { /** * The handle to wait on. * @type {!MojoHandle} * @private */ this.handle_ = handle; /** * The signals to wait for. * @type {number} * @private */ this.signals_ = signals; /** * The callback to invoke when * |[handle_]{@link module:async_waiter.AsyncWaiter#handle_}| is ready. * @type {module:async_waiter.AsyncWaiter.Callback} * @private */ this.callback_ = callback; this.id_ = null; } /** * Start waiting for the handle to be ready. * @throws Will throw if this is already waiting. */ AsyncWaiter.prototype.start = function() { if (this.id_) throw new Error('Already started'); this.id_ = supportModule.asyncWait( this.handle_, this.signals_, this.onHandleReady_.bind(this)); }; /** * Stop waiting for the handle to be ready. */ AsyncWaiter.prototype.stop = function() { if (!this.id_) return; supportModule.cancelWait(this.id_); this.id_ = null; }; /** * Returns whether this {@link AsyncWaiter} is waiting. * @return {boolean} Whether this AsyncWaiter is waiting. */ AsyncWaiter.prototype.isWaiting = function() { return !!this.id_; }; /** * Invoked when |[handle_]{@link module:async_waiter.AsyncWaiter#handle_}| is * ready. * @param {number} result The result of the wait. * @private */ AsyncWaiter.prototype.onHandleReady_ = function(result) { this.id_ = null; this.callback_(result); }; return {AsyncWaiter: AsyncWaiter}; }); // Copyright 2014 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. function registerHooks(api) { } function testDone(runNextTest) { // Use setTimeout here to allow previous test contexts to be // eligible for garbage collection. setTimeout(runNextTest, 0); } exports.$set('registerHooks', registerHooks); exports.$set('testDone', testDone); // Copyright 2014 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. var fileSystemNatives = requireNative('file_system_natives'); var nameToIds = {}; var idsToEntries = {}; function computeName(entry) { return entry.filesystem.name + ':' + entry.fullPath; } function computeId(entry) { var fileSystemId = fileSystemNatives.CrackIsolatedFileSystemName( entry.filesystem.name); if (!fileSystemId) return null; // Strip the leading '/' from the path. return fileSystemId + ':' + $String.slice(entry.fullPath, 1); } function registerEntry(id, entry) { var name = computeName(entry); nameToIds[name] = id; idsToEntries[id] = entry; } function getEntryId(entry) { var name = null; try { name = computeName(entry); } catch(e) { return null; } var id = nameToIds[name]; if (id != null) return id; // If an entry has not been registered, compute its id and register it. id = computeId(entry); registerEntry(id, entry); return id; } function getEntryById(id) { return idsToEntries[id]; } exports.$set('registerEntry', registerEntry); exports.$set('getEntryId', getEntryId); exports.$set('getEntryById', getEntryById); // Copyright 2014 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. // TODO(robwu): Fix indentation. var exceptionHandler = require('uncaught_exception_handler'); var eventNatives = requireNative('event_natives'); var logging = requireNative('logging'); var schemaRegistry = requireNative('schema_registry'); var sendRequest = require('sendRequest').sendRequest; var utils = require('utils'); var validate = require('schemaUtils').validate; // Schemas for the rule-style functions on the events API that // only need to be generated occasionally, so populate them lazily. var ruleFunctionSchemas = { __proto__: null, // These values are set lazily: // addRules: {}, // getRules: {}, // removeRules: {} }; // This function ensures that |ruleFunctionSchemas| is populated. function ensureRuleSchemasLoaded() { if (ruleFunctionSchemas.addRules) return; var eventsSchema = schemaRegistry.GetSchema("events"); var eventType = utils.lookup(eventsSchema.types, 'id', 'events.Event'); ruleFunctionSchemas.addRules = utils.lookup(eventType.functions, 'name', 'addRules'); ruleFunctionSchemas.getRules = utils.lookup(eventType.functions, 'name', 'getRules'); ruleFunctionSchemas.removeRules = utils.lookup(eventType.functions, 'name', 'removeRules'); } // A map of event names to the event object that is registered to that name. var attachedNamedEvents = {__proto__: null}; // A map of functions that massage event arguments before they are dispatched. // Key is event name, value is function. var eventArgumentMassagers = {__proto__: null}; // An attachment strategy for events that aren't attached to the browser. // This applies to events with the "unmanaged" option and events without // names. function NullAttachmentStrategy(event) { this.event_ = event; } $Object.setPrototypeOf(NullAttachmentStrategy.prototype, null); NullAttachmentStrategy.prototype.onAddedListener = function(listener) { }; NullAttachmentStrategy.prototype.onRemovedListener = function(listener) { }; NullAttachmentStrategy.prototype.detach = function(manual) { }; NullAttachmentStrategy.prototype.getListenersByIDs = function(ids) { // |ids| is for filtered events only. return this.event_.listeners; }; // Handles adding/removing/dispatching listeners for unfiltered events. function UnfilteredAttachmentStrategy(event) { this.event_ = event; } $Object.setPrototypeOf(UnfilteredAttachmentStrategy.prototype, null); UnfilteredAttachmentStrategy.prototype.onAddedListener = function(listener) { // Only attach / detach on the first / last listener removed. if (this.event_.listeners.length == 0) eventNatives.AttachEvent(this.event_.eventName); }; UnfilteredAttachmentStrategy.prototype.onRemovedListener = function(listener) { if (this.event_.listeners.length == 0) this.detach(true); }; UnfilteredAttachmentStrategy.prototype.detach = function(manual) { eventNatives.DetachEvent(this.event_.eventName, manual); }; UnfilteredAttachmentStrategy.prototype.getListenersByIDs = function(ids) { // |ids| is for filtered events only. return this.event_.listeners; }; function FilteredAttachmentStrategy(event) { this.event_ = event; this.listenerMap_ = {__proto__: null}; } $Object.setPrototypeOf(FilteredAttachmentStrategy.prototype, null); utils.defineProperty(FilteredAttachmentStrategy, 'idToEventMap', {__proto__: null}); FilteredAttachmentStrategy.prototype.onAddedListener = function(listener) { var id = eventNatives.AttachFilteredEvent(this.event_.eventName, listener.filters || {}); if (id == -1) throw new Error("Can't add listener"); listener.id = id; this.listenerMap_[id] = listener; FilteredAttachmentStrategy.idToEventMap[id] = this.event_; }; FilteredAttachmentStrategy.prototype.onRemovedListener = function(listener) { this.detachListener(listener, true); }; FilteredAttachmentStrategy.prototype.detachListener = function(listener, manual) { if (listener.id == undefined) throw new Error("listener.id undefined - '" + listener + "'"); var id = listener.id; delete this.listenerMap_[id]; delete FilteredAttachmentStrategy.idToEventMap[id]; eventNatives.DetachFilteredEvent(id, manual); }; FilteredAttachmentStrategy.prototype.detach = function(manual) { for (var i in this.listenerMap_) this.detachListener(this.listenerMap_[i], manual); }; FilteredAttachmentStrategy.prototype.getListenersByIDs = function(ids) { var result = []; for (var i = 0; i < ids.length; i++) $Array.push(result, this.listenerMap_[ids[i]]); return result; }; function parseEventOptions(opt_eventOptions) { return $Object.assign({ __proto__: null, }, { // Event supports adding listeners with filters ("filtered events"), for // example as used in the webNavigation API. // // event.addListener(listener, [filter1, filter2]); supportsFilters: false, // Events supports vanilla events. Most APIs use these. // // event.addListener(listener); supportsListeners: true, // Event supports adding rules ("declarative events") rather than // listeners, for example as used in the declarativeWebRequest API. // // event.addRules([rule1, rule2]); supportsRules: false, // Event is unmanaged in that the browser has no knowledge of its // existence; it's never invoked, doesn't keep the renderer alive, and // the bindings system has no knowledge of it. // // Both events created by user code (new chrome.Event()) and messaging // events are unmanaged, though in the latter case the browser *does* // interact indirectly with them via IPCs written by hand. unmanaged: false, }, opt_eventOptions); } // Event object. If opt_eventName is provided, this object represents // the unique instance of that named event, and dispatching an event // with that name will route through this object's listeners. Note that // opt_eventName is required for events that support rules. // // Example: // var Event = require('event_bindings').Event; // chrome.tabs.onChanged = new Event("tab-changed"); // chrome.tabs.onChanged.addListener(function(data) { alert(data); }); // Event.dispatch("tab-changed", "hi"); // will result in an alert dialog that says 'hi'. // // If opt_eventOptions exists, it is a dictionary that contains the boolean // entries "supportsListeners" and "supportsRules". // If opt_webViewInstanceId exists, it is an integer uniquely identifying a // tag within the embedder. If it does not exist, then this is an // extension event rather than a event. function EventImpl(opt_eventName, opt_argSchemas, opt_eventOptions, opt_webViewInstanceId) { this.eventName = opt_eventName; this.argSchemas = opt_argSchemas; this.listeners = []; this.eventOptions = parseEventOptions(opt_eventOptions); this.webViewInstanceId = opt_webViewInstanceId || 0; if (!this.eventName) { if (this.eventOptions.supportsRules) throw new Error("Events that support rules require an event name."); // Events without names cannot be managed by the browser by definition // (the browser has no way of identifying them). this.eventOptions.unmanaged = true; } // Track whether the event has been destroyed to help track down the cause // of http://crbug.com/258526. // This variable will eventually hold the stack trace of the destroy call. // TODO(kalman): Delete this and replace with more sound logic that catches // when events are used without being *attached*. this.destroyed = null; if (this.eventOptions.unmanaged) this.attachmentStrategy = new NullAttachmentStrategy(this); else if (this.eventOptions.supportsFilters) this.attachmentStrategy = new FilteredAttachmentStrategy(this); else this.attachmentStrategy = new UnfilteredAttachmentStrategy(this); } $Object.setPrototypeOf(EventImpl.prototype, null); // callback is a function(args, dispatch). args are the args we receive from // dispatchEvent(), and dispatch is a function(args) that dispatches args to // its listeners. function registerArgumentMassager(name, callback) { if (eventArgumentMassagers[name]) throw new Error("Massager already registered for event: " + name); eventArgumentMassagers[name] = callback; } // Dispatches a named event with the given argument array. The args array is // the list of arguments that will be sent to the event callback. function dispatchEvent(name, args, filteringInfo) { var listenerIDs = []; if (filteringInfo) listenerIDs = eventNatives.MatchAgainstEventFilter(name, filteringInfo); var event = attachedNamedEvents[name]; if (!event) return; var dispatchArgs = function(args) { var result = event.dispatch_(args, listenerIDs); if (result) logging.DCHECK(!result.validationErrors, result.validationErrors); return result; }; if (eventArgumentMassagers[name]) eventArgumentMassagers[name](args, dispatchArgs); else dispatchArgs(args); } // Registers a callback to be called when this event is dispatched. EventImpl.prototype.addListener = function(cb, filters) { if (!this.eventOptions.supportsListeners) throw new Error("This event does not support listeners."); if (this.eventOptions.maxListeners && this.getListenerCount_() >= this.eventOptions.maxListeners) { throw new Error("Too many listeners for " + this.eventName); } if (filters) { if (!this.eventOptions.supportsFilters) throw new Error("This event does not support filters."); if (filters.url && !(filters.url instanceof Array)) throw new Error("filters.url should be an array."); if (filters.serviceType && !(typeof filters.serviceType === 'string')) { throw new Error("filters.serviceType should be a string.") } } var listener = {callback: cb, filters: filters}; this.attach_(listener); $Array.push(this.listeners, listener); }; EventImpl.prototype.attach_ = function(listener) { this.attachmentStrategy.onAddedListener(listener); if (this.listeners.length == 0) { if (this.eventName) { if (attachedNamedEvents[this.eventName]) { throw new Error("Event '" + this.eventName + "' is already attached."); } attachedNamedEvents[this.eventName] = this; } } }; // Unregisters a callback. EventImpl.prototype.removeListener = function(cb) { if (!this.eventOptions.supportsListeners) throw new Error("This event does not support listeners."); var idx = this.findListener_(cb); if (idx == -1) return; var removedListener = $Array.splice(this.listeners, idx, 1)[0]; this.attachmentStrategy.onRemovedListener(removedListener); if (this.listeners.length == 0) { if (this.eventName) { if (!attachedNamedEvents[this.eventName]) { throw new Error( "Event '" + this.eventName + "' is not attached."); } delete attachedNamedEvents[this.eventName]; } } }; // Test if the given callback is registered for this event. EventImpl.prototype.hasListener = function(cb) { if (!this.eventOptions.supportsListeners) throw new Error("This event does not support listeners."); return this.findListener_(cb) > -1; }; // Test if any callbacks are registered for this event. EventImpl.prototype.hasListeners = function() { return this.getListenerCount_() > 0; }; // Returns the number of listeners on this event. EventImpl.prototype.getListenerCount_ = function() { if (!this.eventOptions.supportsListeners) throw new Error("This event does not support listeners."); return this.listeners.length; }; // Returns the index of the given callback if registered, or -1 if not // found. EventImpl.prototype.findListener_ = function(cb) { for (var i = 0; i < this.listeners.length; i++) { if (this.listeners[i].callback == cb) { return i; } } return -1; }; EventImpl.prototype.dispatch_ = function(args, listenerIDs) { if (this.destroyed) { throw new Error(this.eventName + ' was already destroyed at: ' + this.destroyed); } if (!this.eventOptions.supportsListeners) throw new Error("This event does not support listeners."); if (this.argSchemas && logging.DCHECK_IS_ON()) { try { validate(args, this.argSchemas); } catch (e) { e.message += ' in ' + this.eventName; throw e; } } // Make a copy of the listeners in case the listener list is modified // while dispatching the event. var listeners = $Array.slice( this.attachmentStrategy.getListenersByIDs(listenerIDs)); var results = []; for (var i = 0; i < listeners.length; i++) { try { var result = this.wrapper.dispatchToListener(listeners[i].callback, args); if (result !== undefined) $Array.push(results, result); } catch (e) { exceptionHandler.handle('Error in event handler for ' + (this.eventName ? this.eventName : '(unknown)'), e); } } if (results.length) return {results: results}; } // Can be overridden to support custom dispatching. EventImpl.prototype.dispatchToListener = function(callback, args) { return $Function.apply(callback, null, args); } // Dispatches this event object to all listeners, passing all supplied // arguments to this function each listener. EventImpl.prototype.dispatch = function(varargs) { return this.dispatch_($Array.slice(arguments), undefined); }; // Detaches this event object from its name. EventImpl.prototype.detach_ = function() { this.attachmentStrategy.detach(false); }; EventImpl.prototype.destroy_ = function() { this.listeners.length = 0; this.detach_(); this.destroyed = exceptionHandler.getStackTrace(); }; EventImpl.prototype.addRules = function(rules, opt_cb) { if (!this.eventOptions.supportsRules) throw new Error("This event does not support rules."); // Takes a list of JSON datatype identifiers and returns a schema fragment // that verifies that a JSON object corresponds to an array of only these // data types. function buildArrayOfChoicesSchema(typesList) { return { __proto__: null, 'type': 'array', 'items': { __proto__: null, 'choices': $Array.map(typesList, function(el) { return { __proto__: null, '$ref': el, }; }), } }; } // Validate conditions and actions against specific schemas of this // event object type. // |rules| is an array of JSON objects that follow the Rule type of the // declarative extension APIs. |conditions| is an array of JSON type // identifiers that are allowed to occur in the conditions attribute of each // rule. Likewise, |actions| is an array of JSON type identifiers that are // allowed to occur in the actions attribute of each rule. function validateRules(rules, conditions, actions) { var conditionsSchema = buildArrayOfChoicesSchema(conditions); var actionsSchema = buildArrayOfChoicesSchema(actions); $Array.forEach(rules, function(rule) { validate([rule.conditions], [conditionsSchema]); validate([rule.actions], [actionsSchema]); }); }; if (!this.eventOptions.conditions || !this.eventOptions.actions) { throw new Error('Event ' + this.eventName + ' misses ' + 'conditions or actions in the API specification.'); } validateRules(rules, this.eventOptions.conditions, this.eventOptions.actions); ensureRuleSchemasLoaded(); // We remove the first parameter from the validation to give the user more // meaningful error messages. validate([this.webViewInstanceId, rules, opt_cb], $Array.slice(ruleFunctionSchemas.addRules.parameters, 1)); sendRequest( "events.addRules", [this.eventName, this.webViewInstanceId, rules, opt_cb], ruleFunctionSchemas.addRules.parameters); } EventImpl.prototype.removeRules = function(ruleIdentifiers, opt_cb) { if (!this.eventOptions.supportsRules) throw new Error("This event does not support rules."); ensureRuleSchemasLoaded(); // We remove the first parameter from the validation to give the user more // meaningful error messages. validate([this.webViewInstanceId, ruleIdentifiers, opt_cb], $Array.slice(ruleFunctionSchemas.removeRules.parameters, 1)); sendRequest("events.removeRules", [this.eventName, this.webViewInstanceId, ruleIdentifiers, opt_cb], ruleFunctionSchemas.removeRules.parameters); } EventImpl.prototype.getRules = function(ruleIdentifiers, cb) { if (!this.eventOptions.supportsRules) throw new Error("This event does not support rules."); ensureRuleSchemasLoaded(); // We remove the first parameter from the validation to give the user more // meaningful error messages. validate([this.webViewInstanceId, ruleIdentifiers, cb], $Array.slice(ruleFunctionSchemas.getRules.parameters, 1)); sendRequest( "events.getRules", [this.eventName, this.webViewInstanceId, ruleIdentifiers, cb], ruleFunctionSchemas.getRules.parameters); } function Event() { privates(Event).constructPrivate(this, arguments); } utils.expose(Event, EventImpl, { functions: [ 'addListener', 'removeListener', 'hasListener', 'hasListeners', 'dispatchToListener', 'dispatch', 'addRules', 'removeRules', 'getRules', ], }); // NOTE: Event is (lazily) exposed as chrome.Event from dispatcher.cc. exports.$set('Event', Event); exports.$set('dispatchEvent', dispatchEvent); exports.$set('parseEventOptions', parseEventOptions); exports.$set('registerArgumentMassager', registerArgumentMassager); // Copyright 2014 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. var ExtensionOptionsConstants = require('extensionOptionsConstants').ExtensionOptionsConstants; var ExtensionOptionsEvents = require('extensionOptionsEvents').ExtensionOptionsEvents; var GuestViewContainer = require('guestViewContainer').GuestViewContainer; function ExtensionOptionsImpl(extensionoptionsElement) { GuestViewContainer.call(this, extensionoptionsElement, 'extensionoptions'); new ExtensionOptionsEvents(this); }; ExtensionOptionsImpl.prototype.__proto__ = GuestViewContainer.prototype; ExtensionOptionsImpl.VIEW_TYPE = 'ExtensionOptions'; ExtensionOptionsImpl.prototype.onElementAttached = function() { this.createGuest(); } ExtensionOptionsImpl.prototype.buildContainerParams = function() { var params = {}; for (var i in this.attributes) { params[i] = this.attributes[i].getValue(); } return params; }; ExtensionOptionsImpl.prototype.createGuest = function() { // Destroy the old guest if one exists. this.guest.destroy(); this.guest.create(this.buildParams(), function() { if (!this.guest.getId()) { // Fire a createfailed event here rather than in ExtensionOptionsGuest // because the guest will not be created, and cannot fire an event. var createFailedEvent = new Event('createfailed', { bubbles: true }); this.dispatchEvent(createFailedEvent); } else { this.attachWindow$(); } }.bind(this)); }; GuestViewContainer.registerElement(ExtensionOptionsImpl); // Exports. exports.$set('ExtensionOptionsImpl', ExtensionOptionsImpl); // Copyright 2015 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. // This module implements the attributes of the tag. var GuestViewAttributes = require('guestViewAttributes').GuestViewAttributes; var ExtensionOptionsConstants = require('extensionOptionsConstants').ExtensionOptionsConstants; var ExtensionOptionsImpl = require('extensionOptions').ExtensionOptionsImpl; // ----------------------------------------------------------------------------- // ExtensionAttribute object. // Attribute that handles extension binded to the extensionoptions. function ExtensionAttribute(view) { GuestViewAttributes.Attribute.call( this, ExtensionOptionsConstants.ATTRIBUTE_EXTENSION, view); } ExtensionAttribute.prototype.__proto__ = GuestViewAttributes.Attribute.prototype; ExtensionAttribute.prototype.handleMutation = function(oldValue, newValue) { // Once this attribute has been set, it cannot be unset. if (!newValue && oldValue) { this.setValueIgnoreMutation(oldValue); return; } if (!newValue || !this.elementAttached) return; this.view.createGuest(); }; // ----------------------------------------------------------------------------- // Sets up all of the extensionoptions attributes. ExtensionOptionsImpl.prototype.setupAttributes = function() { this.attributes[ExtensionOptionsConstants.ATTRIBUTE_EXTENSION] = new ExtensionAttribute(this); }; // Copyright 2015 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. // This module contains constants used in extensionoptions. // Container for the extensionview constants. var ExtensionOptionsConstants = { // Attributes. ATTRIBUTE_EXTENSION: 'extension' }; exports.$set('ExtensionOptionsConstants', $Object.freeze(ExtensionOptionsConstants)); // Copyright 2014 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. var CreateEvent = require('guestViewEvents').CreateEvent; var GuestViewEvents = require('guestViewEvents').GuestViewEvents; function ExtensionOptionsEvents(extensionOptionsImpl) { GuestViewEvents.call(this, extensionOptionsImpl); // |setupEventProperty| is normally called automatically, but the // 'createfailed' event is registered here because the event is fired from // ExtensionOptionsImpl instead of in response to an extension event. this.setupEventProperty('createfailed'); } ExtensionOptionsEvents.prototype.__proto__ = GuestViewEvents.prototype; // A dictionary of extension events to be listened for. This // dictionary augments |GuestViewEvents.EVENTS| in guest_view_events.js. See the // documentation there for details. ExtensionOptionsEvents.EVENTS = { 'close': { evt: CreateEvent('extensionOptionsInternal.onClose') }, 'load': { evt: CreateEvent('extensionOptionsInternal.onLoad') }, 'preferredsizechanged': { evt: CreateEvent('extensionOptionsInternal.onPreferredSizeChanged'), fields:['width', 'height'] } } ExtensionOptionsEvents.prototype.getEvents = function() { return ExtensionOptionsEvents.EVENTS; }; // Exports. exports.$set('ExtensionOptionsEvents', ExtensionOptionsEvents); // Copyright 2015 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. // This module implements the ExtensionView . var GuestViewContainer = require('guestViewContainer').GuestViewContainer; var ExtensionViewConstants = require('extensionViewConstants').ExtensionViewConstants; var ExtensionViewEvents = require('extensionViewEvents').ExtensionViewEvents; var ExtensionViewInternal = require('extensionViewInternal').ExtensionViewInternal; function ExtensionViewImpl(extensionviewElement) { GuestViewContainer.call(this, extensionviewElement, 'extensionview'); // A queue of objects in the order they should be loaded. // Every load call will add the given src, as well as the resolve and reject // functions. Each src will be loaded in the order they were called. this.loadQueue = []; // The current src that is loading. // @type {Object} this.pendingLoad = null; new ExtensionViewEvents(this, this.viewInstanceId); } ExtensionViewImpl.prototype.__proto__ = GuestViewContainer.prototype; ExtensionViewImpl.VIEW_TYPE = 'ExtensionView'; ExtensionViewImpl.setupElement = function(proto) { var apiMethods = ExtensionViewImpl.getApiMethods(); GuestViewContainer.forwardApiMethods(proto, apiMethods); }; ExtensionViewImpl.prototype.createGuest = function(callback) { this.guest.create(this.buildParams(), function() { this.attachWindow$(); callback(); }.bind(this)); }; ExtensionViewImpl.prototype.buildContainerParams = function() { var params = {}; for (var i in this.attributes) { params[i] = this.attributes[i].getValue(); } return params; }; ExtensionViewImpl.prototype.onElementDetached = function() { this.guest.destroy(); // Reset all attributes. for (var i in this.attributes) { this.attributes[i].setValueIgnoreMutation(); } }; // Updates src upon loadcommit. ExtensionViewImpl.prototype.onLoadCommit = function(url) { this.attributes[ExtensionViewConstants.ATTRIBUTE_SRC]. setValueIgnoreMutation(url); }; // Loads the next pending src from |loadQueue| to the extensionview. ExtensionViewImpl.prototype.loadNextSrc = function() { // If extensionview isn't currently loading a src, load the next src // in |loadQueue|. Otherwise, do nothing. if (!this.pendingLoad && this.loadQueue.length) { this.pendingLoad = this.loadQueue.shift(); var src = this.pendingLoad.src; var resolve = this.pendingLoad.resolve; var reject = this.pendingLoad.reject; // The extensionview validates the |src| twice, once in |parseSrc| and then // in |loadSrc|. The |src| isn't checked directly in |loadNextSrc| for // validity since the sending renderer (WebUI) is trusted. ExtensionViewInternal.parseSrc(src, function(isSrcValid, extensionId) { // Check if the src is valid. if (!isSrcValid) { reject('Failed to load: src is not valid.'); return; } // Destroy the current guest and create a new one if extension ID // is different. // // This may happen if the extensionview is loads an extension page, and // is then intended to load a page served from a different extension in // the same part of the WebUI. // // The two calls may look like the following: // extensionview.load('chrome-extension://firstId/page.html'); // extensionview.load('chrome-extension://secondId/page.html'); // The second time load is called, we destroy the current guest since // we will be loading content from a different extension. if (extensionId != this.attributes[ExtensionViewConstants.ATTRIBUTE_EXTENSION] .getValue()) { this.guest.destroy(); // Update the extension and src attributes. this.attributes[ExtensionViewConstants.ATTRIBUTE_EXTENSION] .setValueIgnoreMutation(extensionId); this.attributes[ExtensionViewConstants.ATTRIBUTE_SRC] .setValueIgnoreMutation(src); this.createGuest(function() { if (this.guest.getId() <= 0) { reject('Failed to load: guest creation failed.'); } else { resolve('Successful load.'); } }.bind(this)); } else { ExtensionViewInternal.loadSrc(this.guest.getId(), src, function(hasLoadSucceeded) { if (!hasLoadSucceeded) { reject('Failed to load.'); } else { // Update the src attribute. this.attributes[ExtensionViewConstants.ATTRIBUTE_SRC] .setValueIgnoreMutation(src); resolve('Successful load.'); } }.bind(this)); } }.bind(this)); } }; GuestViewContainer.registerElement(ExtensionViewImpl); // Exports. exports.$set('ExtensionViewImpl', ExtensionViewImpl); // Copyright 2015 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. // This module implements the public-facing API functions for the // tag. var ExtensionViewInternal = require('extensionViewInternal').ExtensionViewInternal; var ExtensionViewImpl = require('extensionView').ExtensionViewImpl; var ExtensionViewConstants = require('extensionViewConstants').ExtensionViewConstants; // An array of 's public-facing API methods. var EXTENSION_VIEW_API_METHODS = [ // Loads the given src into extensionview. Must be called every time the // the extensionview should load a new page. This is the only way to set // the extension and src attributes. Returns a promise indicating whether // or not load was successful. 'load' ]; // ----------------------------------------------------------------------------- // Custom API method implementations. ExtensionViewImpl.prototype.load = function(src) { return new Promise(function(resolve, reject) { this.loadQueue.push({src: src, resolve: resolve, reject: reject}); this.loadNextSrc(); }.bind(this)) .then(function onLoadResolved() { this.pendingLoad = null; this.loadNextSrc(); }.bind(this), function onLoadRejected() { this.pendingLoad.reject('Failed to load.'); this.pendingLoad = null; this.loadNextSrc(); }.bind(this)); }; // ----------------------------------------------------------------------------- ExtensionViewImpl.getApiMethods = function() { return EXTENSION_VIEW_API_METHODS; }; // Copyright 2015 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. // This module implements the attributes of the tag. var GuestViewAttributes = require('guestViewAttributes').GuestViewAttributes; var ExtensionViewConstants = require('extensionViewConstants').ExtensionViewConstants; var ExtensionViewImpl = require('extensionView').ExtensionViewImpl; var ExtensionViewInternal = require('extensionViewInternal').ExtensionViewInternal; // ----------------------------------------------------------------------------- // ExtensionAttribute object. // Attribute that handles the extension associated with the extensionview. function ExtensionAttribute(view) { GuestViewAttributes.ReadOnlyAttribute.call( this, ExtensionViewConstants.ATTRIBUTE_EXTENSION, view); } ExtensionAttribute.prototype.__proto__ = GuestViewAttributes.ReadOnlyAttribute.prototype; // ----------------------------------------------------------------------------- // SrcAttribute object. // Attribute that handles the location and navigation of the extensionview. // This is read only because we only want to be able to navigate to a src // through the load API call, which checks for URL validity and the extension // ID of the new src. function SrcAttribute(view) { GuestViewAttributes.ReadOnlyAttribute.call( this, ExtensionViewConstants.ATTRIBUTE_SRC, view); } SrcAttribute.prototype.__proto__ = GuestViewAttributes.ReadOnlyAttribute.prototype; SrcAttribute.prototype.handleMutation = function(oldValue, newValue) { console.log('src is read only. Use .load(url) to navigate to a new ' + 'extension page.'); this.setValueIgnoreMutation(oldValue); } // ----------------------------------------------------------------------------- // Sets up all of the extensionview attributes. ExtensionViewImpl.prototype.setupAttributes = function() { this.attributes[ExtensionViewConstants.ATTRIBUTE_EXTENSION] = new ExtensionAttribute(this); this.attributes[ExtensionViewConstants.ATTRIBUTE_SRC] = new SrcAttribute(this); }; // Copyright 2015 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. // This module contains constants used in extensionview. // Container for the extensionview constants. var ExtensionViewConstants = { // Attributes. ATTRIBUTE_EXTENSION: 'extension', ATTRIBUTE_SRC: 'src', }; exports.$set('ExtensionViewConstants', $Object.freeze(ExtensionViewConstants)); // Copyright 2015 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. // Event management for ExtensionView. var CreateEvent = require('guestViewEvents').CreateEvent; var GuestViewEvents = require('guestViewEvents').GuestViewEvents; function ExtensionViewEvents(extensionViewImpl) { GuestViewEvents.call(this, extensionViewImpl); } ExtensionViewEvents.prototype.__proto__ = GuestViewEvents.prototype; ExtensionViewEvents.EVENTS = { 'loadcommit': { evt: CreateEvent('extensionViewInternal.onLoadCommit'), handler: 'handleLoadCommitEvent', internal: true } }; ExtensionViewEvents.prototype.getEvents = function() { return ExtensionViewEvents.EVENTS; }; ExtensionViewEvents.prototype.handleLoadCommitEvent = function(event) { this.view.onLoadCommit(event.url); }; exports.$set('ExtensionViewEvents', ExtensionViewEvents); // Copyright 2015 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. exports.$set( 'ExtensionViewInternal', require('binding').Binding.create('extensionViewInternal').generate()); // Copyright 2015 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. // This module implements the base attributes of the GuestView tags. // ----------------------------------------------------------------------------- // Attribute object. // Default implementation of a GuestView attribute. function Attribute(name, view) { this.dirty = false; this.ignoreMutation = false; this.name = name; this.view = view; this.defineProperty(); } // Retrieves and returns the attribute's value. Attribute.prototype.getValue = function() { return this.view.element.getAttribute(this.name) || ''; }; // Retrieves and returns the attribute's value if it has been dirtied since // the last time this method was called. Returns null otherwise. Attribute.prototype.getValueIfDirty = function() { if (!this.dirty) return null; this.dirty = false; return this.getValue(); }; // Sets the attribute's value. Attribute.prototype.setValue = function(value) { this.view.element.setAttribute(this.name, value || ''); }; // Changes the attribute's value without triggering its mutation handler. Attribute.prototype.setValueIgnoreMutation = function(value) { this.ignoreMutation = true; this.setValue(value); this.ignoreMutation = false; }; // Defines this attribute as a property on the view's element. Attribute.prototype.defineProperty = function() { $Object.defineProperty(this.view.element, this.name, { get: function() { return this.getValue(); }.bind(this), set: function(value) { this.setValue(value); }.bind(this), enumerable: true }); }; // Called when the attribute's value changes. Attribute.prototype.maybeHandleMutation = function(oldValue, newValue) { if (this.ignoreMutation) return; this.dirty = true; this.handleMutation(oldValue, newValue); }; // Called when a change that isn't ignored occurs to the attribute's value. Attribute.prototype.handleMutation = function(oldValue, newValue) {}; // Called when the view's element is attached to the DOM tree. Attribute.prototype.attach = function() {}; // Called when the view's element is detached from the DOM tree. Attribute.prototype.detach = function() {}; // ----------------------------------------------------------------------------- // BooleanAttribute object. // An attribute that is treated as a Boolean. function BooleanAttribute(name, view) { Attribute.call(this, name, view); } BooleanAttribute.prototype.__proto__ = Attribute.prototype; BooleanAttribute.prototype.getValue = function() { return this.view.element.hasAttribute(this.name); }; BooleanAttribute.prototype.setValue = function(value) { if (!value) { this.view.element.removeAttribute(this.name); } else { this.view.element.setAttribute(this.name, ''); } }; // ----------------------------------------------------------------------------- // IntegerAttribute object. // An attribute that is treated as an integer. function IntegerAttribute(name, view) { Attribute.call(this, name, view); } IntegerAttribute.prototype.__proto__ = Attribute.prototype; IntegerAttribute.prototype.getValue = function() { return parseInt(this.view.element.getAttribute(this.name)) || 0; }; IntegerAttribute.prototype.setValue = function(value) { this.view.element.setAttribute(this.name, parseInt(value) || 0); }; // ----------------------------------------------------------------------------- // ReadOnlyAttribute object. // An attribute that cannot be changed (externally). The only way to set it // internally is via |setValueIgnoreMutation|. function ReadOnlyAttribute(name, view) { Attribute.call(this, name, view); } ReadOnlyAttribute.prototype.__proto__ = Attribute.prototype; ReadOnlyAttribute.prototype.handleMutation = function(oldValue, newValue) { this.setValueIgnoreMutation(oldValue); } // ----------------------------------------------------------------------------- var GuestViewAttributes = { Attribute: Attribute, BooleanAttribute: BooleanAttribute, IntegerAttribute: IntegerAttribute, ReadOnlyAttribute: ReadOnlyAttribute }; // Exports. exports.$set('GuestViewAttributes', GuestViewAttributes); // Copyright 2014 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. // This module implements the shared functionality for different guestview // containers, such as web_view, app_view, etc. var DocumentNatives = requireNative('document_natives'); var GuestView = require('guestView').GuestView; var GuestViewInternalNatives = requireNative('guest_view_internal'); var IdGenerator = requireNative('id_generator'); var MessagingNatives = requireNative('messaging_natives'); function GuestViewContainer(element, viewType) { privates(element).internal = this; this.attributes = {}; this.element = element; this.elementAttached = false; this.viewInstanceId = IdGenerator.GetNextId(); this.viewType = viewType; this.setupGuestProperty(); this.guest = new GuestView(viewType); this.setupAttributes(); privates(this).internalElement = this.createInternalElement$(); this.setupFocusPropagation(); var shadowRoot = this.element.createShadowRoot(); shadowRoot.appendChild(privates(this).internalElement); GuestViewInternalNatives.RegisterView(this.viewInstanceId, this, viewType); } // Prevent GuestViewContainer inadvertently inheriting code from the global // Object, allowing a pathway for executing unintended user code execution. GuestViewContainer.prototype.__proto__ = null; // Forward public API methods from |proto| to their internal implementations. GuestViewContainer.forwardApiMethods = function(proto, apiMethods) { var createProtoHandler = function(m) { return function(var_args) { var internal = privates(this).internal; return $Function.apply(internal[m], internal, arguments); }; }; for (var i = 0; apiMethods[i]; ++i) { proto[apiMethods[i]] = createProtoHandler(apiMethods[i]); } }; // Registers the browserplugin and guestview as custom elements once the // document has loaded. GuestViewContainer.registerElement = function(guestViewContainerType) { var useCapture = true; window.addEventListener('readystatechange', function listener(event) { if (document.readyState == 'loading') return; registerInternalElement(guestViewContainerType.VIEW_TYPE.toLowerCase()); registerGuestViewElement(guestViewContainerType); window.removeEventListener(event.type, listener, useCapture); }, useCapture); }; // Create the 'guest' property to track new GuestViews and always listen for // their resizes. GuestViewContainer.prototype.setupGuestProperty = function() { $Object.defineProperty(this, 'guest', { get: function() { return privates(this).guest; }.bind(this), set: function(value) { privates(this).guest = value; if (!value) { return; } privates(this).guest.onresize = function(e) { // Dispatch the 'contentresize' event. var contentResizeEvent = new Event('contentresize', { bubbles: true }); contentResizeEvent.oldWidth = e.oldWidth; contentResizeEvent.oldHeight = e.oldHeight; contentResizeEvent.newWidth = e.newWidth; contentResizeEvent.newHeight = e.newHeight; this.dispatchEvent(contentResizeEvent); }.bind(this); }.bind(this), enumerable: true }); }; GuestViewContainer.prototype.createInternalElement$ = function() { // We create BrowserPlugin as a custom element in order to observe changes // to attributes synchronously. var browserPluginElement = new GuestViewContainer[this.viewType + 'BrowserPlugin'](); privates(browserPluginElement).internal = this; return browserPluginElement; }; GuestViewContainer.prototype.setupFocusPropagation = function() { if (!this.element.hasAttribute('tabIndex')) { // GuestViewContainer needs a tabIndex in order to be focusable. // TODO(fsamuel): It would be nice to avoid exposing a tabIndex attribute // to allow GuestViewContainer to be focusable. // See http://crbug.com/231664. this.element.setAttribute('tabIndex', -1); } }; GuestViewContainer.prototype.focus = function() { // Focus the internal element when focus() is called on the GuestView element. privates(this).internalElement.focus(); } GuestViewContainer.prototype.attachWindow$ = function() { if (!this.internalInstanceId) { return true; } this.guest.attach(this.internalInstanceId, this.viewInstanceId, this.buildParams()); return true; }; GuestViewContainer.prototype.makeGCOwnContainer = function(internalInstanceId) { MessagingNatives.BindToGC(this, function() { GuestViewInternalNatives.DestroyContainer(internalInstanceId); }, -1); }; GuestViewContainer.prototype.onInternalInstanceId = function( internalInstanceId) { this.internalInstanceId = internalInstanceId; this.makeGCOwnContainer(this.internalInstanceId); // Track when the element resizes using the element resize callback. GuestViewInternalNatives.RegisterElementResizeCallback( this.internalInstanceId, this.weakWrapper(this.onElementResize)); if (!this.guest.getId()) { return; } this.guest.attach(this.internalInstanceId, this.viewInstanceId, this.buildParams()); }; GuestViewContainer.prototype.handleInternalElementAttributeMutation = function(name, oldValue, newValue) { if (name == 'internalinstanceid' && !oldValue && !!newValue) { privates(this).internalElement.removeAttribute('internalinstanceid'); this.onInternalInstanceId(parseInt(newValue)); } }; GuestViewContainer.prototype.onElementResize = function(newWidth, newHeight) { if (!this.guest.getId()) return; this.guest.setSize({normal: {width: newWidth, height: newHeight}}); }; GuestViewContainer.prototype.buildParams = function() { var params = this.buildContainerParams(); params['instanceId'] = this.viewInstanceId; // When the GuestViewContainer is not participating in layout (display:none) // then getBoundingClientRect() would report a width and height of 0. // However, in the case where the GuestViewContainer has a fixed size we can // use that value to initially size the guest so as to avoid a relayout of the // on display:block. var css = window.getComputedStyle(this.element, null); var elementRect = this.element.getBoundingClientRect(); params['elementWidth'] = parseInt(elementRect.width) || parseInt(css.getPropertyValue('width')); params['elementHeight'] = parseInt(elementRect.height) || parseInt(css.getPropertyValue('height')); return params; }; GuestViewContainer.prototype.dispatchEvent = function(event) { return this.element.dispatchEvent(event); } // Returns a wrapper function for |func| with a weak reference to |this|. GuestViewContainer.prototype.weakWrapper = function(func) { var viewInstanceId = this.viewInstanceId; return function() { var view = GuestViewInternalNatives.GetViewFromID(viewInstanceId); if (view) { return $Function.apply(func, view, $Array.slice(arguments)); } }; }; // Implemented by the specific view type, if needed. GuestViewContainer.prototype.buildContainerParams = function() { return {}; }; GuestViewContainer.prototype.willAttachElement = function() {}; GuestViewContainer.prototype.onElementAttached = function() {}; GuestViewContainer.prototype.onElementDetached = function() {}; GuestViewContainer.prototype.setupAttributes = function() {}; // Registers the browser plugin custom element. |viewType| is the // name of the specific guestview container (e.g. 'webview'). function registerInternalElement(viewType) { var proto = $Object.create(HTMLElement.prototype); proto.createdCallback = function() { this.setAttribute('type', 'application/browser-plugin'); this.setAttribute('id', 'browser-plugin-' + IdGenerator.GetNextId()); this.style.width = '100%'; this.style.height = '100%'; }; proto.attachedCallback = function() { // Load the plugin immediately. var unused = this.nonExistentAttribute; }; proto.attributeChangedCallback = function(name, oldValue, newValue) { var internal = privates(this).internal; if (!internal) { return; } internal.handleInternalElementAttributeMutation(name, oldValue, newValue); }; GuestViewContainer[viewType + 'BrowserPlugin'] = DocumentNatives.RegisterElement(viewType + 'browserplugin', {extends: 'object', prototype: proto}); delete proto.createdCallback; delete proto.attachedCallback; delete proto.detachedCallback; delete proto.attributeChangedCallback; }; // Registers the guestview container as a custom element. // |guestViewContainerType| is the type of guestview container // (e.g. WebViewImpl). function registerGuestViewElement(guestViewContainerType) { var proto = $Object.create(HTMLElement.prototype); proto.createdCallback = function() { new guestViewContainerType(this); }; proto.attachedCallback = function() { var internal = privates(this).internal; if (!internal) { return; } internal.elementAttached = true; internal.willAttachElement(); internal.onElementAttached(); }; proto.attributeChangedCallback = function(name, oldValue, newValue) { var internal = privates(this).internal; if (!internal || !internal.attributes[name]) { return; } // Let the changed attribute handle its own mutation. internal.attributes[name].maybeHandleMutation(oldValue, newValue); }; proto.detachedCallback = function() { var internal = privates(this).internal; if (!internal) { return; } internal.elementAttached = false; internal.internalInstanceId = 0; internal.guest.destroy(); internal.onElementDetached(); }; // Override |focus| to let |internal| handle it. proto.focus = function() { var internal = privates(this).internal; if (!internal) { return; } internal.focus(); }; // Let the specific view type add extra functionality to its custom element // through |proto|. if (guestViewContainerType.setupElement) { guestViewContainerType.setupElement(proto); } window[guestViewContainerType.VIEW_TYPE] = DocumentNatives.RegisterElement( guestViewContainerType.VIEW_TYPE.toLowerCase(), {prototype: proto}); // Delete the callbacks so developers cannot call them and produce unexpected // behavior. delete proto.createdCallback; delete proto.attachedCallback; delete proto.detachedCallback; delete proto.attributeChangedCallback; } // Exports. exports.$set('GuestViewContainer', GuestViewContainer); // Copyright 2015 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. // This module implements the registration of guestview elements when // permissions are not available. These elements exist only to provide a useful // error message when developers attempt to use them. var DocumentNatives = requireNative('document_natives'); var GuestViewContainer = require('guestViewContainer').GuestViewContainer; var ERROR_MESSAGE = 'You do not have permission to use the %1 element.' + ' Be sure to declare the "%1" permission in your manifest file.'; // A list of view types that will have custom elements registered if they are // not already registered by the time this module is loaded. var VIEW_TYPES = [ 'AppView', 'ExtensionOptions', 'ExtensionView', 'WebView' ]; // Registers a GuestView custom element. function registerGuestViewElement(viewType) { var proto = Object.create(HTMLElement.prototype); proto.createdCallback = function() { window.console.error(ERROR_MESSAGE.replace(/%1/g, viewType.toLowerCase())); }; window[viewType] = DocumentNatives.RegisterElement(viewType.toLowerCase(), {prototype: proto}); // Delete the callbacks so developers cannot call them and produce unexpected // behavior. delete proto.createdCallback; delete proto.attachedCallback; delete proto.detachedCallback; delete proto.attributeChangedCallback; } var useCapture = true; window.addEventListener('readystatechange', function listener(event) { if (document.readyState == 'loading') return; for (var i = 0; i != VIEW_TYPES.length; ++i) { // Register the error-providing custom element only for those view types // that have not already been registered. Since this module is always loaded // last, all the view types that are available (i.e. have the proper // permissions) will have already been registered on |window|. if (!window[VIEW_TYPES[i]]) registerGuestViewElement(VIEW_TYPES[i]); } window.removeEventListener(event.type, listener, useCapture); }, useCapture); // Copyright 2015 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. // Event management for GuestViewContainers. var EventBindings = require('event_bindings'); var GuestViewInternalNatives = requireNative('guest_view_internal'); var MessagingNatives = requireNative('messaging_natives'); var CreateEvent = function(name) { var eventOpts = {supportsListeners: true, supportsFilters: true}; return new EventBindings.Event(name, undefined, eventOpts); }; function GuestViewEvents(view) { view.events = this; this.view = view; this.on = {}; // |setupEventProperty| is normally called automatically, but these events are // are registered here because they are dispatched from GuestViewContainer // instead of in response to extension events. this.setupEventProperty('contentresize'); this.setupEventProperty('resize'); this.setupEvents(); } // |GuestViewEvents.EVENTS| is a dictionary of extension events to be listened // for, which specifies how each event should be handled. The events are // organized by name, and by default will be dispatched as DOM events with // the same name. // |cancelable| (default: false) specifies whether the DOM event's default // behavior can be canceled. If the default action associated with the event // is prevented, then its dispatch function will return false in its event // handler. The event must have a specified |handler| for this to be // meaningful. // |evt| specifies a descriptor object for the extension event. An event // listener will be attached to this descriptor. // |fields| (default: none) specifies the public-facing fields in the DOM event // that are accessible to developers. // |handler| specifies the name of a handler function to be called each time // that extension event is caught by its event listener. The DOM event // should be dispatched within this handler function (if desired). With no // handler function, the DOM event will be dispatched by default each time // the extension event is caught. // |internal| (default: false) specifies that the event will not be dispatched // as a DOM event, and will also not appear as an on* property on the view’s // element. A |handler| should be specified for all internal events, and // |fields| and |cancelable| should be left unspecified (as they are only // meaningful for DOM events). GuestViewEvents.EVENTS = {}; // Attaches |listener| onto the event descriptor object |evt|, and registers it // to be removed once this GuestViewEvents object is garbage collected. GuestViewEvents.prototype.addScopedListener = function( evt, listener, listenerOpts) { this.listenersToBeRemoved.push({ 'evt': evt, 'listener': listener }); evt.addListener(listener, listenerOpts); }; // Sets up the handling of events. GuestViewEvents.prototype.setupEvents = function() { // An array of registerd event listeners that should be removed when this // GuestViewEvents is garbage collected. this.listenersToBeRemoved = []; MessagingNatives.BindToGC(this, function(listenersToBeRemoved) { for (var i = 0; i != listenersToBeRemoved.length; ++i) { listenersToBeRemoved[i].evt.removeListener( listenersToBeRemoved[i].listener); listenersToBeRemoved[i] = null; } }.bind(undefined, this.listenersToBeRemoved), -1 /* portId */); // Set up the GuestView events. for (var eventName in GuestViewEvents.EVENTS) { this.setupEvent(eventName, GuestViewEvents.EVENTS[eventName]); } // Set up the derived view's events. var events = this.getEvents(); for (var eventName in events) { this.setupEvent(eventName, events[eventName]); } }; // Sets up the handling of the |eventName| event. GuestViewEvents.prototype.setupEvent = function(eventName, eventInfo) { if (!eventInfo.internal) { this.setupEventProperty(eventName); } var listenerOpts = { instanceId: this.view.viewInstanceId }; if (eventInfo.handler) { this.addScopedListener(eventInfo.evt, this.weakWrapper(function(e) { this[eventInfo.handler](e, eventName); }), listenerOpts); return; } // Internal events are not dispatched as DOM events. if (eventInfo.internal) { return; } this.addScopedListener(eventInfo.evt, this.weakWrapper(function(e) { var domEvent = this.makeDomEvent(e, eventName); this.view.dispatchEvent(domEvent); }), listenerOpts); }; // Constructs a DOM event based on the info for the |eventName| event provided // in either |GuestViewEvents.EVENTS| or getEvents(). GuestViewEvents.prototype.makeDomEvent = function(event, eventName) { var eventInfo = GuestViewEvents.EVENTS[eventName] || this.getEvents()[eventName]; // Internal events are not dispatched as DOM events. if (eventInfo.internal) { return null; } var details = { bubbles: true }; if (eventInfo.cancelable) { details.cancelable = true; } var domEvent = new Event(eventName, details); if (eventInfo.fields) { $Array.forEach(eventInfo.fields, function(field) { if (event[field] !== undefined) { domEvent[field] = event[field]; } }.bind(this)); } return domEvent; }; // Adds an 'on' property on the view, which can be used to set/unset // an event handler. GuestViewEvents.prototype.setupEventProperty = function(eventName) { var propertyName = 'on' + eventName.toLowerCase(); $Object.defineProperty(this.view.element, propertyName, { get: function() { return this.on[propertyName]; }.bind(this), set: function(value) { if (this.on[propertyName]) { this.view.element.removeEventListener(eventName, this.on[propertyName]); } this.on[propertyName] = value; if (value) { this.view.element.addEventListener(eventName, value); } }.bind(this), enumerable: true }); }; // returns a wrapper for |func| with a weak reference to |this|. GuestViewEvents.prototype.weakWrapper = function(func) { var viewInstanceId = this.view.viewInstanceId; return function() { var view = GuestViewInternalNatives.GetViewFromID(viewInstanceId); if (!view) { return; } return $Function.apply(func, view.events, $Array.slice(arguments)); }; }; // Implemented by the derived event manager, if one exists. GuestViewEvents.prototype.getEvents = function() { return {}; }; // Exports. exports.$set('GuestViewEvents', GuestViewEvents); exports.$set('CreateEvent', CreateEvent); // Copyright 2015 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. // --site-per-process overrides for guest_view_container.js var GuestViewContainer = require('guestViewContainer').GuestViewContainer; var IdGenerator = requireNative('id_generator'); GuestViewContainer.prototype.createInternalElement$ = function() { var iframeElement = document.createElement('iframe'); iframeElement.style.width = '100%'; iframeElement.style.height = '100%'; iframeElement.style.border = '0'; privates(iframeElement).internal = this; return iframeElement; }; GuestViewContainer.prototype.attachWindow$ = function() { var generatedId = IdGenerator.GetNextId(); // Generate an instance id for the container. this.onInternalInstanceId(generatedId); return true; }; GuestViewContainer.prototype.willAttachElement = function () { if (this.deferredAttachCallback) { this.deferredAttachCallback(); this.deferredAttachCallback = null; } }; // Copyright 2015 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. // --site-per-process overrides for guest_view.js. var GuestView = require('guestView').GuestView; var GuestViewImpl = require('guestView').GuestViewImpl; var GuestViewInternalNatives = requireNative('guest_view_internal'); var ResizeEvent = require('guestView').ResizeEvent; var getIframeContentWindow = function(viewInstanceId) { var view = GuestViewInternalNatives.GetViewFromID(viewInstanceId); if (!view) return null; var internalIframeElement = privates(view).internalElement; if (internalIframeElement) return internalIframeElement.contentWindow; return null; }; // Internal implementation of attach(). GuestViewImpl.prototype.attachImpl$ = function( internalInstanceId, viewInstanceId, attachParams, callback) { var view = GuestViewInternalNatives.GetViewFromID(viewInstanceId); if (!view.elementAttached) { // Defer the attachment until the element is attached. view.deferredAttachCallback = this.attachImpl$.bind( this, internalInstanceId, viewInstanceId, attachParams, callback); return; }; // Check the current state. if (!this.checkState('attach')) { this.handleCallback(callback); return; } // Callback wrapper function to store the contentWindow from the attachGuest() // callback, handle potential attaching failure, register an automatic detach, // and advance the queue. var callbackWrapper = function(callback, contentWindow) { // Check if attaching failed. contentWindow = getIframeContentWindow(viewInstanceId); if (!contentWindow) { this.state = GuestViewImpl.GuestState.GUEST_STATE_CREATED; this.internalInstanceId = 0; } else { // Only update the contentWindow if attaching is successful. this.contentWindow = contentWindow; } this.handleCallback(callback); }; attachParams['instanceId'] = viewInstanceId; var contentWindow = getIframeContentWindow(viewInstanceId); // |contentWindow| is used to retrieve the RenderFrame in cpp. GuestViewInternalNatives.AttachIframeGuest( internalInstanceId, this.id, attachParams, contentWindow, callbackWrapper.bind(this, callback)); this.internalInstanceId = internalInstanceId; this.state = GuestViewImpl.GuestState.GUEST_STATE_ATTACHED; // Detach automatically when the container is destroyed. GuestViewInternalNatives.RegisterDestructionCallback( internalInstanceId, this.weakWrapper(function() { if (this.state != GuestViewImpl.GuestState.GUEST_STATE_ATTACHED || this.internalInstanceId != internalInstanceId) { return; } this.internalInstanceId = 0; this.state = GuestViewImpl.GuestState.GUEST_STATE_CREATED; }, viewInstanceId)); }; // Internal implementation of create(). GuestViewImpl.prototype.createImpl$ = function(createParams, callback) { // Check the current state. if (!this.checkState('create')) { this.handleCallback(callback); return; } // Callback wrapper function to store the guestInstanceId from the // createGuest() callback, handle potential creation failure, and advance the // queue. var callbackWrapper = function(callback, guestInfo) { this.id = guestInfo.id; // Check if creation failed. if (this.id === 0) { this.state = GuestViewImpl.GuestState.GUEST_STATE_START; this.contentWindow = null; } ResizeEvent.addListener(this.callOnResize, {instanceId: this.id}); this.handleCallback(callback); }; this.sendCreateRequest(createParams, callbackWrapper.bind(this, callback)); this.state = GuestViewImpl.GuestState.GUEST_STATE_CREATED; }; // Copyright 2014 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. // This module implements a wrapper for a guestview that manages its // creation, attaching, and destruction. var CreateEvent = require('guestViewEvents').CreateEvent; var EventBindings = require('event_bindings'); var GuestViewInternal = require('binding').Binding.create('guestViewInternal').generate(); var GuestViewInternalNatives = requireNative('guest_view_internal'); // Events. var ResizeEvent = CreateEvent('guestViewInternal.onResize'); // Error messages. var ERROR_MSG_ALREADY_ATTACHED = 'The guest has already been attached.'; var ERROR_MSG_ALREADY_CREATED = 'The guest has already been created.'; var ERROR_MSG_INVALID_STATE = 'The guest is in an invalid state.'; var ERROR_MSG_NOT_ATTACHED = 'The guest is not attached.'; var ERROR_MSG_NOT_CREATED = 'The guest has not been created.'; // Properties. var PROPERTY_ON_RESIZE = 'onresize'; // Contains and hides the internal implementation details of |GuestView|, // including maintaining its state and enforcing the proper usage of its API // fucntions. function GuestViewImpl(guestView, viewType, guestInstanceId) { if (guestInstanceId) { this.id = guestInstanceId; this.state = GuestViewImpl.GuestState.GUEST_STATE_CREATED; } else { this.id = 0; this.state = GuestViewImpl.GuestState.GUEST_STATE_START; } this.actionQueue = []; this.contentWindow = null; this.guestView = guestView; this.pendingAction = null; this.viewType = viewType; this.internalInstanceId = 0; this.setupOnResize(); } // Prevent GuestViewImpl inadvertently inheriting code from the global Object, // allowing a pathway for executing unintended user code execution. GuestViewImpl.prototype.__proto__ = null; // Possible states. GuestViewImpl.GuestState = { GUEST_STATE_START: 0, GUEST_STATE_CREATED: 1, GUEST_STATE_ATTACHED: 2 }; // Sets up the onResize property on the GuestView. GuestViewImpl.prototype.setupOnResize = function() { $Object.defineProperty(this.guestView, PROPERTY_ON_RESIZE, { get: function() { return this[PROPERTY_ON_RESIZE]; }.bind(this), set: function(value) { this[PROPERTY_ON_RESIZE] = value; }.bind(this), enumerable: true }); this.callOnResize = function(e) { if (!this[PROPERTY_ON_RESIZE]) { return; } this[PROPERTY_ON_RESIZE](e); }.bind(this); }; // Callback wrapper that is used to call the callback of the pending action (if // one exists), and then performs the next action in the queue. GuestViewImpl.prototype.handleCallback = function(callback) { if (callback) { callback(); } this.pendingAction = null; this.performNextAction(); }; // Perform the next action in the queue, if one exists. GuestViewImpl.prototype.performNextAction = function() { // Make sure that there is not already an action in progress, and that there // exists a queued action to perform. if (!this.pendingAction && this.actionQueue.length) { this.pendingAction = this.actionQueue.shift(); this.pendingAction(); } }; // Check the current state to see if the proposed action is valid. Returns false // if invalid. GuestViewImpl.prototype.checkState = function(action) { // Create an error prefix based on the proposed action. var errorPrefix = 'Error calling ' + action + ': '; // Check that the current state is valid. if (!(this.state >= 0 && this.state <= 2)) { window.console.error(errorPrefix + ERROR_MSG_INVALID_STATE); return false; } // Map of possible errors for each action. For each action, the errors are // listed for states in the order: GUEST_STATE_START, GUEST_STATE_CREATED, // GUEST_STATE_ATTACHED. var errors = { 'attach': [ERROR_MSG_NOT_CREATED, null, ERROR_MSG_ALREADY_ATTACHED], 'create': [null, ERROR_MSG_ALREADY_CREATED, ERROR_MSG_ALREADY_CREATED], 'destroy': [null, null, null], 'detach': [ERROR_MSG_NOT_ATTACHED, ERROR_MSG_NOT_ATTACHED, null], 'setSize': [ERROR_MSG_NOT_CREATED, null, null] }; // Check that the proposed action is a real action. if (errors[action] == undefined) { window.console.error(errorPrefix + ERROR_MSG_INVALID_ACTION); return false; } // Report the error if the proposed action is found to be invalid for the // current state. var error; if (error = errors[action][this.state]) { window.console.error(errorPrefix + error); return false; } return true; }; // Returns a wrapper function for |func| with a weak reference to |this|. This // implementation of weakWrapper() requires a provided |viewInstanceId| since // GuestViewImpl does not store this ID. GuestViewImpl.prototype.weakWrapper = function(func, viewInstanceId) { return function() { var view = GuestViewInternalNatives.GetViewFromID(viewInstanceId); if (view && view.guest) { return $Function.apply(func, privates(view.guest).internal, $Array.slice(arguments)); } }; }; // Internal implementation of attach(). GuestViewImpl.prototype.attachImpl$ = function( internalInstanceId, viewInstanceId, attachParams, callback) { // Check the current state. if (!this.checkState('attach')) { this.handleCallback(callback); return; } // Callback wrapper function to store the contentWindow from the attachGuest() // callback, handle potential attaching failure, register an automatic detach, // and advance the queue. var callbackWrapper = function(callback, contentWindow) { // Check if attaching failed. if (!contentWindow) { this.state = GuestViewImpl.GuestState.GUEST_STATE_CREATED; this.internalInstanceId = 0; } else { // Only update the contentWindow if attaching is successful. this.contentWindow = contentWindow; } this.handleCallback(callback); }; attachParams['instanceId'] = viewInstanceId; GuestViewInternalNatives.AttachGuest(internalInstanceId, this.id, attachParams, callbackWrapper.bind(this, callback)); this.internalInstanceId = internalInstanceId; this.state = GuestViewImpl.GuestState.GUEST_STATE_ATTACHED; // Detach automatically when the container is destroyed. GuestViewInternalNatives.RegisterDestructionCallback( internalInstanceId, this.weakWrapper(function() { if (this.state != GuestViewImpl.GuestState.GUEST_STATE_ATTACHED || this.internalInstanceId != internalInstanceId) { return; } this.internalInstanceId = 0; this.state = GuestViewImpl.GuestState.GUEST_STATE_CREATED; }, viewInstanceId)); }; // Internal implementation of create(). GuestViewImpl.prototype.createImpl$ = function(createParams, callback) { // Check the current state. if (!this.checkState('create')) { this.handleCallback(callback); return; } // Callback wrapper function to store the guestInstanceId from the // createGuest() callback, handle potential creation failure, and advance the // queue. var callbackWrapper = function(callback, guestInfo) { this.id = guestInfo.id; this.contentWindow = GuestViewInternalNatives.GetContentWindow(guestInfo.contentWindowId); // Check if creation failed. if (this.id === 0) { this.state = GuestViewImpl.GuestState.GUEST_STATE_START; this.contentWindow = null; } ResizeEvent.addListener(this.callOnResize, {instanceId: this.id}); this.handleCallback(callback); }; this.sendCreateRequest(createParams, callbackWrapper.bind(this, callback)); this.state = GuestViewImpl.GuestState.GUEST_STATE_CREATED; }; GuestViewImpl.prototype.sendCreateRequest = function( createParams, boundCallback) { GuestViewInternal.createGuest(this.viewType, createParams, boundCallback); }; // Internal implementation of destroy(). GuestViewImpl.prototype.destroyImpl = function(callback) { // Check the current state. if (!this.checkState('destroy')) { this.handleCallback(callback); return; } if (this.state == GuestViewImpl.GuestState.GUEST_STATE_START) { // destroy() does nothing in this case. this.handleCallback(callback); return; } // If this guest is attached, then detach it first. if (!!this.internalInstanceId) { GuestViewInternalNatives.DetachGuest(this.internalInstanceId); } GuestViewInternal.destroyGuest(this.id, this.handleCallback.bind(this, callback)); // Reset the state of the destroyed guest; this.contentWindow = null; this.id = 0; this.internalInstanceId = 0; this.state = GuestViewImpl.GuestState.GUEST_STATE_START; if (ResizeEvent.hasListener(this.callOnResize)) { ResizeEvent.removeListener(this.callOnResize); } }; // Internal implementation of detach(). GuestViewImpl.prototype.detachImpl = function(callback) { // Check the current state. if (!this.checkState('detach')) { this.handleCallback(callback); return; } GuestViewInternalNatives.DetachGuest( this.internalInstanceId, this.handleCallback.bind(this, callback)); this.internalInstanceId = 0; this.state = GuestViewImpl.GuestState.GUEST_STATE_CREATED; }; // Internal implementation of setSize(). GuestViewImpl.prototype.setSizeImpl = function(sizeParams, callback) { // Check the current state. if (!this.checkState('setSize')) { this.handleCallback(callback); return; } GuestViewInternal.setSize(this.id, sizeParams, this.handleCallback.bind(this, callback)); }; // The exposed interface to a guestview. Exposes in its API the functions // attach(), create(), destroy(), and getId(). All other implementation details // are hidden. function GuestView(viewType, guestInstanceId) { privates(this).internal = new GuestViewImpl(this, viewType, guestInstanceId); } // Attaches the guestview to the container with ID |internalInstanceId|. GuestView.prototype.attach = function( internalInstanceId, viewInstanceId, attachParams, callback) { var internal = privates(this).internal; internal.actionQueue.push(internal.attachImpl$.bind( internal, internalInstanceId, viewInstanceId, attachParams, callback)); internal.performNextAction(); }; // Creates the guestview. GuestView.prototype.create = function(createParams, callback) { var internal = privates(this).internal; internal.actionQueue.push(internal.createImpl$.bind( internal, createParams, callback)); internal.performNextAction(); }; // Destroys the guestview. Nothing can be done with the guestview after it has // been destroyed. GuestView.prototype.destroy = function(callback) { var internal = privates(this).internal; internal.actionQueue.push(internal.destroyImpl.bind(internal, callback)); internal.performNextAction(); }; // Detaches the guestview from its container. // Note: This is not currently used. GuestView.prototype.detach = function(callback) { var internal = privates(this).internal; internal.actionQueue.push(internal.detachImpl.bind(internal, callback)); internal.performNextAction(); }; // Adjusts the guestview's sizing parameters. GuestView.prototype.setSize = function(sizeParams, callback) { var internal = privates(this).internal; internal.actionQueue.push(internal.setSizeImpl.bind( internal, sizeParams, callback)); internal.performNextAction(); }; // Returns the contentWindow for this guestview. GuestView.prototype.getContentWindow = function() { var internal = privates(this).internal; return internal.contentWindow; }; // Returns the ID for this guestview. GuestView.prototype.getId = function() { var internal = privates(this).internal; return internal.id; }; // Exports exports.$set('GuestView', GuestView); exports.$set('GuestViewImpl', GuestViewImpl); exports.$set('ResizeEvent', ResizeEvent); // Copyright 2014 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. // This function takes an object |imageSpec| with the key |path| - // corresponding to the internet URL to be translated - and optionally // |width| and |height| which are the maximum dimensions to be used when // converting the image. function loadImageData(imageSpec, callbacks) { var path = imageSpec.path; var img = new Image(); if (typeof callbacks.onerror === 'function') { img.onerror = function() { callbacks.onerror({ problem: 'could_not_load', path: path }); }; } img.onload = function() { var canvas = document.createElement('canvas'); if (img.width <= 0 || img.height <= 0) { callbacks.onerror({ problem: 'image_size_invalid', path: path}); return; } var scaleFactor = 1; if (imageSpec.width && imageSpec.width < img.width) scaleFactor = imageSpec.width / img.width; if (imageSpec.height && imageSpec.height < img.height) { var heightScale = imageSpec.height / img.height; if (heightScale < scaleFactor) scaleFactor = heightScale; } canvas.width = img.width * scaleFactor; canvas.height = img.height * scaleFactor; var canvas_context = canvas.getContext('2d'); canvas_context.clearRect(0, 0, canvas.width, canvas.height); canvas_context.drawImage(img, 0, 0, canvas.width, canvas.height); try { var imageData = canvas_context.getImageData( 0, 0, canvas.width, canvas.height); if (typeof callbacks.oncomplete === 'function') { callbacks.oncomplete( imageData.width, imageData.height, imageData.data.buffer); } } catch (e) { if (typeof callbacks.onerror === 'function') { callbacks.onerror({ problem: 'data_url_unavailable', path: path }); } } } img.src = path; } function on_complete_index(index, err, loading, finished, callbacks) { return function(width, height, imageData) { delete loading[index]; finished[index] = { width: width, height: height, data: imageData }; if (err) callbacks.onerror(index); if ($Object.keys(loading).length == 0) callbacks.oncomplete(finished); } } function loadAllImages(imageSpecs, callbacks) { var loading = {}, finished = [], index, pathname; for (var index = 0; index < imageSpecs.length; index++) { loading[index] = imageSpecs[index]; loadImageData(imageSpecs[index], { oncomplete: on_complete_index(index, false, loading, finished, callbacks), onerror: on_complete_index(index, true, loading, finished, callbacks) }); } } exports.$set('loadImageData', loadImageData); exports.$set('loadAllImages', loadAllImages); // Copyright 2014 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. // ----------------------------------------------------------------------------- // NOTE: If you change this file you need to touch // extension_renderer_resources.grd to have your change take effect. // ----------------------------------------------------------------------------- //============================================================================== // This file contains a class that implements a subset of JSON Schema. // See: http://www.json.com/json-schema-proposal/ for more details. // // The following features of JSON Schema are not implemented: // - requires // - unique // - disallow // - union types (but replaced with 'choices') // // The following properties are not applicable to the interface exposed by // this class: // - options // - readonly // - title // - description // - format // - default // - transient // - hidden // // There are also these departures from the JSON Schema proposal: // - function and undefined types are supported // - null counts as 'unspecified' for optional values // - added the 'choices' property, to allow specifying a list of possible types // for a value // - by default an "object" typed schema does not allow additional properties. // if present, "additionalProperties" is to be a schema against which all // additional properties will be validated. //============================================================================== var utils = require('utils'); var loggingNative = requireNative('logging'); var getObjectType = requireNative('schema_registry').GetObjectType; var CHECK = loggingNative.CHECK; var DCHECK = loggingNative.DCHECK; function isInstanceOfClass(instance, className) { while ((instance = instance.__proto__)) { if (instance.constructor.name == className) return true; } return false; } function isOptionalValue(value) { return value === undefined || value === null; } function enumToString(enumValue) { if (enumValue.name === undefined) return enumValue; return enumValue.name; } /** * Validates an instance against a schema and accumulates errors. Usage: * * var validator = new JSONSchemaValidator(); * validator.validate(inst, schema); * if (validator.errors.length == 0) * console.log("Valid!"); * else * console.log(validator.errors); * * The errors property contains a list of objects. Each object has two * properties: "path" and "message". The "path" property contains the path to * the key that had the problem, and the "message" property contains a sentence * describing the error. */ function JSONSchemaValidator() { this.errors = []; this.types = []; } $Object.setPrototypeOf(JSONSchemaValidator.prototype, null); var messages = { __proto__: null, invalidEnum: 'Value must be one of: [*].', propertyRequired: 'Property is required.', unexpectedProperty: 'Unexpected property.', arrayMinItems: 'Array must have at least * items.', arrayMaxItems: 'Array must not have more than * items.', itemRequired: 'Item is required.', stringMinLength: 'String must be at least * characters long.', stringMaxLength: 'String must not be more than * characters long.', stringPattern: 'String must match the pattern: *.', numberFiniteNotNan: 'Value must not be *.', numberMinValue: 'Value must not be less than *.', numberMaxValue: 'Value must not be greater than *.', numberIntValue: 'Value must fit in a 32-bit signed integer.', numberMaxDecimal: 'Value must not have more than * decimal places.', invalidType: "Expected '*' but got '*'.", invalidTypeIntegerNumber: "Expected 'integer' but got 'number', consider using Math.round().", invalidChoice: 'Value does not match any valid type choices.', invalidPropertyType: 'Missing property type.', schemaRequired: 'Schema value required.', unknownSchemaReference: 'Unknown schema reference: *.', notInstance: 'Object must be an instance of *.', }; /** * Builds an error message. Key is the property in the |errors| object, and * |opt_replacements| is an array of values to replace "*" characters with. */ utils.defineProperty(JSONSchemaValidator, 'formatError', function(key, opt_replacements) { var message = messages[key]; if (opt_replacements) { for (var i = 0; i < opt_replacements.length; ++i) { DCHECK($String.indexOf(message, '*') != -1, message); message = $String.replace(message, '*', opt_replacements[i]); } } DCHECK($String.indexOf(message, '*') == -1) return message; }); /** * Classifies a value as one of the JSON schema primitive types. Note that we * don't explicitly disallow 'function', because we want to allow functions in * the input values. */ utils.defineProperty(JSONSchemaValidator, 'getType', function(value) { // If we can determine the type safely in JS, it's fastest to do it here. // However, Object types are difficult to classify, so we have to do it in // C++. var s = typeof value; if (s === 'object') return value === null ? 'null' : getObjectType(value); if (s === 'number') return value % 1 === 0 ? 'integer' : 'number'; return s; }); /** * Add types that may be referenced by validated schemas that reference them * with "$ref": . Each type must be a valid schema and define an * "id" property. */ JSONSchemaValidator.prototype.addTypes = function(typeOrTypeList) { function addType(validator, type) { if (!type.id) throw new Error("Attempt to addType with missing 'id' property"); validator.types[type.id] = type; } if ($Array.isArray(typeOrTypeList)) { for (var i = 0; i < typeOrTypeList.length; ++i) { addType(this, typeOrTypeList[i]); } } else { addType(this, typeOrTypeList); } } /** * Returns a list of strings of the types that this schema accepts. */ JSONSchemaValidator.prototype.getAllTypesForSchema = function(schema) { var schemaTypes = []; if (schema.type) $Array.push(schemaTypes, schema.type); if (schema.choices) { for (var i = 0; i < schema.choices.length; ++i) { var choiceTypes = this.getAllTypesForSchema(schema.choices[i]); schemaTypes = $Array.concat(schemaTypes, choiceTypes); } } var ref = schema['$ref']; if (ref) { var type = this.getOrAddType(ref); CHECK(type, 'Could not find type ' + ref); schemaTypes = $Array.concat(schemaTypes, this.getAllTypesForSchema(type)); } return schemaTypes; }; JSONSchemaValidator.prototype.getOrAddType = function(typeName) { if (!this.types[typeName]) this.types[typeName] = utils.loadTypeSchema(typeName); return this.types[typeName]; }; /** * Returns true if |schema| would accept an argument of type |type|. */ JSONSchemaValidator.prototype.isValidSchemaType = function(type, schema) { if (type == 'any') return true; // TODO(kalman): I don't understand this code. How can type be "null"? if (schema.optional && (type == 'null' || type == 'undefined')) return true; var schemaTypes = this.getAllTypesForSchema(schema); for (var i = 0; i < schemaTypes.length; ++i) { if (schemaTypes[i] == 'any' || type == schemaTypes[i] || (type == 'integer' && schemaTypes[i] == 'number')) return true; } return false; }; /** * Returns true if there is a non-null argument that both |schema1| and * |schema2| would accept. */ JSONSchemaValidator.prototype.checkSchemaOverlap = function(schema1, schema2) { var schema1Types = this.getAllTypesForSchema(schema1); for (var i = 0; i < schema1Types.length; ++i) { if (this.isValidSchemaType(schema1Types[i], schema2)) return true; } return false; }; /** * Validates an instance against a schema. The instance can be any JavaScript * value and will be validated recursively. When this method returns, the * |errors| property will contain a list of errors, if any. */ JSONSchemaValidator.prototype.validate = function(instance, schema, opt_path) { var path = opt_path || ''; if (!schema) { this.addError(path, 'schemaRequired'); return; } // If this schema defines itself as reference type, save it in this.types. if (schema.id) this.types[schema.id] = schema; // If the schema has an extends property, the instance must validate against // that schema too. if (schema.extends) this.validate(instance, schema.extends, path); // If the schema has a $ref property, the instance must validate against // that schema too. It must be present in this.types to be referenced. var ref = schema.$ref; if (ref) { if (!this.getOrAddType(ref)) this.addError(path, 'unknownSchemaReference', [ref]); else this.validate(instance, this.getOrAddType(ref), path) } // If the schema has a choices property, the instance must validate against at // least one of the items in that array. if (schema.choices) { this.validateChoices(instance, schema, path); return; } // If the schema has an enum property, the instance must be one of those // values. if (schema.enum) { if (!this.validateEnum(instance, schema, path)) return; } if (schema.type && schema.type != 'any') { if (!this.validateType(instance, schema, path)) return; // Type-specific validation. switch (schema.type) { case 'object': this.validateObject(instance, schema, path); break; case 'array': this.validateArray(instance, schema, path); break; case 'string': this.validateString(instance, schema, path); break; case 'number': case 'integer': this.validateNumber(instance, schema, path); break; } } }; /** * Validates an instance against a choices schema. The instance must match at * least one of the provided choices. */ JSONSchemaValidator.prototype.validateChoices = function(instance, schema, path) { var originalErrors = this.errors; for (var i = 0; i < schema.choices.length; ++i) { this.errors = []; this.validate(instance, schema.choices[i], path); if (this.errors.length == 0) { this.errors = originalErrors; return; } } this.errors = originalErrors; this.addError(path, 'invalidChoice'); }; /** * Validates an instance against a schema with an enum type. Populates the * |errors| property, and returns a boolean indicating whether the instance * validates. */ JSONSchemaValidator.prototype.validateEnum = function(instance, schema, path) { for (var i = 0; i < schema.enum.length; ++i) { if (instance === enumToString(schema.enum[i])) return true; } this.addError(path, 'invalidEnum', [$Array.join($Array.map(schema.enum, enumToString), ', ')]); return false; }; /** * Validates an instance against an object schema and populates the errors * property. */ JSONSchemaValidator.prototype.validateObject = function(instance, schema, path) { if (schema.properties) { $Array.forEach($Object.keys(schema.properties), function(prop) { var propPath = path ? path + '.' + prop : prop; if (schema.properties[prop] == undefined) { this.addError(propPath, 'invalidPropertyType'); } else if (instance[prop] !== undefined && instance[prop] !== null) { this.validate(instance[prop], schema.properties[prop], propPath); } else if (!schema.properties[prop].optional) { this.addError(propPath, 'propertyRequired'); } }, this); } // If "instanceof" property is set, check that this object inherits from // the specified constructor (function). if (schema.isInstanceOf) { if (!isInstanceOfClass(instance, schema.isInstanceOf)) this.addError(path || '', 'notInstance', [schema.isInstanceOf]); } // Exit early from additional property check if "type":"any" is defined. if (schema.additionalProperties && schema.additionalProperties.type && schema.additionalProperties.type == 'any') { return; } // By default, additional properties are not allowed on instance objects. This // can be overridden by setting the additionalProperties property to a schema // which any additional properties must validate against. $Array.forEach($Object.keys(instance), function(prop) { if (schema.properties && $Object.hasOwnProperty(schema.properties, prop)) return; var propPath = path ? path + '.' + prop : prop; if (schema.additionalProperties) this.validate(instance[prop], schema.additionalProperties, propPath); else this.addError(propPath, 'unexpectedProperty'); }, this); }; /** * Validates an instance against an array schema and populates the errors * property. */ JSONSchemaValidator.prototype.validateArray = function(instance, schema, path) { var typeOfItems = JSONSchemaValidator.getType(schema.items); if (typeOfItems == 'object') { if (schema.minItems && instance.length < schema.minItems) { this.addError(path, 'arrayMinItems', [schema.minItems]); } if (typeof schema.maxItems != 'undefined' && instance.length > schema.maxItems) { this.addError(path, 'arrayMaxItems', [schema.maxItems]); } // If the items property is a single schema, each item in the array must // have that schema. for (var i = 0; i < instance.length; ++i) { this.validate(instance[i], schema.items, path + '.' + i); } } else if (typeOfItems == 'array') { // If the items property is an array of schemas, each item in the array must // validate against the corresponding schema. for (var i = 0; i < schema.items.length; ++i) { var itemPath = path ? path + '.' + i : $String.self(i); if ($Object.hasOwnProperty(instance, i) && !isOptionalValue(instance[i])) { this.validate(instance[i], schema.items[i], itemPath); } else if (!schema.items[i].optional) { this.addError(itemPath, 'itemRequired'); } } if (schema.additionalProperties) { for (var i = schema.items.length; i < instance.length; ++i) { var itemPath = path ? path + '.' + i : $String.self(i); this.validate(instance[i], schema.additionalProperties, itemPath); } } else if (instance.length > schema.items.length) { this.addError(path, 'arrayMaxItems', [schema.items.length]); } } }; /** * Validates a string and populates the errors property. */ JSONSchemaValidator.prototype.validateString = function(instance, schema, path) { if (schema.minLength && instance.length < schema.minLength) this.addError(path, 'stringMinLength', [schema.minLength]); if (schema.maxLength && instance.length > schema.maxLength) this.addError(path, 'stringMaxLength', [schema.maxLength]); if (schema.pattern && !schema.pattern.test(instance)) this.addError(path, 'stringPattern', [schema.pattern]); }; /** * Validates a number and populates the errors property. The instance is * assumed to be a number. */ JSONSchemaValidator.prototype.validateNumber = function(instance, schema, path) { // Forbid NaN, +Infinity, and -Infinity. Our APIs don't use them, and // JSON serialization encodes them as 'null'. Re-evaluate supporting // them if we add an API that could reasonably take them as a parameter. if (isNaN(instance) || instance == Number.POSITIVE_INFINITY || instance == Number.NEGATIVE_INFINITY ) this.addError(path, 'numberFiniteNotNan', [instance]); if (schema.minimum !== undefined && instance < schema.minimum) this.addError(path, 'numberMinValue', [schema.minimum]); if (schema.maximum !== undefined && instance > schema.maximum) this.addError(path, 'numberMaxValue', [schema.maximum]); // Check for integer values outside of -2^31..2^31-1. if (schema.type === 'integer' && (instance | 0) !== instance) this.addError(path, 'numberIntValue', []); // We don't have a saved copy of Math, and it's not worth it just for a // 10^x function. var getPowerOfTen = function(pow) { // '10' is kind of an arbitrary number of maximum decimal places, but it // ensures we don't do anything crazy, and we should never need to restrict // decimals to a number higher than that. DCHECK(pow >= 1 && pow <= 10); DCHECK(pow % 1 === 0); var multiplier = 10; while (--pow) multiplier *= 10; return multiplier; }; if (schema.maxDecimal && (instance * getPowerOfTen(schema.maxDecimal)) % 1) { this.addError(path, 'numberMaxDecimal', [schema.maxDecimal]); } }; /** * Validates the primitive type of an instance and populates the errors * property. Returns true if the instance validates, false otherwise. */ JSONSchemaValidator.prototype.validateType = function(instance, schema, path) { var actualType = JSONSchemaValidator.getType(instance); if (schema.type == actualType || (schema.type == 'number' && actualType == 'integer')) { return true; } else if (schema.type == 'integer' && actualType == 'number') { this.addError(path, 'invalidTypeIntegerNumber'); return false; } else { this.addError(path, 'invalidType', [schema.type, actualType]); return false; } }; /** * Adds an error message. |key| is an index into the |messages| object. * |replacements| is an array of values to replace '*' characters in the * message. */ JSONSchemaValidator.prototype.addError = function(path, key, replacements) { $Array.push(this.errors, { __proto__: null, path: path, message: JSONSchemaValidator.formatError(key, replacements) }); }; /** * Resets errors to an empty list so you can call 'validate' again. */ JSONSchemaValidator.prototype.resetErrors = function() { this.errors = []; }; exports.$set('JSONSchemaValidator', JSONSchemaValidator); // Copyright 2014 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. define('keep_alive', [ 'content/public/renderer/frame_interfaces', 'extensions/common/mojo/keep_alive.mojom', 'mojo/public/js/core', ], function(frameInterfaces, mojom, core) { /** * An object that keeps the background page alive until closed. * @constructor * @alias module:keep_alive~KeepAlive */ function KeepAlive() { /** * The handle to the keep-alive object in the browser. * @type {!MojoHandle} * @private */ this.handle_ = frameInterfaces.getInterface(mojom.KeepAlive.name); } /** * Removes this keep-alive. */ KeepAlive.prototype.close = function() { core.close(this.handle_); }; var exports = {}; return { /** * Creates a keep-alive. * @return {!module:keep_alive~KeepAlive} A new keep-alive. */ createKeepAlive: function() { return new KeepAlive(); } }; }); // Copyright 2014 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. define("extensions/common/mojo/keep_alive.mojom", [ "mojo/public/js/bindings", "mojo/public/js/codec", "mojo/public/js/core", "mojo/public/js/validator", ], function(bindings, codec, core, validator) { function KeepAlivePtr(handleOrPtrInfo) { this.ptr = new bindings.InterfacePtrController(KeepAlive, handleOrPtrInfo); } function KeepAliveProxy(receiver) { this.receiver_ = receiver; } function KeepAliveStub(delegate) { this.delegate_ = delegate; } KeepAliveStub.prototype.accept = function(message) { var reader = new codec.MessageReader(message); switch (reader.messageName) { default: return false; } }; KeepAliveStub.prototype.acceptWithResponder = function(message, responder) { var reader = new codec.MessageReader(message); switch (reader.messageName) { default: return Promise.reject(Error("Unhandled message: " + reader.messageName)); } }; function validateKeepAliveRequest(messageValidator) { return validator.validationError.NONE; } function validateKeepAliveResponse(messageValidator) { return validator.validationError.NONE; } var KeepAlive = { name: 'extensions::KeepAlive', ptrClass: KeepAlivePtr, proxyClass: KeepAliveProxy, stubClass: KeepAliveStub, validateRequest: validateKeepAliveRequest, validateResponse: null, }; KeepAliveStub.prototype.validator = validateKeepAliveRequest; KeepAliveProxy.prototype.validator = null; var exports = {}; exports.KeepAlive = KeepAlive; exports.KeepAlivePtr = KeepAlivePtr; return exports; });// Copyright 2014 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. var GetAvailability = requireNative('v8_context').GetAvailability; var GetGlobal = requireNative('sendRequest').GetGlobal; // Utility for setting chrome.*.lastError. // // A utility here is useful for two reasons: // 1. For backwards compatibility we need to set chrome.extension.lastError, // but not all contexts actually have access to the extension namespace. // 2. When calling across contexts, the global object that gets lastError set // needs to be that of the caller. We force callers to explicitly specify // the chrome object to try to prevent bugs here. /** * Sets the last error for |name| on |targetChrome| to |message| with an * optional |stack|. */ function set(name, message, stack, targetChrome) { if (!targetChrome) { var errorMessage = name + ': ' + message; if (stack != null && stack != '') errorMessage += '\n' + stack; throw new Error('No chrome object to set error: ' + errorMessage); } clear(targetChrome); // in case somebody has set a sneaky getter/setter var errorObject = { message: message }; if (GetAvailability('extension.lastError').is_available) targetChrome.extension.lastError = errorObject; assertRuntimeIsAvailable(); // We check to see if developers access runtime.lastError in order to decide // whether or not to log it in the (error) console. privates(targetChrome.runtime).accessedLastError = false; $Object.defineProperty(targetChrome.runtime, 'lastError', { configurable: true, get: function() { privates(targetChrome.runtime).accessedLastError = true; return errorObject; }, set: function(error) { errorObject = errorObject; }}); }; /** * Check if anyone has checked chrome.runtime.lastError since it was set. * @param {Object} targetChrome the Chrome object to check. * @return boolean True if the lastError property was set. */ function hasAccessed(targetChrome) { assertRuntimeIsAvailable(); return privates(targetChrome.runtime).accessedLastError === true; } /** * Check whether there is an error set on |targetChrome| without setting * |accessedLastError|. * @param {Object} targetChrome the Chrome object to check. * @return boolean Whether lastError has been set. */ function hasError(targetChrome) { if (!targetChrome) throw new Error('No target chrome to check'); assertRuntimeIsAvailable(); return $Object.hasOwnProperty(targetChrome.runtime, 'lastError'); }; /** * Clears the last error on |targetChrome|. */ function clear(targetChrome) { if (!targetChrome) throw new Error('No target chrome to clear error'); if (GetAvailability('extension.lastError').is_available) delete targetChrome.extension.lastError; assertRuntimeIsAvailable(); delete targetChrome.runtime.lastError; delete privates(targetChrome.runtime).accessedLastError; }; function assertRuntimeIsAvailable() { // chrome.runtime should always be available, but maybe it's disappeared for // some reason? Add debugging for http://crbug.com/258526. var runtimeAvailability = GetAvailability('runtime.lastError'); if (!runtimeAvailability.is_available) { throw new Error('runtime.lastError is not available: ' + runtimeAvailability.message); } if (!chrome.runtime) throw new Error('runtime namespace is null or undefined'); } /** * Runs |callback(args)| with last error args as in set(). * * The target chrome object is the global object's of the callback, so this * method won't work if the real callback has been wrapped (etc). */ function run(name, message, stack, callback, args) { var global = GetGlobal(callback); var targetChrome = global && global.chrome; set(name, message, stack, targetChrome); try { $Function.apply(callback, undefined, args); } finally { reportIfUnchecked(name, targetChrome, stack); clear(targetChrome); } } /** * Checks whether chrome.runtime.lastError has been accessed if set. * If it was set but not accessed, the error is reported to the console. * * @param {string=} name - name of API. * @param {Object} targetChrome - the Chrome object to check. * @param {string=} stack - Stack trace of the call up to the error. */ function reportIfUnchecked(name, targetChrome, stack) { if (hasAccessed(targetChrome) || !hasError(targetChrome)) return; var message = targetChrome.runtime.lastError.message; console.error("Unchecked runtime.lastError while running " + (name || "unknown") + ": " + message + (stack ? "\n" + stack : "")); } exports.$set('clear', clear); exports.$set('hasAccessed', hasAccessed); exports.$set('hasError', hasError); exports.$set('set', set); exports.$set('run', run); exports.$set('reportIfUnchecked', reportIfUnchecked); // Copyright 2014 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. // chrome.runtime.messaging API implementation. // TODO(robwu): Fix this indentation. // TODO(kalman): factor requiring chrome out of here. var chrome = requireNative('chrome').GetChrome(); var Event = require('event_bindings').Event; var lastError = require('lastError'); var logActivity = requireNative('activityLogger'); var logging = requireNative('logging'); var messagingNatives = requireNative('messaging_natives'); var processNatives = requireNative('process'); var utils = require('utils'); var messagingUtils = require('messaging_utils'); // The reserved channel name for the sendRequest/send(Native)Message APIs. // Note: sendRequest is deprecated. var kRequestChannel = "chrome.extension.sendRequest"; var kMessageChannel = "chrome.runtime.sendMessage"; var kNativeMessageChannel = "chrome.runtime.sendNativeMessage"; var kPortClosedError = 'Attempting to use a disconnected port object'; // Map of port IDs to port object. var ports = {__proto__: null}; // Port object. Represents a connection to another script context through // which messages can be passed. function PortImpl(portId, opt_name) { this.portId_ = portId; this.name = opt_name; // Note: Keep these schemas in sync with the documentation in runtime.json var portSchema = { __proto__: null, name: 'port', $ref: 'runtime.Port', }; var messageSchema = { __proto__: null, name: 'message', type: 'any', optional: true, }; var options = { __proto__: null, unmanaged: true, }; this.onDisconnect = new Event(null, [portSchema], options); this.onMessage = new Event(null, [messageSchema, portSchema], options); } $Object.setPrototypeOf(PortImpl.prototype, null); // Sends a message asynchronously to the context on the other end of this // port. PortImpl.prototype.postMessage = function(msg) { if (!$Object.hasOwnProperty(ports, this.portId_)) throw new Error(kPortClosedError); // JSON.stringify doesn't support a root object which is undefined. if (msg === undefined) msg = null; msg = $JSON.stringify(msg); if (msg === undefined) { // JSON.stringify can fail with unserializable objects. Log an error and // drop the message. // // TODO(kalman/mpcomplete): it would be better to do the same validation // here that we do for runtime.sendMessage (and variants), i.e. throw an // schema validation Error, but just maintain the old behaviour until // there's a good reason not to (http://crbug.com/263077). console.error('Illegal argument to Port.postMessage'); return; } messagingNatives.PostMessage(this.portId_, msg); }; // Disconnects the port from the other end. PortImpl.prototype.disconnect = function() { if (!$Object.hasOwnProperty(ports, this.portId_)) return; // disconnect() on an already-closed port is a no-op. messagingNatives.CloseChannel(this.portId_, true); this.destroy_(); }; // Close this specific port without forcing the channel to close. The channel // will close if this was the only port at this end of the channel. PortImpl.prototype.disconnectSoftly = function() { if (!$Object.hasOwnProperty(ports, this.portId_)) return; messagingNatives.CloseChannel(this.portId_, false); this.destroy_(); }; PortImpl.prototype.destroy_ = function() { privates(this.onDisconnect).impl.destroy_(); privates(this.onMessage).impl.destroy_(); delete ports[this.portId_]; }; // Hidden port creation function. We don't want to expose an API that lets // people add arbitrary port IDs to the port list. function createPort(portId, opt_name) { if (ports[portId]) throw new Error("Port '" + portId + "' already exists."); var port = new Port(portId, opt_name); ports[portId] = port; return port; }; // Helper function for dispatchOnRequest. function handleSendRequestError(isSendMessage, responseCallbackPreserved, sourceExtensionId, targetExtensionId, sourceUrl) { var errorMsg; var eventName = isSendMessage ? 'runtime.onMessage' : 'extension.onRequest'; if (isSendMessage && !responseCallbackPreserved) { errorMsg = 'The chrome.' + eventName + ' listener must return true if you ' + 'want to send a response after the listener returns'; } else { errorMsg = 'Cannot send a response more than once per chrome.' + eventName + ' listener per document'; } errorMsg += ' (message was sent by extension' + sourceExtensionId; if (sourceExtensionId && sourceExtensionId !== targetExtensionId) errorMsg += ' for extension ' + targetExtensionId; if (sourceUrl) errorMsg += ' for URL ' + sourceUrl; errorMsg += ').'; lastError.set(eventName, errorMsg, null, chrome); } // Helper function for dispatchOnConnect function dispatchOnRequest(portId, channelName, sender, sourceExtensionId, targetExtensionId, sourceUrl, isExternal) { var isSendMessage = channelName == kMessageChannel; var requestEvent = null; if (isSendMessage) { if (chrome.runtime) { requestEvent = isExternal ? chrome.runtime.onMessageExternal : chrome.runtime.onMessage; } } else { if (chrome.extension) { requestEvent = isExternal ? chrome.extension.onRequestExternal : chrome.extension.onRequest; } } if (!requestEvent) return false; if (!requestEvent.hasListeners()) return false; var port = createPort(portId, channelName); function messageListener(request) { var responseCallbackPreserved = false; var responseCallback = function(response) { if (port) { port.postMessage(response); // TODO(robwu): This can be changed to disconnect() because there is // no point in allowing other receivers at this end of the port to // keep the channel alive because the opener port can only receive one // message. privates(port).impl.disconnectSoftly(); port = null; } else { // We nulled out port when sending the response, and now the page // is trying to send another response for the same request. handleSendRequestError(isSendMessage, responseCallbackPreserved, sourceExtensionId, targetExtensionId); } }; // In case the extension never invokes the responseCallback, and also // doesn't keep a reference to it, we need to clean up the port. Do // so by attaching to the garbage collection of the responseCallback // using some native hackery. // // If the context is destroyed before this has a chance to execute, // BindToGC knows to release |portId| (important for updating C++ state // both in this renderer and on the other end). We don't need to clear // any JavaScript state, as calling destroy_() would usually do - but // the context has been destroyed, so there isn't any JS state to clear. messagingNatives.BindToGC(responseCallback, function() { if (port) { privates(port).impl.disconnectSoftly(); port = null; } }, portId); var rv = requestEvent.dispatch(request, sender, responseCallback); if (isSendMessage) { responseCallbackPreserved = rv && rv.results && $Array.indexOf(rv.results, true) > -1; if (!responseCallbackPreserved && port) { // If they didn't access the response callback, they're not // going to send a response, so clean up the port immediately. privates(port).impl.disconnectSoftly(); port = null; } } } port.onMessage.addListener(messageListener); var eventName = isSendMessage ? "runtime.onMessage" : "extension.onRequest"; if (isExternal) eventName += "External"; logActivity.LogEvent(targetExtensionId, eventName, [sourceExtensionId, sourceUrl]); return true; } // Called by native code when a channel has been opened to this context. function dispatchOnConnect(portId, channelName, sourceTab, sourceFrameId, guestProcessId, guestRenderFrameRoutingId, sourceExtensionId, targetExtensionId, sourceUrl, tlsChannelId) { // Only create a new Port if someone is actually listening for a connection. // In addition to being an optimization, this also fixes a bug where if 2 // channels were opened to and from the same process, closing one would // close both. var extensionId = processNatives.GetExtensionId(); // messaging_bindings.cc should ensure that this method only gets called for // the right extension. logging.CHECK(targetExtensionId == extensionId); // Determine whether this is coming from another extension, so we can use // the right event. var isExternal = sourceExtensionId != extensionId; var sender = {}; if (sourceExtensionId != '') sender.id = sourceExtensionId; if (sourceUrl) sender.url = sourceUrl; if (sourceTab) sender.tab = sourceTab; if (sourceFrameId >= 0) sender.frameId = sourceFrameId; if (typeof guestProcessId !== 'undefined' && typeof guestRenderFrameRoutingId !== 'undefined') { // Note that |guestProcessId| and |guestRenderFrameRoutingId| are not // standard fields on MessageSender and should not be exposed to drive-by // extensions; it is only exposed to component extensions. logging.CHECK(processNatives.IsComponentExtension(), "GuestProcessId can only be exposed to component extensions."); sender.guestProcessId = guestProcessId; sender.guestRenderFrameRoutingId = guestRenderFrameRoutingId; } if (typeof tlsChannelId != 'undefined') sender.tlsChannelId = tlsChannelId; // Special case for sendRequest/onRequest and sendMessage/onMessage. if (channelName == kRequestChannel || channelName == kMessageChannel) { return dispatchOnRequest(portId, channelName, sender, sourceExtensionId, targetExtensionId, sourceUrl, isExternal); } var connectEvent = null; if (chrome.runtime) { connectEvent = isExternal ? chrome.runtime.onConnectExternal : chrome.runtime.onConnect; } if (!connectEvent) return false; if (!connectEvent.hasListeners()) return false; var port = createPort(portId, channelName); port.sender = sender; if (processNatives.manifestVersion < 2) port.tab = port.sender.tab; var eventName = (isExternal ? "runtime.onConnectExternal" : "runtime.onConnect"); connectEvent.dispatch(port); logActivity.LogEvent(targetExtensionId, eventName, [sourceExtensionId]); return true; }; // Called by native code when a channel has been closed. function dispatchOnDisconnect(portId, errorMessage) { var port = ports[portId]; if (port) { delete ports[portId]; if (errorMessage) lastError.set('Port', errorMessage, null, chrome); try { port.onDisconnect.dispatch(port); } finally { privates(port).impl.destroy_(); lastError.clear(chrome); } } }; // Called by native code when a message has been sent to the given port. function dispatchOnMessage(msg, portId) { var port = ports[portId]; if (port) { if (msg) msg = $JSON.parse(msg); port.onMessage.dispatch(msg, port); } }; // Shared implementation used by tabs.sendMessage and runtime.sendMessage. function sendMessageImpl(port, request, responseCallback) { if (port.name != kNativeMessageChannel) port.postMessage(request); if (port.name == kMessageChannel && !responseCallback) { // TODO(mpcomplete): Do this for the old sendRequest API too, after // verifying it doesn't break anything. // Go ahead and disconnect immediately if the sender is not expecting // a response. port.disconnect(); return; } function sendResponseAndClearCallback(response) { // Save a reference so that we don't re-entrantly call responseCallback. var sendResponse = responseCallback; responseCallback = null; if (arguments.length === 0) { // According to the documentation of chrome.runtime.sendMessage, the // callback is invoked without any arguments when an error occurs. sendResponse(); } else { sendResponse(response); } } // Note: make sure to manually remove the onMessage/onDisconnect listeners // that we added before destroying the Port, a workaround to a bug in Port // where any onMessage/onDisconnect listeners added but not removed will // be leaked when the Port is destroyed. // http://crbug.com/320723 tracks a sustainable fix. function disconnectListener() { if (!responseCallback) return; if (lastError.hasError(chrome)) { sendResponseAndClearCallback(); } else { lastError.set( port.name, 'The message port closed before a reponse was received.', null, chrome); try { sendResponseAndClearCallback(); } finally { lastError.clear(chrome); } } } function messageListener(response) { try { if (responseCallback) sendResponseAndClearCallback(response); } finally { port.disconnect(); } } port.onDisconnect.addListener(disconnectListener); port.onMessage.addListener(messageListener); }; function sendMessageUpdateArguments(functionName, hasOptionsArgument) { // skip functionName and hasOptionsArgument var args = $Array.slice(arguments, 2); var alignedArgs = messagingUtils.alignSendMessageArguments(args, hasOptionsArgument); if (!alignedArgs) throw new Error('Invalid arguments to ' + functionName + '.'); return alignedArgs; } function Port() { privates(Port).constructPrivate(this, arguments); } utils.expose(Port, PortImpl, { functions: [ 'disconnect', 'postMessage', ], properties: [ 'name', 'onDisconnect', 'onMessage', ], }); exports.$set('kRequestChannel', kRequestChannel); exports.$set('kMessageChannel', kMessageChannel); exports.$set('kNativeMessageChannel', kNativeMessageChannel); exports.$set('Port', Port); exports.$set('createPort', createPort); exports.$set('sendMessageImpl', sendMessageImpl); exports.$set('sendMessageUpdateArguments', sendMessageUpdateArguments); // For C++ code to call. exports.$set('dispatchOnConnect', dispatchOnConnect); exports.$set('dispatchOnDisconnect', dispatchOnDisconnect); exports.$set('dispatchOnMessage', dispatchOnMessage); // Copyright 2014 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. // Routines used to normalize arguments to messaging functions. function alignSendMessageArguments(args, hasOptionsArgument) { // Align missing (optional) function arguments with the arguments that // schema validation is expecting, e.g. // extension.sendRequest(req) -> extension.sendRequest(null, req) // extension.sendRequest(req, cb) -> extension.sendRequest(null, req, cb) if (!args || !args.length) return null; var lastArg = args.length - 1; // responseCallback (last argument) is optional. var responseCallback = null; if (typeof args[lastArg] == 'function') responseCallback = args[lastArg--]; var options = null; if (hasOptionsArgument && lastArg >= 1) { // options (third argument) is optional. It can also be ambiguous which // argument it should match. If there are more than two arguments remaining, // options is definitely present: if (lastArg > 1) { options = args[lastArg--]; } else { // Exactly two arguments remaining. If the first argument is a string, // it should bind to targetId, and the second argument should bind to // request, which is required. In other words, when two arguments remain, // only bind options when the first argument cannot bind to targetId. if (!(args[0] === null || typeof args[0] == 'string')) options = args[lastArg--]; } } // request (second argument) is required. var request = args[lastArg--]; // targetId (first argument, extensionId in the manifest) is optional. var targetId = null; if (lastArg >= 0) targetId = args[lastArg--]; if (lastArg != -1) return null; if (hasOptionsArgument) return [targetId, request, options, responseCallback]; return [targetId, request, responseCallback]; } exports.$set('alignSendMessageArguments', alignSendMessageArguments); // Copyright 2015 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. /** * Custom bindings for the mime handler API. */ var binding = require('binding').Binding.create('mimeHandlerPrivate'); var NO_STREAM_ERROR = 'Streams are only available from a mime handler view guest.'; var STREAM_ABORTED_ERROR = 'Stream has been aborted.'; var servicePromise = Promise.all([ requireAsync('content/public/renderer/frame_interfaces'), requireAsync('extensions/common/api/mime_handler.mojom'), ]).then(function(modules) { var frameInterfaces = modules[0]; var mojom = modules[1]; return new mojom.MimeHandlerServicePtr( frameInterfaces.getInterface(mojom.MimeHandlerService.name)); }); // Stores a promise to the GetStreamInfo() result to avoid making additional // calls in response to getStreamInfo() calls. var streamInfoPromise; function throwNoStreamError() { throw new Error(NO_STREAM_ERROR); } function createStreamInfoPromise() { return servicePromise.then(function(service) { return service.getStreamInfo(); }).then(function(result) { if (!result.stream_info) throw new Error(STREAM_ABORTED_ERROR); return result.stream_info; }, throwNoStreamError); } function constructStreamInfoDict(streamInfo) { var headers = {}; for (var header of streamInfo.response_headers) { headers[header[0]] = header[1]; } return { mimeType: streamInfo.mime_type, originalUrl: streamInfo.original_url, streamUrl: streamInfo.stream_url, tabId: streamInfo.tab_id, embedded: !!streamInfo.embedded, responseHeaders: headers, }; } binding.registerCustomHook(function(bindingsAPI) { var apiFunctions = bindingsAPI.apiFunctions; apiFunctions.setHandleRequestWithPromise('getStreamInfo', function() { if (!streamInfoPromise) streamInfoPromise = createStreamInfoPromise(); return streamInfoPromise.then(constructStreamInfoDict); }); apiFunctions.setHandleRequestWithPromise('abortStream', function() { return servicePromise.then(function(service) { return service.abortStream().then(function() {}); }).catch(throwNoStreamError); }); }); exports.$set('binding', binding.generate()); // Copyright 2014 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. define("extensions/common/api/mime_handler.mojom", [ "mojo/public/js/bindings", "mojo/public/js/codec", "mojo/public/js/core", "mojo/public/js/validator", ], function(bindings, codec, core, validator) { function StreamInfo(values) { this.initDefaults_(); this.initFields_(values); } StreamInfo.prototype.initDefaults_ = function() { this.mime_type = null; this.original_url = null; this.stream_url = null; this.tab_id = 0; this.embedded = false; this.response_headers = null; }; StreamInfo.prototype.initFields_ = function(fields) { for(var field in fields) { if (this.hasOwnProperty(field)) this[field] = fields[field]; } }; StreamInfo.validate = function(messageValidator, offset) { var err; err = messageValidator.validateStructHeader(offset, codec.kStructHeaderSize); if (err !== validator.validationError.NONE) return err; var kVersionSizes = [ {version: 0, numBytes: 48} ]; err = messageValidator.validateStructVersion(offset, kVersionSizes); if (err !== validator.validationError.NONE) return err; // validate StreamInfo.mime_type err = messageValidator.validateStringPointer(offset + codec.kStructHeaderSize + 0, false) if (err !== validator.validationError.NONE) return err; // validate StreamInfo.original_url err = messageValidator.validateStringPointer(offset + codec.kStructHeaderSize + 8, false) if (err !== validator.validationError.NONE) return err; // validate StreamInfo.stream_url err = messageValidator.validateStringPointer(offset + codec.kStructHeaderSize + 16, false) if (err !== validator.validationError.NONE) return err; // validate StreamInfo.response_headers err = messageValidator.validateMapPointer(offset + codec.kStructHeaderSize + 32, false, codec.String, codec.String, false); if (err !== validator.validationError.NONE) return err; return validator.validationError.NONE; }; StreamInfo.encodedSize = codec.kStructHeaderSize + 40; StreamInfo.decode = function(decoder) { var packed; var val = new StreamInfo(); var numberOfBytes = decoder.readUint32(); var version = decoder.readUint32(); val.mime_type = decoder.decodeStruct(codec.String); val.original_url = decoder.decodeStruct(codec.String); val.stream_url = decoder.decodeStruct(codec.String); val.tab_id = decoder.decodeStruct(codec.Int32); packed = decoder.readUint8(); val.embedded = (packed >> 0) & 1 ? true : false; decoder.skip(1); decoder.skip(1); decoder.skip(1); val.response_headers = decoder.decodeMapPointer(codec.String, codec.String); return val; }; StreamInfo.encode = function(encoder, val) { var packed; encoder.writeUint32(StreamInfo.encodedSize); encoder.writeUint32(0); encoder.encodeStruct(codec.String, val.mime_type); encoder.encodeStruct(codec.String, val.original_url); encoder.encodeStruct(codec.String, val.stream_url); encoder.encodeStruct(codec.Int32, val.tab_id); packed = 0; packed |= (val.embedded & 1) << 0 encoder.writeUint8(packed); encoder.skip(1); encoder.skip(1); encoder.skip(1); encoder.encodeMapPointer(codec.String, codec.String, val.response_headers); }; function MimeHandlerService_GetStreamInfo_Params(values) { this.initDefaults_(); this.initFields_(values); } MimeHandlerService_GetStreamInfo_Params.prototype.initDefaults_ = function() { }; MimeHandlerService_GetStreamInfo_Params.prototype.initFields_ = function(fields) { for(var field in fields) { if (this.hasOwnProperty(field)) this[field] = fields[field]; } }; MimeHandlerService_GetStreamInfo_Params.validate = function(messageValidator, offset) { var err; err = messageValidator.validateStructHeader(offset, codec.kStructHeaderSize); if (err !== validator.validationError.NONE) return err; var kVersionSizes = [ {version: 0, numBytes: 8} ]; err = messageValidator.validateStructVersion(offset, kVersionSizes); if (err !== validator.validationError.NONE) return err; return validator.validationError.NONE; }; MimeHandlerService_GetStreamInfo_Params.encodedSize = codec.kStructHeaderSize + 0; MimeHandlerService_GetStreamInfo_Params.decode = function(decoder) { var packed; var val = new MimeHandlerService_GetStreamInfo_Params(); var numberOfBytes = decoder.readUint32(); var version = decoder.readUint32(); return val; }; MimeHandlerService_GetStreamInfo_Params.encode = function(encoder, val) { var packed; encoder.writeUint32(MimeHandlerService_GetStreamInfo_Params.encodedSize); encoder.writeUint32(0); }; function MimeHandlerService_GetStreamInfo_ResponseParams(values) { this.initDefaults_(); this.initFields_(values); } MimeHandlerService_GetStreamInfo_ResponseParams.prototype.initDefaults_ = function() { this.stream_info = null; }; MimeHandlerService_GetStreamInfo_ResponseParams.prototype.initFields_ = function(fields) { for(var field in fields) { if (this.hasOwnProperty(field)) this[field] = fields[field]; } }; MimeHandlerService_GetStreamInfo_ResponseParams.validate = function(messageValidator, offset) { var err; err = messageValidator.validateStructHeader(offset, codec.kStructHeaderSize); if (err !== validator.validationError.NONE) return err; var kVersionSizes = [ {version: 0, numBytes: 16} ]; err = messageValidator.validateStructVersion(offset, kVersionSizes); if (err !== validator.validationError.NONE) return err; // validate MimeHandlerService_GetStreamInfo_ResponseParams.stream_info err = messageValidator.validateStructPointer(offset + codec.kStructHeaderSize + 0, StreamInfo, true); if (err !== validator.validationError.NONE) return err; return validator.validationError.NONE; }; MimeHandlerService_GetStreamInfo_ResponseParams.encodedSize = codec.kStructHeaderSize + 8; MimeHandlerService_GetStreamInfo_ResponseParams.decode = function(decoder) { var packed; var val = new MimeHandlerService_GetStreamInfo_ResponseParams(); var numberOfBytes = decoder.readUint32(); var version = decoder.readUint32(); val.stream_info = decoder.decodeStructPointer(StreamInfo); return val; }; MimeHandlerService_GetStreamInfo_ResponseParams.encode = function(encoder, val) { var packed; encoder.writeUint32(MimeHandlerService_GetStreamInfo_ResponseParams.encodedSize); encoder.writeUint32(0); encoder.encodeStructPointer(StreamInfo, val.stream_info); }; function MimeHandlerService_AbortStream_Params(values) { this.initDefaults_(); this.initFields_(values); } MimeHandlerService_AbortStream_Params.prototype.initDefaults_ = function() { }; MimeHandlerService_AbortStream_Params.prototype.initFields_ = function(fields) { for(var field in fields) { if (this.hasOwnProperty(field)) this[field] = fields[field]; } }; MimeHandlerService_AbortStream_Params.validate = function(messageValidator, offset) { var err; err = messageValidator.validateStructHeader(offset, codec.kStructHeaderSize); if (err !== validator.validationError.NONE) return err; var kVersionSizes = [ {version: 0, numBytes: 8} ]; err = messageValidator.validateStructVersion(offset, kVersionSizes); if (err !== validator.validationError.NONE) return err; return validator.validationError.NONE; }; MimeHandlerService_AbortStream_Params.encodedSize = codec.kStructHeaderSize + 0; MimeHandlerService_AbortStream_Params.decode = function(decoder) { var packed; var val = new MimeHandlerService_AbortStream_Params(); var numberOfBytes = decoder.readUint32(); var version = decoder.readUint32(); return val; }; MimeHandlerService_AbortStream_Params.encode = function(encoder, val) { var packed; encoder.writeUint32(MimeHandlerService_AbortStream_Params.encodedSize); encoder.writeUint32(0); }; function MimeHandlerService_AbortStream_ResponseParams(values) { this.initDefaults_(); this.initFields_(values); } MimeHandlerService_AbortStream_ResponseParams.prototype.initDefaults_ = function() { }; MimeHandlerService_AbortStream_ResponseParams.prototype.initFields_ = function(fields) { for(var field in fields) { if (this.hasOwnProperty(field)) this[field] = fields[field]; } }; MimeHandlerService_AbortStream_ResponseParams.validate = function(messageValidator, offset) { var err; err = messageValidator.validateStructHeader(offset, codec.kStructHeaderSize); if (err !== validator.validationError.NONE) return err; var kVersionSizes = [ {version: 0, numBytes: 8} ]; err = messageValidator.validateStructVersion(offset, kVersionSizes); if (err !== validator.validationError.NONE) return err; return validator.validationError.NONE; }; MimeHandlerService_AbortStream_ResponseParams.encodedSize = codec.kStructHeaderSize + 0; MimeHandlerService_AbortStream_ResponseParams.decode = function(decoder) { var packed; var val = new MimeHandlerService_AbortStream_ResponseParams(); var numberOfBytes = decoder.readUint32(); var version = decoder.readUint32(); return val; }; MimeHandlerService_AbortStream_ResponseParams.encode = function(encoder, val) { var packed; encoder.writeUint32(MimeHandlerService_AbortStream_ResponseParams.encodedSize); encoder.writeUint32(0); }; var kMimeHandlerService_GetStreamInfo_Name = 0; var kMimeHandlerService_AbortStream_Name = 1; function MimeHandlerServicePtr(handleOrPtrInfo) { this.ptr = new bindings.InterfacePtrController(MimeHandlerService, handleOrPtrInfo); } function MimeHandlerServiceProxy(receiver) { this.receiver_ = receiver; } MimeHandlerServicePtr.prototype.getStreamInfo = function() { return MimeHandlerServiceProxy.prototype.getStreamInfo .apply(this.ptr.getProxy(), arguments); }; MimeHandlerServiceProxy.prototype.getStreamInfo = function() { var params = new MimeHandlerService_GetStreamInfo_Params(); return new Promise(function(resolve, reject) { var builder = new codec.MessageWithRequestIDBuilder( kMimeHandlerService_GetStreamInfo_Name, codec.align(MimeHandlerService_GetStreamInfo_Params.encodedSize), codec.kMessageExpectsResponse, 0); builder.encodeStruct(MimeHandlerService_GetStreamInfo_Params, params); var message = builder.finish(); this.receiver_.acceptAndExpectResponse(message).then(function(message) { var reader = new codec.MessageReader(message); var responseParams = reader.decodeStruct(MimeHandlerService_GetStreamInfo_ResponseParams); resolve(responseParams); }).catch(function(result) { reject(Error("Connection error: " + result)); }); }.bind(this)); }; MimeHandlerServicePtr.prototype.abortStream = function() { return MimeHandlerServiceProxy.prototype.abortStream .apply(this.ptr.getProxy(), arguments); }; MimeHandlerServiceProxy.prototype.abortStream = function() { var params = new MimeHandlerService_AbortStream_Params(); return new Promise(function(resolve, reject) { var builder = new codec.MessageWithRequestIDBuilder( kMimeHandlerService_AbortStream_Name, codec.align(MimeHandlerService_AbortStream_Params.encodedSize), codec.kMessageExpectsResponse, 0); builder.encodeStruct(MimeHandlerService_AbortStream_Params, params); var message = builder.finish(); this.receiver_.acceptAndExpectResponse(message).then(function(message) { var reader = new codec.MessageReader(message); var responseParams = reader.decodeStruct(MimeHandlerService_AbortStream_ResponseParams); resolve(responseParams); }).catch(function(result) { reject(Error("Connection error: " + result)); }); }.bind(this)); }; function MimeHandlerServiceStub(delegate) { this.delegate_ = delegate; } MimeHandlerServiceStub.prototype.getStreamInfo = function() { return this.delegate_ && this.delegate_.getStreamInfo && this.delegate_.getStreamInfo(); } MimeHandlerServiceStub.prototype.abortStream = function() { return this.delegate_ && this.delegate_.abortStream && this.delegate_.abortStream(); } MimeHandlerServiceStub.prototype.accept = function(message) { var reader = new codec.MessageReader(message); switch (reader.messageName) { default: return false; } }; MimeHandlerServiceStub.prototype.acceptWithResponder = function(message, responder) { var reader = new codec.MessageReader(message); switch (reader.messageName) { case kMimeHandlerService_GetStreamInfo_Name: var params = reader.decodeStruct(MimeHandlerService_GetStreamInfo_Params); return this.getStreamInfo().then(function(response) { var responseParams = new MimeHandlerService_GetStreamInfo_ResponseParams(); responseParams.stream_info = response.stream_info; var builder = new codec.MessageWithRequestIDBuilder( kMimeHandlerService_GetStreamInfo_Name, codec.align(MimeHandlerService_GetStreamInfo_ResponseParams.encodedSize), codec.kMessageIsResponse, reader.requestID); builder.encodeStruct(MimeHandlerService_GetStreamInfo_ResponseParams, responseParams); var message = builder.finish(); responder.accept(message); }); case kMimeHandlerService_AbortStream_Name: var params = reader.decodeStruct(MimeHandlerService_AbortStream_Params); return this.abortStream().then(function(response) { var responseParams = new MimeHandlerService_AbortStream_ResponseParams(); var builder = new codec.MessageWithRequestIDBuilder( kMimeHandlerService_AbortStream_Name, codec.align(MimeHandlerService_AbortStream_ResponseParams.encodedSize), codec.kMessageIsResponse, reader.requestID); builder.encodeStruct(MimeHandlerService_AbortStream_ResponseParams, responseParams); var message = builder.finish(); responder.accept(message); }); default: return Promise.reject(Error("Unhandled message: " + reader.messageName)); } }; function validateMimeHandlerServiceRequest(messageValidator) { var message = messageValidator.message; var paramsClass = null; switch (message.getName()) { case kMimeHandlerService_GetStreamInfo_Name: if (message.expectsResponse()) paramsClass = MimeHandlerService_GetStreamInfo_Params; break; case kMimeHandlerService_AbortStream_Name: if (message.expectsResponse()) paramsClass = MimeHandlerService_AbortStream_Params; break; } if (paramsClass === null) return validator.validationError.NONE; return paramsClass.validate(messageValidator, messageValidator.message.getHeaderNumBytes()); } function validateMimeHandlerServiceResponse(messageValidator) { var message = messageValidator.message; var paramsClass = null; switch (message.getName()) { case kMimeHandlerService_GetStreamInfo_Name: if (message.isResponse()) paramsClass = MimeHandlerService_GetStreamInfo_ResponseParams; break; case kMimeHandlerService_AbortStream_Name: if (message.isResponse()) paramsClass = MimeHandlerService_AbortStream_ResponseParams; break; } if (paramsClass === null) return validator.validationError.NONE; return paramsClass.validate(messageValidator, messageValidator.message.getHeaderNumBytes()); } var MimeHandlerService = { name: 'extensions::mime_handler::MimeHandlerService', ptrClass: MimeHandlerServicePtr, proxyClass: MimeHandlerServiceProxy, stubClass: MimeHandlerServiceStub, validateRequest: validateMimeHandlerServiceRequest, validateResponse: validateMimeHandlerServiceResponse, }; MimeHandlerServiceStub.prototype.validator = validateMimeHandlerServiceRequest; MimeHandlerServiceProxy.prototype.validator = validateMimeHandlerServiceResponse; var exports = {}; exports.StreamInfo = StreamInfo; exports.MimeHandlerService = MimeHandlerService; exports.MimeHandlerServicePtr = MimeHandlerServicePtr; return exports; });// Copyright 2014 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. // Routines used to validate and normalize arguments. // TODO(benwells): unit test this file. var JSONSchemaValidator = require('json_schema').JSONSchemaValidator; var schemaValidator = new JSONSchemaValidator(); // Validate arguments. function validate(args, parameterSchemas) { if (args.length > parameterSchemas.length) throw new Error('Too many arguments.'); for (var i = 0; i < parameterSchemas.length; ++i) { if ($Object.hasOwnProperty(args, i) && args[i] !== null && args[i] !== undefined) { schemaValidator.resetErrors(); schemaValidator.validate(args[i], parameterSchemas[i]); if (schemaValidator.errors.length == 0) continue; var message = 'Invalid value for argument ' + (i + 1) + '. '; $Array.forEach(schemaValidator.errors, function(err) { if (err.path) { message += "Property '" + err.path + "': "; } message += err.message; message = message.substring(0, message.length - 1); message += ', '; }); message = message.substring(0, message.length - 2); message += '.'; throw new Error(message); } else if (!parameterSchemas[i].optional) { throw new Error('Parameter ' + (i + 1) + ' (' + parameterSchemas[i].name + ') is required.'); } } } // Generate all possible signatures for a given API function. function getSignatures(parameterSchemas) { if (parameterSchemas.length === 0) return [[]]; var signatures = []; var remaining = getSignatures($Array.slice(parameterSchemas, 1)); for (var i = 0; i < remaining.length; ++i) $Array.push(signatures, $Array.concat([parameterSchemas[0]], remaining[i])) if (parameterSchemas[0].optional) return $Array.concat(signatures, remaining); return signatures; }; // Return true if arguments match a given signature's schema. function argumentsMatchSignature(args, candidateSignature) { if (args.length != candidateSignature.length) return false; for (var i = 0; i < candidateSignature.length; ++i) { var argType = JSONSchemaValidator.getType(args[i]); if (!schemaValidator.isValidSchemaType(argType, candidateSignature[i])) return false; } return true; }; // Finds the function signature for the given arguments. function resolveSignature(args, definedSignature) { var candidateSignatures = getSignatures(definedSignature); for (var i = 0; i < candidateSignatures.length; ++i) { if (argumentsMatchSignature(args, candidateSignatures[i])) return candidateSignatures[i]; } return null; }; // Returns a string representing the defined signature of the API function. // Example return value for chrome.windows.getCurrent: // "windows.getCurrent(optional object populate, function callback)" function getParameterSignatureString(name, definedSignature) { var getSchemaTypeString = function(schema) { var schemaTypes = schemaValidator.getAllTypesForSchema(schema); var typeName = $Array.join(schemaTypes, ' or ') + ' ' + schema.name; if (schema.optional) return 'optional ' + typeName; return typeName; }; var typeNames = $Array.map(definedSignature, getSchemaTypeString); return name + '(' + $Array.join(typeNames, ', ') + ')'; }; // Returns a string representing a call to an API function. // Example return value for call: chrome.windows.get(1, callback) is: // "windows.get(int, function)" function getArgumentSignatureString(name, args) { var typeNames = $Array.map(args, JSONSchemaValidator.getType); return name + '(' + $Array.join(typeNames, ', ') + ')'; }; // Finds the correct signature for the given arguments, then validates the // arguments against that signature. Returns a 'normalized' arguments list // where nulls are inserted where optional parameters were omitted. // |args| is expected to be an array. function normalizeArgumentsAndValidate(args, funDef) { if (funDef.allowAmbiguousOptionalArguments) { validate(args, funDef.definition.parameters); return args; } var definedSignature = funDef.definition.parameters; var resolvedSignature = resolveSignature(args, definedSignature); if (!resolvedSignature) throw new Error('Invocation of form ' + getArgumentSignatureString(funDef.name, args) + " doesn't match definition " + getParameterSignatureString(funDef.name, definedSignature)); validate(args, resolvedSignature); var normalizedArgs = []; var ai = 0; for (var si = 0; si < definedSignature.length; ++si) { // Handle integer -0 as 0. if (JSONSchemaValidator.getType(args[ai]) === 'integer' && args[ai] === 0) args[ai] = 0; if (definedSignature[si] === resolvedSignature[ai]) $Array.push(normalizedArgs, args[ai++]); else $Array.push(normalizedArgs, null); } return normalizedArgs; }; // Validates that a given schema for an API function is not ambiguous. function isFunctionSignatureAmbiguous(functionDef) { if (functionDef.allowAmbiguousOptionalArguments) return false; var signaturesAmbiguous = function(signature1, signature2) { if (signature1.length != signature2.length) return false; for (var i = 0; i < signature1.length; i++) { if (!schemaValidator.checkSchemaOverlap( signature1[i], signature2[i])) return false; } return true; }; var candidateSignatures = getSignatures(functionDef.parameters); for (var i = 0; i < candidateSignatures.length; ++i) { for (var j = i + 1; j < candidateSignatures.length; ++j) { if (signaturesAmbiguous(candidateSignatures[i], candidateSignatures[j])) return true; } } return false; }; exports.$set('isFunctionSignatureAmbiguous', isFunctionSignatureAmbiguous); exports.$set('normalizeArgumentsAndValidate', normalizeArgumentsAndValidate); exports.$set('schemaValidator', schemaValidator); exports.$set('validate', validate); // Copyright 2014 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. var exceptionHandler = require('uncaught_exception_handler'); var lastError = require('lastError'); var logging = requireNative('logging'); var natives = requireNative('sendRequest'); var validate = require('schemaUtils').validate; // All outstanding requests from sendRequest(). var requests = { __proto__: null }; // Used to prevent double Activity Logging for API calls that use both custom // bindings and ExtensionFunctions (via sendRequest). var calledSendRequest = false; // Runs a user-supplied callback safely. function safeCallbackApply(name, request, callback, args) { try { $Function.apply(callback, request, args); } catch (e) { exceptionHandler.handle('Error in response to ' + name, e, request.stack); } } // Callback handling. function handleResponse(requestId, name, success, responseList, error) { // The chrome objects we will set lastError on. Really we should only be // setting this on the callback's chrome object, but set on ours too since // it's conceivable that something relies on that. var callerChrome = chrome; try { var request = requests[requestId]; logging.DCHECK(request != null); // lastError needs to be set on the caller's chrome object no matter what, // though chances are it's the same as ours (it will be different when // calling API methods on other contexts). if (request.callback) { var global = natives.GetGlobal(request.callback); callerChrome = global ? global.chrome : callerChrome; } lastError.clear(chrome); if (callerChrome !== chrome) lastError.clear(callerChrome); if (!success) { if (!error) error = "Unknown error."; lastError.set(name, error, request.stack, chrome); if (callerChrome !== chrome) lastError.set(name, error, request.stack, callerChrome); } if (request.customCallback) { safeCallbackApply(name, request, request.customCallback, $Array.concat([name, request, request.callback], responseList)); } else if (request.callback) { // Validate callback in debug only -- and only when the // caller has provided a callback. Implementations of api // calls may not return data if they observe the caller // has not provided a callback. if (logging.DCHECK_IS_ON() && !error) { if (!request.callbackSchema.parameters) throw new Error(name + ": no callback schema defined"); validate(responseList, request.callbackSchema.parameters); } safeCallbackApply(name, request, request.callback, responseList); } if (error && !lastError.hasAccessed(chrome)) { // The native call caused an error, but the developer might not have // checked runtime.lastError. lastError.reportIfUnchecked(name, callerChrome, request.stack); } } finally { delete requests[requestId]; lastError.clear(chrome); if (callerChrome !== chrome) lastError.clear(callerChrome); } } function prepareRequest(args, argSchemas) { var request = { __proto__: null }; var argCount = args.length; // Look for callback param. if (argSchemas.length > 0 && argSchemas[argSchemas.length - 1].type == "function") { request.callback = args[args.length - 1]; request.callbackSchema = argSchemas[argSchemas.length - 1]; --argCount; } request.args = $Array.slice(args, 0, argCount); return request; } // Send an API request and optionally register a callback. // |optArgs| is an object with optional parameters as follows: // - customCallback: a callback that should be called instead of the standard // callback. // - forIOThread: true if this function should be handled on the browser IO // thread. // - preserveNullInObjects: true if it is safe for null to be in objects. // - stack: An optional string that contains the stack trace, to be displayed // to the user if an error occurs. function sendRequest(functionName, args, argSchemas, optArgs) { calledSendRequest = true; if (!optArgs) optArgs = { __proto__: null }; logging.DCHECK(optArgs.__proto__ == null); var request = prepareRequest(args, argSchemas); request.stack = optArgs.stack || exceptionHandler.getExtensionStackTrace(); if (optArgs.customCallback) { request.customCallback = optArgs.customCallback; } var hasCallback = request.callback || optArgs.customCallback; var requestId = natives.StartRequest(functionName, request.args, hasCallback, optArgs.forIOThread, optArgs.preserveNullInObjects); request.id = requestId; requests[requestId] = request; } function getCalledSendRequest() { return calledSendRequest; } function clearCalledSendRequest() { calledSendRequest = false; } exports.$set('sendRequest', sendRequest); exports.$set('getCalledSendRequest', getCalledSendRequest); exports.$set('clearCalledSendRequest', clearCalledSendRequest); exports.$set('safeCallbackApply', safeCallbackApply); // Called by C++. exports.$set('handleResponse', handleResponse); // Copyright 2014 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. var SetIconCommon = requireNative('setIcon').SetIconCommon; var sendRequest = require('sendRequest').sendRequest; function loadImagePath(path, callback) { var img = new Image(); img.onerror = function() { console.error('Could not load action icon \'' + path + '\'.'); }; img.onload = function() { var canvas = document.createElement('canvas'); canvas.width = img.width; canvas.height = img.height; var canvas_context = canvas.getContext('2d'); canvas_context.clearRect(0, 0, canvas.width, canvas.height); canvas_context.drawImage(img, 0, 0, canvas.width, canvas.height); var imageData = canvas_context.getImageData(0, 0, canvas.width, canvas.height); callback(imageData); }; img.src = path; } function smellsLikeImageData(imageData) { // See if this object at least looks like an ImageData element. // Unfortunately, we cannot use instanceof because the ImageData // constructor is not public. // // We do this manually instead of using JSONSchema to avoid having these // properties show up in the doc. return (typeof imageData == 'object') && ('width' in imageData) && ('height' in imageData) && ('data' in imageData); } function verifyImageData(imageData) { if (!smellsLikeImageData(imageData)) { throw new Error( 'The imageData property must contain an ImageData object or' + ' dictionary of ImageData objects.'); } } /** * Normalizes |details| to a format suitable for sending to the browser, * for example converting ImageData to a binary representation. * * @param {ImageDetails} details * The ImageDetails passed into an extension action-style API. * @param {Function} callback * The callback function to pass processed imageData back to. Note that this * callback may be called reentrantly. */ function setIcon(details, callback) { // Note that iconIndex is actually deprecated, and only available to the // pageAction API. // TODO(kalman): Investigate whether this is for the pageActions API, and if // so, delete it. if ('iconIndex' in details) { callback(details); return; } if ('imageData' in details) { if (smellsLikeImageData(details.imageData)) { var imageData = details.imageData; details.imageData = {}; details.imageData[imageData.width.toString()] = imageData; } else if (typeof details.imageData == 'object' && Object.getOwnPropertyNames(details.imageData).length !== 0) { for (var sizeKey in details.imageData) { verifyImageData(details.imageData[sizeKey]); } } else { verifyImageData(false); } callback(SetIconCommon(details)); return; } if ('path' in details) { if (typeof details.path == 'object') { details.imageData = {}; var detailKeyCount = 0; for (var iconSize in details.path) { ++detailKeyCount; loadImagePath(details.path[iconSize], function(size, imageData) { details.imageData[size] = imageData; if (--detailKeyCount == 0) callback(SetIconCommon(details)); }.bind(null, iconSize)); } if (detailKeyCount == 0) throw new Error('The path property must not be empty.'); } else if (typeof details.path == 'string') { details.imageData = {}; loadImagePath(details.path, function(imageData) { details.imageData[imageData.width.toString()] = imageData; delete details.path; callback(SetIconCommon(details)); }); } return; } throw new Error('Either the path or imageData property must be specified.'); } exports.$set('setIcon', setIcon); // Copyright 2014 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. // test_custom_bindings.js // mini-framework for ExtensionApiTest browser tests var binding = apiBridge || require('binding').Binding.create('test'); var environmentSpecificBindings = require('test_environment_specific_bindings'); var GetExtensionAPIDefinitionsForTest = requireNative('apiDefinitions').GetExtensionAPIDefinitionsForTest; var GetAPIFeatures = requireNative('test_features').GetAPIFeatures; var natives = requireNative('test_native_handler'); var uncaughtExceptionHandler = require('uncaught_exception_handler'); var userGestures = requireNative('user_gestures'); var GetModuleSystem = requireNative('v8_context').GetModuleSystem; binding.registerCustomHook(function(api) { var chromeTest = api.compiledApi; var apiFunctions = api.apiFunctions; chromeTest.tests = chromeTest.tests || []; var currentTest = null; var lastTest = null; var testsFailed = 0; var testCount = 1; var failureException = 'chrome.test.failure'; // Helper function to get around the fact that function names in javascript // are read-only, and you can't assign one to anonymous functions. function testName(test) { return test ? (test.name || test.generatedName) : "(no test)"; } function testDone() { environmentSpecificBindings.testDone(chromeTest.runNextTest); } function allTestsDone() { if (testsFailed == 0) { chromeTest.notifyPass(); } else { chromeTest.notifyFail('Failed ' + testsFailed + ' of ' + testCount + ' tests'); } } var pendingCallbacks = 0; apiFunctions.setHandleRequest('callbackAdded', function() { pendingCallbacks++; var called = null; return function() { if (called != null) { var redundantPrefix = 'Error\n'; chromeTest.fail( 'Callback has already been run. ' + 'First call:\n' + $String.slice(called, redundantPrefix.length) + '\n' + 'Second call:\n' + $String.slice(new Error().stack, redundantPrefix.length)); } called = new Error().stack; pendingCallbacks--; if (pendingCallbacks == 0) { chromeTest.succeed(); } }; }); apiFunctions.setHandleRequest('runNextTest', function() { // There may have been callbacks which were interrupted by failure // exceptions. pendingCallbacks = 0; lastTest = currentTest; currentTest = chromeTest.tests.shift(); if (!currentTest) { allTestsDone(); return; } try { chromeTest.log("( RUN ) " + testName(currentTest)); uncaughtExceptionHandler.setHandler(function(message, e) { if (e !== failureException) chromeTest.fail('uncaught exception: ' + message); }); currentTest.call(); } catch (e) { uncaughtExceptionHandler.handle(e.message, e); } }); apiFunctions.setHandleRequest('fail', function(message) { chromeTest.log("( FAILED ) " + testName(currentTest)); var stack = {}; Error.captureStackTrace(stack, chromeTest.fail); if (!message) message = "FAIL (no message)"; message += "\n" + stack.stack; console.log("[FAIL] " + testName(currentTest) + ": " + message); testsFailed++; testDone(); // Interrupt the rest of the test. throw failureException; }); apiFunctions.setHandleRequest('succeed', function() { console.log("[SUCCESS] " + testName(currentTest)); chromeTest.log("( SUCCESS )"); testDone(); }); apiFunctions.setHandleRequest('getModuleSystem', function(context) { return GetModuleSystem(context); }); apiFunctions.setHandleRequest('assertTrue', function(test, message) { chromeTest.assertBool(test, true, message); }); apiFunctions.setHandleRequest('assertFalse', function(test, message) { chromeTest.assertBool(test, false, message); }); apiFunctions.setHandleRequest('assertBool', function(test, expected, message) { if (test !== expected) { if (typeof(test) == "string") { if (message) message = test + "\n" + message; else message = test; } chromeTest.fail(message); } }); apiFunctions.setHandleRequest('checkDeepEq', function(expected, actual) { if ((expected === null) != (actual === null)) return false; if (expected === actual) return true; if (typeof(expected) !== typeof(actual)) return false; for (var p in actual) { if ($Object.hasOwnProperty(actual, p) && !$Object.hasOwnProperty(expected, p)) { return false; } } for (var p in expected) { if ($Object.hasOwnProperty(expected, p) && !$Object.hasOwnProperty(actual, p)) { return false; } } for (var p in expected) { var eq = true; switch (typeof(expected[p])) { case 'object': eq = chromeTest.checkDeepEq(expected[p], actual[p]); break; case 'function': eq = (typeof(actual[p]) != 'undefined' && expected[p].toString() == actual[p].toString()); break; default: eq = (expected[p] == actual[p] && typeof(expected[p]) == typeof(actual[p])); break; } if (!eq) return false; } return true; }); apiFunctions.setHandleRequest('assertEq', function(expected, actual, message) { var error_msg = "API Test Error in " + testName(currentTest); if (message) error_msg += ": " + message; if (typeof(expected) == 'object') { if (!chromeTest.checkDeepEq(expected, actual)) { error_msg += "\nActual: " + $JSON.stringify(actual) + "\nExpected: " + $JSON.stringify(expected); chromeTest.fail(error_msg); } return; } if (expected != actual) { chromeTest.fail(error_msg + "\nActual: " + actual + "\nExpected: " + expected); } if (typeof(expected) != typeof(actual)) { chromeTest.fail(error_msg + " (type mismatch)\nActual Type: " + typeof(actual) + "\nExpected Type:" + typeof(expected)); } }); apiFunctions.setHandleRequest('assertNoLastError', function() { if (chrome.runtime.lastError != undefined) { chromeTest.fail("lastError.message == " + chrome.runtime.lastError.message); } }); apiFunctions.setHandleRequest('assertLastError', function(expectedError) { chromeTest.assertEq(typeof(expectedError), 'string'); chromeTest.assertTrue(chrome.runtime.lastError != undefined, "No lastError, but expected " + expectedError); chromeTest.assertEq(expectedError, chrome.runtime.lastError.message); }); apiFunctions.setHandleRequest('assertThrows', function(fn, self, args, message) { chromeTest.assertTrue(typeof fn == 'function'); try { fn.apply(self, args); chromeTest.fail('Did not throw error: ' + fn); } catch (e) { if (e != failureException && message !== undefined) { if (message instanceof RegExp) { chromeTest.assertTrue(message.test(e.message), e.message + ' should match ' + message) } else { chromeTest.assertEq(message, e.message); } } } }); function safeFunctionApply(func, args) { try { if (func) return $Function.apply(func, undefined, args); } catch (e) { if (e === failureException) throw e; uncaughtExceptionHandler.handle(e.message, e); } }; // Wrapper for generating test functions, that takes care of calling // assertNoLastError() and (optionally) succeed() for you. apiFunctions.setHandleRequest('callback', function(func, expectedError) { if (func) { chromeTest.assertEq(typeof(func), 'function'); } var callbackCompleted = chromeTest.callbackAdded(); return function() { if (expectedError == null) { chromeTest.assertNoLastError(); } else { chromeTest.assertLastError(expectedError); } var result; if (func) { result = safeFunctionApply(func, arguments); } callbackCompleted(); return result; }; }); apiFunctions.setHandleRequest('listenOnce', function(event, func) { var callbackCompleted = chromeTest.callbackAdded(); var listener = function() { event.removeListener(listener); safeFunctionApply(func, arguments); callbackCompleted(); }; event.addListener(listener); }); apiFunctions.setHandleRequest('listenForever', function(event, func) { var callbackCompleted = chromeTest.callbackAdded(); var listener = function() { safeFunctionApply(func, arguments); }; var done = function() { event.removeListener(listener); callbackCompleted(); }; event.addListener(listener); return done; }); apiFunctions.setHandleRequest('callbackPass', function(func) { return chromeTest.callback(func); }); apiFunctions.setHandleRequest('callbackFail', function(expectedError, func) { return chromeTest.callback(func, expectedError); }); apiFunctions.setHandleRequest('runTests', function(tests) { chromeTest.tests = tests; testCount = chromeTest.tests.length; chromeTest.runNextTest(); }); apiFunctions.setHandleRequest('getApiDefinitions', function() { return GetExtensionAPIDefinitionsForTest(); }); apiFunctions.setHandleRequest('getApiFeatures', function() { return GetAPIFeatures(); }); apiFunctions.setHandleRequest('isProcessingUserGesture', function() { return userGestures.IsProcessingUserGesture(); }); apiFunctions.setHandleRequest('runWithUserGesture', function(callback) { chromeTest.assertEq(typeof(callback), 'function'); return userGestures.RunWithUserGesture(callback); }); apiFunctions.setHandleRequest('runWithoutUserGesture', function(callback) { chromeTest.assertEq(typeof(callback), 'function'); return userGestures.RunWithoutUserGesture(callback); }); apiFunctions.setHandleRequest('setExceptionHandler', function(callback) { chromeTest.assertEq(typeof(callback), 'function'); uncaughtExceptionHandler.setHandler(callback); }); apiFunctions.setHandleRequest('getWakeEventPage', function() { return natives.GetWakeEventPage(); }); environmentSpecificBindings.registerHooks(api); }); if (!apiBridge) exports.$set('binding', binding.generate()); // Copyright 2014 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. // Handles uncaught exceptions thrown by extensions. By default this is to // log an error message, but tests may override this behaviour. var handler = function(message, e) { console.error(message); }; /** * Append the error description and stack trace to |message|. * * @param {string} message - The prefix of the error message. * @param {Error|*} e - The thrown error object. This object is potentially * unsafe, because it could be generated by an extension. * @param {string=} priorStackTrace - The stack trace to be appended to the * error message. This stack trace must not include stack frames of |e.stack|, * because both stack traces are concatenated. Overlapping stack traces will * confuse extension developers. * @return {string} The formatted error message. */ function formatErrorMessage(message, e, priorStackTrace) { if (e) message += ': ' + safeErrorToString(e, false); var stack; try { // If the stack was set, use it. // |e.stack| could be void in the following common example: // throw "Error message"; stack = $String.self(e && e.stack); } catch (e) {} // If a stack is not provided, capture a stack trace. if (!priorStackTrace && !stack) stack = getStackTrace(); stack = filterExtensionStackTrace(stack); if (stack) message += '\n' + stack; // If an asynchronouse stack trace was set, append it. if (priorStackTrace) message += '\n' + priorStackTrace; return message; } function filterExtensionStackTrace(stack) { if (!stack) return ''; // Remove stack frames in the stack trace that weren't associated with the // extension, to not confuse extension developers with internal details. stack = $String.split(stack, '\n'); stack = $Array.filter(stack, function(line) { return $String.indexOf(line, 'chrome-extension://') >= 0; }); return $Array.join(stack, '\n'); } function getStackTrace() { var e = {}; $Error.captureStackTrace(e, getStackTrace); return e.stack; } function getExtensionStackTrace() { return filterExtensionStackTrace(getStackTrace()); } /** * Convert an object to a string. * * @param {Error|*} e - A thrown object (possibly user-supplied). * @param {boolean=} omitType - Whether to try to serialize |e.message| instead * of |e.toString()|. * @return {string} The error message. */ function safeErrorToString(e, omitType) { try { return $String.self(omitType && e.message || e); } catch (e) { // This error is exceptional and could be triggered by // throw {toString: function() { throw 'Haha' } }; return '(cannot get error message)'; } } /** * Formats the error message and invokes the error handler. * * @param {string} message - Error message prefix. * @param {Error|*} e - Thrown object. * @param {string=} priorStackTrace - Error message suffix. * @see formatErrorMessage */ exports.$set('handle', function(message, e, priorStackTrace) { message = formatErrorMessage(message, e, priorStackTrace); handler(message, e); }); // |newHandler| A function which matches |handler|. exports.$set('setHandler', function(newHandler) { handler = newHandler; }); exports.$set('getStackTrace', getStackTrace); exports.$set('getExtensionStackTrace', getExtensionStackTrace); exports.$set('safeErrorToString', safeErrorToString); // Copyright 2014 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. var nativeDeepCopy = requireNative('utils').deepCopy; var schemaRegistry = requireNative('schema_registry'); var CHECK = requireNative('logging').CHECK; var DCHECK = requireNative('logging').DCHECK; var WARNING = requireNative('logging').WARNING; /** * An object forEach. Calls |f| with each (key, value) pair of |obj|, using * |self| as the target. * @param {Object} obj The object to iterate over. * @param {function} f The function to call in each iteration. * @param {Object} self The object to use as |this| in each function call. */ function forEach(obj, f, self) { for (var key in obj) { if ($Object.hasOwnProperty(obj, key)) $Function.call(f, self, key, obj[key]); } } /** * Assuming |array_of_dictionaries| is structured like this: * [{id: 1, ... }, {id: 2, ...}, ...], you can use * lookup(array_of_dictionaries, 'id', 2) to get the dictionary with id == 2. * @param {Array>} array_of_dictionaries * @param {string} field * @param {?} value */ function lookup(array_of_dictionaries, field, value) { var filter = function (dict) {return dict[field] == value;}; var matches = $Array.filter(array_of_dictionaries, filter); if (matches.length == 0) { return undefined; } else if (matches.length == 1) { return matches[0] } else { throw new Error("Failed lookup of field '" + field + "' with value '" + value + "'"); } } function loadTypeSchema(typeName, defaultSchema) { var parts = $String.split(typeName, '.'); if (parts.length == 1) { if (defaultSchema == null) { WARNING('Trying to reference "' + typeName + '" ' + 'with neither namespace nor default schema.'); return null; } var types = defaultSchema.types; } else { var schemaName = $Array.join($Array.slice(parts, 0, parts.length - 1), '.'); var types = schemaRegistry.GetSchema(schemaName).types; } for (var i = 0; i < types.length; ++i) { if (types[i].id == typeName) return types[i]; } return null; } /** * Sets a property |value| on |obj| with property name |key|. Like * * obj[key] = value; * * but without triggering setters. */ function defineProperty(obj, key, value) { $Object.defineProperty(obj, key, { __proto__: null, configurable: true, enumerable: true, writable: true, value: value, }); } /** * Takes a private class implementation |privateClass| and exposes a subset of * its methods |functions| and properties |properties| and |readonly| to a * public wrapper class that should be passed in. Within bindings code, you can * access the implementation from an instance of the wrapper class using * privates(instance).impl, and from the implementation class you can access * the wrapper using this.wrapper (or implInstance.wrapper if you have another * instance of the implementation class). * * |publicClass| should be a constructor that calls constructPrivate() like so: * * privates(publicClass).constructPrivate(this, arguments); * * @param {function} publicClass The publicly exposed wrapper class. This must * be a named function, and the name appears in stack traces. * @param {Object} privateClass The class implementation. * @param {{superclass: ?Function, * functions: ?Array, * properties: ?Array, * readonly: ?Array}} exposed The names of properties on the * implementation class to be exposed. |superclass| represents the * constructor of the class to be used as the superclass of the exposed * class; |functions| represents the names of functions which should be * delegated to the implementation; |properties| are gettable/settable * properties and |readonly| are read-only properties. */ function expose(publicClass, privateClass, exposed) { $Object.setPrototypeOf(exposed, null); // This should be called by publicClass. privates(publicClass).constructPrivate = function(self, args) { if (!(self instanceof publicClass)) { throw new Error('Please use "new ' + publicClass.name + '"'); } // The "instanceof publicClass" check can easily be spoofed, so we check // whether the private impl is already set before continuing. var privateSelf = privates(self); if ('impl' in privateSelf) { throw new Error('Object ' + publicClass.name + ' is already constructed'); } var privateObj = $Object.create(privateClass.prototype); $Function.apply(privateClass, privateObj, args); privateObj.wrapper = self; privateSelf.impl = privateObj; }; function getPrivateImpl(self) { var impl = privates(self).impl; if (!(impl instanceof privateClass)) { // Either the object is not constructed, or the property descriptor is // used on a target that is not an instance of publicClass. throw new Error('impl is not an instance of ' + privateClass.name); } return impl; } var publicClassPrototype = { // The final prototype will be assigned at the end of this method. __proto__: null, constructor: publicClass, }; if ('functions' in exposed) { $Array.forEach(exposed.functions, function(func) { publicClassPrototype[func] = function() { var impl = getPrivateImpl(this); return $Function.apply(impl[func], impl, arguments); }; }); } if ('properties' in exposed) { $Array.forEach(exposed.properties, function(prop) { $Object.defineProperty(publicClassPrototype, prop, { __proto__: null, enumerable: true, get: function() { return getPrivateImpl(this)[prop]; }, set: function(value) { var impl = getPrivateImpl(this); delete impl[prop]; impl[prop] = value; } }); }); } if ('readonly' in exposed) { $Array.forEach(exposed.readonly, function(readonly) { $Object.defineProperty(publicClassPrototype, readonly, { __proto__: null, enumerable: true, get: function() { return getPrivateImpl(this)[readonly]; }, }); }); } // The prototype properties have been installed. Now we can safely assign an // unsafe prototype and export the class to the public. var superclass = exposed.superclass || $Object.self; $Object.setPrototypeOf(publicClassPrototype, superclass.prototype); publicClass.prototype = publicClassPrototype; return publicClass; } /** * Returns a deep copy of |value|. The copy will have no references to nested * values of |value|. */ function deepCopy(value) { return nativeDeepCopy(value); } /** * Wrap an asynchronous API call to a function |func| in a promise. The * remaining arguments will be passed to |func|. Returns a promise that will be * resolved to the result passed to the callback or rejected if an error occurs * (if chrome.runtime.lastError is set). If there are multiple results, the * promise will be resolved with an array containing those results. * * For example, * promise(chrome.storage.get, 'a').then(function(result) { * // Use result. * }).catch(function(error) { * // Report error.message. * }); */ function promise(func) { var args = $Array.slice(arguments, 1); DCHECK(typeof func == 'function'); return new Promise(function(resolve, reject) { args.push(function() { if (chrome.runtime.lastError) { reject(new Error(chrome.runtime.lastError)); return; } if (arguments.length <= 1) resolve(arguments[0]); else resolve($Array.slice(arguments)); }); $Function.apply(func, null, args); }); } exports.$set('forEach', forEach); exports.$set('loadTypeSchema', loadTypeSchema); exports.$set('lookup', lookup); exports.$set('defineProperty', defineProperty); exports.$set('expose', expose); exports.$set('deepCopy', deepCopy); exports.$set('promise', promise); // Copyright 2014 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. // This module implements helper objects for the dialog, newwindow, and // permissionrequest events. var MessagingNatives = requireNative('messaging_natives'); var WebViewConstants = require('webViewConstants').WebViewConstants; var WebViewInternal = require('webViewInternal').WebViewInternal; var PERMISSION_TYPES = ['media', 'geolocation', 'pointerLock', 'download', 'loadplugin', 'filesystem', 'fullscreen']; // ----------------------------------------------------------------------------- // WebViewActionRequest object. // Default partial implementation of a webview action request. function WebViewActionRequest(webViewImpl, event, webViewEvent, interfaceName) { this.webViewImpl = webViewImpl; this.event = event; this.webViewEvent = webViewEvent; this.interfaceName = interfaceName; this.guestInstanceId = this.webViewImpl.guest.getId(); this.requestId = event.requestId; this.actionTaken = false; // Add on the request information specific to the request type. for (var infoName in this.event.requestInfo) { this.event[infoName] = this.event.requestInfo[infoName]; this.webViewEvent[infoName] = this.event.requestInfo[infoName]; } } // Performs the default action for the request. WebViewActionRequest.prototype.defaultAction = function() { // Do nothing if the action has already been taken or the requester is // already gone (in which case its guestInstanceId will be stale). if (this.actionTaken || this.guestInstanceId != this.webViewImpl.guest.getId()) { return; } this.actionTaken = true; WebViewInternal.setPermission(this.guestInstanceId, this.requestId, 'default', '', function(allowed) { if (allowed) { return; } this.showWarningMessage(); }.bind(this)); }; // Called to handle the action request's event. WebViewActionRequest.prototype.handleActionRequestEvent = function() { // Construct the interface object and attach it to |webViewEvent|. var request = this.getInterfaceObject(); this.webViewEvent[this.interfaceName] = request; var defaultPrevented = !this.webViewImpl.dispatchEvent(this.webViewEvent); // Set |webViewEvent| to null to break the circular reference to |request| so // that the garbage collector can eventually collect it. this.webViewEvent = null; if (this.actionTaken) { return; } if (defaultPrevented) { // Track the lifetime of |request| with the garbage collector. var portId = -1; // (hack) there is no Extension Port to release MessagingNatives.BindToGC(request, this.defaultAction.bind(this), portId); } else { this.defaultAction(); } }; // Displays a warning message when an action request is blocked by default. WebViewActionRequest.prototype.showWarningMessage = function() { window.console.warn(this.WARNING_MSG_REQUEST_BLOCKED); }; // This function ensures that each action is taken at most once. WebViewActionRequest.prototype.validateCall = function() { if (this.actionTaken) { throw new Error(this.ERROR_MSG_ACTION_ALREADY_TAKEN); } this.actionTaken = true; }; // The following are implemented by the specific action request. // Returns the interface object for this action request. WebViewActionRequest.prototype.getInterfaceObject = undefined; // Error/warning messages. WebViewActionRequest.prototype.ERROR_MSG_ACTION_ALREADY_TAKEN = undefined; WebViewActionRequest.prototype.WARNING_MSG_REQUEST_BLOCKED = undefined; // ----------------------------------------------------------------------------- // Dialog object. // Represents a dialog box request (e.g. alert()). function Dialog(webViewImpl, event, webViewEvent) { WebViewActionRequest.call(this, webViewImpl, event, webViewEvent, 'dialog'); this.handleActionRequestEvent(); } Dialog.prototype.__proto__ = WebViewActionRequest.prototype; Dialog.prototype.getInterfaceObject = function() { return { ok: function(user_input) { this.validateCall(); user_input = user_input || ''; WebViewInternal.setPermission( this.guestInstanceId, this.requestId, 'allow', user_input); }.bind(this), cancel: function() { this.validateCall(); WebViewInternal.setPermission( this.guestInstanceId, this.requestId, 'deny'); }.bind(this) }; }; Dialog.prototype.showWarningMessage = function() { var VOWELS = ['a', 'e', 'i', 'o', 'u']; var dialogType = this.event.messageType; var article = (VOWELS.indexOf(dialogType.charAt(0)) >= 0) ? 'An' : 'A'; this.WARNING_MSG_REQUEST_BLOCKED = this.WARNING_MSG_REQUEST_BLOCKED. replace('%1', article).replace('%2', dialogType); window.console.warn(this.WARNING_MSG_REQUEST_BLOCKED); }; Dialog.prototype.ERROR_MSG_ACTION_ALREADY_TAKEN = WebViewConstants.ERROR_MSG_DIALOG_ACTION_ALREADY_TAKEN; Dialog.prototype.WARNING_MSG_REQUEST_BLOCKED = WebViewConstants.WARNING_MSG_DIALOG_REQUEST_BLOCKED; // ----------------------------------------------------------------------------- // NewWindow object. // Represents a new window request. function NewWindow(webViewImpl, event, webViewEvent) { WebViewActionRequest.call(this, webViewImpl, event, webViewEvent, 'window'); this.handleActionRequestEvent(); } NewWindow.prototype.__proto__ = WebViewActionRequest.prototype; NewWindow.prototype.getInterfaceObject = function() { return { attach: function(webview) { this.validateCall(); if (!webview || !webview.tagName || webview.tagName != 'WEBVIEW') { throw new Error(ERROR_MSG_WEBVIEW_EXPECTED); } var webViewImpl = privates(webview).internal; // Update the partition. if (this.event.partition) { webViewImpl.onAttach(this.event.partition); } var attached = webViewImpl.attachWindow$(this.event.windowId); if (!attached) { window.console.error(ERROR_MSG_NEWWINDOW_UNABLE_TO_ATTACH); } if (this.guestInstanceId != this.webViewImpl.guest.getId()) { // If the opener is already gone, then its guestInstanceId will be // stale. return; } // If the object being passed into attach is not a valid // then we will fail and it will be treated as if the new window // was rejected. The permission API plumbing is used here to clean // up the state created for the new window if attaching fails. WebViewInternal.setPermission(this.guestInstanceId, this.requestId, attached ? 'allow' : 'deny'); }.bind(this), discard: function() { this.validateCall(); if (!this.guestInstanceId) { // If the opener is already gone, then we won't have its // guestInstanceId. return; } WebViewInternal.setPermission( this.guestInstanceId, this.requestId, 'deny'); }.bind(this) }; }; NewWindow.prototype.ERROR_MSG_ACTION_ALREADY_TAKEN = WebViewConstants.ERROR_MSG_NEWWINDOW_ACTION_ALREADY_TAKEN; NewWindow.prototype.WARNING_MSG_REQUEST_BLOCKED = WebViewConstants.WARNING_MSG_NEWWINDOW_REQUEST_BLOCKED; // ----------------------------------------------------------------------------- // PermissionRequest object. // Represents a permission request (e.g. to access the filesystem). function PermissionRequest(webViewImpl, event, webViewEvent) { WebViewActionRequest.call(this, webViewImpl, event, webViewEvent, 'request'); if (!this.validPermissionCheck()) { return; } this.handleActionRequestEvent(); } PermissionRequest.prototype.__proto__ = WebViewActionRequest.prototype; PermissionRequest.prototype.allow = function() { this.validateCall(); WebViewInternal.setPermission(this.guestInstanceId, this.requestId, 'allow'); }; PermissionRequest.prototype.deny = function() { this.validateCall(); WebViewInternal.setPermission(this.guestInstanceId, this.requestId, 'deny'); }; PermissionRequest.prototype.getInterfaceObject = function() { var request = { allow: this.allow.bind(this), deny: this.deny.bind(this) }; // Add on the request information specific to the request type. for (var infoName in this.event.requestInfo) { request[infoName] = this.event.requestInfo[infoName]; } return $Object.freeze(request); }; PermissionRequest.prototype.showWarningMessage = function() { window.console.warn( this.WARNING_MSG_REQUEST_BLOCKED.replace('%1', this.event.permission)); }; // Checks that the requested permission is valid. Returns true if valid. PermissionRequest.prototype.validPermissionCheck = function() { if (PERMISSION_TYPES.indexOf(this.event.permission) < 0) { // The permission type is not allowed. Trigger the default response. this.defaultAction(); return false; } return true; }; PermissionRequest.prototype.ERROR_MSG_ACTION_ALREADY_TAKEN = WebViewConstants.ERROR_MSG_PERMISSION_ACTION_ALREADY_TAKEN; PermissionRequest.prototype.WARNING_MSG_REQUEST_BLOCKED = WebViewConstants.WARNING_MSG_PERMISSION_REQUEST_BLOCKED; // ----------------------------------------------------------------------------- // FullscreenPermissionRequest object. // Represents a fullscreen permission request. function FullscreenPermissionRequest(webViewImpl, event, webViewEvent) { PermissionRequest.call(this, webViewImpl, event, webViewEvent); } FullscreenPermissionRequest.prototype.__proto__ = PermissionRequest.prototype; FullscreenPermissionRequest.prototype.allow = function() { PermissionRequest.prototype.allow.call(this); // Now make the element go fullscreen. this.webViewImpl.makeElementFullscreen(); }; // ----------------------------------------------------------------------------- var WebViewActionRequests = { WebViewActionRequest: WebViewActionRequest, Dialog: Dialog, NewWindow: NewWindow, PermissionRequest: PermissionRequest, FullscreenPermissionRequest: FullscreenPermissionRequest }; // Exports. exports.$set('WebViewActionRequests', WebViewActionRequests); // Copyright 2014 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. // This module implements the public-facing API functions for the tag. var WebViewInternal = require('webViewInternal').WebViewInternal; var WebViewImpl = require('webView').WebViewImpl; // An array of 's public-facing API methods. Methods without custom // implementations will be given default implementations that call into the // internal API method with the same name in |WebViewInternal|. For example, a // method called 'someApiMethod' would be given the following default // implementation: // // WebViewImpl.prototype.someApiMethod = function(var_args) { // if (!this.guest.getId()) { // return false; // } // var args = $Array.concat([this.guest.getId()], $Array.slice(arguments)); // $Function.apply(WebViewInternal.someApiMethod, null, args); // return true; // }; // // These default implementations come from createDefaultApiMethod() in // web_view.js. var WEB_VIEW_API_METHODS = [ // Add content scripts for the guest page. 'addContentScripts', // Navigates to the previous history entry. 'back', // Returns whether there is a previous history entry to navigate to. 'canGoBack', // Returns whether there is a subsequent history entry to navigate to. 'canGoForward', // Captures the visible region of the WebView contents into a bitmap. 'captureVisibleRegion', // Clears browsing data for the WebView partition. 'clearData', // Injects JavaScript code into the guest page. 'executeScript', // Initiates a find-in-page request. 'find', // Navigates to the subsequent history entry. 'forward', // Returns Chrome's internal process ID for the guest web page's current // process. 'getProcessId', // Returns the user agent string used by the webview for guest page requests. 'getUserAgent', // Gets the current zoom factor. 'getZoom', // Gets the current zoom mode of the webview. 'getZoomMode', // Navigates to a history entry using a history index relative to the current // navigation. 'go', // Injects CSS into the guest page. 'insertCSS', // Indicates whether or not the webview's user agent string has been // overridden. 'isUserAgentOverridden', // Loads a data URL with a specified base URL used for relative links. // Optionally, a virtual URL can be provided to be shown to the user instead // of the data URL. 'loadDataWithBaseUrl', // Prints the contents of the webview. 'print', // Removes content scripts for the guest page. 'removeContentScripts', // Reloads the current top-level page. 'reload', // Override the user agent string used by the webview for guest page requests. 'setUserAgentOverride', // Changes the zoom factor of the page. 'setZoom', // Changes the zoom mode of the webview. 'setZoomMode', // Stops loading the current navigation if one is in progress. 'stop', // Ends the current find session. 'stopFinding', // Forcibly kills the guest web page's renderer process. 'terminate' ]; // ----------------------------------------------------------------------------- // Custom API method implementations. WebViewImpl.prototype.addContentScripts = function(rules) { return WebViewInternal.addContentScripts(this.viewInstanceId, rules); }; WebViewImpl.prototype.back = function(callback) { return this.go(-1, callback); }; WebViewImpl.prototype.canGoBack = function() { return this.entryCount > 1 && this.currentEntryIndex > 0; }; WebViewImpl.prototype.canGoForward = function() { return this.currentEntryIndex >= 0 && this.currentEntryIndex < (this.entryCount - 1); }; WebViewImpl.prototype.executeScript = function(var_args) { return this.executeCode(WebViewInternal.executeScript, $Array.slice(arguments)); }; WebViewImpl.prototype.forward = function(callback) { return this.go(1, callback); }; WebViewImpl.prototype.getProcessId = function() { return this.processId; }; WebViewImpl.prototype.getUserAgent = function() { return this.userAgentOverride || navigator.userAgent; }; WebViewImpl.prototype.insertCSS = function(var_args) { return this.executeCode(WebViewInternal.insertCSS, $Array.slice(arguments)); }; WebViewImpl.prototype.isUserAgentOverridden = function() { return !!this.userAgentOverride && this.userAgentOverride != navigator.userAgent; }; WebViewImpl.prototype.loadDataWithBaseUrl = function( dataUrl, baseUrl, virtualUrl) { if (!this.guest.getId()) { return; } WebViewInternal.loadDataWithBaseUrl( this.guest.getId(), dataUrl, baseUrl, virtualUrl, function() { // Report any errors. if (chrome.runtime.lastError != undefined) { window.console.error( 'Error while running webview.loadDataWithBaseUrl: ' + chrome.runtime.lastError.message); } }); }; WebViewImpl.prototype.print = function() { return this.executeScript({code: 'window.print();'}); }; WebViewImpl.prototype.removeContentScripts = function(names) { return WebViewInternal.removeContentScripts(this.viewInstanceId, names); }; WebViewImpl.prototype.setUserAgentOverride = function(userAgentOverride) { this.userAgentOverride = userAgentOverride; if (!this.guest.getId()) { // If we are not attached yet, then we will pick up the user agent on // attachment. return false; } WebViewInternal.overrideUserAgent(this.guest.getId(), userAgentOverride); return true; }; WebViewImpl.prototype.setZoom = function(zoomFactor, callback) { if (!this.guest.getId()) { this.cachedZoomFactor = zoomFactor; return false; } this.cachedZoomFactor = 1; WebViewInternal.setZoom(this.guest.getId(), zoomFactor, callback); return true; }; // ----------------------------------------------------------------------------- WebViewImpl.getApiMethods = function() { return WEB_VIEW_API_METHODS; }; // Copyright 2014 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. // This module implements the attributes of the tag. var GuestViewAttributes = require('guestViewAttributes').GuestViewAttributes; var WebViewConstants = require('webViewConstants').WebViewConstants; var WebViewImpl = require('webView').WebViewImpl; var WebViewInternal = require('webViewInternal').WebViewInternal; // ----------------------------------------------------------------------------- // AllowScalingAttribute object. // Attribute that specifies whether scaling is allowed in the webview. function AllowScalingAttribute(view) { GuestViewAttributes.BooleanAttribute.call( this, WebViewConstants.ATTRIBUTE_ALLOWSCALING, view); } AllowScalingAttribute.prototype.__proto__ = GuestViewAttributes.BooleanAttribute.prototype; AllowScalingAttribute.prototype.handleMutation = function(oldValue, newValue) { if (!this.view.guest.getId()) return; WebViewInternal.setAllowScaling(this.view.guest.getId(), this.getValue()); }; // ----------------------------------------------------------------------------- // AllowTransparencyAttribute object. // Attribute that specifies whether transparency is allowed in the webview. function AllowTransparencyAttribute(view) { GuestViewAttributes.BooleanAttribute.call( this, WebViewConstants.ATTRIBUTE_ALLOWTRANSPARENCY, view); } AllowTransparencyAttribute.prototype.__proto__ = GuestViewAttributes.BooleanAttribute.prototype; AllowTransparencyAttribute.prototype.handleMutation = function(oldValue, newValue) { if (!this.view.guest.getId()) return; WebViewInternal.setAllowTransparency(this.view.guest.getId(), this.getValue()); }; // ----------------------------------------------------------------------------- // AutosizeDimensionAttribute object. // Attribute used to define the demension limits of autosizing. function AutosizeDimensionAttribute(name, view) { GuestViewAttributes.IntegerAttribute.call(this, name, view); } AutosizeDimensionAttribute.prototype.__proto__ = GuestViewAttributes.IntegerAttribute.prototype; AutosizeDimensionAttribute.prototype.handleMutation = function( oldValue, newValue) { if (!this.view.guest.getId()) return; this.view.guest.setSize({ 'enableAutoSize': this.view.attributes[ WebViewConstants.ATTRIBUTE_AUTOSIZE].getValue(), 'min': { 'width': this.view.attributes[ WebViewConstants.ATTRIBUTE_MINWIDTH].getValue(), 'height': this.view.attributes[ WebViewConstants.ATTRIBUTE_MINHEIGHT].getValue() }, 'max': { 'width': this.view.attributes[ WebViewConstants.ATTRIBUTE_MAXWIDTH].getValue(), 'height': this.view.attributes[ WebViewConstants.ATTRIBUTE_MAXHEIGHT].getValue() } }); return; }; // ----------------------------------------------------------------------------- // AutosizeAttribute object. // Attribute that specifies whether the webview should be autosized. function AutosizeAttribute(view) { GuestViewAttributes.BooleanAttribute.call( this, WebViewConstants.ATTRIBUTE_AUTOSIZE, view); } AutosizeAttribute.prototype.__proto__ = GuestViewAttributes.BooleanAttribute.prototype; AutosizeAttribute.prototype.handleMutation = AutosizeDimensionAttribute.prototype.handleMutation; // ----------------------------------------------------------------------------- // NameAttribute object. // Attribute that sets the guest content's window.name object. function NameAttribute(view) { GuestViewAttributes.Attribute.call( this, WebViewConstants.ATTRIBUTE_NAME, view); } NameAttribute.prototype.__proto__ = GuestViewAttributes.Attribute.prototype NameAttribute.prototype.handleMutation = function(oldValue, newValue) { oldValue = oldValue || ''; newValue = newValue || ''; if (oldValue === newValue || !this.view.guest.getId()) return; WebViewInternal.setName(this.view.guest.getId(), newValue); }; NameAttribute.prototype.setValue = function(value) { value = value || ''; if (value === '') this.view.element.removeAttribute(this.name); else this.view.element.setAttribute(this.name, value); }; // ----------------------------------------------------------------------------- // PartitionAttribute object. // Attribute representing the state of the storage partition. function PartitionAttribute(view) { GuestViewAttributes.Attribute.call( this, WebViewConstants.ATTRIBUTE_PARTITION, view); this.validPartitionId = true; } PartitionAttribute.prototype.__proto__ = GuestViewAttributes.Attribute.prototype; PartitionAttribute.prototype.handleMutation = function(oldValue, newValue) { newValue = newValue || ''; // The partition cannot change if the webview has already navigated. if (!this.view.attributes[ WebViewConstants.ATTRIBUTE_SRC].beforeFirstNavigation) { window.console.error(WebViewConstants.ERROR_MSG_ALREADY_NAVIGATED); this.setValueIgnoreMutation(oldValue); return; } if (newValue == 'persist:') { this.validPartitionId = false; window.console.error( WebViewConstants.ERROR_MSG_INVALID_PARTITION_ATTRIBUTE); } }; PartitionAttribute.prototype.detach = function() { this.validPartitionId = true; }; // ----------------------------------------------------------------------------- // SrcAttribute object. // Attribute that handles the location and navigation of the webview. function SrcAttribute(view) { GuestViewAttributes.Attribute.call( this, WebViewConstants.ATTRIBUTE_SRC, view); this.setupMutationObserver(); this.beforeFirstNavigation = true; } SrcAttribute.prototype.__proto__ = GuestViewAttributes.Attribute.prototype; SrcAttribute.prototype.setValueIgnoreMutation = function(value) { GuestViewAttributes.Attribute.prototype.setValueIgnoreMutation.call( this, value); // takeRecords() is needed to clear queued up src mutations. Without it, it is // possible for this change to get picked up asyncronously by src's mutation // observer |observer|, and then get handled even though we do not want to // handle this mutation. this.observer.takeRecords(); } SrcAttribute.prototype.handleMutation = function(oldValue, newValue) { // Once we have navigated, we don't allow clearing the src attribute. // Once enters a navigated state, it cannot return to a // placeholder state. if (!newValue && oldValue) { // src attribute changes normally initiate a navigation. We suppress // the next src attribute handler call to avoid reloading the page // on every guest-initiated navigation. this.setValueIgnoreMutation(oldValue); return; } this.parse(); }; SrcAttribute.prototype.attach = function() { this.parse(); }; SrcAttribute.prototype.detach = function() { this.beforeFirstNavigation = true; }; // The purpose of this mutation observer is to catch assignment to the src // attribute without any changes to its value. This is useful in the case // where the webview guest has crashed and navigating to the same address // spawns off a new process. SrcAttribute.prototype.setupMutationObserver = function() { this.observer = new MutationObserver(function(mutations) { $Array.forEach(mutations, function(mutation) { var oldValue = mutation.oldValue; var newValue = this.getValue(); if (oldValue != newValue) { return; } this.handleMutation(oldValue, newValue); }.bind(this)); }.bind(this)); var params = { attributes: true, attributeOldValue: true, attributeFilter: [this.name] }; this.observer.observe(this.view.element, params); }; SrcAttribute.prototype.parse = function() { if (!this.view.elementAttached || !this.view.attributes[ WebViewConstants.ATTRIBUTE_PARTITION].validPartitionId || !this.getValue()) { return; } if (!this.view.guest.getId()) { if (this.beforeFirstNavigation) { this.beforeFirstNavigation = false; this.view.createGuest(); } return; } WebViewInternal.navigate(this.view.guest.getId(), this.getValue()); }; // ----------------------------------------------------------------------------- // Sets up all of the webview attributes. WebViewImpl.prototype.setupAttributes = function() { this.attributes[WebViewConstants.ATTRIBUTE_ALLOWSCALING] = new AllowScalingAttribute(this); this.attributes[WebViewConstants.ATTRIBUTE_ALLOWTRANSPARENCY] = new AllowTransparencyAttribute(this); this.attributes[WebViewConstants.ATTRIBUTE_AUTOSIZE] = new AutosizeAttribute(this); this.attributes[WebViewConstants.ATTRIBUTE_NAME] = new NameAttribute(this); this.attributes[WebViewConstants.ATTRIBUTE_PARTITION] = new PartitionAttribute(this); this.attributes[WebViewConstants.ATTRIBUTE_SRC] = new SrcAttribute(this); var autosizeAttributes = [WebViewConstants.ATTRIBUTE_MAXHEIGHT, WebViewConstants.ATTRIBUTE_MAXWIDTH, WebViewConstants.ATTRIBUTE_MINHEIGHT, WebViewConstants.ATTRIBUTE_MINWIDTH]; for (var i = 0; autosizeAttributes[i]; ++i) { this.attributes[autosizeAttributes[i]] = new AutosizeDimensionAttribute(autosizeAttributes[i], this); } }; // Copyright (c) 2014 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. // This module contains constants used in webview. // Container for the webview constants. var WebViewConstants = { // Attributes. ATTRIBUTE_ALLOWTRANSPARENCY: 'allowtransparency', ATTRIBUTE_ALLOWSCALING: 'allowscaling', ATTRIBUTE_AUTOSIZE: 'autosize', ATTRIBUTE_MAXHEIGHT: 'maxheight', ATTRIBUTE_MAXWIDTH: 'maxwidth', ATTRIBUTE_MINHEIGHT: 'minheight', ATTRIBUTE_MINWIDTH: 'minwidth', ATTRIBUTE_NAME: 'name', ATTRIBUTE_PARTITION: 'partition', ATTRIBUTE_SRC: 'src', // Error/warning messages. ERROR_MSG_ALREADY_NAVIGATED: ': ' + 'The object has already navigated, so its partition cannot be changed.', ERROR_MSG_CANNOT_INJECT_SCRIPT: ': ' + 'Script cannot be injected into content until the page has loaded.', ERROR_MSG_DIALOG_ACTION_ALREADY_TAKEN: ': ' + 'An action has already been taken for this "dialog" event.', ERROR_MSG_NEWWINDOW_ACTION_ALREADY_TAKEN: ': ' + 'An action has already been taken for this "newwindow" event.', ERROR_MSG_PERMISSION_ACTION_ALREADY_TAKEN: ': ' + 'Permission has already been decided for this "permissionrequest" event.', ERROR_MSG_INVALID_PARTITION_ATTRIBUTE: ': ' + 'Invalid partition attribute.', WARNING_MSG_DIALOG_REQUEST_BLOCKED: ': %1 %2 dialog was blocked.', WARNING_MSG_NEWWINDOW_REQUEST_BLOCKED: ': A new window was blocked.', WARNING_MSG_PERMISSION_REQUEST_BLOCKED: ': ' + 'The permission request for "%1" has been denied.' }; exports.$set('WebViewConstants', $Object.freeze(WebViewConstants)); // Copyright 2014 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. // Event management for WebView. var CreateEvent = require('guestViewEvents').CreateEvent; var DeclarativeWebRequestSchema = requireNative('schema_registry').GetSchema('declarativeWebRequest'); var EventBindings = require('event_bindings'); var GuestViewEvents = require('guestViewEvents').GuestViewEvents; var GuestViewInternalNatives = requireNative('guest_view_internal'); var IdGenerator = requireNative('id_generator'); var WebRequestEvent = require('webRequestInternal').WebRequestEvent; var WebRequestSchema = requireNative('schema_registry').GetSchema('webRequest'); var WebViewActionRequests = require('webViewActionRequests').WebViewActionRequests; var WebRequestMessageEvent = CreateEvent('webViewInternal.onMessage'); function WebViewEvents(webViewImpl) { GuestViewEvents.call(this, webViewImpl); this.setupWebRequestEvents(); this.view.maybeSetupContextMenus(); } WebViewEvents.prototype.__proto__ = GuestViewEvents.prototype; // A dictionary of extension events to be listened for. This // dictionary augments |GuestViewEvents.EVENTS| in guest_view_events.js. See the // documentation there for details. WebViewEvents.EVENTS = { 'close': { evt: CreateEvent('webViewInternal.onClose') }, 'consolemessage': { evt: CreateEvent('webViewInternal.onConsoleMessage'), fields: ['level', 'message', 'line', 'sourceId'] }, 'contentload': { evt: CreateEvent('webViewInternal.onContentLoad') }, 'dialog': { cancelable: true, evt: CreateEvent('webViewInternal.onDialog'), fields: ['defaultPromptText', 'messageText', 'messageType', 'url'], handler: 'handleDialogEvent' }, 'droplink': { evt: CreateEvent('webViewInternal.onDropLink'), fields: ['url'] }, 'exit': { evt: CreateEvent('webViewInternal.onExit'), fields: ['processId', 'reason'] }, 'exitfullscreen': { evt: CreateEvent('webViewInternal.onExitFullscreen'), fields: ['url'], handler: 'handleFullscreenExitEvent', internal: true }, 'findupdate': { evt: CreateEvent('webViewInternal.onFindReply'), fields: [ 'searchText', 'numberOfMatches', 'activeMatchOrdinal', 'selectionRect', 'canceled', 'finalUpdate' ] }, 'framenamechanged': { evt: CreateEvent('webViewInternal.onFrameNameChanged'), handler: 'handleFrameNameChangedEvent', internal: true }, 'loadabort': { cancelable: true, evt: CreateEvent('webViewInternal.onLoadAbort'), fields: ['url', 'isTopLevel', 'code', 'reason'], handler: 'handleLoadAbortEvent' }, 'loadcommit': { evt: CreateEvent('webViewInternal.onLoadCommit'), fields: ['url', 'isTopLevel'], handler: 'handleLoadCommitEvent' }, 'loadprogress': { evt: CreateEvent('webViewInternal.onLoadProgress'), fields: ['url', 'progress'] }, 'loadredirect': { evt: CreateEvent('webViewInternal.onLoadRedirect'), fields: ['isTopLevel', 'oldUrl', 'newUrl'] }, 'loadstart': { evt: CreateEvent('webViewInternal.onLoadStart'), fields: ['url', 'isTopLevel'] }, 'loadstop': { evt: CreateEvent('webViewInternal.onLoadStop') }, 'newwindow': { cancelable: true, evt: CreateEvent('webViewInternal.onNewWindow'), fields: [ 'initialHeight', 'initialWidth', 'targetUrl', 'windowOpenDisposition', 'name' ], handler: 'handleNewWindowEvent' }, 'permissionrequest': { cancelable: true, evt: CreateEvent('webViewInternal.onPermissionRequest'), fields: [ 'identifier', 'lastUnlockedBySelf', 'name', 'permission', 'requestMethod', 'url', 'userGesture' ], handler: 'handlePermissionEvent' }, 'responsive': { evt: CreateEvent('webViewInternal.onResponsive'), fields: ['processId'] }, 'sizechanged': { evt: CreateEvent('webViewInternal.onSizeChanged'), fields: ['oldHeight', 'oldWidth', 'newHeight', 'newWidth'], handler: 'handleSizeChangedEvent' }, 'unresponsive': { evt: CreateEvent('webViewInternal.onUnresponsive'), fields: ['processId'] }, 'zoomchange': { evt: CreateEvent('webViewInternal.onZoomChange'), fields: ['oldZoomFactor', 'newZoomFactor'] } }; WebViewEvents.prototype.setupWebRequestEvents = function() { var request = {}; var createWebRequestEvent = function(webRequestEvent) { return this.weakWrapper(function() { if (!this[webRequestEvent.name]) { this[webRequestEvent.name] = new WebRequestEvent( 'webViewInternal.' + webRequestEvent.name, webRequestEvent.parameters, webRequestEvent.extraParameters, webRequestEvent.options, this.view.viewInstanceId); } return this[webRequestEvent.name]; }); }.bind(this); var createDeclarativeWebRequestEvent = function(webRequestEvent) { return this.weakWrapper(function() { if (!this[webRequestEvent.name]) { // The onMessage event gets a special event type because we want // the listener to fire only for messages targeted for this particular // . var EventClass = webRequestEvent.name === 'onMessage' ? DeclarativeWebRequestEvent : EventBindings.Event; this[webRequestEvent.name] = new EventClass( 'webViewInternal.declarativeWebRequest.' + webRequestEvent.name, webRequestEvent.parameters, webRequestEvent.options, this.view.viewInstanceId); } return this[webRequestEvent.name]; }); }.bind(this); for (var i = 0; i < DeclarativeWebRequestSchema.events.length; ++i) { var eventSchema = DeclarativeWebRequestSchema.events[i]; var webRequestEvent = createDeclarativeWebRequestEvent(eventSchema); Object.defineProperty( request, eventSchema.name, { get: webRequestEvent, enumerable: true } ); } // Populate the WebRequest events from the API definition. for (var i = 0; i < WebRequestSchema.events.length; ++i) { var webRequestEvent = createWebRequestEvent(WebRequestSchema.events[i]); Object.defineProperty( request, WebRequestSchema.events[i].name, { get: webRequestEvent, enumerable: true } ); } this.view.setRequestPropertyOnWebViewElement(request); }; WebViewEvents.prototype.getEvents = function() { return WebViewEvents.EVENTS; }; WebViewEvents.prototype.handleDialogEvent = function(event, eventName) { var webViewEvent = this.makeDomEvent(event, eventName); new WebViewActionRequests.Dialog(this.view, event, webViewEvent); }; WebViewEvents.prototype.handleFrameNameChangedEvent = function(event) { this.view.onFrameNameChanged(event.name); }; WebViewEvents.prototype.handleFullscreenExitEvent = function(event, eventName) { document.webkitCancelFullScreen(); }; WebViewEvents.prototype.handleLoadAbortEvent = function(event, eventName) { var showWarningMessage = function(code, reason) { var WARNING_MSG_LOAD_ABORTED = ': ' + 'The load has aborted with error %1: %2.'; window.console.warn( WARNING_MSG_LOAD_ABORTED.replace('%1', code).replace('%2', reason)); }; var webViewEvent = this.makeDomEvent(event, eventName); if (this.view.dispatchEvent(webViewEvent)) { showWarningMessage(event.code, event.reason); } }; WebViewEvents.prototype.handleLoadCommitEvent = function(event, eventName) { this.view.onLoadCommit(event.baseUrlForDataUrl, event.currentEntryIndex, event.entryCount, event.processId, event.url, event.isTopLevel); var webViewEvent = this.makeDomEvent(event, eventName); this.view.dispatchEvent(webViewEvent); }; WebViewEvents.prototype.handleNewWindowEvent = function(event, eventName) { var webViewEvent = this.makeDomEvent(event, eventName); new WebViewActionRequests.NewWindow(this.view, event, webViewEvent); }; WebViewEvents.prototype.handlePermissionEvent = function(event, eventName) { var webViewEvent = this.makeDomEvent(event, eventName); if (event.permission === 'fullscreen') { new WebViewActionRequests.FullscreenPermissionRequest( this.view, event, webViewEvent); } else { new WebViewActionRequests.PermissionRequest(this.view, event, webViewEvent); } }; WebViewEvents.prototype.handleSizeChangedEvent = function(event, eventName) { var webViewEvent = this.makeDomEvent(event, eventName); this.view.onSizeChanged(webViewEvent); }; function DeclarativeWebRequestEvent(opt_eventName, opt_argSchemas, opt_eventOptions, opt_webViewInstanceId) { var subEventName = opt_eventName + '/' + IdGenerator.GetNextId(); EventBindings.Event.call(this, subEventName, opt_argSchemas, opt_eventOptions, opt_webViewInstanceId); var view = GuestViewInternalNatives.GetViewFromID(opt_webViewInstanceId || 0); if (!view) { return; } view.events.addScopedListener(WebRequestMessageEvent, function() { // Re-dispatch to subEvent's listeners. $Function.apply(this.dispatch, this, $Array.slice(arguments)); }.bind(this), {instanceId: opt_webViewInstanceId || 0}); } DeclarativeWebRequestEvent.prototype.__proto__ = EventBindings.Event.prototype; // Exports. exports.$set('WebViewEvents', WebViewEvents); // Copyright 2014 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. exports.$set( 'WebViewInternal', require('binding').Binding.create('webViewInternal').generate()); // Copyright (c) 2012 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. // This module implements WebView () as a custom element that wraps a // BrowserPlugin object element. The object element is hidden within // the shadow DOM of the WebView element. var DocumentNatives = requireNative('document_natives'); var GuestView = require('guestView').GuestView; var GuestViewContainer = require('guestViewContainer').GuestViewContainer; var GuestViewInternalNatives = requireNative('guest_view_internal'); var WebViewConstants = require('webViewConstants').WebViewConstants; var WebViewEvents = require('webViewEvents').WebViewEvents; var WebViewInternal = require('webViewInternal').WebViewInternal; // Represents the internal state of . function WebViewImpl(webviewElement) { GuestViewContainer.call(this, webviewElement, 'webview'); this.cachedZoom = 1; this.setupElementProperties(); new WebViewEvents(this, this.viewInstanceId); } WebViewImpl.prototype.__proto__ = GuestViewContainer.prototype; WebViewImpl.VIEW_TYPE = 'WebView'; // Add extra functionality to |this.element|. WebViewImpl.setupElement = function(proto) { // Public-facing API methods. var apiMethods = WebViewImpl.getApiMethods(); // Create default implementations for undefined API methods. var createDefaultApiMethod = function(m) { return function(var_args) { if (!this.guest.getId()) { return false; } var args = $Array.concat([this.guest.getId()], $Array.slice(arguments)); $Function.apply(WebViewInternal[m], null, args); return true; }; }; for (var i = 0; i != apiMethods.length; ++i) { if (WebViewImpl.prototype[apiMethods[i]] == undefined) { WebViewImpl.prototype[apiMethods[i]] = createDefaultApiMethod(apiMethods[i]); } } // Forward proto.foo* method calls to WebViewImpl.foo*. GuestViewContainer.forwardApiMethods(proto, apiMethods); }; // Initiates navigation once the element is attached to the DOM. WebViewImpl.prototype.onElementAttached = function() { // Mark all attributes as dirty on attachment. for (var i in this.attributes) { this.attributes[i].dirty = true; } for (var i in this.attributes) { this.attributes[i].attach(); } }; // Resets some state upon detaching element from the DOM. WebViewImpl.prototype.onElementDetached = function() { this.guest.destroy(); for (var i in this.attributes) { this.attributes[i].dirty = false; } for (var i in this.attributes) { this.attributes[i].detach(); } }; // Sets the .request property. WebViewImpl.prototype.setRequestPropertyOnWebViewElement = function(request) { Object.defineProperty( this.element, 'request', { value: request, enumerable: true } ); }; WebViewImpl.prototype.setupElementProperties = function() { // We cannot use {writable: true} property descriptor because we want a // dynamic getter value. Object.defineProperty(this.element, 'contentWindow', { get: function() { return this.guest.getContentWindow(); }.bind(this), // No setter. enumerable: true }); }; WebViewImpl.prototype.onSizeChanged = function(webViewEvent) { var newWidth = webViewEvent.newWidth; var newHeight = webViewEvent.newHeight; var element = this.element; var width = element.offsetWidth; var height = element.offsetHeight; // Check the current bounds to make sure we do not resize // outside of current constraints. var maxWidth = this.attributes[ WebViewConstants.ATTRIBUTE_MAXWIDTH].getValue() || width; var minWidth = this.attributes[ WebViewConstants.ATTRIBUTE_MINWIDTH].getValue() || width; var maxHeight = this.attributes[ WebViewConstants.ATTRIBUTE_MAXHEIGHT].getValue() || height; var minHeight = this.attributes[ WebViewConstants.ATTRIBUTE_MINHEIGHT].getValue() || height; minWidth = Math.min(minWidth, maxWidth); minHeight = Math.min(minHeight, maxHeight); if (!this.attributes[WebViewConstants.ATTRIBUTE_AUTOSIZE].getValue() || (newWidth >= minWidth && newWidth <= maxWidth && newHeight >= minHeight && newHeight <= maxHeight)) { element.style.width = newWidth + 'px'; element.style.height = newHeight + 'px'; // Only fire the DOM event if the size of the has actually // changed. this.dispatchEvent(webViewEvent); } }; WebViewImpl.prototype.createGuest = function() { this.guest.create(this.buildParams(), function() { this.attachWindow$(); }.bind(this)); }; WebViewImpl.prototype.onFrameNameChanged = function(name) { this.attributes[WebViewConstants.ATTRIBUTE_NAME].setValueIgnoreMutation(name); }; // Updates state upon loadcommit. WebViewImpl.prototype.onLoadCommit = function( baseUrlForDataUrl, currentEntryIndex, entryCount, processId, url, isTopLevel) { this.baseUrlForDataUrl = baseUrlForDataUrl; this.currentEntryIndex = currentEntryIndex; this.entryCount = entryCount; this.processId = processId; if (isTopLevel) { // Touching the src attribute triggers a navigation. To avoid // triggering a page reload on every guest-initiated navigation, // we do not handle this mutation. this.attributes[ WebViewConstants.ATTRIBUTE_SRC].setValueIgnoreMutation(url); } }; WebViewImpl.prototype.onAttach = function(storagePartitionId) { this.attributes[WebViewConstants.ATTRIBUTE_PARTITION].setValueIgnoreMutation( storagePartitionId); }; WebViewImpl.prototype.buildContainerParams = function() { var params = { 'initialZoomFactor': this.cachedZoomFactor, 'userAgentOverride': this.userAgentOverride }; for (var i in this.attributes) { var value = this.attributes[i].getValueIfDirty(); if (value) params[i] = value; } return params; }; WebViewImpl.prototype.attachWindow$ = function(opt_guestInstanceId) { // If |opt_guestInstanceId| was provided, then a different existing guest is // being attached to this webview, and the current one will get destroyed. if (opt_guestInstanceId) { if (this.guest.getId() == opt_guestInstanceId) { return true; } this.guest.destroy(); this.guest = new GuestView('webview', opt_guestInstanceId); } return GuestViewContainer.prototype.attachWindow$.call(this); }; // Shared implementation of executeScript() and insertCSS(). WebViewImpl.prototype.executeCode = function(func, args) { if (!this.guest.getId()) { window.console.error(WebViewConstants.ERROR_MSG_CANNOT_INJECT_SCRIPT); return false; } var webviewSrc = this.attributes[WebViewConstants.ATTRIBUTE_SRC].getValue(); if (this.baseUrlForDataUrl) { webviewSrc = this.baseUrlForDataUrl; } args = $Array.concat([this.guest.getId(), webviewSrc], $Array.slice(args)); $Function.apply(func, null, args); return true; } // Requests the element wihtin the embedder to enter fullscreen. WebViewImpl.prototype.makeElementFullscreen = function() { GuestViewInternalNatives.RunWithGesture(function() { this.element.webkitRequestFullScreen(); }.bind(this)); }; // Implemented when the ChromeWebView API is available. WebViewImpl.prototype.maybeSetupContextMenus = function() {}; GuestViewContainer.registerElement(WebViewImpl); // Exports. exports.$set('WebViewImpl', WebViewImpl); // Copyright 2014 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. // Custom binding for the chrome.app.runtime API. var binding = require('binding').Binding.create('app.runtime'); var AppViewGuestInternal = require('binding').Binding.create('appViewGuestInternal').generate(); var eventBindings = require('event_bindings'); var fileSystemHelpers = requireNative('file_system_natives'); var GetIsolatedFileSystem = fileSystemHelpers.GetIsolatedFileSystem; var entryIdManager = require('entryIdManager'); eventBindings.registerArgumentMassager('app.runtime.onEmbedRequested', function(args, dispatch) { var appEmbeddingRequest = args[0]; var id = appEmbeddingRequest.guestInstanceId; delete appEmbeddingRequest.guestInstanceId; appEmbeddingRequest.allow = function(url) { AppViewGuestInternal.attachFrame(url, id); }; appEmbeddingRequest.deny = function() { AppViewGuestInternal.denyRequest(id); }; dispatch([appEmbeddingRequest]); }); eventBindings.registerArgumentMassager('app.runtime.onLaunched', function(args, dispatch) { var launchData = args[0]; if (launchData.items) { // An onLaunched corresponding to file_handlers in the app's manifest. var items = []; var numItems = launchData.items.length; var itemLoaded = function(err, item) { if (err) { console.error('Error getting fileEntry, code: ' + err.code); } else { $Array.push(items, item); } if (--numItems === 0) { var data = { isKioskSession: launchData.isKioskSession, isPublicSession: launchData.isPublicSession, source: launchData.source, actionData: launchData.actionData }; if (items.length !== 0) { data.id = launchData.id; data.items = items; } dispatch([data]); } }; $Array.forEach(launchData.items, function(item) { var fs = GetIsolatedFileSystem(item.fileSystemId); if (item.isDirectory) { fs.root.getDirectory(item.baseName, {}, function(dirEntry) { entryIdManager.registerEntry(item.entryId, dirEntry); itemLoaded(null, {entry: dirEntry}); }, function(fileError) { itemLoaded(fileError); }); } else { fs.root.getFile(item.baseName, {}, function(fileEntry) { entryIdManager.registerEntry(item.entryId, fileEntry); itemLoaded(null, {entry: fileEntry, type: item.mimeType}); }, function(fileError) { itemLoaded(fileError); }); } }); } else { // Default case. This currently covers an onLaunched corresponding to // url_handlers in the app's manifest. dispatch([launchData]); } }); exports.$set('binding', binding.generate()); // Copyright (c) 2012 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. // Custom binding for the app_window API. var appWindowNatives = requireNative('app_window_natives'); var runtimeNatives = requireNative('runtime'); var Binding = require('binding').Binding; var Event = require('event_bindings').Event; var forEach = require('utils').forEach; var renderFrameObserverNatives = requireNative('renderFrameObserverNatives'); var appWindowData = null; var currentAppWindow = null; var currentWindowInternal = null; var kSetBoundsFunction = 'setBounds'; var kSetSizeConstraintsFunction = 'setSizeConstraints'; // Bounds class definition. var Bounds = function(boundsKey) { privates(this).boundsKey_ = boundsKey; }; Object.defineProperty(Bounds.prototype, 'left', { get: function() { return appWindowData[privates(this).boundsKey_].left; }, set: function(left) { this.setPosition(left, null); }, enumerable: true }); Object.defineProperty(Bounds.prototype, 'top', { get: function() { return appWindowData[privates(this).boundsKey_].top; }, set: function(top) { this.setPosition(null, top); }, enumerable: true }); Object.defineProperty(Bounds.prototype, 'width', { get: function() { return appWindowData[privates(this).boundsKey_].width; }, set: function(width) { this.setSize(width, null); }, enumerable: true }); Object.defineProperty(Bounds.prototype, 'height', { get: function() { return appWindowData[privates(this).boundsKey_].height; }, set: function(height) { this.setSize(null, height); }, enumerable: true }); Object.defineProperty(Bounds.prototype, 'minWidth', { get: function() { return appWindowData[privates(this).boundsKey_].minWidth; }, set: function(minWidth) { updateSizeConstraints(privates(this).boundsKey_, { minWidth: minWidth }); }, enumerable: true }); Object.defineProperty(Bounds.prototype, 'maxWidth', { get: function() { return appWindowData[privates(this).boundsKey_].maxWidth; }, set: function(maxWidth) { updateSizeConstraints(privates(this).boundsKey_, { maxWidth: maxWidth }); }, enumerable: true }); Object.defineProperty(Bounds.prototype, 'minHeight', { get: function() { return appWindowData[privates(this).boundsKey_].minHeight; }, set: function(minHeight) { updateSizeConstraints(privates(this).boundsKey_, { minHeight: minHeight }); }, enumerable: true }); Object.defineProperty(Bounds.prototype, 'maxHeight', { get: function() { return appWindowData[privates(this).boundsKey_].maxHeight; }, set: function(maxHeight) { updateSizeConstraints(privates(this).boundsKey_, { maxHeight: maxHeight }); }, enumerable: true }); Bounds.prototype.setPosition = function(left, top) { updateBounds(privates(this).boundsKey_, { left: left, top: top }); }; Bounds.prototype.setSize = function(width, height) { updateBounds(privates(this).boundsKey_, { width: width, height: height }); }; Bounds.prototype.setMinimumSize = function(minWidth, minHeight) { updateSizeConstraints(privates(this).boundsKey_, { minWidth: minWidth, minHeight: minHeight }); }; Bounds.prototype.setMaximumSize = function(maxWidth, maxHeight) { updateSizeConstraints(privates(this).boundsKey_, { maxWidth: maxWidth, maxHeight: maxHeight }); }; var appWindow = Binding.create('app.window'); appWindow.registerCustomHook(function(bindingsAPI) { var apiFunctions = bindingsAPI.apiFunctions; apiFunctions.setCustomCallback('create', function(name, request, callback, windowParams) { var view = null; // When window creation fails, |windowParams| will be undefined. if (windowParams && windowParams.frameId) { view = appWindowNatives.GetFrame( windowParams.frameId, true /* notifyBrowser */); } if (!view) { // No route to created window. If given a callback, trigger it with an // undefined object. if (callback) callback(); return; } if (windowParams.existingWindow) { // Not creating a new window, but activating an existing one, so trigger // callback with existing window and don't do anything else. if (callback) callback(view.chrome.app.window.current()); return; } // Initialize appWindowData in the newly created JS context if (view.chrome.app) { view.chrome.app.window.initializeAppWindow(windowParams); } else { var sandbox_window_message = 'Creating sandboxed window, it doesn\'t ' + 'have access to the chrome.app API.'; if (callback) { sandbox_window_message = sandbox_window_message + ' The chrome.app.window.create callback will be called, but ' + 'there will be no object provided for the sandboxed window.'; } console.warn(sandbox_window_message); } if (callback) { if (!view || !view.chrome.app /* sandboxed window */) { callback(undefined); return; } var willCallback = renderFrameObserverNatives.OnDocumentElementCreated( windowParams.frameId, function(success) { if (success) { callback(view.chrome.app.window.current()); } else { callback(undefined); } }); if (!willCallback) { callback(undefined); } } }); apiFunctions.setHandleRequest('current', function() { if (!currentAppWindow) { console.error('The JavaScript context calling ' + 'chrome.app.window.current() has no associated AppWindow.'); return null; } return currentAppWindow; }); apiFunctions.setHandleRequest('getAll', function() { var views = runtimeNatives.GetExtensionViews(-1, -1, 'APP_WINDOW'); return $Array.map(views, function(win) { return win.chrome.app.window.current(); }); }); apiFunctions.setHandleRequest('get', function(id) { var windows = $Array.filter(chrome.app.window.getAll(), function(win) { return win.id == id; }); return windows.length > 0 ? windows[0] : null; }); apiFunctions.setHandleRequest('canSetVisibleOnAllWorkspaces', function() { return /Mac/.test(navigator.platform) || /Linux/.test(navigator.userAgent); }); // This is an internal function, but needs to be bound into a closure // so the correct JS context is used for global variables such as // currentWindowInternal, appWindowData, etc. apiFunctions.setHandleRequest('initializeAppWindow', function(params) { currentWindowInternal = Binding.create('app.currentWindowInternal').generate(); var AppWindow = function() { this.innerBounds = new Bounds('innerBounds'); this.outerBounds = new Bounds('outerBounds'); }; forEach(currentWindowInternal, function(key, value) { // Do not add internal functions that should not appear in the AppWindow // interface. They are called by Bounds mutators. if (key !== kSetBoundsFunction && key !== kSetSizeConstraintsFunction) AppWindow.prototype[key] = value; }); AppWindow.prototype.moveTo = $Function.bind(window.moveTo, window); AppWindow.prototype.resizeTo = $Function.bind(window.resizeTo, window); AppWindow.prototype.contentWindow = window; AppWindow.prototype.onClosed = new Event(); AppWindow.prototype.close = function() { this.contentWindow.close(); }; AppWindow.prototype.getBounds = function() { // This is to maintain backcompatibility with a bug on Windows and // ChromeOS, which returns the position of the window but the size of // the content. var innerBounds = appWindowData.innerBounds; var outerBounds = appWindowData.outerBounds; return { left: outerBounds.left, top: outerBounds.top, width: innerBounds.width, height: innerBounds.height }; }; AppWindow.prototype.setBounds = function(bounds) { updateBounds('bounds', bounds); }; AppWindow.prototype.isFullscreen = function() { return appWindowData.fullscreen; }; AppWindow.prototype.isMinimized = function() { return appWindowData.minimized; }; AppWindow.prototype.isMaximized = function() { return appWindowData.maximized; }; AppWindow.prototype.isAlwaysOnTop = function() { return appWindowData.alwaysOnTop; }; AppWindow.prototype.alphaEnabled = function() { return appWindowData.alphaEnabled; }; Object.defineProperty(AppWindow.prototype, 'id', {get: function() { return appWindowData.id; }}); // These properties are for testing. Object.defineProperty( AppWindow.prototype, 'hasFrameColor', {get: function() { return appWindowData.hasFrameColor; }}); Object.defineProperty(AppWindow.prototype, 'activeFrameColor', {get: function() { return appWindowData.activeFrameColor; }}); Object.defineProperty(AppWindow.prototype, 'inactiveFrameColor', {get: function() { return appWindowData.inactiveFrameColor; }}); appWindowData = { id: params.id || '', innerBounds: { left: params.innerBounds.left, top: params.innerBounds.top, width: params.innerBounds.width, height: params.innerBounds.height, minWidth: params.innerBounds.minWidth, minHeight: params.innerBounds.minHeight, maxWidth: params.innerBounds.maxWidth, maxHeight: params.innerBounds.maxHeight }, outerBounds: { left: params.outerBounds.left, top: params.outerBounds.top, width: params.outerBounds.width, height: params.outerBounds.height, minWidth: params.outerBounds.minWidth, minHeight: params.outerBounds.minHeight, maxWidth: params.outerBounds.maxWidth, maxHeight: params.outerBounds.maxHeight }, fullscreen: params.fullscreen, minimized: params.minimized, maximized: params.maximized, alwaysOnTop: params.alwaysOnTop, hasFrameColor: params.hasFrameColor, activeFrameColor: params.activeFrameColor, inactiveFrameColor: params.inactiveFrameColor, alphaEnabled: params.alphaEnabled }; currentAppWindow = new AppWindow; }); }); function boundsEqual(bounds1, bounds2) { if (!bounds1 || !bounds2) return false; return (bounds1.left == bounds2.left && bounds1.top == bounds2.top && bounds1.width == bounds2.width && bounds1.height == bounds2.height); } function dispatchEventIfExists(target, name) { // Sometimes apps like to put their own properties on the window which // break our assumptions. var event = target[name]; if (event && (typeof event.dispatch == 'function')) event.dispatch(); else console.warn('Could not dispatch ' + name + ', event has been clobbered'); } function updateAppWindowProperties(update) { if (!appWindowData) return; var oldData = appWindowData; update.id = oldData.id; appWindowData = update; var currentWindow = currentAppWindow; if (!boundsEqual(oldData.innerBounds, update.innerBounds)) dispatchEventIfExists(currentWindow, "onBoundsChanged"); if (!oldData.fullscreen && update.fullscreen) dispatchEventIfExists(currentWindow, "onFullscreened"); if (!oldData.minimized && update.minimized) dispatchEventIfExists(currentWindow, "onMinimized"); if (!oldData.maximized && update.maximized) dispatchEventIfExists(currentWindow, "onMaximized"); if ((oldData.fullscreen && !update.fullscreen) || (oldData.minimized && !update.minimized) || (oldData.maximized && !update.maximized)) dispatchEventIfExists(currentWindow, "onRestored"); if (oldData.alphaEnabled !== update.alphaEnabled) dispatchEventIfExists(currentWindow, "onAlphaEnabledChanged"); }; function onAppWindowClosed() { if (!currentAppWindow) return; dispatchEventIfExists(currentAppWindow, "onClosed"); } function updateBounds(boundsType, bounds) { if (!currentWindowInternal) return; currentWindowInternal.setBounds(boundsType, bounds); } function updateSizeConstraints(boundsType, constraints) { if (!currentWindowInternal) return; forEach(constraints, function(key, value) { // From the perspective of the API, null is used to reset constraints. // We need to convert this to 0 because a value of null is interpreted // the same as undefined in the browser and leaves the constraint unchanged. if (value === null) constraints[key] = 0; }); currentWindowInternal.setSizeConstraints(boundsType, constraints); } exports.$set('binding', appWindow.generate()); exports.$set('onAppWindowClosed', onAppWindowClosed); exports.$set('updateAppWindowProperties', updateAppWindowProperties); // Copyright 2014 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. var Event = require('event_bindings').Event; var forEach = require('utils').forEach; // Note: Beware sneaky getters/setters when using GetAvailbility(). Use safe/raw // variables as arguments. var GetAvailability = requireNative('v8_context').GetAvailability; var exceptionHandler = require('uncaught_exception_handler'); var lastError = require('lastError'); var logActivity = requireNative('activityLogger'); var logging = requireNative('logging'); var process = requireNative('process'); var schemaRegistry = requireNative('schema_registry'); var schemaUtils = require('schemaUtils'); var utils = require('utils'); var sendRequestHandler = require('sendRequest'); var contextType = process.GetContextType(); var extensionId = process.GetExtensionId(); var manifestVersion = process.GetManifestVersion(); var sendRequest = sendRequestHandler.sendRequest; // Stores the name and definition of each API function, with methods to // modify their behaviour (such as a custom way to handle requests to the // API, a custom callback, etc). function APIFunctions(namespace) { this.apiFunctions_ = { __proto__: null }; this.unavailableApiFunctions_ = { __proto__: null }; this.namespace = namespace; } APIFunctions.prototype = { __proto__: null, }; APIFunctions.prototype.register = function(apiName, apiFunction) { this.apiFunctions_[apiName] = apiFunction; }; // Registers a function as existing but not available, meaning that calls to // the set* methods that reference this function should be ignored rather // than throwing Errors. APIFunctions.prototype.registerUnavailable = function(apiName) { this.unavailableApiFunctions_[apiName] = apiName; }; APIFunctions.prototype.setHook_ = function(apiName, propertyName, customizedFunction) { if ($Object.hasOwnProperty(this.unavailableApiFunctions_, apiName)) return; if (!$Object.hasOwnProperty(this.apiFunctions_, apiName)) throw new Error('Tried to set hook for unknown API "' + apiName + '"'); this.apiFunctions_[apiName][propertyName] = customizedFunction; }; APIFunctions.prototype.setHandleRequest = function(apiName, customizedFunction) { var prefix = this.namespace; return this.setHook_(apiName, 'handleRequest', function() { var ret = $Function.apply(customizedFunction, this, arguments); // Logs API calls to the Activity Log if it doesn't go through an // ExtensionFunction. if (!sendRequestHandler.getCalledSendRequest()) logActivity.LogAPICall(extensionId, prefix + "." + apiName, $Array.slice(arguments)); return ret; }); }; APIFunctions.prototype.setHandleRequestWithPromise = function(apiName, customizedFunction) { var prefix = this.namespace; return this.setHook_(apiName, 'handleRequest', function() { var name = prefix + '.' + apiName; logActivity.LogAPICall(extensionId, name, $Array.slice(arguments)); var stack = exceptionHandler.getExtensionStackTrace(); var callback = arguments[arguments.length - 1]; var args = $Array.slice(arguments, 0, arguments.length - 1); var keepAlivePromise = requireAsync('keep_alive').then(function(module) { return module.createKeepAlive(); }); $Function.apply(customizedFunction, this, args).then(function(result) { if (callback) { sendRequestHandler.safeCallbackApply(name, {'stack': stack}, callback, [result]); } }).catch(function(error) { if (callback) { var message = exceptionHandler.safeErrorToString(error, true); lastError.run(name, message, stack, callback); } }).then(function() { keepAlivePromise.then(function(keepAlive) { keepAlive.close(); }); }); }); }; APIFunctions.prototype.setUpdateArgumentsPostValidate = function(apiName, customizedFunction) { return this.setHook_( apiName, 'updateArgumentsPostValidate', customizedFunction); }; APIFunctions.prototype.setUpdateArgumentsPreValidate = function(apiName, customizedFunction) { return this.setHook_( apiName, 'updateArgumentsPreValidate', customizedFunction); }; APIFunctions.prototype.setCustomCallback = function(apiName, customizedFunction) { return this.setHook_(apiName, 'customCallback', customizedFunction); }; // Get the platform from navigator.appVersion. function getPlatform() { var platforms = [ [/CrOS Touch/, "chromeos touch"], [/CrOS/, "chromeos"], [/Linux/, "linux"], [/Mac/, "mac"], [/Win/, "win"], ]; for (var i = 0; i < platforms.length; i++) { if ($RegExp.exec(platforms[i][0], navigator.appVersion)) { return platforms[i][1]; } } return "unknown"; } function isPlatformSupported(schemaNode, platform) { return !schemaNode.platforms || $Array.indexOf(schemaNode.platforms, platform) > -1; } function isManifestVersionSupported(schemaNode, manifestVersion) { return !schemaNode.maximumManifestVersion || manifestVersion <= schemaNode.maximumManifestVersion; } function isSchemaNodeSupported(schemaNode, platform, manifestVersion) { return isPlatformSupported(schemaNode, platform) && isManifestVersionSupported(schemaNode, manifestVersion); } function createCustomType(type) { var jsModuleName = type.js_module; logging.CHECK(jsModuleName, 'Custom type ' + type.id + ' has no "js_module" property.'); // This list contains all types that has a js_module property. It is ugly to // hard-code them here, but the number of APIs that use js_module has not // changed since the introduction of js_modules in crbug.com/222156. // This whitelist serves as an extra line of defence to avoid exposing // arbitrary extension modules when the |type| definition is poisoned. var whitelistedModules = [ 'ChromeDirectSetting', 'ChromeSetting', 'ContentSetting', 'StorageArea', ]; logging.CHECK($Array.indexOf(whitelistedModules, jsModuleName) !== -1, 'Module ' + jsModuleName + ' does not define a custom type.'); var jsModule = require(jsModuleName); logging.CHECK(jsModule, 'No module ' + jsModuleName + ' found for ' + type.id + '.'); var customType = jsModule[jsModuleName]; logging.CHECK(customType, jsModuleName + ' must export itself.'); return customType; } var platform = getPlatform(); function Binding(apiName) { this.apiName_ = apiName; this.apiFunctions_ = new APIFunctions(apiName); this.customHooks_ = []; }; $Object.defineProperty(Binding, 'create', { __proto__: null, configurable: false, enumerable: false, value: function(apiName) { return new Binding(apiName); }, writable: false, }); Binding.prototype = { // Sneaky workaround for Object.prototype getters/setters - our prototype // isn't Object.prototype. SafeBuiltins (e.g. $Object.hasOwnProperty()) // should still work. __proto__: null, // Forward-declare properties. apiName_: undefined, apiFunctions_: undefined, customEvent_: undefined, customHooks_: undefined, // The API through which the ${api_name}_custom_bindings.js files customize // their API bindings beyond what can be generated. // // There are 2 types of customizations available: those which are required in // order to do the schema generation (registerCustomEvent and // registerCustomType), and those which can only run after the bindings have // been generated (registerCustomHook). // Registers a custom event type for the API identified by |namespace|. // |event| is the event's constructor. registerCustomEvent: function(event) { this.customEvent_ = event; }, // Registers a function |hook| to run after the schema for all APIs has been // generated. The hook is passed as its first argument an "API" object to // interact with, and second the current extension ID. See where // |customHooks| is used. registerCustomHook: function(fn) { $Array.push(this.customHooks_, fn); }, // TODO(kalman/cduvall): Refactor this so |runHooks_| is not needed. runHooks_: function(api, schema) { $Array.forEach(this.customHooks_, function(hook) { if (!isSchemaNodeSupported(schema, platform, manifestVersion)) return; if (!hook) return; hook({ __proto__: null, apiFunctions: this.apiFunctions_, schema: schema, compiledApi: api }, extensionId, contextType); }, this); }, // Generates the bindings from the schema for |this.apiName_| and integrates // any custom bindings that might be present. generate: function() { // NB: It's important to load the schema during generation rather than // setting it beforehand so that we're more confident the schema we're // loading is real, and not one that was injected by a page intercepting // Binding.generate. // Additionally, since the schema is an object returned from a native // handler, its properties don't have the custom getters/setters that a page // may have put on Object.prototype, and the object is frozen by v8. var schema = schemaRegistry.GetSchema(this.apiName_); function shouldCheckUnprivileged() { var shouldCheck = 'unprivileged' in schema; if (shouldCheck) return shouldCheck; $Array.forEach(['functions', 'events'], function(type) { if ($Object.hasOwnProperty(schema, type)) { $Array.forEach(schema[type], function(node) { if ('unprivileged' in node) shouldCheck = true; }); } }); if (shouldCheck) return shouldCheck; for (var property in schema.properties) { if ($Object.hasOwnProperty(schema, property) && 'unprivileged' in schema.properties[property]) { shouldCheck = true; break; } } return shouldCheck; } var checkUnprivileged = shouldCheckUnprivileged(); // TODO(kalman/cduvall): Make GetAvailability handle this, then delete the // supporting code. if (!isSchemaNodeSupported(schema, platform, manifestVersion)) { console.error('chrome.' + schema.namespace + ' is not supported on ' + 'this platform or manifest version'); return undefined; } var mod = {}; var namespaces = $String.split(schema.namespace, '.'); for (var index = 0, name; name = namespaces[index]; index++) { mod[name] = mod[name] || {}; mod = mod[name]; } if (schema.types) { $Array.forEach(schema.types, function(t) { if (!isSchemaNodeSupported(t, platform, manifestVersion)) return; // Add types to global schemaValidator; the types we depend on from // other namespaces will be added as needed. schemaUtils.schemaValidator.addTypes(t); // Generate symbols for enums. var enumValues = t['enum']; if (enumValues) { // Type IDs are qualified with the namespace during compilation, // unfortunately, so remove it here. logging.DCHECK($String.substr(t.id, 0, schema.namespace.length) == schema.namespace); // Note: + 1 because it ends in a '.', e.g., 'fooApi.Type'. var id = $String.substr(t.id, schema.namespace.length + 1); mod[id] = {}; $Array.forEach(enumValues, function(enumValue) { // Note: enums can be declared either as a list of strings // ['foo', 'bar'] or as a list of objects // [{'name': 'foo'}, {'name': 'bar'}]. enumValue = $Object.hasOwnProperty(enumValue, 'name') ? enumValue.name : enumValue; if (enumValue) { // Avoid setting any empty enums. // Make all properties in ALL_CAPS_STYLE. // // The built-in versions of $String.replace call other built-ins, // which may be clobbered. Instead, manually build the property // name. // // If the first character is a digit (we know it must be one of // a digit, a letter, or an underscore), precede it with an // underscore. var propertyName = ($RegExp.exec(/\d/, enumValue[0])) ? '_' : ''; for (var i = 0; i < enumValue.length; ++i) { var next; if (i > 0 && $RegExp.exec(/[a-z]/, enumValue[i-1]) && $RegExp.exec(/[A-Z]/, enumValue[i])) { // Replace myEnum-Foo with my_Enum-Foo: next = '_' + enumValue[i]; } else if ($RegExp.exec(/\W/, enumValue[i])) { // Replace my_Enum-Foo with my_Enum_Foo: next = '_'; } else { next = enumValue[i]; } propertyName += next; } // Uppercase (replace my_Enum_Foo with MY_ENUM_FOO): propertyName = $String.toUpperCase(propertyName); mod[id][propertyName] = enumValue; } }); } }, this); } // TODO(cduvall): Take out when all APIs have been converted to features. // Returns whether access to the content of a schema should be denied, // based on the presence of "unprivileged" and whether this is an // extension process (versus e.g. a content script). function isSchemaAccessAllowed(itemSchema) { return (contextType == 'BLESSED_EXTENSION') || schema.unprivileged || itemSchema.unprivileged; }; // Setup Functions. if (schema.functions) { $Array.forEach(schema.functions, function(functionDef) { if (functionDef.name in mod) { throw new Error('Function ' + functionDef.name + ' already defined in ' + schema.namespace); } if (!isSchemaNodeSupported(functionDef, platform, manifestVersion)) { this.apiFunctions_.registerUnavailable(functionDef.name); return; } var apiFunction = { __proto__: null }; apiFunction.definition = functionDef; apiFunction.name = schema.namespace + '.' + functionDef.name; if (!GetAvailability(apiFunction.name).is_available || (checkUnprivileged && !isSchemaAccessAllowed(functionDef))) { this.apiFunctions_.registerUnavailable(functionDef.name); return; } // TODO(aa): It would be best to run this in a unit test, but in order // to do that we would need to better factor this code so that it // doesn't depend on so much v8::Extension machinery. if (logging.DCHECK_IS_ON() && schemaUtils.isFunctionSignatureAmbiguous(apiFunction.definition)) { throw new Error( apiFunction.name + ' has ambiguous optional arguments. ' + 'To implement custom disambiguation logic, add ' + '"allowAmbiguousOptionalArguments" to the function\'s schema.'); } this.apiFunctions_.register(functionDef.name, apiFunction); mod[functionDef.name] = $Function.bind(function() { var args = $Array.slice(arguments); if (this.updateArgumentsPreValidate) args = $Function.apply(this.updateArgumentsPreValidate, this, args); args = schemaUtils.normalizeArgumentsAndValidate(args, this); if (this.updateArgumentsPostValidate) { args = $Function.apply(this.updateArgumentsPostValidate, this, args); } sendRequestHandler.clearCalledSendRequest(); var retval; if (this.handleRequest) { retval = $Function.apply(this.handleRequest, this, args); } else { var optArgs = { __proto__: null, customCallback: this.customCallback }; retval = sendRequest(this.name, args, this.definition.parameters, optArgs); } sendRequestHandler.clearCalledSendRequest(); // Validate return value if in sanity check mode. if (logging.DCHECK_IS_ON() && this.definition.returns) schemaUtils.validate([retval], [this.definition.returns]); return retval; }, apiFunction); }, this); } // Setup Events if (schema.events) { $Array.forEach(schema.events, function(eventDef) { if (eventDef.name in mod) { throw new Error('Event ' + eventDef.name + ' already defined in ' + schema.namespace); } if (!isSchemaNodeSupported(eventDef, platform, manifestVersion)) return; var eventName = schema.namespace + "." + eventDef.name; if (!GetAvailability(eventName).is_available || (checkUnprivileged && !isSchemaAccessAllowed(eventDef))) { return; } var options = eventDef.options || {}; if (eventDef.filters && eventDef.filters.length > 0) options.supportsFilters = true; var parameters = eventDef.parameters; if (this.customEvent_) { mod[eventDef.name] = new this.customEvent_( eventName, parameters, eventDef.extraParameters, options); } else { mod[eventDef.name] = new Event(eventName, parameters, options); } }, this); } function addProperties(m, parentDef) { var properties = parentDef.properties; if (!properties) return; forEach(properties, function(propertyName, propertyDef) { if (propertyName in m) return; // TODO(kalman): be strict like functions/events somehow. if (!isSchemaNodeSupported(propertyDef, platform, manifestVersion)) return; if (!GetAvailability(schema.namespace + "." + propertyName).is_available || (checkUnprivileged && !isSchemaAccessAllowed(propertyDef))) { return; } // |value| is eventually added to |m|, the exposed API. Make copies // of everything from the schema. (The schema is also frozen, so as long // as we don't make any modifications, shallow copies are fine.) var value; if ($Array.isArray(propertyDef.value)) value = $Array.slice(propertyDef.value); else if (typeof propertyDef.value === 'object') value = $Object.assign({}, propertyDef.value); else value = propertyDef.value; if (value) { // Values may just have raw types as defined in the JSON, such // as "WINDOW_ID_NONE": { "value": -1 }. We handle this here. // TODO(kalman): enforce that things with a "value" property can't // define their own types. var type = propertyDef.type || typeof(value); if (type === 'integer' || type === 'number') { value = parseInt(value); } else if (type === 'boolean') { value = value === 'true'; } else if (propertyDef['$ref']) { var ref = propertyDef['$ref']; var type = utils.loadTypeSchema(propertyDef['$ref'], schema); logging.CHECK(type, 'Schema for $ref type ' + ref + ' not found'); var constructor = createCustomType(type); var args = value; logging.DCHECK($Array.isArray(args)); $Array.push(args, type); // For an object propertyDef, |value| is an array of constructor // arguments, but we want to pass the arguments directly (i.e. // not as an array), so we have to fake calling |new| on the // constructor. value = { __proto__: constructor.prototype }; $Function.apply(constructor, value, args); // Recursively add properties. addProperties(value, propertyDef); } else if (type === 'object') { // Recursively add properties. addProperties(value, propertyDef); } else if (type !== 'string') { throw new Error('NOT IMPLEMENTED (extension_api.json error): ' + 'Cannot parse values for type "' + type + '"'); } m[propertyName] = value; } }); }; addProperties(mod, schema); // This generate() call is considered successful if any functions, // properties, or events were created. var success = ($Object.keys(mod).length > 0); // Special case: webViewRequest is a vacuous API which just copies its // implementation from declarativeWebRequest. // // TODO(kalman): This would be unnecessary if we did these checks after the // hooks (i.e. this.runHooks_(mod)). The reason we don't is to be very // conservative with running any JS which might actually be for an API // which isn't available, but this is probably overly cautious given the // C++ is only giving us APIs which are available. FIXME. if (schema.namespace == 'webViewRequest') { success = true; } // Special case: runtime.lastError is only occasionally set, so // specifically check its availability. if (schema.namespace == 'runtime' && GetAvailability('runtime.lastError').is_available) { success = true; } if (!success) { var availability = GetAvailability(schema.namespace); // If an API was available it should have been successfully generated. logging.DCHECK(!availability.is_available, schema.namespace + ' was available but not generated'); console.error('chrome.' + schema.namespace + ' is not available: ' + availability.message); return; } this.runHooks_(mod, schema); return mod; } }; exports.$set('Binding', Binding); // Copyright 2014 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. // Custom binding for the contextMenus API. var binding = require('binding').Binding.create('contextMenus'); var contextMenusHandlers = require('contextMenusHandlers'); binding.registerCustomHook(function(bindingsAPI) { var apiFunctions = bindingsAPI.apiFunctions; var handlers = contextMenusHandlers.create(false /* isWebview */); apiFunctions.setHandleRequest('create', handlers.requestHandlers.create); apiFunctions.setCustomCallback('create', handlers.callbacks.create); apiFunctions.setCustomCallback('remove', handlers.callbacks.remove); apiFunctions.setCustomCallback('update', handlers.callbacks.update); apiFunctions.setCustomCallback('removeAll', handlers.callbacks.removeAll); }); exports.$set('binding', binding.generate()); // Copyright 2015 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. // Implementation of custom bindings for the contextMenus API. // This is used to implement the contextMenus API for extensions and for the // tag (see chrome_web_view_experimental.js). var contextMenuNatives = requireNative('context_menus'); var sendRequest = require('sendRequest').sendRequest; var Event = require('event_bindings').Event; var lastError = require('lastError'); // Add the bindings to the contextMenus API. function createContextMenusHandlers(isWebview) { var eventName = isWebview ? 'webViewInternal.contextMenus' : 'contextMenus'; // Some dummy value for chrome.contextMenus instances. // Webviews use positive integers, and 0 to denote an invalid webview ID. // The following constant is -1 to avoid any conflicts between webview IDs and // extensions. var INSTANCEID_NON_WEBVIEW = -1; // Generates a customCallback for a given method. |handleCallback| will be // invoked with |request.args| as parameters. function createCustomCallback(handleCallback) { return function(name, request, callback) { if (lastError.hasError(chrome)) { if (callback) callback(); return; } var args = request.args; if (!isWebview) { // s have an extra item in front of the parameter list, which // specifies the viewInstanceId of the webview. This is used to hide // context menu events in one webview from another. // The non-webview chrome.contextMenus API is not called with such an // ID, so we prepend an ID to match the function signature. args = $Array.concat([INSTANCEID_NON_WEBVIEW], args); } $Function.apply(handleCallback, null, args); if (callback) callback(); }; } var contextMenus = {}; contextMenus.handlers = {}; contextMenus.event = new Event(eventName); contextMenus.getIdFromCreateProperties = function(createProperties) { if (typeof createProperties.id !== 'undefined') return createProperties.id; return createProperties.generatedId; }; contextMenus.handlersForId = function(instanceId, id) { if (!contextMenus.handlers[instanceId]) { contextMenus.handlers[instanceId] = { generated: {}, string: {} }; } if (typeof id === 'number') return contextMenus.handlers[instanceId].generated; return contextMenus.handlers[instanceId].string; }; contextMenus.ensureListenerSetup = function() { if (contextMenus.listening) { return; } contextMenus.listening = true; contextMenus.event.addListener(function(info) { var instanceId = INSTANCEID_NON_WEBVIEW; if (isWebview) { instanceId = info.webviewInstanceId; // Don't expose |webviewInstanceId| via the public API. delete info.webviewInstanceId; } var id = info.menuItemId; var onclick = contextMenus.handlersForId(instanceId, id)[id]; if (onclick) { $Function.apply(onclick, null, arguments); } }); }; // To be used with apiFunctions.setHandleRequest var requestHandlers = { __proto__: null }; // To be used with apiFunctions.setCustomCallback var callbacks = { __proto__: null }; requestHandlers.create = function() { var createProperties = isWebview ? arguments[1] : arguments[0]; createProperties.generatedId = contextMenuNatives.GetNextContextMenuId(); var optArgs = { __proto__: null, customCallback: this.customCallback, }; sendRequest(this.name, arguments, this.definition.parameters, optArgs); return contextMenus.getIdFromCreateProperties(createProperties); }; callbacks.create = createCustomCallback(function(instanceId, createProperties) { var id = contextMenus.getIdFromCreateProperties(createProperties); var onclick = createProperties.onclick; if (onclick) { contextMenus.ensureListenerSetup(); contextMenus.handlersForId(instanceId, id)[id] = onclick; } }); callbacks.remove = createCustomCallback(function(instanceId, id) { delete contextMenus.handlersForId(instanceId, id)[id]; }); callbacks.update = createCustomCallback(function(instanceId, id, updateProperties) { var onclick = updateProperties.onclick; if (onclick) { contextMenus.ensureListenerSetup(); contextMenus.handlersForId(instanceId, id)[id] = onclick; } else if (onclick === null) { // When onclick is explicitly set to null, remove the event listener. delete contextMenus.handlersForId(instanceId, id)[id]; } }); callbacks.removeAll = createCustomCallback(function(instanceId) { delete contextMenus.handlers[instanceId]; }); return { requestHandlers: requestHandlers, callbacks: callbacks }; } exports.$set('create', createContextMenusHandlers); // Copyright (c) 2012 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. // Custom binding for the declarativeWebRequest API. var binding = require('binding').Binding.create('declarativeWebRequest'); var utils = require('utils'); var validate = require('schemaUtils').validate; binding.registerCustomHook(function(api) { var declarativeWebRequest = api.compiledApi; // Returns the schema definition of type |typeId| defined in |namespace|. function getSchema(typeId) { return utils.lookup(api.schema.types, 'id', 'declarativeWebRequest.' + typeId); } // Helper function for the constructor of concrete datatypes of the // declarative webRequest API. // Makes sure that |this| contains the union of parameters and // {'instanceType': 'declarativeWebRequest.' + typeId} and validates the // generated union dictionary against the schema for |typeId|. function setupInstance(instance, parameters, typeId) { for (var key in parameters) { if ($Object.hasOwnProperty(parameters, key)) { instance[key] = parameters[key]; } } instance.instanceType = 'declarativeWebRequest.' + typeId; var schema = getSchema(typeId); validate([instance], [schema]); } // Setup all data types for the declarative webRequest API. declarativeWebRequest.RequestMatcher = function(parameters) { setupInstance(this, parameters, 'RequestMatcher'); }; declarativeWebRequest.CancelRequest = function(parameters) { setupInstance(this, parameters, 'CancelRequest'); }; declarativeWebRequest.RedirectRequest = function(parameters) { setupInstance(this, parameters, 'RedirectRequest'); }; declarativeWebRequest.SetRequestHeader = function(parameters) { setupInstance(this, parameters, 'SetRequestHeader'); }; declarativeWebRequest.RemoveRequestHeader = function(parameters) { setupInstance(this, parameters, 'RemoveRequestHeader'); }; declarativeWebRequest.AddResponseHeader = function(parameters) { setupInstance(this, parameters, 'AddResponseHeader'); }; declarativeWebRequest.RemoveResponseHeader = function(parameters) { setupInstance(this, parameters, 'RemoveResponseHeader'); }; declarativeWebRequest.RedirectToTransparentImage = function(parameters) { setupInstance(this, parameters, 'RedirectToTransparentImage'); }; declarativeWebRequest.RedirectToEmptyDocument = function(parameters) { setupInstance(this, parameters, 'RedirectToEmptyDocument'); }; declarativeWebRequest.RedirectByRegEx = function(parameters) { setupInstance(this, parameters, 'RedirectByRegEx'); }; declarativeWebRequest.IgnoreRules = function(parameters) { setupInstance(this, parameters, 'IgnoreRules'); }; declarativeWebRequest.AddRequestCookie = function(parameters) { setupInstance(this, parameters, 'AddRequestCookie'); }; declarativeWebRequest.AddResponseCookie = function(parameters) { setupInstance(this, parameters, 'AddResponseCookie'); }; declarativeWebRequest.EditRequestCookie = function(parameters) { setupInstance(this, parameters, 'EditRequestCookie'); }; declarativeWebRequest.EditResponseCookie = function(parameters) { setupInstance(this, parameters, 'EditResponseCookie'); }; declarativeWebRequest.RemoveRequestCookie = function(parameters) { setupInstance(this, parameters, 'RemoveRequestCookie'); }; declarativeWebRequest.RemoveResponseCookie = function(parameters) { setupInstance(this, parameters, 'RemoveResponseCookie'); }; declarativeWebRequest.SendMessageToExtension = function(parameters) { setupInstance(this, parameters, 'SendMessageToExtension'); }; }); exports.$set('binding', binding.generate()); // Copyright 2015 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. // Custom binding for the Display Source API. var binding = require('binding').Binding.create('displaySource'); var chrome = requireNative('chrome').GetChrome(); var lastError = require('lastError'); var natives = requireNative('display_source'); var logging = requireNative('logging'); var callbacksInfo = {}; function callbackWrapper(callback, method, message) { if (callback == undefined) return; try { if (message !== null) lastError.set(method, message, null, chrome); callback(); } finally { lastError.clear(chrome); } } function callCompletionCallback(callbackId, error_message) { try { var callbackInfo = callbacksInfo[callbackId]; logging.DCHECK(callbackInfo != null); callbackWrapper(callbackInfo.callback, callbackInfo.method, error_message); } finally { delete callbacksInfo[callbackId]; } } binding.registerCustomHook(function(bindingsAPI, extensionId) { var apiFunctions = bindingsAPI.apiFunctions; apiFunctions.setHandleRequest( 'startSession', function(sessionInfo, callback) { try { var callId = natives.StartSession(sessionInfo); callbacksInfo[callId] = { callback: callback, method: 'displaySource.startSession' }; } catch (e) { callbackWrapper(callback, 'displaySource.startSession', e.message); } }); apiFunctions.setHandleRequest( 'terminateSession', function(sink_id, callback) { try { var callId = natives.TerminateSession(sink_id); callbacksInfo[callId] = { callback: callback, method: 'displaySource.terminateSession' }; } catch (e) { callbackWrapper( callback, 'displaySource.terminateSession', e.message); } }); }); exports.$set('binding', binding.generate()); // Called by C++. exports.$set('callCompletionCallback', callCompletionCallback); // Copyright 2014 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. // Custom binding for the extension API. var binding = require('binding').Binding.create('extension'); var messaging = require('messaging'); var runtimeNatives = requireNative('runtime'); var GetExtensionViews = runtimeNatives.GetExtensionViews; var chrome = requireNative('chrome').GetChrome(); var inIncognitoContext = requireNative('process').InIncognitoContext(); var sendRequestIsDisabled = requireNative('process').IsSendRequestDisabled(); var contextType = requireNative('process').GetContextType(); var manifestVersion = requireNative('process').GetManifestVersion(); // This should match chrome.windows.WINDOW_ID_NONE. // // We can't use chrome.windows.WINDOW_ID_NONE directly because the // chrome.windows API won't exist unless this extension has permission for it; // which may not be the case. var WINDOW_ID_NONE = -1; var TAB_ID_NONE = -1; binding.registerCustomHook(function(bindingsAPI, extensionId) { var extension = bindingsAPI.compiledApi; if (manifestVersion < 2) { chrome.self = extension; extension.inIncognitoTab = inIncognitoContext; } extension.inIncognitoContext = inIncognitoContext; var apiFunctions = bindingsAPI.apiFunctions; apiFunctions.setHandleRequest('getViews', function(properties) { var windowId = WINDOW_ID_NONE; var tabId = TAB_ID_NONE; var type = 'ALL'; if (properties) { if (properties.type != null) { type = properties.type; } if (properties.windowId != null) { windowId = properties.windowId; } if (properties.tabId != null) { tabId = properties.tabId; } } return GetExtensionViews(windowId, tabId, type); }); apiFunctions.setHandleRequest('getBackgroundPage', function() { return GetExtensionViews(-1, -1, 'BACKGROUND')[0] || null; }); apiFunctions.setHandleRequest('getExtensionTabs', function(windowId) { if (windowId == null) windowId = WINDOW_ID_NONE; return GetExtensionViews(windowId, -1, 'TAB'); }); apiFunctions.setHandleRequest('getURL', function(path) { path = String(path); if (!path.length || path[0] != '/') path = '/' + path; return 'chrome-extension://' + extensionId + path; }); // Alias several messaging deprecated APIs to their runtime counterparts. var mayNeedAlias = [ // Types 'Port', // Functions 'connect', 'sendMessage', 'connectNative', 'sendNativeMessage', // Events 'onConnect', 'onConnectExternal', 'onMessage', 'onMessageExternal' ]; $Array.forEach(mayNeedAlias, function(alias) { // Checking existence isn't enough since some functions are disabled via // getters that throw exceptions. Assume that any getter is such a function. if (chrome.runtime && $Object.hasOwnProperty(chrome.runtime, alias) && chrome.runtime.__lookupGetter__(alias) === undefined) { extension[alias] = chrome.runtime[alias]; } }); apiFunctions.setUpdateArgumentsPreValidate('sendRequest', $Function.bind(messaging.sendMessageUpdateArguments, null, 'sendRequest', false /* hasOptionsArgument */)); apiFunctions.setHandleRequest('sendRequest', function(targetId, request, responseCallback) { if (sendRequestIsDisabled) throw new Error(sendRequestIsDisabled); var port = chrome.runtime.connect(targetId || extensionId, {name: messaging.kRequestChannel}); messaging.sendMessageImpl(port, request, responseCallback); }); if (sendRequestIsDisabled) { extension.onRequest.addListener = function() { throw new Error(sendRequestIsDisabled); }; if (contextType == 'BLESSED_EXTENSION') { extension.onRequestExternal.addListener = function() { throw new Error(sendRequestIsDisabled); }; } } }); exports.$set('binding', binding.generate()); // Copyright 2014 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. // ----------------------------------------------------------------------------- // NOTE: If you change this file you need to touch renderer_resources.grd to // have your change take effect. // ----------------------------------------------------------------------------- // Partial implementation of the Greasemonkey API, see: // http://wiki.greasespot.net/Greasemonkey_Manual:APIs function GM_addStyle(css) { var parent = document.getElementsByTagName("head")[0]; if (!parent) { parent = document.documentElement; } var style = document.createElement("style"); style.type = "text/css"; var textNode = document.createTextNode(css); style.appendChild(textNode); parent.appendChild(style); } function GM_xmlhttpRequest(details) { function setupEvent(xhr, url, eventName, callback) { xhr[eventName] = function () { var isComplete = xhr.readyState == 4; var responseState = { responseText: xhr.responseText, readyState: xhr.readyState, responseHeaders: isComplete ? xhr.getAllResponseHeaders() : "", status: isComplete ? xhr.status : 0, statusText: isComplete ? xhr.statusText : "", finalUrl: isComplete ? url : "" }; callback(responseState); }; } var xhr = new XMLHttpRequest(); var eventNames = ["onload", "onerror", "onreadystatechange"]; for (var i = 0; i < eventNames.length; i++ ) { var eventName = eventNames[i]; if (eventName in details) { setupEvent(xhr, details.url, eventName, details[eventName]); } } xhr.open(details.method, details.url); if (details.overrideMimeType) { xhr.overrideMimeType(details.overrideMimeType); } if (details.headers) { for (var header in details.headers) { xhr.setRequestHeader(header, details.headers[header]); } } xhr.send(details.data ? details.data : null); } function GM_openInTab(url) { window.open(url, ""); } function GM_log(message) { window.console.log(message); } (function() { function generateGreasemonkeyStub(name) { return function() { console.log("%s is not supported.", name); }; } var apis = ["GM_getValue", "GM_setValue", "GM_registerMenuCommand"]; for (var i = 0, api; api = apis[i]; i++) { window[api] = generateGreasemonkeyStub(api); } })(); // Copyright 2014 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. // Custom binding for the i18n API. var binding = require('binding').Binding.create('i18n'); var i18nNatives = requireNative('i18n'); var GetL10nMessage = i18nNatives.GetL10nMessage; var GetL10nUILanguage = i18nNatives.GetL10nUILanguage; var DetectTextLanguage = i18nNatives.DetectTextLanguage; binding.registerCustomHook(function(bindingsAPI, extensionId) { var apiFunctions = bindingsAPI.apiFunctions; apiFunctions.setUpdateArgumentsPreValidate('getMessage', function() { var args = $Array.slice(arguments); // The first argument is the message, and should be a string. var message = args[0]; if (typeof(message) !== 'string') { console.warn(extensionId + ': the first argument to getMessage should ' + 'be type "string", was ' + message + ' (type "' + typeof(message) + '")'); args[0] = String(message); } return args; }); apiFunctions.setHandleRequest('getMessage', function(messageName, substitutions) { return GetL10nMessage(messageName, substitutions, extensionId); }); apiFunctions.setHandleRequest('getUILanguage', function() { return GetL10nUILanguage(); }); apiFunctions.setHandleRequest('detectLanguage', function(text, callback) { window.setTimeout(function() { var response = DetectTextLanguage(text); callback(response); }, 0); }); }); exports.$set('binding', binding.generate()); // Copyright 2015 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. /** * Custom bindings for the mojoPrivate API. */ let binding = require('binding').Binding.create('mojoPrivate'); binding.registerCustomHook(function(bindingsAPI) { let apiFunctions = bindingsAPI.apiFunctions; apiFunctions.setHandleRequest('define', function(name, deps, factory) { define(name, deps || [], factory); }); apiFunctions.setHandleRequest('requireAsync', function(moduleName) { return requireAsync(moduleName); }); }); exports.$set('binding', binding.generate()); // Copyright 2014 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. // Custom binding for the Permissions API. var binding = require('binding').Binding.create('permissions'); var Event = require('event_bindings').Event; // These custom binding are only necessary because it is not currently // possible to have a union of types as the type of the items in an array. // Once that is fixed, this entire file should go away. // See, // https://code.google.com/p/chromium/issues/detail?id=162044 // https://code.google.com/p/chromium/issues/detail?id=162042 // TODO(bryeung): delete this file. binding.registerCustomHook(function(api) { var apiFunctions = api.apiFunctions; var permissions = api.compiledApi; function maybeConvertToObject(str) { var parts = $String.split(str, '|'); if (parts.length != 2) return str; var ret = {}; ret[parts[0]] = JSON.parse(parts[1]); return ret; } function convertObjectPermissionsToStrings() { if (arguments.length < 1) return arguments; var args = arguments[0].permissions; if (!args) return arguments; for (var i = 0; i < args.length; i += 1) { if (typeof(args[i]) == 'object') { var a = args[i]; var keys = $Object.keys(a); if (keys.length != 1) { throw new Error("Too many keys in object-style permission."); } arguments[0].permissions[i] = keys[0] + '|' + JSON.stringify(a[keys[0]]); } } return arguments; } // Convert complex permissions to strings so they validate against the schema apiFunctions.setUpdateArgumentsPreValidate( 'contains', convertObjectPermissionsToStrings); apiFunctions.setUpdateArgumentsPreValidate( 'remove', convertObjectPermissionsToStrings); apiFunctions.setUpdateArgumentsPreValidate( 'request', convertObjectPermissionsToStrings); // Convert complex permissions back to objects apiFunctions.setCustomCallback('getAll', function(name, request, callback, response) { for (var i = 0; i < response.permissions.length; i += 1) { response.permissions[i] = maybeConvertToObject(response.permissions[i]); } // Since the schema says Permissions.permissions contains strings and // not objects, validation will fail after the for-loop above. This // skips validation and calls the callback directly. if (callback) callback(response); }); // Also convert complex permissions back to objects for events. The // dispatchToListener call happens after argument validation, which works // around the problem that Permissions.permissions is supposed to be a list // of strings. permissions.onAdded.dispatchToListener = function(callback, args) { for (var i = 0; i < args[0].permissions.length; i += 1) { args[0].permissions[i] = maybeConvertToObject(args[0].permissions[i]); } $Function.call(Event.prototype.dispatchToListener, this, callback, args); }; permissions.onRemoved.dispatchToListener = permissions.onAdded.dispatchToListener; }); exports.$set('binding', binding.generate()); // Copyright 2015 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. var binding = require('binding').Binding.create('printerProvider'); var printerProviderInternal = require('binding').Binding.create( 'printerProviderInternal').generate(); var eventBindings = require('event_bindings'); var blobNatives = requireNative('blob_natives'); var printerProviderSchema = requireNative('schema_registry').GetSchema('printerProvider') var utils = require('utils'); var validate = require('schemaUtils').validate; // Custom bindings for chrome.printerProvider API. // The bindings are used to implement callbacks for the API events. Internally // each event is passed requestId argument used to identify the callback // associated with the event. This argument is massaged out from the event // arguments before dispatching the event to consumers. A callback is appended // to the event arguments. The callback wraps an appropriate // chrome.printerProviderInternal API function that is used to report the event // result from the extension. The function is passed requestId and values // provided by the extension. It validates that the values provided by the // extension match chrome.printerProvider event callback schemas. It also // ensures that a callback is run at most once. In case there is an exception // during event dispatching, the chrome.printerProviderInternal function // is called with a default error value. // // Handles a chrome.printerProvider event as described in the file comment. // |eventName|: The event name. // |prepareArgsForDispatch|: Function called before dispatching the event to // the extension. It's called with original event |args| list and callback // that should be called when the |args| are ready for dispatch. The // callbacks should report whether the argument preparation was successful. // The function should not change the first argument, which contains the // request id. // |resultreporter|: The function that should be called to report event result. // One of chrome.printerProviderInternal API functions. function handleEvent(eventName, prepareArgsForDispatch, resultReporter) { eventBindings.registerArgumentMassager( 'printerProvider.' + eventName, function(args, dispatch) { var responded = false; // Validates that the result passed by the extension to the event // callback matches the callback schema. Throws an exception in case of // an error. var validateResult = function(result) { var eventSchema = utils.lookup(printerProviderSchema.events, 'name', eventName); var callbackSchema = utils.lookup(eventSchema.parameters, 'type', 'function'); validate([result], callbackSchema.parameters); }; // Function provided to the extension as the event callback argument. // It makes sure that the event result hasn't previously been returned // and that the provided result matches the callback schema. In case of // an error it throws an exception. var reportResult = function(result) { if (responded) { throw new Error( 'Event callback must not be called more than once.'); } var finalResult = null; try { validateResult(result); // throws on failure finalResult = result; } finally { responded = true; resultReporter(args[0] /* requestId */, finalResult); } }; prepareArgsForDispatch(args, function(success) { if (!success) { // Do not throw an exception since the extension should not yet be // aware of the event. resultReporter(args[0] /* requestId */, null); return; } dispatch(args.slice(1).concat(reportResult)); }); }); } // Sets up printJob.document property for a print request. function createPrintRequestBlobArguments(args, callback) { printerProviderInternal.getPrintData(args[0] /* requestId */, function(blobInfo) { if (chrome.runtime.lastError) { callback(false); return; } // |args[1]| is printJob. args[1].document = blobNatives.TakeBrowserProcessBlob( blobInfo.blobUuid, blobInfo.type, blobInfo.size); callback(true); }); } handleEvent('onGetPrintersRequested', function(args, callback) { callback(true); }, printerProviderInternal.reportPrinters); handleEvent('onGetCapabilityRequested', function(args, callback) { callback(true); }, printerProviderInternal.reportPrinterCapability); handleEvent('onPrintRequested', createPrintRequestBlobArguments, printerProviderInternal.reportPrintResult); handleEvent('onGetUsbPrinterInfoRequested', function(args, callback) { callback(true); }, printerProviderInternal.reportUsbPrinterInfo); exports.$set('binding', binding.generate()); // Copyright 2014 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. // Custom binding for the runtime API. var binding = apiBridge || require('binding').Binding.create('runtime'); var messaging = require('messaging'); var runtimeNatives = requireNative('runtime'); var messagingNatives = requireNative('messaging_natives'); var process = requireNative('process'); var utils = require('utils'); var WINDOW = {}; try { WINDOW = window; } catch (e) { // Running in SW context. // TODO(lazyboy): Synchronous access to background page is not possible from // service worker context. Decide what we should do in this case for the class // of APIs that require access to background page or window object } var backgroundPage = WINDOW; var backgroundRequire = require; var contextType = process.GetContextType(); if (contextType == 'BLESSED_EXTENSION' || contextType == 'UNBLESSED_EXTENSION') { var manifest = runtimeNatives.GetManifest(); if (manifest.app && manifest.app.background) { // Get the background page if one exists. Otherwise, default to the current // window. backgroundPage = runtimeNatives.GetExtensionViews(-1, -1, 'BACKGROUND')[0]; if (backgroundPage) { var GetModuleSystem = requireNative('v8_context').GetModuleSystem; backgroundRequire = GetModuleSystem(backgroundPage).require; } else { backgroundPage = WINDOW; } } } // For packaged apps, all windows use the bindFileEntryCallback from the // background page so their FileEntry objects have the background page's context // as their own. This allows them to be used from other windows (including the // background page) after the original window is closed. if (WINDOW == backgroundPage) { var lastError = require('lastError'); var fileSystemNatives = requireNative('file_system_natives'); var GetIsolatedFileSystem = fileSystemNatives.GetIsolatedFileSystem; var bindDirectoryEntryCallback = function(functionName, apiFunctions) { apiFunctions.setCustomCallback(functionName, function(name, request, callback, response) { if (callback) { if (!response) { callback(); return; } var fileSystemId = response.fileSystemId; var baseName = response.baseName; var fs = GetIsolatedFileSystem(fileSystemId); try { fs.root.getDirectory(baseName, {}, callback, function(fileError) { lastError.run('runtime.' + functionName, 'Error getting Entry, code: ' + fileError.code, request.stack, callback); }); } catch (e) { lastError.run('runtime.' + functionName, 'Error: ' + e.stack, request.stack, callback); } } }); }; } else { // Force the runtime API to be loaded in the background page. Using // backgroundPageModuleSystem.require('runtime') is insufficient as // requireNative is only allowed while lazily loading an API. backgroundPage.chrome.runtime; var bindDirectoryEntryCallback = backgroundRequire('runtime').bindDirectoryEntryCallback; } binding.registerCustomHook(function(binding, id, contextType) { var apiFunctions = binding.apiFunctions; var runtime = binding.compiledApi; // // Unprivileged APIs. // if (id != '') utils.defineProperty(runtime, 'id', id); apiFunctions.setHandleRequest('getManifest', function() { return runtimeNatives.GetManifest(); }); apiFunctions.setHandleRequest('getURL', function(path) { path = $String.self(path); if (!path.length || path[0] != '/') path = '/' + path; return 'chrome-extension://' + id + path; }); var sendMessageUpdateArguments = messaging.sendMessageUpdateArguments; apiFunctions.setUpdateArgumentsPreValidate( 'sendMessage', $Function.bind(sendMessageUpdateArguments, null, 'sendMessage', true /* hasOptionsArgument */)); apiFunctions.setUpdateArgumentsPreValidate( 'sendNativeMessage', $Function.bind(sendMessageUpdateArguments, null, 'sendNativeMessage', false /* hasOptionsArgument */)); apiFunctions.setHandleRequest( 'sendMessage', function(targetId, message, options, responseCallback) { var connectOptions = $Object.assign({ __proto__: null, name: messaging.kMessageChannel, }, options); var port = runtime.connect(targetId, connectOptions); messaging.sendMessageImpl(port, message, responseCallback); }); apiFunctions.setHandleRequest('sendNativeMessage', function(targetId, message, responseCallback) { var port = runtime.connectNative(targetId); messaging.sendMessageImpl(port, message, responseCallback); }); apiFunctions.setHandleRequest('connect', function(targetId, connectInfo) { if (!targetId) { // id is only defined inside extensions. If we're in a webpage, the best // we can do at this point is to fail. if (!id) { throw new Error('chrome.runtime.connect() called from a webpage must ' + 'specify an Extension ID (string) for its first ' + 'argument'); } targetId = id; } var name = ''; if (connectInfo && connectInfo.name) name = connectInfo.name; var includeTlsChannelId = !!(connectInfo && connectInfo.includeTlsChannelId); var portId = messagingNatives.OpenChannelToExtension(targetId, name, includeTlsChannelId); if (portId >= 0) return messaging.createPort(portId, name); }); // // Privileged APIs. // if (contextType != 'BLESSED_EXTENSION') return; apiFunctions.setHandleRequest('connectNative', function(nativeAppName) { var portId = messagingNatives.OpenChannelToNativeApp(nativeAppName); if (portId >= 0) return messaging.createPort(portId, ''); throw new Error('Error connecting to native app: ' + nativeAppName); }); apiFunctions.setCustomCallback('getBackgroundPage', function(name, request, callback, response) { if (callback) { var bg = runtimeNatives.GetExtensionViews(-1, -1, 'BACKGROUND')[0] || null; callback(bg); } }); bindDirectoryEntryCallback('getPackageDirectoryEntry', apiFunctions); }); exports.$set('bindDirectoryEntryCallback', bindDirectoryEntryCallback); if (!apiBridge) exports.$set('binding', binding.generate()); // Copyright 2015 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. // This function is returned to DidInitializeServiceWorkerContextOnWorkerThread // then executed, passing in dependencies as function arguments. // // |backgroundUrl| is the URL of the extension's background page. // |wakeEventPage| is a function that wakes up the current extension's event // page, then runs its callback on completion or failure. // |logging| is an object equivalent to a subset of base/debug/logging.h, with // CHECK/DCHECK/etc. (function(backgroundUrl, wakeEventPage, logging) { 'use strict'; self.chrome = self.chrome || {}; self.chrome.runtime = self.chrome.runtime || {}; // Returns a Promise that resolves to the background page's client, or null // if there is no background client. function findBackgroundClient() { return self.clients.matchAll({ includeUncontrolled: true, type: 'window' }).then(function(clients) { return clients.find(function(client) { return client.url == backgroundUrl; }); }); } // Returns a Promise wrapper around wakeEventPage, that resolves on success, // or rejects on failure. function makeWakeEventPagePromise() { return new Promise(function(resolve, reject) { wakeEventPage(function(success) { if (success) resolve(); else reject('Failed to start background client "' + backgroundUrl + '"'); }); }); } // The chrome.runtime.getBackgroundClient function is documented in // runtime.json. It returns a Promise that resolves to the background page's // client, or is rejected if there is no background client or if the // background client failed to wake. self.chrome.runtime.getBackgroundClient = function() { return findBackgroundClient().then(function(client) { if (client) { // Background client is already awake, or it was persistent. return client; } // Event page needs to be woken. return makeWakeEventPagePromise().then(function() { return findBackgroundClient(); }).then(function(client) { if (!client) { return Promise.reject( 'Background client "' + backgroundUrl + '" not found'); } return client; }); }); }; }); // Copyright (c) 2012 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. // Custom binding for the webRequest API. var binding = require('binding').Binding.create('webRequest'); var sendRequest = require('sendRequest').sendRequest; var WebRequestEvent = require('webRequestInternal').WebRequestEvent; binding.registerCustomHook(function(api) { var apiFunctions = api.apiFunctions; apiFunctions.setHandleRequest('handlerBehaviorChanged', function() { var args = $Array.slice(arguments); sendRequest(this.name, args, this.definition.parameters, {__proto__: null, forIOThread: true}); }); }); binding.registerCustomEvent(WebRequestEvent); exports.$set('binding', binding.generate()); // Copyright (c) 2012 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. // Custom binding for the webRequestInternal API. var binding = require('binding').Binding.create('webRequestInternal'); var eventBindings = require('event_bindings'); var sendRequest = require('sendRequest').sendRequest; var validate = require('schemaUtils').validate; var utils = require('utils'); var idGeneratorNatives = requireNative('id_generator'); var webRequestInternal; function GetUniqueSubEventName(eventName) { return eventName + '/' + idGeneratorNatives.GetNextId(); } // WebRequestEventImpl object. This is used for special webRequest events // with extra parameters. Each invocation of addListener creates a new named // sub-event. That sub-event is associated with the extra parameters in the // browser process, so that only it is dispatched when the main event occurs // matching the extra parameters. // // Example: // chrome.webRequest.onBeforeRequest.addListener( // callback, {urls: 'http://*.google.com/*'}); // ^ callback will only be called for onBeforeRequests matching the filter. function WebRequestEventImpl(eventName, opt_argSchemas, opt_extraArgSchemas, opt_eventOptions, opt_webViewInstanceId) { if (typeof eventName != 'string') throw new Error('chrome.WebRequestEvent requires an event name.'); this.eventName = eventName; this.argSchemas = opt_argSchemas; this.extraArgSchemas = opt_extraArgSchemas; this.webViewInstanceId = opt_webViewInstanceId || 0; this.subEvents = []; this.eventOptions = eventBindings.parseEventOptions(opt_eventOptions); if (this.eventOptions.supportsRules) { this.eventForRules = new eventBindings.Event(eventName, opt_argSchemas, opt_eventOptions, opt_webViewInstanceId); } } $Object.setPrototypeOf(WebRequestEventImpl.prototype, null); // Test if the given callback is registered for this event. WebRequestEventImpl.prototype.hasListener = function(cb) { if (!this.eventOptions.supportsListeners) throw new Error('This event does not support listeners.'); return this.findListener_(cb) > -1; }; // Test if any callbacks are registered fur thus event. WebRequestEventImpl.prototype.hasListeners = function() { if (!this.eventOptions.supportsListeners) throw new Error('This event does not support listeners.'); return this.subEvents.length > 0; }; // Registers a callback to be called when this event is dispatched. If // opt_filter is specified, then the callback is only called for events that // match the given filters. If opt_extraInfo is specified, the given optional // info is sent to the callback. WebRequestEventImpl.prototype.addListener = function(cb, opt_filter, opt_extraInfo) { if (!this.eventOptions.supportsListeners) throw new Error('This event does not support listeners.'); // NOTE(benjhayden) New APIs should not use this subEventName trick! It does // not play well with event pages. See downloads.onDeterminingFilename and // ExtensionDownloadsEventRouter for an alternative approach. var subEventName = GetUniqueSubEventName(this.eventName); // Note: this could fail to validate, in which case we would not add the // subEvent listener. validate($Array.slice(arguments, 1), this.extraArgSchemas); webRequestInternal.addEventListener( cb, opt_filter, opt_extraInfo, this.eventName, subEventName, this.webViewInstanceId); var subEvent = new eventBindings.Event(subEventName, this.argSchemas); var subEventCallback = cb; if (opt_extraInfo && opt_extraInfo.indexOf('blocking') >= 0) { var eventName = this.eventName; subEventCallback = function() { var requestId = arguments[0].requestId; try { var result = $Function.apply(cb, null, arguments); webRequestInternal.eventHandled( eventName, subEventName, requestId, result); } catch (e) { webRequestInternal.eventHandled( eventName, subEventName, requestId); throw e; } }; } else if (opt_extraInfo && opt_extraInfo.indexOf('asyncBlocking') >= 0) { var eventName = this.eventName; subEventCallback = function() { var details = arguments[0]; var requestId = details.requestId; var handledCallback = function(response) { webRequestInternal.eventHandled( eventName, subEventName, requestId, response); }; $Function.apply(cb, null, [details, handledCallback]); }; } $Array.push(this.subEvents, {subEvent: subEvent, callback: cb, subEventCallback: subEventCallback}); subEvent.addListener(subEventCallback); }; // Unregisters a callback. WebRequestEventImpl.prototype.removeListener = function(cb) { if (!this.eventOptions.supportsListeners) throw new Error('This event does not support listeners.'); var idx; while ((idx = this.findListener_(cb)) >= 0) { var e = this.subEvents[idx]; e.subEvent.removeListener(e.subEventCallback); if (e.subEvent.hasListeners()) { console.error( 'Internal error: webRequest subEvent has orphaned listeners.'); } $Array.splice(this.subEvents, idx, 1); } }; WebRequestEventImpl.prototype.findListener_ = function(cb) { for (var i in this.subEvents) { var e = this.subEvents[i]; if (e.callback === cb) { if (e.subEvent.hasListener(e.subEventCallback)) return i; console.error('Internal error: webRequest subEvent has no callback.'); } } return -1; }; WebRequestEventImpl.prototype.addRules = function(rules, opt_cb) { if (!this.eventOptions.supportsRules) throw new Error('This event does not support rules.'); this.eventForRules.addRules(rules, opt_cb); }; WebRequestEventImpl.prototype.removeRules = function(ruleIdentifiers, opt_cb) { if (!this.eventOptions.supportsRules) throw new Error('This event does not support rules.'); this.eventForRules.removeRules(ruleIdentifiers, opt_cb); }; WebRequestEventImpl.prototype.getRules = function(ruleIdentifiers, cb) { if (!this.eventOptions.supportsRules) throw new Error('This event does not support rules.'); this.eventForRules.getRules(ruleIdentifiers, cb); }; binding.registerCustomHook(function(api) { var apiFunctions = api.apiFunctions; apiFunctions.setHandleRequest('addEventListener', function() { var args = $Array.slice(arguments); sendRequest(this.name, args, this.definition.parameters, {__proto__: null, forIOThread: true}); }); apiFunctions.setHandleRequest('eventHandled', function() { var args = $Array.slice(arguments); sendRequest(this.name, args, this.definition.parameters, {__proto__: null, forIOThread: true}); }); }); function WebRequestEvent() { privates(WebRequestEvent).constructPrivate(this, arguments); } utils.expose(WebRequestEvent, WebRequestEventImpl, { functions: [ 'hasListener', 'hasListeners', 'addListener', 'removeListener', 'addRules', 'removeRules', 'getRules', ], }); webRequestInternal = binding.generate(); exports.$set('binding', webRequestInternal); exports.$set('WebRequestEvent', WebRequestEvent); // Copyright 2014 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. // Custom binding for the webViewRequest API. var binding = require('binding').Binding.create('webViewRequest'); var declarativeWebRequestSchema = requireNative('schema_registry').GetSchema('declarativeWebRequest'); var utils = require('utils'); var validate = require('schemaUtils').validate; binding.registerCustomHook(function(api) { var webViewRequest = api.compiledApi; // Returns the schema definition of type |typeId| defined in // |declarativeWebRequestScheme.types|. function getSchema(typeId) { return utils.lookup(declarativeWebRequestSchema.types, 'id', 'declarativeWebRequest.' + typeId); } // Helper function for the constructor of concrete datatypes of the // declarative webRequest API. // Makes sure that |this| contains the union of parameters and // {'instanceType': 'declarativeWebRequest.' + typeId} and validates the // generated union dictionary against the schema for |typeId|. function setupInstance(instance, parameters, typeId) { for (var key in parameters) { if ($Object.hasOwnProperty(parameters, key)) { instance[key] = parameters[key]; } } instance.instanceType = 'declarativeWebRequest.' + typeId; var schema = getSchema(typeId); validate([instance], [schema]); } // Setup all data types for the declarative webRequest API from the schema. for (var i = 0; i < declarativeWebRequestSchema.types.length; ++i) { var typeSchema = declarativeWebRequestSchema.types[i]; var typeId = typeSchema.id.replace('declarativeWebRequest.', ''); var action = function(typeId) { return function(parameters) { setupInstance(this, parameters, typeId); }; }(typeId); webViewRequest[typeId] = action; } }); exports.$set('binding', binding.generate()); // Copyright 2014 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. var normalizeArgumentsAndValidate = require('schemaUtils').normalizeArgumentsAndValidate var sendRequest = require('sendRequest').sendRequest; function extendSchema(schema) { var extendedSchema = $Array.slice(schema); $Array.unshift(extendedSchema, {'type': 'string'}); return extendedSchema; } // TODO(devlin): Combine parts of this and other custom types (ChromeSetting, // ContentSetting, etc). function StorageArea(namespace, schema) { // Binds an API function for a namespace to its browser-side call, e.g. // storage.sync.get('foo') -> (binds to) -> // storage.get('sync', 'foo'). var self = this; function bindApiFunction(functionName) { var rawFunSchema = $Array.filter(schema.functions, function(f) { return f.name === functionName; })[0]; // normalizeArgumentsAndValidate expects a function schema of the form // { name: , definition: }. var funSchema = { __proto__: null, name: rawFunSchema.name, definition: rawFunSchema }; self[functionName] = function() { var args = $Array.slice(arguments); args = normalizeArgumentsAndValidate(args, funSchema); return sendRequest( 'storage.' + functionName, $Array.concat([namespace], args), extendSchema(funSchema.definition.parameters), {__proto__: null, preserveNullInObjects: true}); }; } var apiFunctions = ['get', 'set', 'remove', 'clear', 'getBytesInUse']; $Array.forEach(apiFunctions, bindApiFunction); } exports.$set('StorageArea', StorageArea); /* * Copyright 2014 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. * * A style sheet for Chrome apps. */ @namespace "http://www.w3.org/1999/xhtml"; body { -webkit-user-select: none; cursor: default; font-family: $FONTFAMILY; font-size: $FONTSIZE; } webview, appview { display: inline-block; width: 300px; height: 300px; } html, body { overflow: hidden; } img, a { -webkit-user-drag: none; } [contenteditable], input { -webkit-user-select: auto; } // Copyright 2014 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. var logging = requireNative('logging'); /** * Returns a function that logs a 'not available' error to the console and * returns undefined. * * @param {string} messagePrefix text to prepend to the exception message. */ function generateDisabledMethodStub(messagePrefix, opt_messageSuffix) { var message = messagePrefix + ' is not available in packaged apps.'; if (opt_messageSuffix) message = message + ' ' + opt_messageSuffix; return function() { console.error(message); return; }; } /** * Returns a function that throws a 'not available' error. * * @param {string} messagePrefix text to prepend to the exception message. */ function generateThrowingMethodStub(messagePrefix, opt_messageSuffix) { var message = messagePrefix + ' is not available in packaged apps.'; if (opt_messageSuffix) message = message + ' ' + opt_messageSuffix; return function() { throw new Error(message); }; } /** * Replaces the given methods of the passed in object with stubs that log * 'not available' errors to the console and return undefined. * * This should be used on methods attached via non-configurable properties, * such as window.alert. disableGetters should be used when possible, because * it is friendlier towards feature detection. * * In most cases, the useThrowingStubs should be false, so the stubs used to * replace the methods log an error to the console, but allow the calling code * to continue. We shouldn't break library code that uses feature detection * responsibly, such as: * if(window.confirm) { * var result = window.confirm('Are you sure you want to delete ...?'); * ... * } * * useThrowingStubs should only be true for methods that are deprecated in the * Web platform, and should not be used by a responsible library, even in * conjunction with feature detection. A great example is document.write(), as * the HTML5 specification recommends against using it, and says that its * behavior is unreliable. No reasonable library code should ever use it. * HTML5 spec: http://www.w3.org/TR/html5/dom.html#dom-document-write * * @param {Object} object The object with methods to disable. The prototype is * preferred. * @param {string} objectName The display name to use in the error message * thrown by the stub (this is the name that the object is commonly referred * to by web developers, e.g. "document" instead of "HTMLDocument"). * @param {Array} methodNames names of methods to disable. * @param {Boolean} useThrowingStubs if true, the replaced methods will throw * an error instead of silently returning undefined */ function disableMethods(object, objectName, methodNames, useThrowingStubs) { $Array.forEach(methodNames, function(methodName) { logging.DCHECK($Object.getOwnPropertyDescriptor(object, methodName), objectName + ': ' + methodName); var messagePrefix = objectName + '.' + methodName + '()'; $Object.defineProperty(object, methodName, { configurable: false, enumerable: false, value: useThrowingStubs ? generateThrowingMethodStub(messagePrefix) : generateDisabledMethodStub(messagePrefix) }); }); } /** * Replaces the given properties of the passed in object with stubs that log * 'not available' warnings to the console and return undefined when gotten. If * a property's setter is later invoked, the getter and setter are restored to * default behaviors. * * @param {Object} object The object with properties to disable. The prototype * is preferred. * @param {string} objectName The display name to use in the error message * thrown by the getter stub (this is the name that the object is commonly * referred to by web developers, e.g. "document" instead of * "HTMLDocument"). * @param {Array} propertyNames names of properties to disable. * @param {?string=} opt_messageSuffix An optional suffix for the message. * @param {boolean=} opt_ignoreMissingProperty True if we allow disabling * getters for non-existent properties. */ function disableGetters(object, objectName, propertyNames, opt_messageSuffix, opt_ignoreMissingProperty) { $Array.forEach(propertyNames, function(propertyName) { logging.DCHECK(opt_ignoreMissingProperty || $Object.getOwnPropertyDescriptor(object, propertyName), objectName + ': ' + propertyName); var stub = generateDisabledMethodStub(objectName + '.' + propertyName, opt_messageSuffix); stub._is_platform_app_disabled_getter = true; $Object.defineProperty(object, propertyName, { configurable: true, enumerable: false, get: stub, set: function(value) { var descriptor = $Object.getOwnPropertyDescriptor(this, propertyName); if (!descriptor || !descriptor.get || descriptor.get._is_platform_app_disabled_getter) { // The stub getter is still defined. Blow-away the property to // restore default getter/setter behaviors and re-create it with the // given value. delete this[propertyName]; this[propertyName] = value; } else { // Do nothing. If some custom getter (not ours) has been defined, // there would be no way to read back the value stored by a default // setter. Also, the only way to clear a custom getter is to first // delete the property. Therefore, the value we have here should // just go into a black hole. } } }); }); } /** * Replaces the given properties of the passed in object with stubs that log * 'not available' warnings to the console when set. * * @param {Object} object The object with properties to disable. The prototype * is preferred. * @param {string} objectName The display name to use in the error message * thrown by the setter stub (this is the name that the object is commonly * referred to by web developers, e.g. "document" instead of * "HTMLDocument"). * @param {Array} propertyNames names of properties to disable. */ function disableSetters(object, objectName, propertyNames, opt_messageSuffix) { $Array.forEach(propertyNames, function(propertyName) { logging.DCHECK($Object.getOwnPropertyDescriptor(object, propertyName), objectName + ': ' + propertyName); var stub = generateDisabledMethodStub(objectName + '.' + propertyName, opt_messageSuffix); $Object.defineProperty(object, propertyName, { configurable: false, enumerable: false, get: function() { return; }, set: stub }); }); } // Disable benign Document methods. disableMethods(Document.prototype, 'document', ['open', 'close']); disableMethods(HTMLDocument.prototype, 'document', ['clear']); // Replace evil Document methods with exception-throwing stubs. disableMethods(Document.prototype, 'document', ['write', 'writeln'], true); // Disable history. Object.defineProperty(window, "history", { value: {} }); // Note: we just blew away the history object, so we need to ignore the fact // that these properties aren't defined on the object. disableGetters(window.history, 'history', ['back', 'forward', 'go', 'length', 'pushState', 'replaceState', 'state'], null, true); // Disable find. disableMethods(window, 'window', ['find']); // Disable modal dialogs. Shell windows disable these anyway, but it's nice to // warn. disableMethods(window, 'window', ['alert', 'confirm', 'prompt']); // Disable window.*bar. disableGetters(window, 'window', ['locationbar', 'menubar', 'personalbar', 'scrollbars', 'statusbar', 'toolbar']); // Disable window.localStorage. // Sometimes DOM security policy prevents us from doing this (e.g. for data: // URLs) so wrap in try-catch. try { disableGetters(window, 'window', ['localStorage'], 'Use chrome.storage.local instead.'); } catch (e) {} // Document instance properties that we wish to disable need to be set when // the document begins loading, since only then will the "document" reference // point to the page's document (it will be reset between now and then). // We can't listen for the "readystatechange" event on the document (because // the object that it's dispatched on doesn't exist yet), but we can instead // do it at the window level in the capturing phase. window.addEventListener('readystatechange', function(event) { if (document.readyState != 'loading') return; // Deprecated document properties from // https://developer.mozilla.org/en/DOM/document. // To deprecate document.all, simply changing its getter and setter would // activate its cache mechanism, and degrade the performance. Here we assign // it first to 'undefined' to avoid this. document.all = undefined; disableGetters(document, 'document', ['alinkColor', 'all', 'bgColor', 'fgColor', 'linkColor', 'vlinkColor'], null, true); }, true); // Disable onunload, onbeforeunload. disableSetters(window, 'window', ['onbeforeunload', 'onunload']); var eventTargetAddEventListener = EventTarget.prototype.addEventListener; EventTarget.prototype.addEventListener = function(type) { var args = $Array.slice(arguments); // Note: Force conversion to a string in order to catch any funny attempts // to pass in something that evals to 'unload' but wouldn't === 'unload'. var type = (args[0] += ''); if (type === 'unload' || type === 'beforeunload') generateDisabledMethodStub(type)(); else return $Function.apply(eventTargetAddEventListener, this, args); }; /* * Copyright 2014 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. * * This stylesheet is used to apply Chrome system fonts to all extension pages. */ body { font-family: $FONTFAMILY; font-size: $FONTSIZE; } // Copyright 2014 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. define("chrome/browser/media/router/mojo/media_router.mojom", [ "mojo/public/js/bindings", "mojo/public/js/codec", "mojo/public/js/core", "mojo/public/js/validator", "mojo/common/time.mojom", ], function(bindings, codec, core, validator, time$) { var RouteRequestResultCode = {}; RouteRequestResultCode.UNKNOWN_ERROR = 0; RouteRequestResultCode.OK = RouteRequestResultCode.UNKNOWN_ERROR + 1; RouteRequestResultCode.TIMED_OUT = RouteRequestResultCode.OK + 1; RouteRequestResultCode.ROUTE_NOT_FOUND = RouteRequestResultCode.TIMED_OUT + 1; RouteRequestResultCode.SINK_NOT_FOUND = RouteRequestResultCode.ROUTE_NOT_FOUND + 1; RouteRequestResultCode.INVALID_ORIGIN = RouteRequestResultCode.SINK_NOT_FOUND + 1; RouteRequestResultCode.INCOGNITO_MISMATCH = RouteRequestResultCode.INVALID_ORIGIN + 1; RouteRequestResultCode.NO_SUPPORTED_PROVIDER = RouteRequestResultCode.INCOGNITO_MISMATCH + 1; RouteRequestResultCode.CANCELLED = RouteRequestResultCode.NO_SUPPORTED_PROVIDER + 1; RouteRequestResultCode.isKnownEnumValue = function(value) { switch (value) { case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7: case 8: return true; } return false; }; RouteRequestResultCode.validate = function(enumValue) { var isExtensible = false; if (isExtensible || this.isKnownEnumValue(enumValue)) return validator.validationError.NONE; return validator.validationError.UNKNOWN_ENUM_VALUE; }; function MediaSink(values) { this.initDefaults_(); this.initFields_(values); } MediaSink.IconType = {}; MediaSink.IconType.CAST = 0; MediaSink.IconType.CAST_AUDIO = MediaSink.IconType.CAST + 1; MediaSink.IconType.CAST_AUDIO_GROUP = MediaSink.IconType.CAST_AUDIO + 1; MediaSink.IconType.GENERIC = MediaSink.IconType.CAST_AUDIO_GROUP + 1; MediaSink.IconType.HANGOUT = MediaSink.IconType.GENERIC + 1; MediaSink.IconType.isKnownEnumValue = function(value) { switch (value) { case 0: case 1: case 2: case 3: case 4: return true; } return false; }; MediaSink.IconType.validate = function(enumValue) { var isExtensible = false; if (isExtensible || this.isKnownEnumValue(enumValue)) return validator.validationError.NONE; return validator.validationError.UNKNOWN_ENUM_VALUE; }; MediaSink.prototype.initDefaults_ = function() { this.sink_id = null; this.name = null; this.description = null; this.domain = null; this.icon_type = 0; }; MediaSink.prototype.initFields_ = function(fields) { for(var field in fields) { if (this.hasOwnProperty(field)) this[field] = fields[field]; } }; MediaSink.validate = function(messageValidator, offset) { var err; err = messageValidator.validateStructHeader(offset, codec.kStructHeaderSize); if (err !== validator.validationError.NONE) return err; var kVersionSizes = [ {version: 0, numBytes: 48} ]; err = messageValidator.validateStructVersion(offset, kVersionSizes); if (err !== validator.validationError.NONE) return err; // validate MediaSink.sink_id err = messageValidator.validateStringPointer(offset + codec.kStructHeaderSize + 0, false) if (err !== validator.validationError.NONE) return err; // validate MediaSink.name err = messageValidator.validateStringPointer(offset + codec.kStructHeaderSize + 8, false) if (err !== validator.validationError.NONE) return err; // validate MediaSink.description err = messageValidator.validateStringPointer(offset + codec.kStructHeaderSize + 16, true) if (err !== validator.validationError.NONE) return err; // validate MediaSink.domain err = messageValidator.validateStringPointer(offset + codec.kStructHeaderSize + 24, true) if (err !== validator.validationError.NONE) return err; // validate MediaSink.icon_type err = messageValidator.validateEnum(offset + codec.kStructHeaderSize + 32, MediaSink.IconType); if (err !== validator.validationError.NONE) return err; return validator.validationError.NONE; }; MediaSink.encodedSize = codec.kStructHeaderSize + 40; MediaSink.decode = function(decoder) { var packed; var val = new MediaSink(); var numberOfBytes = decoder.readUint32(); var version = decoder.readUint32(); val.sink_id = decoder.decodeStruct(codec.String); val.name = decoder.decodeStruct(codec.String); val.description = decoder.decodeStruct(codec.NullableString); val.domain = decoder.decodeStruct(codec.NullableString); val.icon_type = decoder.decodeStruct(codec.Int32); decoder.skip(1); decoder.skip(1); decoder.skip(1); decoder.skip(1); return val; }; MediaSink.encode = function(encoder, val) { var packed; encoder.writeUint32(MediaSink.encodedSize); encoder.writeUint32(0); encoder.encodeStruct(codec.String, val.sink_id); encoder.encodeStruct(codec.String, val.name); encoder.encodeStruct(codec.NullableString, val.description); encoder.encodeStruct(codec.NullableString, val.domain); encoder.encodeStruct(codec.Int32, val.icon_type); encoder.skip(1); encoder.skip(1); encoder.skip(1); encoder.skip(1); }; function MediaRoute(values) { this.initDefaults_(); this.initFields_(values); } MediaRoute.prototype.initDefaults_ = function() { this.media_route_id = null; this.media_source = null; this.media_sink_id = null; this.description = null; this.is_local = false; this.for_display = false; this.is_incognito = false; this.is_offscreen_presentation = false; this.custom_controller_path = null; }; MediaRoute.prototype.initFields_ = function(fields) { for(var field in fields) { if (this.hasOwnProperty(field)) this[field] = fields[field]; } }; MediaRoute.validate = function(messageValidator, offset) { var err; err = messageValidator.validateStructHeader(offset, codec.kStructHeaderSize); if (err !== validator.validationError.NONE) return err; var kVersionSizes = [ {version: 0, numBytes: 56} ]; err = messageValidator.validateStructVersion(offset, kVersionSizes); if (err !== validator.validationError.NONE) return err; // validate MediaRoute.media_route_id err = messageValidator.validateStringPointer(offset + codec.kStructHeaderSize + 0, false) if (err !== validator.validationError.NONE) return err; // validate MediaRoute.media_source err = messageValidator.validateStringPointer(offset + codec.kStructHeaderSize + 8, true) if (err !== validator.validationError.NONE) return err; // validate MediaRoute.media_sink_id err = messageValidator.validateStringPointer(offset + codec.kStructHeaderSize + 16, false) if (err !== validator.validationError.NONE) return err; // validate MediaRoute.description err = messageValidator.validateStringPointer(offset + codec.kStructHeaderSize + 24, false) if (err !== validator.validationError.NONE) return err; // validate MediaRoute.custom_controller_path err = messageValidator.validateStringPointer(offset + codec.kStructHeaderSize + 40, true) if (err !== validator.validationError.NONE) return err; return validator.validationError.NONE; }; MediaRoute.encodedSize = codec.kStructHeaderSize + 48; MediaRoute.decode = function(decoder) { var packed; var val = new MediaRoute(); var numberOfBytes = decoder.readUint32(); var version = decoder.readUint32(); val.media_route_id = decoder.decodeStruct(codec.String); val.media_source = decoder.decodeStruct(codec.NullableString); val.media_sink_id = decoder.decodeStruct(codec.String); val.description = decoder.decodeStruct(codec.String); packed = decoder.readUint8(); val.is_local = (packed >> 0) & 1 ? true : false; val.for_display = (packed >> 1) & 1 ? true : false; val.is_incognito = (packed >> 2) & 1 ? true : false; val.is_offscreen_presentation = (packed >> 3) & 1 ? true : false; decoder.skip(1); decoder.skip(1); decoder.skip(1); decoder.skip(1); decoder.skip(1); decoder.skip(1); decoder.skip(1); val.custom_controller_path = decoder.decodeStruct(codec.NullableString); return val; }; MediaRoute.encode = function(encoder, val) { var packed; encoder.writeUint32(MediaRoute.encodedSize); encoder.writeUint32(0); encoder.encodeStruct(codec.String, val.media_route_id); encoder.encodeStruct(codec.NullableString, val.media_source); encoder.encodeStruct(codec.String, val.media_sink_id); encoder.encodeStruct(codec.String, val.description); packed = 0; packed |= (val.is_local & 1) << 0 packed |= (val.for_display & 1) << 1 packed |= (val.is_incognito & 1) << 2 packed |= (val.is_offscreen_presentation & 1) << 3 encoder.writeUint8(packed); encoder.skip(1); encoder.skip(1); encoder.skip(1); encoder.skip(1); encoder.skip(1); encoder.skip(1); encoder.skip(1); encoder.encodeStruct(codec.NullableString, val.custom_controller_path); }; function Issue(values) { this.initDefaults_(); this.initFields_(values); } Issue.Severity = {}; Issue.Severity.FATAL = 0; Issue.Severity.WARNING = Issue.Severity.FATAL + 1; Issue.Severity.NOTIFICATION = Issue.Severity.WARNING + 1; Issue.Severity.isKnownEnumValue = function(value) { switch (value) { case 0: case 1: case 2: return true; } return false; }; Issue.Severity.validate = function(enumValue) { var isExtensible = false; if (isExtensible || this.isKnownEnumValue(enumValue)) return validator.validationError.NONE; return validator.validationError.UNKNOWN_ENUM_VALUE; }; Issue.ActionType = {}; Issue.ActionType.DISMISS = 0; Issue.ActionType.LEARN_MORE = Issue.ActionType.DISMISS + 1; Issue.ActionType.isKnownEnumValue = function(value) { switch (value) { case 0: case 1: return true; } return false; }; Issue.ActionType.validate = function(enumValue) { var isExtensible = false; if (isExtensible || this.isKnownEnumValue(enumValue)) return validator.validationError.NONE; return validator.validationError.UNKNOWN_ENUM_VALUE; }; Issue.prototype.initDefaults_ = function() { this.route_id = null; this.severity = 0; this.is_blocking = false; this.title = null; this.message = null; this.default_action = 0; this.help_page_id = 0; this.secondary_actions = null; }; Issue.prototype.initFields_ = function(fields) { for(var field in fields) { if (this.hasOwnProperty(field)) this[field] = fields[field]; } }; Issue.validate = function(messageValidator, offset) { var err; err = messageValidator.validateStructHeader(offset, codec.kStructHeaderSize); if (err !== validator.validationError.NONE) return err; var kVersionSizes = [ {version: 0, numBytes: 56} ]; err = messageValidator.validateStructVersion(offset, kVersionSizes); if (err !== validator.validationError.NONE) return err; // validate Issue.route_id err = messageValidator.validateStringPointer(offset + codec.kStructHeaderSize + 0, true) if (err !== validator.validationError.NONE) return err; // validate Issue.severity err = messageValidator.validateEnum(offset + codec.kStructHeaderSize + 8, Issue.Severity); if (err !== validator.validationError.NONE) return err; // validate Issue.title err = messageValidator.validateStringPointer(offset + codec.kStructHeaderSize + 16, false) if (err !== validator.validationError.NONE) return err; // validate Issue.message err = messageValidator.validateStringPointer(offset + codec.kStructHeaderSize + 24, true) if (err !== validator.validationError.NONE) return err; // validate Issue.default_action err = messageValidator.validateEnum(offset + codec.kStructHeaderSize + 32, Issue.ActionType); if (err !== validator.validationError.NONE) return err; // validate Issue.secondary_actions err = messageValidator.validateArrayPointer(offset + codec.kStructHeaderSize + 40, 4, new codec.Enum(Issue.ActionType), true, [0], 0); if (err !== validator.validationError.NONE) return err; return validator.validationError.NONE; }; Issue.encodedSize = codec.kStructHeaderSize + 48; Issue.decode = function(decoder) { var packed; var val = new Issue(); var numberOfBytes = decoder.readUint32(); var version = decoder.readUint32(); val.route_id = decoder.decodeStruct(codec.NullableString); val.severity = decoder.decodeStruct(codec.Int32); packed = decoder.readUint8(); val.is_blocking = (packed >> 0) & 1 ? true : false; decoder.skip(1); decoder.skip(1); decoder.skip(1); val.title = decoder.decodeStruct(codec.String); val.message = decoder.decodeStruct(codec.NullableString); val.default_action = decoder.decodeStruct(codec.Int32); val.help_page_id = decoder.decodeStruct(codec.Int32); val.secondary_actions = decoder.decodeArrayPointer(new codec.Enum(Issue.ActionType)); return val; }; Issue.encode = function(encoder, val) { var packed; encoder.writeUint32(Issue.encodedSize); encoder.writeUint32(0); encoder.encodeStruct(codec.NullableString, val.route_id); encoder.encodeStruct(codec.Int32, val.severity); packed = 0; packed |= (val.is_blocking & 1) << 0 encoder.writeUint8(packed); encoder.skip(1); encoder.skip(1); encoder.skip(1); encoder.encodeStruct(codec.String, val.title); encoder.encodeStruct(codec.NullableString, val.message); encoder.encodeStruct(codec.Int32, val.default_action); encoder.encodeStruct(codec.Int32, val.help_page_id); encoder.encodeArrayPointer(new codec.Enum(Issue.ActionType), val.secondary_actions); }; function RouteMessage(values) { this.initDefaults_(); this.initFields_(values); } RouteMessage.Type = {}; RouteMessage.Type.TEXT = 0; RouteMessage.Type.BINARY = RouteMessage.Type.TEXT + 1; RouteMessage.Type.isKnownEnumValue = function(value) { switch (value) { case 0: case 1: return true; } return false; }; RouteMessage.Type.validate = function(enumValue) { var isExtensible = false; if (isExtensible || this.isKnownEnumValue(enumValue)) return validator.validationError.NONE; return validator.validationError.UNKNOWN_ENUM_VALUE; }; RouteMessage.prototype.initDefaults_ = function() { this.type = 0; this.message = null; this.data = null; }; RouteMessage.prototype.initFields_ = function(fields) { for(var field in fields) { if (this.hasOwnProperty(field)) this[field] = fields[field]; } }; RouteMessage.validate = function(messageValidator, offset) { var err; err = messageValidator.validateStructHeader(offset, codec.kStructHeaderSize); if (err !== validator.validationError.NONE) return err; var kVersionSizes = [ {version: 0, numBytes: 32} ]; err = messageValidator.validateStructVersion(offset, kVersionSizes); if (err !== validator.validationError.NONE) return err; // validate RouteMessage.type err = messageValidator.validateEnum(offset + codec.kStructHeaderSize + 0, RouteMessage.Type); if (err !== validator.validationError.NONE) return err; // validate RouteMessage.message err = messageValidator.validateStringPointer(offset + codec.kStructHeaderSize + 8, true) if (err !== validator.validationError.NONE) return err; // validate RouteMessage.data err = messageValidator.validateArrayPointer(offset + codec.kStructHeaderSize + 16, 1, codec.Uint8, true, [0], 0); if (err !== validator.validationError.NONE) return err; return validator.validationError.NONE; }; RouteMessage.encodedSize = codec.kStructHeaderSize + 24; RouteMessage.decode = function(decoder) { var packed; var val = new RouteMessage(); var numberOfBytes = decoder.readUint32(); var version = decoder.readUint32(); val.type = decoder.decodeStruct(codec.Int32); decoder.skip(1); decoder.skip(1); decoder.skip(1); decoder.skip(1); val.message = decoder.decodeStruct(codec.NullableString); val.data = decoder.decodeArrayPointer(codec.Uint8); return val; }; RouteMessage.encode = function(encoder, val) { var packed; encoder.writeUint32(RouteMessage.encodedSize); encoder.writeUint32(0); encoder.encodeStruct(codec.Int32, val.type); encoder.skip(1); encoder.skip(1); encoder.skip(1); encoder.skip(1); encoder.encodeStruct(codec.NullableString, val.message); encoder.encodeArrayPointer(codec.Uint8, val.data); }; function SinkSearchCriteria(values) { this.initDefaults_(); this.initFields_(values); } SinkSearchCriteria.prototype.initDefaults_ = function() { this.input = null; this.domain = null; }; SinkSearchCriteria.prototype.initFields_ = function(fields) { for(var field in fields) { if (this.hasOwnProperty(field)) this[field] = fields[field]; } }; SinkSearchCriteria.validate = function(messageValidator, offset) { var err; err = messageValidator.validateStructHeader(offset, codec.kStructHeaderSize); if (err !== validator.validationError.NONE) return err; var kVersionSizes = [ {version: 0, numBytes: 24} ]; err = messageValidator.validateStructVersion(offset, kVersionSizes); if (err !== validator.validationError.NONE) return err; // validate SinkSearchCriteria.input err = messageValidator.validateStringPointer(offset + codec.kStructHeaderSize + 0, false) if (err !== validator.validationError.NONE) return err; // validate SinkSearchCriteria.domain err = messageValidator.validateStringPointer(offset + codec.kStructHeaderSize + 8, false) if (err !== validator.validationError.NONE) return err; return validator.validationError.NONE; }; SinkSearchCriteria.encodedSize = codec.kStructHeaderSize + 16; SinkSearchCriteria.decode = function(decoder) { var packed; var val = new SinkSearchCriteria(); var numberOfBytes = decoder.readUint32(); var version = decoder.readUint32(); val.input = decoder.decodeStruct(codec.String); val.domain = decoder.decodeStruct(codec.String); return val; }; SinkSearchCriteria.encode = function(encoder, val) { var packed; encoder.writeUint32(SinkSearchCriteria.encodedSize); encoder.writeUint32(0); encoder.encodeStruct(codec.String, val.input); encoder.encodeStruct(codec.String, val.domain); }; function MediaRouteProvider_CreateRoute_Params(values) { this.initDefaults_(); this.initFields_(values); } MediaRouteProvider_CreateRoute_Params.prototype.initDefaults_ = function() { this.media_source = null; this.sink_id = null; this.original_presentation_id = null; this.origin = null; this.tab_id = 0; this.incognito = false; this.timeout = null; }; MediaRouteProvider_CreateRoute_Params.prototype.initFields_ = function(fields) { for(var field in fields) { if (this.hasOwnProperty(field)) this[field] = fields[field]; } }; MediaRouteProvider_CreateRoute_Params.validate = function(messageValidator, offset) { var err; err = messageValidator.validateStructHeader(offset, codec.kStructHeaderSize); if (err !== validator.validationError.NONE) return err; var kVersionSizes = [ {version: 0, numBytes: 56} ]; err = messageValidator.validateStructVersion(offset, kVersionSizes); if (err !== validator.validationError.NONE) return err; // validate MediaRouteProvider_CreateRoute_Params.media_source err = messageValidator.validateStringPointer(offset + codec.kStructHeaderSize + 0, false) if (err !== validator.validationError.NONE) return err; // validate MediaRouteProvider_CreateRoute_Params.sink_id err = messageValidator.validateStringPointer(offset + codec.kStructHeaderSize + 8, false) if (err !== validator.validationError.NONE) return err; // validate MediaRouteProvider_CreateRoute_Params.original_presentation_id err = messageValidator.validateStringPointer(offset + codec.kStructHeaderSize + 16, false) if (err !== validator.validationError.NONE) return err; // validate MediaRouteProvider_CreateRoute_Params.origin err = messageValidator.validateStringPointer(offset + codec.kStructHeaderSize + 24, false) if (err !== validator.validationError.NONE) return err; // validate MediaRouteProvider_CreateRoute_Params.timeout err = messageValidator.validateStructPointer(offset + codec.kStructHeaderSize + 40, time$.TimeDelta, false); if (err !== validator.validationError.NONE) return err; return validator.validationError.NONE; }; MediaRouteProvider_CreateRoute_Params.encodedSize = codec.kStructHeaderSize + 48; MediaRouteProvider_CreateRoute_Params.decode = function(decoder) { var packed; var val = new MediaRouteProvider_CreateRoute_Params(); var numberOfBytes = decoder.readUint32(); var version = decoder.readUint32(); val.media_source = decoder.decodeStruct(codec.String); val.sink_id = decoder.decodeStruct(codec.String); val.original_presentation_id = decoder.decodeStruct(codec.String); val.origin = decoder.decodeStruct(codec.String); val.tab_id = decoder.decodeStruct(codec.Int32); packed = decoder.readUint8(); val.incognito = (packed >> 0) & 1 ? true : false; decoder.skip(1); decoder.skip(1); decoder.skip(1); val.timeout = decoder.decodeStructPointer(time$.TimeDelta); return val; }; MediaRouteProvider_CreateRoute_Params.encode = function(encoder, val) { var packed; encoder.writeUint32(MediaRouteProvider_CreateRoute_Params.encodedSize); encoder.writeUint32(0); encoder.encodeStruct(codec.String, val.media_source); encoder.encodeStruct(codec.String, val.sink_id); encoder.encodeStruct(codec.String, val.original_presentation_id); encoder.encodeStruct(codec.String, val.origin); encoder.encodeStruct(codec.Int32, val.tab_id); packed = 0; packed |= (val.incognito & 1) << 0 encoder.writeUint8(packed); encoder.skip(1); encoder.skip(1); encoder.skip(1); encoder.encodeStructPointer(time$.TimeDelta, val.timeout); }; function MediaRouteProvider_CreateRoute_ResponseParams(values) { this.initDefaults_(); this.initFields_(values); } MediaRouteProvider_CreateRoute_ResponseParams.prototype.initDefaults_ = function() { this.route = null; this.error_text = null; this.result_code = 0; }; MediaRouteProvider_CreateRoute_ResponseParams.prototype.initFields_ = function(fields) { for(var field in fields) { if (this.hasOwnProperty(field)) this[field] = fields[field]; } }; MediaRouteProvider_CreateRoute_ResponseParams.validate = function(messageValidator, offset) { var err; err = messageValidator.validateStructHeader(offset, codec.kStructHeaderSize); if (err !== validator.validationError.NONE) return err; var kVersionSizes = [ {version: 0, numBytes: 32} ]; err = messageValidator.validateStructVersion(offset, kVersionSizes); if (err !== validator.validationError.NONE) return err; // validate MediaRouteProvider_CreateRoute_ResponseParams.route err = messageValidator.validateStructPointer(offset + codec.kStructHeaderSize + 0, MediaRoute, true); if (err !== validator.validationError.NONE) return err; // validate MediaRouteProvider_CreateRoute_ResponseParams.error_text err = messageValidator.validateStringPointer(offset + codec.kStructHeaderSize + 8, true) if (err !== validator.validationError.NONE) return err; // validate MediaRouteProvider_CreateRoute_ResponseParams.result_code err = messageValidator.validateEnum(offset + codec.kStructHeaderSize + 16, RouteRequestResultCode); if (err !== validator.validationError.NONE) return err; return validator.validationError.NONE; }; MediaRouteProvider_CreateRoute_ResponseParams.encodedSize = codec.kStructHeaderSize + 24; MediaRouteProvider_CreateRoute_ResponseParams.decode = function(decoder) { var packed; var val = new MediaRouteProvider_CreateRoute_ResponseParams(); var numberOfBytes = decoder.readUint32(); var version = decoder.readUint32(); val.route = decoder.decodeStructPointer(MediaRoute); val.error_text = decoder.decodeStruct(codec.NullableString); val.result_code = decoder.decodeStruct(codec.Int32); decoder.skip(1); decoder.skip(1); decoder.skip(1); decoder.skip(1); return val; }; MediaRouteProvider_CreateRoute_ResponseParams.encode = function(encoder, val) { var packed; encoder.writeUint32(MediaRouteProvider_CreateRoute_ResponseParams.encodedSize); encoder.writeUint32(0); encoder.encodeStructPointer(MediaRoute, val.route); encoder.encodeStruct(codec.NullableString, val.error_text); encoder.encodeStruct(codec.Int32, val.result_code); encoder.skip(1); encoder.skip(1); encoder.skip(1); encoder.skip(1); }; function MediaRouteProvider_JoinRoute_Params(values) { this.initDefaults_(); this.initFields_(values); } MediaRouteProvider_JoinRoute_Params.prototype.initDefaults_ = function() { this.media_source = null; this.presentation_id = null; this.origin = null; this.tab_id = 0; this.incognito = false; this.timeout = null; }; MediaRouteProvider_JoinRoute_Params.prototype.initFields_ = function(fields) { for(var field in fields) { if (this.hasOwnProperty(field)) this[field] = fields[field]; } }; MediaRouteProvider_JoinRoute_Params.validate = function(messageValidator, offset) { var err; err = messageValidator.validateStructHeader(offset, codec.kStructHeaderSize); if (err !== validator.validationError.NONE) return err; var kVersionSizes = [ {version: 0, numBytes: 48} ]; err = messageValidator.validateStructVersion(offset, kVersionSizes); if (err !== validator.validationError.NONE) return err; // validate MediaRouteProvider_JoinRoute_Params.media_source err = messageValidator.validateStringPointer(offset + codec.kStructHeaderSize + 0, false) if (err !== validator.validationError.NONE) return err; // validate MediaRouteProvider_JoinRoute_Params.presentation_id err = messageValidator.validateStringPointer(offset + codec.kStructHeaderSize + 8, false) if (err !== validator.validationError.NONE) return err; // validate MediaRouteProvider_JoinRoute_Params.origin err = messageValidator.validateStringPointer(offset + codec.kStructHeaderSize + 16, false) if (err !== validator.validationError.NONE) return err; // validate MediaRouteProvider_JoinRoute_Params.timeout err = messageValidator.validateStructPointer(offset + codec.kStructHeaderSize + 32, time$.TimeDelta, false); if (err !== validator.validationError.NONE) return err; return validator.validationError.NONE; }; MediaRouteProvider_JoinRoute_Params.encodedSize = codec.kStructHeaderSize + 40; MediaRouteProvider_JoinRoute_Params.decode = function(decoder) { var packed; var val = new MediaRouteProvider_JoinRoute_Params(); var numberOfBytes = decoder.readUint32(); var version = decoder.readUint32(); val.media_source = decoder.decodeStruct(codec.String); val.presentation_id = decoder.decodeStruct(codec.String); val.origin = decoder.decodeStruct(codec.String); val.tab_id = decoder.decodeStruct(codec.Int32); packed = decoder.readUint8(); val.incognito = (packed >> 0) & 1 ? true : false; decoder.skip(1); decoder.skip(1); decoder.skip(1); val.timeout = decoder.decodeStructPointer(time$.TimeDelta); return val; }; MediaRouteProvider_JoinRoute_Params.encode = function(encoder, val) { var packed; encoder.writeUint32(MediaRouteProvider_JoinRoute_Params.encodedSize); encoder.writeUint32(0); encoder.encodeStruct(codec.String, val.media_source); encoder.encodeStruct(codec.String, val.presentation_id); encoder.encodeStruct(codec.String, val.origin); encoder.encodeStruct(codec.Int32, val.tab_id); packed = 0; packed |= (val.incognito & 1) << 0 encoder.writeUint8(packed); encoder.skip(1); encoder.skip(1); encoder.skip(1); encoder.encodeStructPointer(time$.TimeDelta, val.timeout); }; function MediaRouteProvider_JoinRoute_ResponseParams(values) { this.initDefaults_(); this.initFields_(values); } MediaRouteProvider_JoinRoute_ResponseParams.prototype.initDefaults_ = function() { this.route = null; this.error_text = null; this.result_code = 0; }; MediaRouteProvider_JoinRoute_ResponseParams.prototype.initFields_ = function(fields) { for(var field in fields) { if (this.hasOwnProperty(field)) this[field] = fields[field]; } }; MediaRouteProvider_JoinRoute_ResponseParams.validate = function(messageValidator, offset) { var err; err = messageValidator.validateStructHeader(offset, codec.kStructHeaderSize); if (err !== validator.validationError.NONE) return err; var kVersionSizes = [ {version: 0, numBytes: 32} ]; err = messageValidator.validateStructVersion(offset, kVersionSizes); if (err !== validator.validationError.NONE) return err; // validate MediaRouteProvider_JoinRoute_ResponseParams.route err = messageValidator.validateStructPointer(offset + codec.kStructHeaderSize + 0, MediaRoute, true); if (err !== validator.validationError.NONE) return err; // validate MediaRouteProvider_JoinRoute_ResponseParams.error_text err = messageValidator.validateStringPointer(offset + codec.kStructHeaderSize + 8, true) if (err !== validator.validationError.NONE) return err; // validate MediaRouteProvider_JoinRoute_ResponseParams.result_code err = messageValidator.validateEnum(offset + codec.kStructHeaderSize + 16, RouteRequestResultCode); if (err !== validator.validationError.NONE) return err; return validator.validationError.NONE; }; MediaRouteProvider_JoinRoute_ResponseParams.encodedSize = codec.kStructHeaderSize + 24; MediaRouteProvider_JoinRoute_ResponseParams.decode = function(decoder) { var packed; var val = new MediaRouteProvider_JoinRoute_ResponseParams(); var numberOfBytes = decoder.readUint32(); var version = decoder.readUint32(); val.route = decoder.decodeStructPointer(MediaRoute); val.error_text = decoder.decodeStruct(codec.NullableString); val.result_code = decoder.decodeStruct(codec.Int32); decoder.skip(1); decoder.skip(1); decoder.skip(1); decoder.skip(1); return val; }; MediaRouteProvider_JoinRoute_ResponseParams.encode = function(encoder, val) { var packed; encoder.writeUint32(MediaRouteProvider_JoinRoute_ResponseParams.encodedSize); encoder.writeUint32(0); encoder.encodeStructPointer(MediaRoute, val.route); encoder.encodeStruct(codec.NullableString, val.error_text); encoder.encodeStruct(codec.Int32, val.result_code); encoder.skip(1); encoder.skip(1); encoder.skip(1); encoder.skip(1); }; function MediaRouteProvider_ConnectRouteByRouteId_Params(values) { this.initDefaults_(); this.initFields_(values); } MediaRouteProvider_ConnectRouteByRouteId_Params.prototype.initDefaults_ = function() { this.media_source = null; this.route_id = null; this.presentation_id = null; this.origin = null; this.tab_id = 0; this.incognito = false; this.timeout = null; }; MediaRouteProvider_ConnectRouteByRouteId_Params.prototype.initFields_ = function(fields) { for(var field in fields) { if (this.hasOwnProperty(field)) this[field] = fields[field]; } }; MediaRouteProvider_ConnectRouteByRouteId_Params.validate = function(messageValidator, offset) { var err; err = messageValidator.validateStructHeader(offset, codec.kStructHeaderSize); if (err !== validator.validationError.NONE) return err; var kVersionSizes = [ {version: 0, numBytes: 56} ]; err = messageValidator.validateStructVersion(offset, kVersionSizes); if (err !== validator.validationError.NONE) return err; // validate MediaRouteProvider_ConnectRouteByRouteId_Params.media_source err = messageValidator.validateStringPointer(offset + codec.kStructHeaderSize + 0, false) if (err !== validator.validationError.NONE) return err; // validate MediaRouteProvider_ConnectRouteByRouteId_Params.route_id err = messageValidator.validateStringPointer(offset + codec.kStructHeaderSize + 8, false) if (err !== validator.validationError.NONE) return err; // validate MediaRouteProvider_ConnectRouteByRouteId_Params.presentation_id err = messageValidator.validateStringPointer(offset + codec.kStructHeaderSize + 16, false) if (err !== validator.validationError.NONE) return err; // validate MediaRouteProvider_ConnectRouteByRouteId_Params.origin err = messageValidator.validateStringPointer(offset + codec.kStructHeaderSize + 24, false) if (err !== validator.validationError.NONE) return err; // validate MediaRouteProvider_ConnectRouteByRouteId_Params.timeout err = messageValidator.validateStructPointer(offset + codec.kStructHeaderSize + 40, time$.TimeDelta, false); if (err !== validator.validationError.NONE) return err; return validator.validationError.NONE; }; MediaRouteProvider_ConnectRouteByRouteId_Params.encodedSize = codec.kStructHeaderSize + 48; MediaRouteProvider_ConnectRouteByRouteId_Params.decode = function(decoder) { var packed; var val = new MediaRouteProvider_ConnectRouteByRouteId_Params(); var numberOfBytes = decoder.readUint32(); var version = decoder.readUint32(); val.media_source = decoder.decodeStruct(codec.String); val.route_id = decoder.decodeStruct(codec.String); val.presentation_id = decoder.decodeStruct(codec.String); val.origin = decoder.decodeStruct(codec.String); val.tab_id = decoder.decodeStruct(codec.Int32); packed = decoder.readUint8(); val.incognito = (packed >> 0) & 1 ? true : false; decoder.skip(1); decoder.skip(1); decoder.skip(1); val.timeout = decoder.decodeStructPointer(time$.TimeDelta); return val; }; MediaRouteProvider_ConnectRouteByRouteId_Params.encode = function(encoder, val) { var packed; encoder.writeUint32(MediaRouteProvider_ConnectRouteByRouteId_Params.encodedSize); encoder.writeUint32(0); encoder.encodeStruct(codec.String, val.media_source); encoder.encodeStruct(codec.String, val.route_id); encoder.encodeStruct(codec.String, val.presentation_id); encoder.encodeStruct(codec.String, val.origin); encoder.encodeStruct(codec.Int32, val.tab_id); packed = 0; packed |= (val.incognito & 1) << 0 encoder.writeUint8(packed); encoder.skip(1); encoder.skip(1); encoder.skip(1); encoder.encodeStructPointer(time$.TimeDelta, val.timeout); }; function MediaRouteProvider_ConnectRouteByRouteId_ResponseParams(values) { this.initDefaults_(); this.initFields_(values); } MediaRouteProvider_ConnectRouteByRouteId_ResponseParams.prototype.initDefaults_ = function() { this.route = null; this.error_text = null; this.result_code = 0; }; MediaRouteProvider_ConnectRouteByRouteId_ResponseParams.prototype.initFields_ = function(fields) { for(var field in fields) { if (this.hasOwnProperty(field)) this[field] = fields[field]; } }; MediaRouteProvider_ConnectRouteByRouteId_ResponseParams.validate = function(messageValidator, offset) { var err; err = messageValidator.validateStructHeader(offset, codec.kStructHeaderSize); if (err !== validator.validationError.NONE) return err; var kVersionSizes = [ {version: 0, numBytes: 32} ]; err = messageValidator.validateStructVersion(offset, kVersionSizes); if (err !== validator.validationError.NONE) return err; // validate MediaRouteProvider_ConnectRouteByRouteId_ResponseParams.route err = messageValidator.validateStructPointer(offset + codec.kStructHeaderSize + 0, MediaRoute, true); if (err !== validator.validationError.NONE) return err; // validate MediaRouteProvider_ConnectRouteByRouteId_ResponseParams.error_text err = messageValidator.validateStringPointer(offset + codec.kStructHeaderSize + 8, true) if (err !== validator.validationError.NONE) return err; // validate MediaRouteProvider_ConnectRouteByRouteId_ResponseParams.result_code err = messageValidator.validateEnum(offset + codec.kStructHeaderSize + 16, RouteRequestResultCode); if (err !== validator.validationError.NONE) return err; return validator.validationError.NONE; }; MediaRouteProvider_ConnectRouteByRouteId_ResponseParams.encodedSize = codec.kStructHeaderSize + 24; MediaRouteProvider_ConnectRouteByRouteId_ResponseParams.decode = function(decoder) { var packed; var val = new MediaRouteProvider_ConnectRouteByRouteId_ResponseParams(); var numberOfBytes = decoder.readUint32(); var version = decoder.readUint32(); val.route = decoder.decodeStructPointer(MediaRoute); val.error_text = decoder.decodeStruct(codec.NullableString); val.result_code = decoder.decodeStruct(codec.Int32); decoder.skip(1); decoder.skip(1); decoder.skip(1); decoder.skip(1); return val; }; MediaRouteProvider_ConnectRouteByRouteId_ResponseParams.encode = function(encoder, val) { var packed; encoder.writeUint32(MediaRouteProvider_ConnectRouteByRouteId_ResponseParams.encodedSize); encoder.writeUint32(0); encoder.encodeStructPointer(MediaRoute, val.route); encoder.encodeStruct(codec.NullableString, val.error_text); encoder.encodeStruct(codec.Int32, val.result_code); encoder.skip(1); encoder.skip(1); encoder.skip(1); encoder.skip(1); }; function MediaRouteProvider_TerminateRoute_Params(values) { this.initDefaults_(); this.initFields_(values); } MediaRouteProvider_TerminateRoute_Params.prototype.initDefaults_ = function() { this.route_id = null; }; MediaRouteProvider_TerminateRoute_Params.prototype.initFields_ = function(fields) { for(var field in fields) { if (this.hasOwnProperty(field)) this[field] = fields[field]; } }; MediaRouteProvider_TerminateRoute_Params.validate = function(messageValidator, offset) { var err; err = messageValidator.validateStructHeader(offset, codec.kStructHeaderSize); if (err !== validator.validationError.NONE) return err; var kVersionSizes = [ {version: 0, numBytes: 16} ]; err = messageValidator.validateStructVersion(offset, kVersionSizes); if (err !== validator.validationError.NONE) return err; // validate MediaRouteProvider_TerminateRoute_Params.route_id err = messageValidator.validateStringPointer(offset + codec.kStructHeaderSize + 0, false) if (err !== validator.validationError.NONE) return err; return validator.validationError.NONE; }; MediaRouteProvider_TerminateRoute_Params.encodedSize = codec.kStructHeaderSize + 8; MediaRouteProvider_TerminateRoute_Params.decode = function(decoder) { var packed; var val = new MediaRouteProvider_TerminateRoute_Params(); var numberOfBytes = decoder.readUint32(); var version = decoder.readUint32(); val.route_id = decoder.decodeStruct(codec.String); return val; }; MediaRouteProvider_TerminateRoute_Params.encode = function(encoder, val) { var packed; encoder.writeUint32(MediaRouteProvider_TerminateRoute_Params.encodedSize); encoder.writeUint32(0); encoder.encodeStruct(codec.String, val.route_id); }; function MediaRouteProvider_TerminateRoute_ResponseParams(values) { this.initDefaults_(); this.initFields_(values); } MediaRouteProvider_TerminateRoute_ResponseParams.prototype.initDefaults_ = function() { this.error_text = null; this.result_code = 0; }; MediaRouteProvider_TerminateRoute_ResponseParams.prototype.initFields_ = function(fields) { for(var field in fields) { if (this.hasOwnProperty(field)) this[field] = fields[field]; } }; MediaRouteProvider_TerminateRoute_ResponseParams.validate = function(messageValidator, offset) { var err; err = messageValidator.validateStructHeader(offset, codec.kStructHeaderSize); if (err !== validator.validationError.NONE) return err; var kVersionSizes = [ {version: 0, numBytes: 24} ]; err = messageValidator.validateStructVersion(offset, kVersionSizes); if (err !== validator.validationError.NONE) return err; // validate MediaRouteProvider_TerminateRoute_ResponseParams.error_text err = messageValidator.validateStringPointer(offset + codec.kStructHeaderSize + 0, true) if (err !== validator.validationError.NONE) return err; // validate MediaRouteProvider_TerminateRoute_ResponseParams.result_code err = messageValidator.validateEnum(offset + codec.kStructHeaderSize + 8, RouteRequestResultCode); if (err !== validator.validationError.NONE) return err; return validator.validationError.NONE; }; MediaRouteProvider_TerminateRoute_ResponseParams.encodedSize = codec.kStructHeaderSize + 16; MediaRouteProvider_TerminateRoute_ResponseParams.decode = function(decoder) { var packed; var val = new MediaRouteProvider_TerminateRoute_ResponseParams(); var numberOfBytes = decoder.readUint32(); var version = decoder.readUint32(); val.error_text = decoder.decodeStruct(codec.NullableString); val.result_code = decoder.decodeStruct(codec.Int32); decoder.skip(1); decoder.skip(1); decoder.skip(1); decoder.skip(1); return val; }; MediaRouteProvider_TerminateRoute_ResponseParams.encode = function(encoder, val) { var packed; encoder.writeUint32(MediaRouteProvider_TerminateRoute_ResponseParams.encodedSize); encoder.writeUint32(0); encoder.encodeStruct(codec.NullableString, val.error_text); encoder.encodeStruct(codec.Int32, val.result_code); encoder.skip(1); encoder.skip(1); encoder.skip(1); encoder.skip(1); }; function MediaRouteProvider_SendRouteMessage_Params(values) { this.initDefaults_(); this.initFields_(values); } MediaRouteProvider_SendRouteMessage_Params.prototype.initDefaults_ = function() { this.media_route_id = null; this.message = null; }; MediaRouteProvider_SendRouteMessage_Params.prototype.initFields_ = function(fields) { for(var field in fields) { if (this.hasOwnProperty(field)) this[field] = fields[field]; } }; MediaRouteProvider_SendRouteMessage_Params.validate = function(messageValidator, offset) { var err; err = messageValidator.validateStructHeader(offset, codec.kStructHeaderSize); if (err !== validator.validationError.NONE) return err; var kVersionSizes = [ {version: 0, numBytes: 24} ]; err = messageValidator.validateStructVersion(offset, kVersionSizes); if (err !== validator.validationError.NONE) return err; // validate MediaRouteProvider_SendRouteMessage_Params.media_route_id err = messageValidator.validateStringPointer(offset + codec.kStructHeaderSize + 0, false) if (err !== validator.validationError.NONE) return err; // validate MediaRouteProvider_SendRouteMessage_Params.message err = messageValidator.validateStringPointer(offset + codec.kStructHeaderSize + 8, false) if (err !== validator.validationError.NONE) return err; return validator.validationError.NONE; }; MediaRouteProvider_SendRouteMessage_Params.encodedSize = codec.kStructHeaderSize + 16; MediaRouteProvider_SendRouteMessage_Params.decode = function(decoder) { var packed; var val = new MediaRouteProvider_SendRouteMessage_Params(); var numberOfBytes = decoder.readUint32(); var version = decoder.readUint32(); val.media_route_id = decoder.decodeStruct(codec.String); val.message = decoder.decodeStruct(codec.String); return val; }; MediaRouteProvider_SendRouteMessage_Params.encode = function(encoder, val) { var packed; encoder.writeUint32(MediaRouteProvider_SendRouteMessage_Params.encodedSize); encoder.writeUint32(0); encoder.encodeStruct(codec.String, val.media_route_id); encoder.encodeStruct(codec.String, val.message); }; function MediaRouteProvider_SendRouteMessage_ResponseParams(values) { this.initDefaults_(); this.initFields_(values); } MediaRouteProvider_SendRouteMessage_ResponseParams.prototype.initDefaults_ = function() { this.sent = false; }; MediaRouteProvider_SendRouteMessage_ResponseParams.prototype.initFields_ = function(fields) { for(var field in fields) { if (this.hasOwnProperty(field)) this[field] = fields[field]; } }; MediaRouteProvider_SendRouteMessage_ResponseParams.validate = function(messageValidator, offset) { var err; err = messageValidator.validateStructHeader(offset, codec.kStructHeaderSize); if (err !== validator.validationError.NONE) return err; var kVersionSizes = [ {version: 0, numBytes: 16} ]; err = messageValidator.validateStructVersion(offset, kVersionSizes); if (err !== validator.validationError.NONE) return err; return validator.validationError.NONE; }; MediaRouteProvider_SendRouteMessage_ResponseParams.encodedSize = codec.kStructHeaderSize + 8; MediaRouteProvider_SendRouteMessage_ResponseParams.decode = function(decoder) { var packed; var val = new MediaRouteProvider_SendRouteMessage_ResponseParams(); var numberOfBytes = decoder.readUint32(); var version = decoder.readUint32(); packed = decoder.readUint8(); val.sent = (packed >> 0) & 1 ? true : false; decoder.skip(1); decoder.skip(1); decoder.skip(1); decoder.skip(1); decoder.skip(1); decoder.skip(1); decoder.skip(1); return val; }; MediaRouteProvider_SendRouteMessage_ResponseParams.encode = function(encoder, val) { var packed; encoder.writeUint32(MediaRouteProvider_SendRouteMessage_ResponseParams.encodedSize); encoder.writeUint32(0); packed = 0; packed |= (val.sent & 1) << 0 encoder.writeUint8(packed); encoder.skip(1); encoder.skip(1); encoder.skip(1); encoder.skip(1); encoder.skip(1); encoder.skip(1); encoder.skip(1); }; function MediaRouteProvider_SendRouteBinaryMessage_Params(values) { this.initDefaults_(); this.initFields_(values); } MediaRouteProvider_SendRouteBinaryMessage_Params.prototype.initDefaults_ = function() { this.media_route_id = null; this.data = null; }; MediaRouteProvider_SendRouteBinaryMessage_Params.prototype.initFields_ = function(fields) { for(var field in fields) { if (this.hasOwnProperty(field)) this[field] = fields[field]; } }; MediaRouteProvider_SendRouteBinaryMessage_Params.validate = function(messageValidator, offset) { var err; err = messageValidator.validateStructHeader(offset, codec.kStructHeaderSize); if (err !== validator.validationError.NONE) return err; var kVersionSizes = [ {version: 0, numBytes: 24} ]; err = messageValidator.validateStructVersion(offset, kVersionSizes); if (err !== validator.validationError.NONE) return err; // validate MediaRouteProvider_SendRouteBinaryMessage_Params.media_route_id err = messageValidator.validateStringPointer(offset + codec.kStructHeaderSize + 0, false) if (err !== validator.validationError.NONE) return err; // validate MediaRouteProvider_SendRouteBinaryMessage_Params.data err = messageValidator.validateArrayPointer(offset + codec.kStructHeaderSize + 8, 1, codec.Uint8, false, [0], 0); if (err !== validator.validationError.NONE) return err; return validator.validationError.NONE; }; MediaRouteProvider_SendRouteBinaryMessage_Params.encodedSize = codec.kStructHeaderSize + 16; MediaRouteProvider_SendRouteBinaryMessage_Params.decode = function(decoder) { var packed; var val = new MediaRouteProvider_SendRouteBinaryMessage_Params(); var numberOfBytes = decoder.readUint32(); var version = decoder.readUint32(); val.media_route_id = decoder.decodeStruct(codec.String); val.data = decoder.decodeArrayPointer(codec.Uint8); return val; }; MediaRouteProvider_SendRouteBinaryMessage_Params.encode = function(encoder, val) { var packed; encoder.writeUint32(MediaRouteProvider_SendRouteBinaryMessage_Params.encodedSize); encoder.writeUint32(0); encoder.encodeStruct(codec.String, val.media_route_id); encoder.encodeArrayPointer(codec.Uint8, val.data); }; function MediaRouteProvider_SendRouteBinaryMessage_ResponseParams(values) { this.initDefaults_(); this.initFields_(values); } MediaRouteProvider_SendRouteBinaryMessage_ResponseParams.prototype.initDefaults_ = function() { this.sent = false; }; MediaRouteProvider_SendRouteBinaryMessage_ResponseParams.prototype.initFields_ = function(fields) { for(var field in fields) { if (this.hasOwnProperty(field)) this[field] = fields[field]; } }; MediaRouteProvider_SendRouteBinaryMessage_ResponseParams.validate = function(messageValidator, offset) { var err; err = messageValidator.validateStructHeader(offset, codec.kStructHeaderSize); if (err !== validator.validationError.NONE) return err; var kVersionSizes = [ {version: 0, numBytes: 16} ]; err = messageValidator.validateStructVersion(offset, kVersionSizes); if (err !== validator.validationError.NONE) return err; return validator.validationError.NONE; }; MediaRouteProvider_SendRouteBinaryMessage_ResponseParams.encodedSize = codec.kStructHeaderSize + 8; MediaRouteProvider_SendRouteBinaryMessage_ResponseParams.decode = function(decoder) { var packed; var val = new MediaRouteProvider_SendRouteBinaryMessage_ResponseParams(); var numberOfBytes = decoder.readUint32(); var version = decoder.readUint32(); packed = decoder.readUint8(); val.sent = (packed >> 0) & 1 ? true : false; decoder.skip(1); decoder.skip(1); decoder.skip(1); decoder.skip(1); decoder.skip(1); decoder.skip(1); decoder.skip(1); return val; }; MediaRouteProvider_SendRouteBinaryMessage_ResponseParams.encode = function(encoder, val) { var packed; encoder.writeUint32(MediaRouteProvider_SendRouteBinaryMessage_ResponseParams.encodedSize); encoder.writeUint32(0); packed = 0; packed |= (val.sent & 1) << 0 encoder.writeUint8(packed); encoder.skip(1); encoder.skip(1); encoder.skip(1); encoder.skip(1); encoder.skip(1); encoder.skip(1); encoder.skip(1); }; function MediaRouteProvider_StartObservingMediaSinks_Params(values) { this.initDefaults_(); this.initFields_(values); } MediaRouteProvider_StartObservingMediaSinks_Params.prototype.initDefaults_ = function() { this.media_source = null; }; MediaRouteProvider_StartObservingMediaSinks_Params.prototype.initFields_ = function(fields) { for(var field in fields) { if (this.hasOwnProperty(field)) this[field] = fields[field]; } }; MediaRouteProvider_StartObservingMediaSinks_Params.validate = function(messageValidator, offset) { var err; err = messageValidator.validateStructHeader(offset, codec.kStructHeaderSize); if (err !== validator.validationError.NONE) return err; var kVersionSizes = [ {version: 0, numBytes: 16} ]; err = messageValidator.validateStructVersion(offset, kVersionSizes); if (err !== validator.validationError.NONE) return err; // validate MediaRouteProvider_StartObservingMediaSinks_Params.media_source err = messageValidator.validateStringPointer(offset + codec.kStructHeaderSize + 0, false) if (err !== validator.validationError.NONE) return err; return validator.validationError.NONE; }; MediaRouteProvider_StartObservingMediaSinks_Params.encodedSize = codec.kStructHeaderSize + 8; MediaRouteProvider_StartObservingMediaSinks_Params.decode = function(decoder) { var packed; var val = new MediaRouteProvider_StartObservingMediaSinks_Params(); var numberOfBytes = decoder.readUint32(); var version = decoder.readUint32(); val.media_source = decoder.decodeStruct(codec.String); return val; }; MediaRouteProvider_StartObservingMediaSinks_Params.encode = function(encoder, val) { var packed; encoder.writeUint32(MediaRouteProvider_StartObservingMediaSinks_Params.encodedSize); encoder.writeUint32(0); encoder.encodeStruct(codec.String, val.media_source); }; function MediaRouteProvider_StopObservingMediaSinks_Params(values) { this.initDefaults_(); this.initFields_(values); } MediaRouteProvider_StopObservingMediaSinks_Params.prototype.initDefaults_ = function() { this.media_source = null; }; MediaRouteProvider_StopObservingMediaSinks_Params.prototype.initFields_ = function(fields) { for(var field in fields) { if (this.hasOwnProperty(field)) this[field] = fields[field]; } }; MediaRouteProvider_StopObservingMediaSinks_Params.validate = function(messageValidator, offset) { var err; err = messageValidator.validateStructHeader(offset, codec.kStructHeaderSize); if (err !== validator.validationError.NONE) return err; var kVersionSizes = [ {version: 0, numBytes: 16} ]; err = messageValidator.validateStructVersion(offset, kVersionSizes); if (err !== validator.validationError.NONE) return err; // validate MediaRouteProvider_StopObservingMediaSinks_Params.media_source err = messageValidator.validateStringPointer(offset + codec.kStructHeaderSize + 0, false) if (err !== validator.validationError.NONE) return err; return validator.validationError.NONE; }; MediaRouteProvider_StopObservingMediaSinks_Params.encodedSize = codec.kStructHeaderSize + 8; MediaRouteProvider_StopObservingMediaSinks_Params.decode = function(decoder) { var packed; var val = new MediaRouteProvider_StopObservingMediaSinks_Params(); var numberOfBytes = decoder.readUint32(); var version = decoder.readUint32(); val.media_source = decoder.decodeStruct(codec.String); return val; }; MediaRouteProvider_StopObservingMediaSinks_Params.encode = function(encoder, val) { var packed; encoder.writeUint32(MediaRouteProvider_StopObservingMediaSinks_Params.encodedSize); encoder.writeUint32(0); encoder.encodeStruct(codec.String, val.media_source); }; function MediaRouteProvider_StartObservingMediaRoutes_Params(values) { this.initDefaults_(); this.initFields_(values); } MediaRouteProvider_StartObservingMediaRoutes_Params.prototype.initDefaults_ = function() { this.media_source = null; }; MediaRouteProvider_StartObservingMediaRoutes_Params.prototype.initFields_ = function(fields) { for(var field in fields) { if (this.hasOwnProperty(field)) this[field] = fields[field]; } }; MediaRouteProvider_StartObservingMediaRoutes_Params.validate = function(messageValidator, offset) { var err; err = messageValidator.validateStructHeader(offset, codec.kStructHeaderSize); if (err !== validator.validationError.NONE) return err; var kVersionSizes = [ {version: 0, numBytes: 16} ]; err = messageValidator.validateStructVersion(offset, kVersionSizes); if (err !== validator.validationError.NONE) return err; // validate MediaRouteProvider_StartObservingMediaRoutes_Params.media_source err = messageValidator.validateStringPointer(offset + codec.kStructHeaderSize + 0, false) if (err !== validator.validationError.NONE) return err; return validator.validationError.NONE; }; MediaRouteProvider_StartObservingMediaRoutes_Params.encodedSize = codec.kStructHeaderSize + 8; MediaRouteProvider_StartObservingMediaRoutes_Params.decode = function(decoder) { var packed; var val = new MediaRouteProvider_StartObservingMediaRoutes_Params(); var numberOfBytes = decoder.readUint32(); var version = decoder.readUint32(); val.media_source = decoder.decodeStruct(codec.String); return val; }; MediaRouteProvider_StartObservingMediaRoutes_Params.encode = function(encoder, val) { var packed; encoder.writeUint32(MediaRouteProvider_StartObservingMediaRoutes_Params.encodedSize); encoder.writeUint32(0); encoder.encodeStruct(codec.String, val.media_source); }; function MediaRouteProvider_StopObservingMediaRoutes_Params(values) { this.initDefaults_(); this.initFields_(values); } MediaRouteProvider_StopObservingMediaRoutes_Params.prototype.initDefaults_ = function() { this.media_source = null; }; MediaRouteProvider_StopObservingMediaRoutes_Params.prototype.initFields_ = function(fields) { for(var field in fields) { if (this.hasOwnProperty(field)) this[field] = fields[field]; } }; MediaRouteProvider_StopObservingMediaRoutes_Params.validate = function(messageValidator, offset) { var err; err = messageValidator.validateStructHeader(offset, codec.kStructHeaderSize); if (err !== validator.validationError.NONE) return err; var kVersionSizes = [ {version: 0, numBytes: 16} ]; err = messageValidator.validateStructVersion(offset, kVersionSizes); if (err !== validator.validationError.NONE) return err; // validate MediaRouteProvider_StopObservingMediaRoutes_Params.media_source err = messageValidator.validateStringPointer(offset + codec.kStructHeaderSize + 0, false) if (err !== validator.validationError.NONE) return err; return validator.validationError.NONE; }; MediaRouteProvider_StopObservingMediaRoutes_Params.encodedSize = codec.kStructHeaderSize + 8; MediaRouteProvider_StopObservingMediaRoutes_Params.decode = function(decoder) { var packed; var val = new MediaRouteProvider_StopObservingMediaRoutes_Params(); var numberOfBytes = decoder.readUint32(); var version = decoder.readUint32(); val.media_source = decoder.decodeStruct(codec.String); return val; }; MediaRouteProvider_StopObservingMediaRoutes_Params.encode = function(encoder, val) { var packed; encoder.writeUint32(MediaRouteProvider_StopObservingMediaRoutes_Params.encodedSize); encoder.writeUint32(0); encoder.encodeStruct(codec.String, val.media_source); }; function MediaRouteProvider_StartListeningForRouteMessages_Params(values) { this.initDefaults_(); this.initFields_(values); } MediaRouteProvider_StartListeningForRouteMessages_Params.prototype.initDefaults_ = function() { this.route_id = null; }; MediaRouteProvider_StartListeningForRouteMessages_Params.prototype.initFields_ = function(fields) { for(var field in fields) { if (this.hasOwnProperty(field)) this[field] = fields[field]; } }; MediaRouteProvider_StartListeningForRouteMessages_Params.validate = function(messageValidator, offset) { var err; err = messageValidator.validateStructHeader(offset, codec.kStructHeaderSize); if (err !== validator.validationError.NONE) return err; var kVersionSizes = [ {version: 0, numBytes: 16} ]; err = messageValidator.validateStructVersion(offset, kVersionSizes); if (err !== validator.validationError.NONE) return err; // validate MediaRouteProvider_StartListeningForRouteMessages_Params.route_id err = messageValidator.validateStringPointer(offset + codec.kStructHeaderSize + 0, false) if (err !== validator.validationError.NONE) return err; return validator.validationError.NONE; }; MediaRouteProvider_StartListeningForRouteMessages_Params.encodedSize = codec.kStructHeaderSize + 8; MediaRouteProvider_StartListeningForRouteMessages_Params.decode = function(decoder) { var packed; var val = new MediaRouteProvider_StartListeningForRouteMessages_Params(); var numberOfBytes = decoder.readUint32(); var version = decoder.readUint32(); val.route_id = decoder.decodeStruct(codec.String); return val; }; MediaRouteProvider_StartListeningForRouteMessages_Params.encode = function(encoder, val) { var packed; encoder.writeUint32(MediaRouteProvider_StartListeningForRouteMessages_Params.encodedSize); encoder.writeUint32(0); encoder.encodeStruct(codec.String, val.route_id); }; function MediaRouteProvider_StopListeningForRouteMessages_Params(values) { this.initDefaults_(); this.initFields_(values); } MediaRouteProvider_StopListeningForRouteMessages_Params.prototype.initDefaults_ = function() { this.route_id = null; }; MediaRouteProvider_StopListeningForRouteMessages_Params.prototype.initFields_ = function(fields) { for(var field in fields) { if (this.hasOwnProperty(field)) this[field] = fields[field]; } }; MediaRouteProvider_StopListeningForRouteMessages_Params.validate = function(messageValidator, offset) { var err; err = messageValidator.validateStructHeader(offset, codec.kStructHeaderSize); if (err !== validator.validationError.NONE) return err; var kVersionSizes = [ {version: 0, numBytes: 16} ]; err = messageValidator.validateStructVersion(offset, kVersionSizes); if (err !== validator.validationError.NONE) return err; // validate MediaRouteProvider_StopListeningForRouteMessages_Params.route_id err = messageValidator.validateStringPointer(offset + codec.kStructHeaderSize + 0, false) if (err !== validator.validationError.NONE) return err; return validator.validationError.NONE; }; MediaRouteProvider_StopListeningForRouteMessages_Params.encodedSize = codec.kStructHeaderSize + 8; MediaRouteProvider_StopListeningForRouteMessages_Params.decode = function(decoder) { var packed; var val = new MediaRouteProvider_StopListeningForRouteMessages_Params(); var numberOfBytes = decoder.readUint32(); var version = decoder.readUint32(); val.route_id = decoder.decodeStruct(codec.String); return val; }; MediaRouteProvider_StopListeningForRouteMessages_Params.encode = function(encoder, val) { var packed; encoder.writeUint32(MediaRouteProvider_StopListeningForRouteMessages_Params.encodedSize); encoder.writeUint32(0); encoder.encodeStruct(codec.String, val.route_id); }; function MediaRouteProvider_DetachRoute_Params(values) { this.initDefaults_(); this.initFields_(values); } MediaRouteProvider_DetachRoute_Params.prototype.initDefaults_ = function() { this.route_id = null; }; MediaRouteProvider_DetachRoute_Params.prototype.initFields_ = function(fields) { for(var field in fields) { if (this.hasOwnProperty(field)) this[field] = fields[field]; } }; MediaRouteProvider_DetachRoute_Params.validate = function(messageValidator, offset) { var err; err = messageValidator.validateStructHeader(offset, codec.kStructHeaderSize); if (err !== validator.validationError.NONE) return err; var kVersionSizes = [ {version: 0, numBytes: 16} ]; err = messageValidator.validateStructVersion(offset, kVersionSizes); if (err !== validator.validationError.NONE) return err; // validate MediaRouteProvider_DetachRoute_Params.route_id err = messageValidator.validateStringPointer(offset + codec.kStructHeaderSize + 0, false) if (err !== validator.validationError.NONE) return err; return validator.validationError.NONE; }; MediaRouteProvider_DetachRoute_Params.encodedSize = codec.kStructHeaderSize + 8; MediaRouteProvider_DetachRoute_Params.decode = function(decoder) { var packed; var val = new MediaRouteProvider_DetachRoute_Params(); var numberOfBytes = decoder.readUint32(); var version = decoder.readUint32(); val.route_id = decoder.decodeStruct(codec.String); return val; }; MediaRouteProvider_DetachRoute_Params.encode = function(encoder, val) { var packed; encoder.writeUint32(MediaRouteProvider_DetachRoute_Params.encodedSize); encoder.writeUint32(0); encoder.encodeStruct(codec.String, val.route_id); }; function MediaRouteProvider_EnableMdnsDiscovery_Params(values) { this.initDefaults_(); this.initFields_(values); } MediaRouteProvider_EnableMdnsDiscovery_Params.prototype.initDefaults_ = function() { }; MediaRouteProvider_EnableMdnsDiscovery_Params.prototype.initFields_ = function(fields) { for(var field in fields) { if (this.hasOwnProperty(field)) this[field] = fields[field]; } }; MediaRouteProvider_EnableMdnsDiscovery_Params.validate = function(messageValidator, offset) { var err; err = messageValidator.validateStructHeader(offset, codec.kStructHeaderSize); if (err !== validator.validationError.NONE) return err; var kVersionSizes = [ {version: 0, numBytes: 8} ]; err = messageValidator.validateStructVersion(offset, kVersionSizes); if (err !== validator.validationError.NONE) return err; return validator.validationError.NONE; }; MediaRouteProvider_EnableMdnsDiscovery_Params.encodedSize = codec.kStructHeaderSize + 0; MediaRouteProvider_EnableMdnsDiscovery_Params.decode = function(decoder) { var packed; var val = new MediaRouteProvider_EnableMdnsDiscovery_Params(); var numberOfBytes = decoder.readUint32(); var version = decoder.readUint32(); return val; }; MediaRouteProvider_EnableMdnsDiscovery_Params.encode = function(encoder, val) { var packed; encoder.writeUint32(MediaRouteProvider_EnableMdnsDiscovery_Params.encodedSize); encoder.writeUint32(0); }; function MediaRouteProvider_UpdateMediaSinks_Params(values) { this.initDefaults_(); this.initFields_(values); } MediaRouteProvider_UpdateMediaSinks_Params.prototype.initDefaults_ = function() { this.media_source = null; }; MediaRouteProvider_UpdateMediaSinks_Params.prototype.initFields_ = function(fields) { for(var field in fields) { if (this.hasOwnProperty(field)) this[field] = fields[field]; } }; MediaRouteProvider_UpdateMediaSinks_Params.validate = function(messageValidator, offset) { var err; err = messageValidator.validateStructHeader(offset, codec.kStructHeaderSize); if (err !== validator.validationError.NONE) return err; var kVersionSizes = [ {version: 0, numBytes: 16} ]; err = messageValidator.validateStructVersion(offset, kVersionSizes); if (err !== validator.validationError.NONE) return err; // validate MediaRouteProvider_UpdateMediaSinks_Params.media_source err = messageValidator.validateStringPointer(offset + codec.kStructHeaderSize + 0, false) if (err !== validator.validationError.NONE) return err; return validator.validationError.NONE; }; MediaRouteProvider_UpdateMediaSinks_Params.encodedSize = codec.kStructHeaderSize + 8; MediaRouteProvider_UpdateMediaSinks_Params.decode = function(decoder) { var packed; var val = new MediaRouteProvider_UpdateMediaSinks_Params(); var numberOfBytes = decoder.readUint32(); var version = decoder.readUint32(); val.media_source = decoder.decodeStruct(codec.String); return val; }; MediaRouteProvider_UpdateMediaSinks_Params.encode = function(encoder, val) { var packed; encoder.writeUint32(MediaRouteProvider_UpdateMediaSinks_Params.encodedSize); encoder.writeUint32(0); encoder.encodeStruct(codec.String, val.media_source); }; function MediaRouteProvider_SearchSinks_Params(values) { this.initDefaults_(); this.initFields_(values); } MediaRouteProvider_SearchSinks_Params.prototype.initDefaults_ = function() { this.sink_id = null; this.media_source = null; this.search_criteria = null; }; MediaRouteProvider_SearchSinks_Params.prototype.initFields_ = function(fields) { for(var field in fields) { if (this.hasOwnProperty(field)) this[field] = fields[field]; } }; MediaRouteProvider_SearchSinks_Params.validate = function(messageValidator, offset) { var err; err = messageValidator.validateStructHeader(offset, codec.kStructHeaderSize); if (err !== validator.validationError.NONE) return err; var kVersionSizes = [ {version: 0, numBytes: 32} ]; err = messageValidator.validateStructVersion(offset, kVersionSizes); if (err !== validator.validationError.NONE) return err; // validate MediaRouteProvider_SearchSinks_Params.sink_id err = messageValidator.validateStringPointer(offset + codec.kStructHeaderSize + 0, false) if (err !== validator.validationError.NONE) return err; // validate MediaRouteProvider_SearchSinks_Params.media_source err = messageValidator.validateStringPointer(offset + codec.kStructHeaderSize + 8, false) if (err !== validator.validationError.NONE) return err; // validate MediaRouteProvider_SearchSinks_Params.search_criteria err = messageValidator.validateStructPointer(offset + codec.kStructHeaderSize + 16, SinkSearchCriteria, false); if (err !== validator.validationError.NONE) return err; return validator.validationError.NONE; }; MediaRouteProvider_SearchSinks_Params.encodedSize = codec.kStructHeaderSize + 24; MediaRouteProvider_SearchSinks_Params.decode = function(decoder) { var packed; var val = new MediaRouteProvider_SearchSinks_Params(); var numberOfBytes = decoder.readUint32(); var version = decoder.readUint32(); val.sink_id = decoder.decodeStruct(codec.String); val.media_source = decoder.decodeStruct(codec.String); val.search_criteria = decoder.decodeStructPointer(SinkSearchCriteria); return val; }; MediaRouteProvider_SearchSinks_Params.encode = function(encoder, val) { var packed; encoder.writeUint32(MediaRouteProvider_SearchSinks_Params.encodedSize); encoder.writeUint32(0); encoder.encodeStruct(codec.String, val.sink_id); encoder.encodeStruct(codec.String, val.media_source); encoder.encodeStructPointer(SinkSearchCriteria, val.search_criteria); }; function MediaRouteProvider_SearchSinks_ResponseParams(values) { this.initDefaults_(); this.initFields_(values); } MediaRouteProvider_SearchSinks_ResponseParams.prototype.initDefaults_ = function() { this.sink_id = null; }; MediaRouteProvider_SearchSinks_ResponseParams.prototype.initFields_ = function(fields) { for(var field in fields) { if (this.hasOwnProperty(field)) this[field] = fields[field]; } }; MediaRouteProvider_SearchSinks_ResponseParams.validate = function(messageValidator, offset) { var err; err = messageValidator.validateStructHeader(offset, codec.kStructHeaderSize); if (err !== validator.validationError.NONE) return err; var kVersionSizes = [ {version: 0, numBytes: 16} ]; err = messageValidator.validateStructVersion(offset, kVersionSizes); if (err !== validator.validationError.NONE) return err; // validate MediaRouteProvider_SearchSinks_ResponseParams.sink_id err = messageValidator.validateStringPointer(offset + codec.kStructHeaderSize + 0, false) if (err !== validator.validationError.NONE) return err; return validator.validationError.NONE; }; MediaRouteProvider_SearchSinks_ResponseParams.encodedSize = codec.kStructHeaderSize + 8; MediaRouteProvider_SearchSinks_ResponseParams.decode = function(decoder) { var packed; var val = new MediaRouteProvider_SearchSinks_ResponseParams(); var numberOfBytes = decoder.readUint32(); var version = decoder.readUint32(); val.sink_id = decoder.decodeStruct(codec.String); return val; }; MediaRouteProvider_SearchSinks_ResponseParams.encode = function(encoder, val) { var packed; encoder.writeUint32(MediaRouteProvider_SearchSinks_ResponseParams.encodedSize); encoder.writeUint32(0); encoder.encodeStruct(codec.String, val.sink_id); }; function MediaRouter_RegisterMediaRouteProvider_Params(values) { this.initDefaults_(); this.initFields_(values); } MediaRouter_RegisterMediaRouteProvider_Params.prototype.initDefaults_ = function() { this.media_router_provider = new MediaRouteProviderPtr(); }; MediaRouter_RegisterMediaRouteProvider_Params.prototype.initFields_ = function(fields) { for(var field in fields) { if (this.hasOwnProperty(field)) this[field] = fields[field]; } }; MediaRouter_RegisterMediaRouteProvider_Params.validate = function(messageValidator, offset) { var err; err = messageValidator.validateStructHeader(offset, codec.kStructHeaderSize); if (err !== validator.validationError.NONE) return err; var kVersionSizes = [ {version: 0, numBytes: 16} ]; err = messageValidator.validateStructVersion(offset, kVersionSizes); if (err !== validator.validationError.NONE) return err; // validate MediaRouter_RegisterMediaRouteProvider_Params.media_router_provider err = messageValidator.validateInterface(offset + codec.kStructHeaderSize + 0, false); if (err !== validator.validationError.NONE) return err; return validator.validationError.NONE; }; MediaRouter_RegisterMediaRouteProvider_Params.encodedSize = codec.kStructHeaderSize + 8; MediaRouter_RegisterMediaRouteProvider_Params.decode = function(decoder) { var packed; var val = new MediaRouter_RegisterMediaRouteProvider_Params(); var numberOfBytes = decoder.readUint32(); var version = decoder.readUint32(); val.media_router_provider = decoder.decodeStruct(new codec.Interface(MediaRouteProviderPtr)); return val; }; MediaRouter_RegisterMediaRouteProvider_Params.encode = function(encoder, val) { var packed; encoder.writeUint32(MediaRouter_RegisterMediaRouteProvider_Params.encodedSize); encoder.writeUint32(0); encoder.encodeStruct(new codec.Interface(MediaRouteProviderPtr), val.media_router_provider); }; function MediaRouter_RegisterMediaRouteProvider_ResponseParams(values) { this.initDefaults_(); this.initFields_(values); } MediaRouter_RegisterMediaRouteProvider_ResponseParams.prototype.initDefaults_ = function() { this.instance_id = null; }; MediaRouter_RegisterMediaRouteProvider_ResponseParams.prototype.initFields_ = function(fields) { for(var field in fields) { if (this.hasOwnProperty(field)) this[field] = fields[field]; } }; MediaRouter_RegisterMediaRouteProvider_ResponseParams.validate = function(messageValidator, offset) { var err; err = messageValidator.validateStructHeader(offset, codec.kStructHeaderSize); if (err !== validator.validationError.NONE) return err; var kVersionSizes = [ {version: 0, numBytes: 16} ]; err = messageValidator.validateStructVersion(offset, kVersionSizes); if (err !== validator.validationError.NONE) return err; // validate MediaRouter_RegisterMediaRouteProvider_ResponseParams.instance_id err = messageValidator.validateStringPointer(offset + codec.kStructHeaderSize + 0, false) if (err !== validator.validationError.NONE) return err; return validator.validationError.NONE; }; MediaRouter_RegisterMediaRouteProvider_ResponseParams.encodedSize = codec.kStructHeaderSize + 8; MediaRouter_RegisterMediaRouteProvider_ResponseParams.decode = function(decoder) { var packed; var val = new MediaRouter_RegisterMediaRouteProvider_ResponseParams(); var numberOfBytes = decoder.readUint32(); var version = decoder.readUint32(); val.instance_id = decoder.decodeStruct(codec.String); return val; }; MediaRouter_RegisterMediaRouteProvider_ResponseParams.encode = function(encoder, val) { var packed; encoder.writeUint32(MediaRouter_RegisterMediaRouteProvider_ResponseParams.encodedSize); encoder.writeUint32(0); encoder.encodeStruct(codec.String, val.instance_id); }; function MediaRouter_OnSinksReceived_Params(values) { this.initDefaults_(); this.initFields_(values); } MediaRouter_OnSinksReceived_Params.prototype.initDefaults_ = function() { this.media_source = null; this.sinks = null; this.origins = null; }; MediaRouter_OnSinksReceived_Params.prototype.initFields_ = function(fields) { for(var field in fields) { if (this.hasOwnProperty(field)) this[field] = fields[field]; } }; MediaRouter_OnSinksReceived_Params.validate = function(messageValidator, offset) { var err; err = messageValidator.validateStructHeader(offset, codec.kStructHeaderSize); if (err !== validator.validationError.NONE) return err; var kVersionSizes = [ {version: 0, numBytes: 32} ]; err = messageValidator.validateStructVersion(offset, kVersionSizes); if (err !== validator.validationError.NONE) return err; // validate MediaRouter_OnSinksReceived_Params.media_source err = messageValidator.validateStringPointer(offset + codec.kStructHeaderSize + 0, false) if (err !== validator.validationError.NONE) return err; // validate MediaRouter_OnSinksReceived_Params.sinks err = messageValidator.validateArrayPointer(offset + codec.kStructHeaderSize + 8, 8, new codec.PointerTo(MediaSink), false, [0], 0); if (err !== validator.validationError.NONE) return err; // validate MediaRouter_OnSinksReceived_Params.origins err = messageValidator.validateArrayPointer(offset + codec.kStructHeaderSize + 16, 8, codec.String, false, [0, 0], 0); if (err !== validator.validationError.NONE) return err; return validator.validationError.NONE; }; MediaRouter_OnSinksReceived_Params.encodedSize = codec.kStructHeaderSize + 24; MediaRouter_OnSinksReceived_Params.decode = function(decoder) { var packed; var val = new MediaRouter_OnSinksReceived_Params(); var numberOfBytes = decoder.readUint32(); var version = decoder.readUint32(); val.media_source = decoder.decodeStruct(codec.String); val.sinks = decoder.decodeArrayPointer(new codec.PointerTo(MediaSink)); val.origins = decoder.decodeArrayPointer(codec.String); return val; }; MediaRouter_OnSinksReceived_Params.encode = function(encoder, val) { var packed; encoder.writeUint32(MediaRouter_OnSinksReceived_Params.encodedSize); encoder.writeUint32(0); encoder.encodeStruct(codec.String, val.media_source); encoder.encodeArrayPointer(new codec.PointerTo(MediaSink), val.sinks); encoder.encodeArrayPointer(codec.String, val.origins); }; function MediaRouter_OnIssue_Params(values) { this.initDefaults_(); this.initFields_(values); } MediaRouter_OnIssue_Params.prototype.initDefaults_ = function() { this.issue = null; }; MediaRouter_OnIssue_Params.prototype.initFields_ = function(fields) { for(var field in fields) { if (this.hasOwnProperty(field)) this[field] = fields[field]; } }; MediaRouter_OnIssue_Params.validate = function(messageValidator, offset) { var err; err = messageValidator.validateStructHeader(offset, codec.kStructHeaderSize); if (err !== validator.validationError.NONE) return err; var kVersionSizes = [ {version: 0, numBytes: 16} ]; err = messageValidator.validateStructVersion(offset, kVersionSizes); if (err !== validator.validationError.NONE) return err; // validate MediaRouter_OnIssue_Params.issue err = messageValidator.validateStructPointer(offset + codec.kStructHeaderSize + 0, Issue, false); if (err !== validator.validationError.NONE) return err; return validator.validationError.NONE; }; MediaRouter_OnIssue_Params.encodedSize = codec.kStructHeaderSize + 8; MediaRouter_OnIssue_Params.decode = function(decoder) { var packed; var val = new MediaRouter_OnIssue_Params(); var numberOfBytes = decoder.readUint32(); var version = decoder.readUint32(); val.issue = decoder.decodeStructPointer(Issue); return val; }; MediaRouter_OnIssue_Params.encode = function(encoder, val) { var packed; encoder.writeUint32(MediaRouter_OnIssue_Params.encodedSize); encoder.writeUint32(0); encoder.encodeStructPointer(Issue, val.issue); }; function MediaRouter_OnRoutesUpdated_Params(values) { this.initDefaults_(); this.initFields_(values); } MediaRouter_OnRoutesUpdated_Params.prototype.initDefaults_ = function() { this.routes = null; this.media_source = null; this.joinable_route_ids = null; }; MediaRouter_OnRoutesUpdated_Params.prototype.initFields_ = function(fields) { for(var field in fields) { if (this.hasOwnProperty(field)) this[field] = fields[field]; } }; MediaRouter_OnRoutesUpdated_Params.validate = function(messageValidator, offset) { var err; err = messageValidator.validateStructHeader(offset, codec.kStructHeaderSize); if (err !== validator.validationError.NONE) return err; var kVersionSizes = [ {version: 0, numBytes: 32} ]; err = messageValidator.validateStructVersion(offset, kVersionSizes); if (err !== validator.validationError.NONE) return err; // validate MediaRouter_OnRoutesUpdated_Params.routes err = messageValidator.validateArrayPointer(offset + codec.kStructHeaderSize + 0, 8, new codec.PointerTo(MediaRoute), false, [0], 0); if (err !== validator.validationError.NONE) return err; // validate MediaRouter_OnRoutesUpdated_Params.media_source err = messageValidator.validateStringPointer(offset + codec.kStructHeaderSize + 8, false) if (err !== validator.validationError.NONE) return err; // validate MediaRouter_OnRoutesUpdated_Params.joinable_route_ids err = messageValidator.validateArrayPointer(offset + codec.kStructHeaderSize + 16, 8, codec.String, false, [0, 0], 0); if (err !== validator.validationError.NONE) return err; return validator.validationError.NONE; }; MediaRouter_OnRoutesUpdated_Params.encodedSize = codec.kStructHeaderSize + 24; MediaRouter_OnRoutesUpdated_Params.decode = function(decoder) { var packed; var val = new MediaRouter_OnRoutesUpdated_Params(); var numberOfBytes = decoder.readUint32(); var version = decoder.readUint32(); val.routes = decoder.decodeArrayPointer(new codec.PointerTo(MediaRoute)); val.media_source = decoder.decodeStruct(codec.String); val.joinable_route_ids = decoder.decodeArrayPointer(codec.String); return val; }; MediaRouter_OnRoutesUpdated_Params.encode = function(encoder, val) { var packed; encoder.writeUint32(MediaRouter_OnRoutesUpdated_Params.encodedSize); encoder.writeUint32(0); encoder.encodeArrayPointer(new codec.PointerTo(MediaRoute), val.routes); encoder.encodeStruct(codec.String, val.media_source); encoder.encodeArrayPointer(codec.String, val.joinable_route_ids); }; function MediaRouter_OnSinkAvailabilityUpdated_Params(values) { this.initDefaults_(); this.initFields_(values); } MediaRouter_OnSinkAvailabilityUpdated_Params.prototype.initDefaults_ = function() { this.availability = 0; }; MediaRouter_OnSinkAvailabilityUpdated_Params.prototype.initFields_ = function(fields) { for(var field in fields) { if (this.hasOwnProperty(field)) this[field] = fields[field]; } }; MediaRouter_OnSinkAvailabilityUpdated_Params.validate = function(messageValidator, offset) { var err; err = messageValidator.validateStructHeader(offset, codec.kStructHeaderSize); if (err !== validator.validationError.NONE) return err; var kVersionSizes = [ {version: 0, numBytes: 16} ]; err = messageValidator.validateStructVersion(offset, kVersionSizes); if (err !== validator.validationError.NONE) return err; // validate MediaRouter_OnSinkAvailabilityUpdated_Params.availability err = messageValidator.validateEnum(offset + codec.kStructHeaderSize + 0, MediaRouter.SinkAvailability); if (err !== validator.validationError.NONE) return err; return validator.validationError.NONE; }; MediaRouter_OnSinkAvailabilityUpdated_Params.encodedSize = codec.kStructHeaderSize + 8; MediaRouter_OnSinkAvailabilityUpdated_Params.decode = function(decoder) { var packed; var val = new MediaRouter_OnSinkAvailabilityUpdated_Params(); var numberOfBytes = decoder.readUint32(); var version = decoder.readUint32(); val.availability = decoder.decodeStruct(codec.Int32); decoder.skip(1); decoder.skip(1); decoder.skip(1); decoder.skip(1); return val; }; MediaRouter_OnSinkAvailabilityUpdated_Params.encode = function(encoder, val) { var packed; encoder.writeUint32(MediaRouter_OnSinkAvailabilityUpdated_Params.encodedSize); encoder.writeUint32(0); encoder.encodeStruct(codec.Int32, val.availability); encoder.skip(1); encoder.skip(1); encoder.skip(1); encoder.skip(1); }; function MediaRouter_OnPresentationConnectionStateChanged_Params(values) { this.initDefaults_(); this.initFields_(values); } MediaRouter_OnPresentationConnectionStateChanged_Params.prototype.initDefaults_ = function() { this.route_id = null; this.state = 0; }; MediaRouter_OnPresentationConnectionStateChanged_Params.prototype.initFields_ = function(fields) { for(var field in fields) { if (this.hasOwnProperty(field)) this[field] = fields[field]; } }; MediaRouter_OnPresentationConnectionStateChanged_Params.validate = function(messageValidator, offset) { var err; err = messageValidator.validateStructHeader(offset, codec.kStructHeaderSize); if (err !== validator.validationError.NONE) return err; var kVersionSizes = [ {version: 0, numBytes: 24} ]; err = messageValidator.validateStructVersion(offset, kVersionSizes); if (err !== validator.validationError.NONE) return err; // validate MediaRouter_OnPresentationConnectionStateChanged_Params.route_id err = messageValidator.validateStringPointer(offset + codec.kStructHeaderSize + 0, false) if (err !== validator.validationError.NONE) return err; // validate MediaRouter_OnPresentationConnectionStateChanged_Params.state err = messageValidator.validateEnum(offset + codec.kStructHeaderSize + 8, MediaRouter.PresentationConnectionState); if (err !== validator.validationError.NONE) return err; return validator.validationError.NONE; }; MediaRouter_OnPresentationConnectionStateChanged_Params.encodedSize = codec.kStructHeaderSize + 16; MediaRouter_OnPresentationConnectionStateChanged_Params.decode = function(decoder) { var packed; var val = new MediaRouter_OnPresentationConnectionStateChanged_Params(); var numberOfBytes = decoder.readUint32(); var version = decoder.readUint32(); val.route_id = decoder.decodeStruct(codec.String); val.state = decoder.decodeStruct(codec.Int32); decoder.skip(1); decoder.skip(1); decoder.skip(1); decoder.skip(1); return val; }; MediaRouter_OnPresentationConnectionStateChanged_Params.encode = function(encoder, val) { var packed; encoder.writeUint32(MediaRouter_OnPresentationConnectionStateChanged_Params.encodedSize); encoder.writeUint32(0); encoder.encodeStruct(codec.String, val.route_id); encoder.encodeStruct(codec.Int32, val.state); encoder.skip(1); encoder.skip(1); encoder.skip(1); encoder.skip(1); }; function MediaRouter_OnPresentationConnectionClosed_Params(values) { this.initDefaults_(); this.initFields_(values); } MediaRouter_OnPresentationConnectionClosed_Params.prototype.initDefaults_ = function() { this.route_id = null; this.reason = 0; this.message = null; }; MediaRouter_OnPresentationConnectionClosed_Params.prototype.initFields_ = function(fields) { for(var field in fields) { if (this.hasOwnProperty(field)) this[field] = fields[field]; } }; MediaRouter_OnPresentationConnectionClosed_Params.validate = function(messageValidator, offset) { var err; err = messageValidator.validateStructHeader(offset, codec.kStructHeaderSize); if (err !== validator.validationError.NONE) return err; var kVersionSizes = [ {version: 0, numBytes: 32} ]; err = messageValidator.validateStructVersion(offset, kVersionSizes); if (err !== validator.validationError.NONE) return err; // validate MediaRouter_OnPresentationConnectionClosed_Params.route_id err = messageValidator.validateStringPointer(offset + codec.kStructHeaderSize + 0, false) if (err !== validator.validationError.NONE) return err; // validate MediaRouter_OnPresentationConnectionClosed_Params.reason err = messageValidator.validateEnum(offset + codec.kStructHeaderSize + 8, MediaRouter.PresentationConnectionCloseReason); if (err !== validator.validationError.NONE) return err; // validate MediaRouter_OnPresentationConnectionClosed_Params.message err = messageValidator.validateStringPointer(offset + codec.kStructHeaderSize + 16, false) if (err !== validator.validationError.NONE) return err; return validator.validationError.NONE; }; MediaRouter_OnPresentationConnectionClosed_Params.encodedSize = codec.kStructHeaderSize + 24; MediaRouter_OnPresentationConnectionClosed_Params.decode = function(decoder) { var packed; var val = new MediaRouter_OnPresentationConnectionClosed_Params(); var numberOfBytes = decoder.readUint32(); var version = decoder.readUint32(); val.route_id = decoder.decodeStruct(codec.String); val.reason = decoder.decodeStruct(codec.Int32); decoder.skip(1); decoder.skip(1); decoder.skip(1); decoder.skip(1); val.message = decoder.decodeStruct(codec.String); return val; }; MediaRouter_OnPresentationConnectionClosed_Params.encode = function(encoder, val) { var packed; encoder.writeUint32(MediaRouter_OnPresentationConnectionClosed_Params.encodedSize); encoder.writeUint32(0); encoder.encodeStruct(codec.String, val.route_id); encoder.encodeStruct(codec.Int32, val.reason); encoder.skip(1); encoder.skip(1); encoder.skip(1); encoder.skip(1); encoder.encodeStruct(codec.String, val.message); }; function MediaRouter_OnRouteMessagesReceived_Params(values) { this.initDefaults_(); this.initFields_(values); } MediaRouter_OnRouteMessagesReceived_Params.prototype.initDefaults_ = function() { this.route_id = null; this.messages = null; }; MediaRouter_OnRouteMessagesReceived_Params.prototype.initFields_ = function(fields) { for(var field in fields) { if (this.hasOwnProperty(field)) this[field] = fields[field]; } }; MediaRouter_OnRouteMessagesReceived_Params.validate = function(messageValidator, offset) { var err; err = messageValidator.validateStructHeader(offset, codec.kStructHeaderSize); if (err !== validator.validationError.NONE) return err; var kVersionSizes = [ {version: 0, numBytes: 24} ]; err = messageValidator.validateStructVersion(offset, kVersionSizes); if (err !== validator.validationError.NONE) return err; // validate MediaRouter_OnRouteMessagesReceived_Params.route_id err = messageValidator.validateStringPointer(offset + codec.kStructHeaderSize + 0, false) if (err !== validator.validationError.NONE) return err; // validate MediaRouter_OnRouteMessagesReceived_Params.messages err = messageValidator.validateArrayPointer(offset + codec.kStructHeaderSize + 8, 8, new codec.PointerTo(RouteMessage), false, [0], 0); if (err !== validator.validationError.NONE) return err; return validator.validationError.NONE; }; MediaRouter_OnRouteMessagesReceived_Params.encodedSize = codec.kStructHeaderSize + 16; MediaRouter_OnRouteMessagesReceived_Params.decode = function(decoder) { var packed; var val = new MediaRouter_OnRouteMessagesReceived_Params(); var numberOfBytes = decoder.readUint32(); var version = decoder.readUint32(); val.route_id = decoder.decodeStruct(codec.String); val.messages = decoder.decodeArrayPointer(new codec.PointerTo(RouteMessage)); return val; }; MediaRouter_OnRouteMessagesReceived_Params.encode = function(encoder, val) { var packed; encoder.writeUint32(MediaRouter_OnRouteMessagesReceived_Params.encodedSize); encoder.writeUint32(0); encoder.encodeStruct(codec.String, val.route_id); encoder.encodeArrayPointer(new codec.PointerTo(RouteMessage), val.messages); }; var kMediaRouteProvider_CreateRoute_Name = 0; var kMediaRouteProvider_JoinRoute_Name = 1; var kMediaRouteProvider_ConnectRouteByRouteId_Name = 2; var kMediaRouteProvider_TerminateRoute_Name = 3; var kMediaRouteProvider_SendRouteMessage_Name = 4; var kMediaRouteProvider_SendRouteBinaryMessage_Name = 5; var kMediaRouteProvider_StartObservingMediaSinks_Name = 6; var kMediaRouteProvider_StopObservingMediaSinks_Name = 7; var kMediaRouteProvider_StartObservingMediaRoutes_Name = 8; var kMediaRouteProvider_StopObservingMediaRoutes_Name = 9; var kMediaRouteProvider_StartListeningForRouteMessages_Name = 10; var kMediaRouteProvider_StopListeningForRouteMessages_Name = 11; var kMediaRouteProvider_DetachRoute_Name = 12; var kMediaRouteProvider_EnableMdnsDiscovery_Name = 13; var kMediaRouteProvider_UpdateMediaSinks_Name = 14; var kMediaRouteProvider_SearchSinks_Name = 15; function MediaRouteProviderPtr(handleOrPtrInfo) { this.ptr = new bindings.InterfacePtrController(MediaRouteProvider, handleOrPtrInfo); } function MediaRouteProviderProxy(receiver) { this.receiver_ = receiver; } MediaRouteProviderPtr.prototype.createRoute = function() { return MediaRouteProviderProxy.prototype.createRoute .apply(this.ptr.getProxy(), arguments); }; MediaRouteProviderProxy.prototype.createRoute = function(media_source, sink_id, original_presentation_id, origin, tab_id, timeout, incognito) { var params = new MediaRouteProvider_CreateRoute_Params(); params.media_source = media_source; params.sink_id = sink_id; params.original_presentation_id = original_presentation_id; params.origin = origin; params.tab_id = tab_id; params.timeout = timeout; params.incognito = incognito; return new Promise(function(resolve, reject) { var builder = new codec.MessageWithRequestIDBuilder( kMediaRouteProvider_CreateRoute_Name, codec.align(MediaRouteProvider_CreateRoute_Params.encodedSize), codec.kMessageExpectsResponse, 0); builder.encodeStruct(MediaRouteProvider_CreateRoute_Params, params); var message = builder.finish(); this.receiver_.acceptAndExpectResponse(message).then(function(message) { var reader = new codec.MessageReader(message); var responseParams = reader.decodeStruct(MediaRouteProvider_CreateRoute_ResponseParams); resolve(responseParams); }).catch(function(result) { reject(Error("Connection error: " + result)); }); }.bind(this)); }; MediaRouteProviderPtr.prototype.joinRoute = function() { return MediaRouteProviderProxy.prototype.joinRoute .apply(this.ptr.getProxy(), arguments); }; MediaRouteProviderProxy.prototype.joinRoute = function(media_source, presentation_id, origin, tab_id, timeout, incognito) { var params = new MediaRouteProvider_JoinRoute_Params(); params.media_source = media_source; params.presentation_id = presentation_id; params.origin = origin; params.tab_id = tab_id; params.timeout = timeout; params.incognito = incognito; return new Promise(function(resolve, reject) { var builder = new codec.MessageWithRequestIDBuilder( kMediaRouteProvider_JoinRoute_Name, codec.align(MediaRouteProvider_JoinRoute_Params.encodedSize), codec.kMessageExpectsResponse, 0); builder.encodeStruct(MediaRouteProvider_JoinRoute_Params, params); var message = builder.finish(); this.receiver_.acceptAndExpectResponse(message).then(function(message) { var reader = new codec.MessageReader(message); var responseParams = reader.decodeStruct(MediaRouteProvider_JoinRoute_ResponseParams); resolve(responseParams); }).catch(function(result) { reject(Error("Connection error: " + result)); }); }.bind(this)); }; MediaRouteProviderPtr.prototype.connectRouteByRouteId = function() { return MediaRouteProviderProxy.prototype.connectRouteByRouteId .apply(this.ptr.getProxy(), arguments); }; MediaRouteProviderProxy.prototype.connectRouteByRouteId = function(media_source, route_id, presentation_id, origin, tab_id, timeout, incognito) { var params = new MediaRouteProvider_ConnectRouteByRouteId_Params(); params.media_source = media_source; params.route_id = route_id; params.presentation_id = presentation_id; params.origin = origin; params.tab_id = tab_id; params.timeout = timeout; params.incognito = incognito; return new Promise(function(resolve, reject) { var builder = new codec.MessageWithRequestIDBuilder( kMediaRouteProvider_ConnectRouteByRouteId_Name, codec.align(MediaRouteProvider_ConnectRouteByRouteId_Params.encodedSize), codec.kMessageExpectsResponse, 0); builder.encodeStruct(MediaRouteProvider_ConnectRouteByRouteId_Params, params); var message = builder.finish(); this.receiver_.acceptAndExpectResponse(message).then(function(message) { var reader = new codec.MessageReader(message); var responseParams = reader.decodeStruct(MediaRouteProvider_ConnectRouteByRouteId_ResponseParams); resolve(responseParams); }).catch(function(result) { reject(Error("Connection error: " + result)); }); }.bind(this)); }; MediaRouteProviderPtr.prototype.terminateRoute = function() { return MediaRouteProviderProxy.prototype.terminateRoute .apply(this.ptr.getProxy(), arguments); }; MediaRouteProviderProxy.prototype.terminateRoute = function(route_id) { var params = new MediaRouteProvider_TerminateRoute_Params(); params.route_id = route_id; return new Promise(function(resolve, reject) { var builder = new codec.MessageWithRequestIDBuilder( kMediaRouteProvider_TerminateRoute_Name, codec.align(MediaRouteProvider_TerminateRoute_Params.encodedSize), codec.kMessageExpectsResponse, 0); builder.encodeStruct(MediaRouteProvider_TerminateRoute_Params, params); var message = builder.finish(); this.receiver_.acceptAndExpectResponse(message).then(function(message) { var reader = new codec.MessageReader(message); var responseParams = reader.decodeStruct(MediaRouteProvider_TerminateRoute_ResponseParams); resolve(responseParams); }).catch(function(result) { reject(Error("Connection error: " + result)); }); }.bind(this)); }; MediaRouteProviderPtr.prototype.sendRouteMessage = function() { return MediaRouteProviderProxy.prototype.sendRouteMessage .apply(this.ptr.getProxy(), arguments); }; MediaRouteProviderProxy.prototype.sendRouteMessage = function(media_route_id, message) { var params = new MediaRouteProvider_SendRouteMessage_Params(); params.media_route_id = media_route_id; params.message = message; return new Promise(function(resolve, reject) { var builder = new codec.MessageWithRequestIDBuilder( kMediaRouteProvider_SendRouteMessage_Name, codec.align(MediaRouteProvider_SendRouteMessage_Params.encodedSize), codec.kMessageExpectsResponse, 0); builder.encodeStruct(MediaRouteProvider_SendRouteMessage_Params, params); var message = builder.finish(); this.receiver_.acceptAndExpectResponse(message).then(function(message) { var reader = new codec.MessageReader(message); var responseParams = reader.decodeStruct(MediaRouteProvider_SendRouteMessage_ResponseParams); resolve(responseParams); }).catch(function(result) { reject(Error("Connection error: " + result)); }); }.bind(this)); }; MediaRouteProviderPtr.prototype.sendRouteBinaryMessage = function() { return MediaRouteProviderProxy.prototype.sendRouteBinaryMessage .apply(this.ptr.getProxy(), arguments); }; MediaRouteProviderProxy.prototype.sendRouteBinaryMessage = function(media_route_id, data) { var params = new MediaRouteProvider_SendRouteBinaryMessage_Params(); params.media_route_id = media_route_id; params.data = data; return new Promise(function(resolve, reject) { var builder = new codec.MessageWithRequestIDBuilder( kMediaRouteProvider_SendRouteBinaryMessage_Name, codec.align(MediaRouteProvider_SendRouteBinaryMessage_Params.encodedSize), codec.kMessageExpectsResponse, 0); builder.encodeStruct(MediaRouteProvider_SendRouteBinaryMessage_Params, params); var message = builder.finish(); this.receiver_.acceptAndExpectResponse(message).then(function(message) { var reader = new codec.MessageReader(message); var responseParams = reader.decodeStruct(MediaRouteProvider_SendRouteBinaryMessage_ResponseParams); resolve(responseParams); }).catch(function(result) { reject(Error("Connection error: " + result)); }); }.bind(this)); }; MediaRouteProviderPtr.prototype.startObservingMediaSinks = function() { return MediaRouteProviderProxy.prototype.startObservingMediaSinks .apply(this.ptr.getProxy(), arguments); }; MediaRouteProviderProxy.prototype.startObservingMediaSinks = function(media_source) { var params = new MediaRouteProvider_StartObservingMediaSinks_Params(); params.media_source = media_source; var builder = new codec.MessageBuilder( kMediaRouteProvider_StartObservingMediaSinks_Name, codec.align(MediaRouteProvider_StartObservingMediaSinks_Params.encodedSize)); builder.encodeStruct(MediaRouteProvider_StartObservingMediaSinks_Params, params); var message = builder.finish(); this.receiver_.accept(message); }; MediaRouteProviderPtr.prototype.stopObservingMediaSinks = function() { return MediaRouteProviderProxy.prototype.stopObservingMediaSinks .apply(this.ptr.getProxy(), arguments); }; MediaRouteProviderProxy.prototype.stopObservingMediaSinks = function(media_source) { var params = new MediaRouteProvider_StopObservingMediaSinks_Params(); params.media_source = media_source; var builder = new codec.MessageBuilder( kMediaRouteProvider_StopObservingMediaSinks_Name, codec.align(MediaRouteProvider_StopObservingMediaSinks_Params.encodedSize)); builder.encodeStruct(MediaRouteProvider_StopObservingMediaSinks_Params, params); var message = builder.finish(); this.receiver_.accept(message); }; MediaRouteProviderPtr.prototype.startObservingMediaRoutes = function() { return MediaRouteProviderProxy.prototype.startObservingMediaRoutes .apply(this.ptr.getProxy(), arguments); }; MediaRouteProviderProxy.prototype.startObservingMediaRoutes = function(media_source) { var params = new MediaRouteProvider_StartObservingMediaRoutes_Params(); params.media_source = media_source; var builder = new codec.MessageBuilder( kMediaRouteProvider_StartObservingMediaRoutes_Name, codec.align(MediaRouteProvider_StartObservingMediaRoutes_Params.encodedSize)); builder.encodeStruct(MediaRouteProvider_StartObservingMediaRoutes_Params, params); var message = builder.finish(); this.receiver_.accept(message); }; MediaRouteProviderPtr.prototype.stopObservingMediaRoutes = function() { return MediaRouteProviderProxy.prototype.stopObservingMediaRoutes .apply(this.ptr.getProxy(), arguments); }; MediaRouteProviderProxy.prototype.stopObservingMediaRoutes = function(media_source) { var params = new MediaRouteProvider_StopObservingMediaRoutes_Params(); params.media_source = media_source; var builder = new codec.MessageBuilder( kMediaRouteProvider_StopObservingMediaRoutes_Name, codec.align(MediaRouteProvider_StopObservingMediaRoutes_Params.encodedSize)); builder.encodeStruct(MediaRouteProvider_StopObservingMediaRoutes_Params, params); var message = builder.finish(); this.receiver_.accept(message); }; MediaRouteProviderPtr.prototype.startListeningForRouteMessages = function() { return MediaRouteProviderProxy.prototype.startListeningForRouteMessages .apply(this.ptr.getProxy(), arguments); }; MediaRouteProviderProxy.prototype.startListeningForRouteMessages = function(route_id) { var params = new MediaRouteProvider_StartListeningForRouteMessages_Params(); params.route_id = route_id; var builder = new codec.MessageBuilder( kMediaRouteProvider_StartListeningForRouteMessages_Name, codec.align(MediaRouteProvider_StartListeningForRouteMessages_Params.encodedSize)); builder.encodeStruct(MediaRouteProvider_StartListeningForRouteMessages_Params, params); var message = builder.finish(); this.receiver_.accept(message); }; MediaRouteProviderPtr.prototype.stopListeningForRouteMessages = function() { return MediaRouteProviderProxy.prototype.stopListeningForRouteMessages .apply(this.ptr.getProxy(), arguments); }; MediaRouteProviderProxy.prototype.stopListeningForRouteMessages = function(route_id) { var params = new MediaRouteProvider_StopListeningForRouteMessages_Params(); params.route_id = route_id; var builder = new codec.MessageBuilder( kMediaRouteProvider_StopListeningForRouteMessages_Name, codec.align(MediaRouteProvider_StopListeningForRouteMessages_Params.encodedSize)); builder.encodeStruct(MediaRouteProvider_StopListeningForRouteMessages_Params, params); var message = builder.finish(); this.receiver_.accept(message); }; MediaRouteProviderPtr.prototype.detachRoute = function() { return MediaRouteProviderProxy.prototype.detachRoute .apply(this.ptr.getProxy(), arguments); }; MediaRouteProviderProxy.prototype.detachRoute = function(route_id) { var params = new MediaRouteProvider_DetachRoute_Params(); params.route_id = route_id; var builder = new codec.MessageBuilder( kMediaRouteProvider_DetachRoute_Name, codec.align(MediaRouteProvider_DetachRoute_Params.encodedSize)); builder.encodeStruct(MediaRouteProvider_DetachRoute_Params, params); var message = builder.finish(); this.receiver_.accept(message); }; MediaRouteProviderPtr.prototype.enableMdnsDiscovery = function() { return MediaRouteProviderProxy.prototype.enableMdnsDiscovery .apply(this.ptr.getProxy(), arguments); }; MediaRouteProviderProxy.prototype.enableMdnsDiscovery = function() { var params = new MediaRouteProvider_EnableMdnsDiscovery_Params(); var builder = new codec.MessageBuilder( kMediaRouteProvider_EnableMdnsDiscovery_Name, codec.align(MediaRouteProvider_EnableMdnsDiscovery_Params.encodedSize)); builder.encodeStruct(MediaRouteProvider_EnableMdnsDiscovery_Params, params); var message = builder.finish(); this.receiver_.accept(message); }; MediaRouteProviderPtr.prototype.updateMediaSinks = function() { return MediaRouteProviderProxy.prototype.updateMediaSinks .apply(this.ptr.getProxy(), arguments); }; MediaRouteProviderProxy.prototype.updateMediaSinks = function(media_source) { var params = new MediaRouteProvider_UpdateMediaSinks_Params(); params.media_source = media_source; var builder = new codec.MessageBuilder( kMediaRouteProvider_UpdateMediaSinks_Name, codec.align(MediaRouteProvider_UpdateMediaSinks_Params.encodedSize)); builder.encodeStruct(MediaRouteProvider_UpdateMediaSinks_Params, params); var message = builder.finish(); this.receiver_.accept(message); }; MediaRouteProviderPtr.prototype.searchSinks = function() { return MediaRouteProviderProxy.prototype.searchSinks .apply(this.ptr.getProxy(), arguments); }; MediaRouteProviderProxy.prototype.searchSinks = function(sink_id, media_source, search_criteria) { var params = new MediaRouteProvider_SearchSinks_Params(); params.sink_id = sink_id; params.media_source = media_source; params.search_criteria = search_criteria; return new Promise(function(resolve, reject) { var builder = new codec.MessageWithRequestIDBuilder( kMediaRouteProvider_SearchSinks_Name, codec.align(MediaRouteProvider_SearchSinks_Params.encodedSize), codec.kMessageExpectsResponse, 0); builder.encodeStruct(MediaRouteProvider_SearchSinks_Params, params); var message = builder.finish(); this.receiver_.acceptAndExpectResponse(message).then(function(message) { var reader = new codec.MessageReader(message); var responseParams = reader.decodeStruct(MediaRouteProvider_SearchSinks_ResponseParams); resolve(responseParams); }).catch(function(result) { reject(Error("Connection error: " + result)); }); }.bind(this)); }; function MediaRouteProviderStub(delegate) { this.delegate_ = delegate; } MediaRouteProviderStub.prototype.createRoute = function(media_source, sink_id, original_presentation_id, origin, tab_id, timeout, incognito) { return this.delegate_ && this.delegate_.createRoute && this.delegate_.createRoute(media_source, sink_id, original_presentation_id, origin, tab_id, timeout, incognito); } MediaRouteProviderStub.prototype.joinRoute = function(media_source, presentation_id, origin, tab_id, timeout, incognito) { return this.delegate_ && this.delegate_.joinRoute && this.delegate_.joinRoute(media_source, presentation_id, origin, tab_id, timeout, incognito); } MediaRouteProviderStub.prototype.connectRouteByRouteId = function(media_source, route_id, presentation_id, origin, tab_id, timeout, incognito) { return this.delegate_ && this.delegate_.connectRouteByRouteId && this.delegate_.connectRouteByRouteId(media_source, route_id, presentation_id, origin, tab_id, timeout, incognito); } MediaRouteProviderStub.prototype.terminateRoute = function(route_id) { return this.delegate_ && this.delegate_.terminateRoute && this.delegate_.terminateRoute(route_id); } MediaRouteProviderStub.prototype.sendRouteMessage = function(media_route_id, message) { return this.delegate_ && this.delegate_.sendRouteMessage && this.delegate_.sendRouteMessage(media_route_id, message); } MediaRouteProviderStub.prototype.sendRouteBinaryMessage = function(media_route_id, data) { return this.delegate_ && this.delegate_.sendRouteBinaryMessage && this.delegate_.sendRouteBinaryMessage(media_route_id, data); } MediaRouteProviderStub.prototype.startObservingMediaSinks = function(media_source) { return this.delegate_ && this.delegate_.startObservingMediaSinks && this.delegate_.startObservingMediaSinks(media_source); } MediaRouteProviderStub.prototype.stopObservingMediaSinks = function(media_source) { return this.delegate_ && this.delegate_.stopObservingMediaSinks && this.delegate_.stopObservingMediaSinks(media_source); } MediaRouteProviderStub.prototype.startObservingMediaRoutes = function(media_source) { return this.delegate_ && this.delegate_.startObservingMediaRoutes && this.delegate_.startObservingMediaRoutes(media_source); } MediaRouteProviderStub.prototype.stopObservingMediaRoutes = function(media_source) { return this.delegate_ && this.delegate_.stopObservingMediaRoutes && this.delegate_.stopObservingMediaRoutes(media_source); } MediaRouteProviderStub.prototype.startListeningForRouteMessages = function(route_id) { return this.delegate_ && this.delegate_.startListeningForRouteMessages && this.delegate_.startListeningForRouteMessages(route_id); } MediaRouteProviderStub.prototype.stopListeningForRouteMessages = function(route_id) { return this.delegate_ && this.delegate_.stopListeningForRouteMessages && this.delegate_.stopListeningForRouteMessages(route_id); } MediaRouteProviderStub.prototype.detachRoute = function(route_id) { return this.delegate_ && this.delegate_.detachRoute && this.delegate_.detachRoute(route_id); } MediaRouteProviderStub.prototype.enableMdnsDiscovery = function() { return this.delegate_ && this.delegate_.enableMdnsDiscovery && this.delegate_.enableMdnsDiscovery(); } MediaRouteProviderStub.prototype.updateMediaSinks = function(media_source) { return this.delegate_ && this.delegate_.updateMediaSinks && this.delegate_.updateMediaSinks(media_source); } MediaRouteProviderStub.prototype.searchSinks = function(sink_id, media_source, search_criteria) { return this.delegate_ && this.delegate_.searchSinks && this.delegate_.searchSinks(sink_id, media_source, search_criteria); } MediaRouteProviderStub.prototype.accept = function(message) { var reader = new codec.MessageReader(message); switch (reader.messageName) { case kMediaRouteProvider_StartObservingMediaSinks_Name: var params = reader.decodeStruct(MediaRouteProvider_StartObservingMediaSinks_Params); this.startObservingMediaSinks(params.media_source); return true; case kMediaRouteProvider_StopObservingMediaSinks_Name: var params = reader.decodeStruct(MediaRouteProvider_StopObservingMediaSinks_Params); this.stopObservingMediaSinks(params.media_source); return true; case kMediaRouteProvider_StartObservingMediaRoutes_Name: var params = reader.decodeStruct(MediaRouteProvider_StartObservingMediaRoutes_Params); this.startObservingMediaRoutes(params.media_source); return true; case kMediaRouteProvider_StopObservingMediaRoutes_Name: var params = reader.decodeStruct(MediaRouteProvider_StopObservingMediaRoutes_Params); this.stopObservingMediaRoutes(params.media_source); return true; case kMediaRouteProvider_StartListeningForRouteMessages_Name: var params = reader.decodeStruct(MediaRouteProvider_StartListeningForRouteMessages_Params); this.startListeningForRouteMessages(params.route_id); return true; case kMediaRouteProvider_StopListeningForRouteMessages_Name: var params = reader.decodeStruct(MediaRouteProvider_StopListeningForRouteMessages_Params); this.stopListeningForRouteMessages(params.route_id); return true; case kMediaRouteProvider_DetachRoute_Name: var params = reader.decodeStruct(MediaRouteProvider_DetachRoute_Params); this.detachRoute(params.route_id); return true; case kMediaRouteProvider_EnableMdnsDiscovery_Name: var params = reader.decodeStruct(MediaRouteProvider_EnableMdnsDiscovery_Params); this.enableMdnsDiscovery(); return true; case kMediaRouteProvider_UpdateMediaSinks_Name: var params = reader.decodeStruct(MediaRouteProvider_UpdateMediaSinks_Params); this.updateMediaSinks(params.media_source); return true; default: return false; } }; MediaRouteProviderStub.prototype.acceptWithResponder = function(message, responder) { var reader = new codec.MessageReader(message); switch (reader.messageName) { case kMediaRouteProvider_CreateRoute_Name: var params = reader.decodeStruct(MediaRouteProvider_CreateRoute_Params); return this.createRoute(params.media_source, params.sink_id, params.original_presentation_id, params.origin, params.tab_id, params.timeout, params.incognito).then(function(response) { var responseParams = new MediaRouteProvider_CreateRoute_ResponseParams(); responseParams.route = response.route; responseParams.error_text = response.error_text; responseParams.result_code = response.result_code; var builder = new codec.MessageWithRequestIDBuilder( kMediaRouteProvider_CreateRoute_Name, codec.align(MediaRouteProvider_CreateRoute_ResponseParams.encodedSize), codec.kMessageIsResponse, reader.requestID); builder.encodeStruct(MediaRouteProvider_CreateRoute_ResponseParams, responseParams); var message = builder.finish(); responder.accept(message); }); case kMediaRouteProvider_JoinRoute_Name: var params = reader.decodeStruct(MediaRouteProvider_JoinRoute_Params); return this.joinRoute(params.media_source, params.presentation_id, params.origin, params.tab_id, params.timeout, params.incognito).then(function(response) { var responseParams = new MediaRouteProvider_JoinRoute_ResponseParams(); responseParams.route = response.route; responseParams.error_text = response.error_text; responseParams.result_code = response.result_code; var builder = new codec.MessageWithRequestIDBuilder( kMediaRouteProvider_JoinRoute_Name, codec.align(MediaRouteProvider_JoinRoute_ResponseParams.encodedSize), codec.kMessageIsResponse, reader.requestID); builder.encodeStruct(MediaRouteProvider_JoinRoute_ResponseParams, responseParams); var message = builder.finish(); responder.accept(message); }); case kMediaRouteProvider_ConnectRouteByRouteId_Name: var params = reader.decodeStruct(MediaRouteProvider_ConnectRouteByRouteId_Params); return this.connectRouteByRouteId(params.media_source, params.route_id, params.presentation_id, params.origin, params.tab_id, params.timeout, params.incognito).then(function(response) { var responseParams = new MediaRouteProvider_ConnectRouteByRouteId_ResponseParams(); responseParams.route = response.route; responseParams.error_text = response.error_text; responseParams.result_code = response.result_code; var builder = new codec.MessageWithRequestIDBuilder( kMediaRouteProvider_ConnectRouteByRouteId_Name, codec.align(MediaRouteProvider_ConnectRouteByRouteId_ResponseParams.encodedSize), codec.kMessageIsResponse, reader.requestID); builder.encodeStruct(MediaRouteProvider_ConnectRouteByRouteId_ResponseParams, responseParams); var message = builder.finish(); responder.accept(message); }); case kMediaRouteProvider_TerminateRoute_Name: var params = reader.decodeStruct(MediaRouteProvider_TerminateRoute_Params); return this.terminateRoute(params.route_id).then(function(response) { var responseParams = new MediaRouteProvider_TerminateRoute_ResponseParams(); responseParams.error_text = response.error_text; responseParams.result_code = response.result_code; var builder = new codec.MessageWithRequestIDBuilder( kMediaRouteProvider_TerminateRoute_Name, codec.align(MediaRouteProvider_TerminateRoute_ResponseParams.encodedSize), codec.kMessageIsResponse, reader.requestID); builder.encodeStruct(MediaRouteProvider_TerminateRoute_ResponseParams, responseParams); var message = builder.finish(); responder.accept(message); }); case kMediaRouteProvider_SendRouteMessage_Name: var params = reader.decodeStruct(MediaRouteProvider_SendRouteMessage_Params); return this.sendRouteMessage(params.media_route_id, params.message).then(function(response) { var responseParams = new MediaRouteProvider_SendRouteMessage_ResponseParams(); responseParams.sent = response.sent; var builder = new codec.MessageWithRequestIDBuilder( kMediaRouteProvider_SendRouteMessage_Name, codec.align(MediaRouteProvider_SendRouteMessage_ResponseParams.encodedSize), codec.kMessageIsResponse, reader.requestID); builder.encodeStruct(MediaRouteProvider_SendRouteMessage_ResponseParams, responseParams); var message = builder.finish(); responder.accept(message); }); case kMediaRouteProvider_SendRouteBinaryMessage_Name: var params = reader.decodeStruct(MediaRouteProvider_SendRouteBinaryMessage_Params); return this.sendRouteBinaryMessage(params.media_route_id, params.data).then(function(response) { var responseParams = new MediaRouteProvider_SendRouteBinaryMessage_ResponseParams(); responseParams.sent = response.sent; var builder = new codec.MessageWithRequestIDBuilder( kMediaRouteProvider_SendRouteBinaryMessage_Name, codec.align(MediaRouteProvider_SendRouteBinaryMessage_ResponseParams.encodedSize), codec.kMessageIsResponse, reader.requestID); builder.encodeStruct(MediaRouteProvider_SendRouteBinaryMessage_ResponseParams, responseParams); var message = builder.finish(); responder.accept(message); }); case kMediaRouteProvider_SearchSinks_Name: var params = reader.decodeStruct(MediaRouteProvider_SearchSinks_Params); return this.searchSinks(params.sink_id, params.media_source, params.search_criteria).then(function(response) { var responseParams = new MediaRouteProvider_SearchSinks_ResponseParams(); responseParams.sink_id = response.sink_id; var builder = new codec.MessageWithRequestIDBuilder( kMediaRouteProvider_SearchSinks_Name, codec.align(MediaRouteProvider_SearchSinks_ResponseParams.encodedSize), codec.kMessageIsResponse, reader.requestID); builder.encodeStruct(MediaRouteProvider_SearchSinks_ResponseParams, responseParams); var message = builder.finish(); responder.accept(message); }); default: return Promise.reject(Error("Unhandled message: " + reader.messageName)); } }; function validateMediaRouteProviderRequest(messageValidator) { var message = messageValidator.message; var paramsClass = null; switch (message.getName()) { case kMediaRouteProvider_CreateRoute_Name: if (message.expectsResponse()) paramsClass = MediaRouteProvider_CreateRoute_Params; break; case kMediaRouteProvider_JoinRoute_Name: if (message.expectsResponse()) paramsClass = MediaRouteProvider_JoinRoute_Params; break; case kMediaRouteProvider_ConnectRouteByRouteId_Name: if (message.expectsResponse()) paramsClass = MediaRouteProvider_ConnectRouteByRouteId_Params; break; case kMediaRouteProvider_TerminateRoute_Name: if (message.expectsResponse()) paramsClass = MediaRouteProvider_TerminateRoute_Params; break; case kMediaRouteProvider_SendRouteMessage_Name: if (message.expectsResponse()) paramsClass = MediaRouteProvider_SendRouteMessage_Params; break; case kMediaRouteProvider_SendRouteBinaryMessage_Name: if (message.expectsResponse()) paramsClass = MediaRouteProvider_SendRouteBinaryMessage_Params; break; case kMediaRouteProvider_StartObservingMediaSinks_Name: if (!message.expectsResponse() && !message.isResponse()) paramsClass = MediaRouteProvider_StartObservingMediaSinks_Params; break; case kMediaRouteProvider_StopObservingMediaSinks_Name: if (!message.expectsResponse() && !message.isResponse()) paramsClass = MediaRouteProvider_StopObservingMediaSinks_Params; break; case kMediaRouteProvider_StartObservingMediaRoutes_Name: if (!message.expectsResponse() && !message.isResponse()) paramsClass = MediaRouteProvider_StartObservingMediaRoutes_Params; break; case kMediaRouteProvider_StopObservingMediaRoutes_Name: if (!message.expectsResponse() && !message.isResponse()) paramsClass = MediaRouteProvider_StopObservingMediaRoutes_Params; break; case kMediaRouteProvider_StartListeningForRouteMessages_Name: if (!message.expectsResponse() && !message.isResponse()) paramsClass = MediaRouteProvider_StartListeningForRouteMessages_Params; break; case kMediaRouteProvider_StopListeningForRouteMessages_Name: if (!message.expectsResponse() && !message.isResponse()) paramsClass = MediaRouteProvider_StopListeningForRouteMessages_Params; break; case kMediaRouteProvider_DetachRoute_Name: if (!message.expectsResponse() && !message.isResponse()) paramsClass = MediaRouteProvider_DetachRoute_Params; break; case kMediaRouteProvider_EnableMdnsDiscovery_Name: if (!message.expectsResponse() && !message.isResponse()) paramsClass = MediaRouteProvider_EnableMdnsDiscovery_Params; break; case kMediaRouteProvider_UpdateMediaSinks_Name: if (!message.expectsResponse() && !message.isResponse()) paramsClass = MediaRouteProvider_UpdateMediaSinks_Params; break; case kMediaRouteProvider_SearchSinks_Name: if (message.expectsResponse()) paramsClass = MediaRouteProvider_SearchSinks_Params; break; } if (paramsClass === null) return validator.validationError.NONE; return paramsClass.validate(messageValidator, messageValidator.message.getHeaderNumBytes()); } function validateMediaRouteProviderResponse(messageValidator) { var message = messageValidator.message; var paramsClass = null; switch (message.getName()) { case kMediaRouteProvider_CreateRoute_Name: if (message.isResponse()) paramsClass = MediaRouteProvider_CreateRoute_ResponseParams; break; case kMediaRouteProvider_JoinRoute_Name: if (message.isResponse()) paramsClass = MediaRouteProvider_JoinRoute_ResponseParams; break; case kMediaRouteProvider_ConnectRouteByRouteId_Name: if (message.isResponse()) paramsClass = MediaRouteProvider_ConnectRouteByRouteId_ResponseParams; break; case kMediaRouteProvider_TerminateRoute_Name: if (message.isResponse()) paramsClass = MediaRouteProvider_TerminateRoute_ResponseParams; break; case kMediaRouteProvider_SendRouteMessage_Name: if (message.isResponse()) paramsClass = MediaRouteProvider_SendRouteMessage_ResponseParams; break; case kMediaRouteProvider_SendRouteBinaryMessage_Name: if (message.isResponse()) paramsClass = MediaRouteProvider_SendRouteBinaryMessage_ResponseParams; break; case kMediaRouteProvider_SearchSinks_Name: if (message.isResponse()) paramsClass = MediaRouteProvider_SearchSinks_ResponseParams; break; } if (paramsClass === null) return validator.validationError.NONE; return paramsClass.validate(messageValidator, messageValidator.message.getHeaderNumBytes()); } var MediaRouteProvider = { name: 'media_router::mojom::MediaRouteProvider', ptrClass: MediaRouteProviderPtr, proxyClass: MediaRouteProviderProxy, stubClass: MediaRouteProviderStub, validateRequest: validateMediaRouteProviderRequest, validateResponse: validateMediaRouteProviderResponse, }; MediaRouteProviderStub.prototype.validator = validateMediaRouteProviderRequest; MediaRouteProviderProxy.prototype.validator = validateMediaRouteProviderResponse; var kMediaRouter_RegisterMediaRouteProvider_Name = 0; var kMediaRouter_OnSinksReceived_Name = 1; var kMediaRouter_OnIssue_Name = 2; var kMediaRouter_OnRoutesUpdated_Name = 3; var kMediaRouter_OnSinkAvailabilityUpdated_Name = 4; var kMediaRouter_OnPresentationConnectionStateChanged_Name = 5; var kMediaRouter_OnPresentationConnectionClosed_Name = 6; var kMediaRouter_OnRouteMessagesReceived_Name = 7; function MediaRouterPtr(handleOrPtrInfo) { this.ptr = new bindings.InterfacePtrController(MediaRouter, handleOrPtrInfo); } function MediaRouterProxy(receiver) { this.receiver_ = receiver; } MediaRouterPtr.prototype.registerMediaRouteProvider = function() { return MediaRouterProxy.prototype.registerMediaRouteProvider .apply(this.ptr.getProxy(), arguments); }; MediaRouterProxy.prototype.registerMediaRouteProvider = function(media_router_provider) { var params = new MediaRouter_RegisterMediaRouteProvider_Params(); params.media_router_provider = media_router_provider; return new Promise(function(resolve, reject) { var builder = new codec.MessageWithRequestIDBuilder( kMediaRouter_RegisterMediaRouteProvider_Name, codec.align(MediaRouter_RegisterMediaRouteProvider_Params.encodedSize), codec.kMessageExpectsResponse, 0); builder.encodeStruct(MediaRouter_RegisterMediaRouteProvider_Params, params); var message = builder.finish(); this.receiver_.acceptAndExpectResponse(message).then(function(message) { var reader = new codec.MessageReader(message); var responseParams = reader.decodeStruct(MediaRouter_RegisterMediaRouteProvider_ResponseParams); resolve(responseParams); }).catch(function(result) { reject(Error("Connection error: " + result)); }); }.bind(this)); }; MediaRouterPtr.prototype.onSinksReceived = function() { return MediaRouterProxy.prototype.onSinksReceived .apply(this.ptr.getProxy(), arguments); }; MediaRouterProxy.prototype.onSinksReceived = function(media_source, sinks, origins) { var params = new MediaRouter_OnSinksReceived_Params(); params.media_source = media_source; params.sinks = sinks; params.origins = origins; var builder = new codec.MessageBuilder( kMediaRouter_OnSinksReceived_Name, codec.align(MediaRouter_OnSinksReceived_Params.encodedSize)); builder.encodeStruct(MediaRouter_OnSinksReceived_Params, params); var message = builder.finish(); this.receiver_.accept(message); }; MediaRouterPtr.prototype.onIssue = function() { return MediaRouterProxy.prototype.onIssue .apply(this.ptr.getProxy(), arguments); }; MediaRouterProxy.prototype.onIssue = function(issue) { var params = new MediaRouter_OnIssue_Params(); params.issue = issue; var builder = new codec.MessageBuilder( kMediaRouter_OnIssue_Name, codec.align(MediaRouter_OnIssue_Params.encodedSize)); builder.encodeStruct(MediaRouter_OnIssue_Params, params); var message = builder.finish(); this.receiver_.accept(message); }; MediaRouterPtr.prototype.onRoutesUpdated = function() { return MediaRouterProxy.prototype.onRoutesUpdated .apply(this.ptr.getProxy(), arguments); }; MediaRouterProxy.prototype.onRoutesUpdated = function(routes, media_source, joinable_route_ids) { var params = new MediaRouter_OnRoutesUpdated_Params(); params.routes = routes; params.media_source = media_source; params.joinable_route_ids = joinable_route_ids; var builder = new codec.MessageBuilder( kMediaRouter_OnRoutesUpdated_Name, codec.align(MediaRouter_OnRoutesUpdated_Params.encodedSize)); builder.encodeStruct(MediaRouter_OnRoutesUpdated_Params, params); var message = builder.finish(); this.receiver_.accept(message); }; MediaRouterPtr.prototype.onSinkAvailabilityUpdated = function() { return MediaRouterProxy.prototype.onSinkAvailabilityUpdated .apply(this.ptr.getProxy(), arguments); }; MediaRouterProxy.prototype.onSinkAvailabilityUpdated = function(availability) { var params = new MediaRouter_OnSinkAvailabilityUpdated_Params(); params.availability = availability; var builder = new codec.MessageBuilder( kMediaRouter_OnSinkAvailabilityUpdated_Name, codec.align(MediaRouter_OnSinkAvailabilityUpdated_Params.encodedSize)); builder.encodeStruct(MediaRouter_OnSinkAvailabilityUpdated_Params, params); var message = builder.finish(); this.receiver_.accept(message); }; MediaRouterPtr.prototype.onPresentationConnectionStateChanged = function() { return MediaRouterProxy.prototype.onPresentationConnectionStateChanged .apply(this.ptr.getProxy(), arguments); }; MediaRouterProxy.prototype.onPresentationConnectionStateChanged = function(route_id, state) { var params = new MediaRouter_OnPresentationConnectionStateChanged_Params(); params.route_id = route_id; params.state = state; var builder = new codec.MessageBuilder( kMediaRouter_OnPresentationConnectionStateChanged_Name, codec.align(MediaRouter_OnPresentationConnectionStateChanged_Params.encodedSize)); builder.encodeStruct(MediaRouter_OnPresentationConnectionStateChanged_Params, params); var message = builder.finish(); this.receiver_.accept(message); }; MediaRouterPtr.prototype.onPresentationConnectionClosed = function() { return MediaRouterProxy.prototype.onPresentationConnectionClosed .apply(this.ptr.getProxy(), arguments); }; MediaRouterProxy.prototype.onPresentationConnectionClosed = function(route_id, reason, message) { var params = new MediaRouter_OnPresentationConnectionClosed_Params(); params.route_id = route_id; params.reason = reason; params.message = message; var builder = new codec.MessageBuilder( kMediaRouter_OnPresentationConnectionClosed_Name, codec.align(MediaRouter_OnPresentationConnectionClosed_Params.encodedSize)); builder.encodeStruct(MediaRouter_OnPresentationConnectionClosed_Params, params); var message = builder.finish(); this.receiver_.accept(message); }; MediaRouterPtr.prototype.onRouteMessagesReceived = function() { return MediaRouterProxy.prototype.onRouteMessagesReceived .apply(this.ptr.getProxy(), arguments); }; MediaRouterProxy.prototype.onRouteMessagesReceived = function(route_id, messages) { var params = new MediaRouter_OnRouteMessagesReceived_Params(); params.route_id = route_id; params.messages = messages; var builder = new codec.MessageBuilder( kMediaRouter_OnRouteMessagesReceived_Name, codec.align(MediaRouter_OnRouteMessagesReceived_Params.encodedSize)); builder.encodeStruct(MediaRouter_OnRouteMessagesReceived_Params, params); var message = builder.finish(); this.receiver_.accept(message); }; function MediaRouterStub(delegate) { this.delegate_ = delegate; } MediaRouterStub.prototype.registerMediaRouteProvider = function(media_router_provider) { return this.delegate_ && this.delegate_.registerMediaRouteProvider && this.delegate_.registerMediaRouteProvider(media_router_provider); } MediaRouterStub.prototype.onSinksReceived = function(media_source, sinks, origins) { return this.delegate_ && this.delegate_.onSinksReceived && this.delegate_.onSinksReceived(media_source, sinks, origins); } MediaRouterStub.prototype.onIssue = function(issue) { return this.delegate_ && this.delegate_.onIssue && this.delegate_.onIssue(issue); } MediaRouterStub.prototype.onRoutesUpdated = function(routes, media_source, joinable_route_ids) { return this.delegate_ && this.delegate_.onRoutesUpdated && this.delegate_.onRoutesUpdated(routes, media_source, joinable_route_ids); } MediaRouterStub.prototype.onSinkAvailabilityUpdated = function(availability) { return this.delegate_ && this.delegate_.onSinkAvailabilityUpdated && this.delegate_.onSinkAvailabilityUpdated(availability); } MediaRouterStub.prototype.onPresentationConnectionStateChanged = function(route_id, state) { return this.delegate_ && this.delegate_.onPresentationConnectionStateChanged && this.delegate_.onPresentationConnectionStateChanged(route_id, state); } MediaRouterStub.prototype.onPresentationConnectionClosed = function(route_id, reason, message) { return this.delegate_ && this.delegate_.onPresentationConnectionClosed && this.delegate_.onPresentationConnectionClosed(route_id, reason, message); } MediaRouterStub.prototype.onRouteMessagesReceived = function(route_id, messages) { return this.delegate_ && this.delegate_.onRouteMessagesReceived && this.delegate_.onRouteMessagesReceived(route_id, messages); } MediaRouterStub.prototype.accept = function(message) { var reader = new codec.MessageReader(message); switch (reader.messageName) { case kMediaRouter_OnSinksReceived_Name: var params = reader.decodeStruct(MediaRouter_OnSinksReceived_Params); this.onSinksReceived(params.media_source, params.sinks, params.origins); return true; case kMediaRouter_OnIssue_Name: var params = reader.decodeStruct(MediaRouter_OnIssue_Params); this.onIssue(params.issue); return true; case kMediaRouter_OnRoutesUpdated_Name: var params = reader.decodeStruct(MediaRouter_OnRoutesUpdated_Params); this.onRoutesUpdated(params.routes, params.media_source, params.joinable_route_ids); return true; case kMediaRouter_OnSinkAvailabilityUpdated_Name: var params = reader.decodeStruct(MediaRouter_OnSinkAvailabilityUpdated_Params); this.onSinkAvailabilityUpdated(params.availability); return true; case kMediaRouter_OnPresentationConnectionStateChanged_Name: var params = reader.decodeStruct(MediaRouter_OnPresentationConnectionStateChanged_Params); this.onPresentationConnectionStateChanged(params.route_id, params.state); return true; case kMediaRouter_OnPresentationConnectionClosed_Name: var params = reader.decodeStruct(MediaRouter_OnPresentationConnectionClosed_Params); this.onPresentationConnectionClosed(params.route_id, params.reason, params.message); return true; case kMediaRouter_OnRouteMessagesReceived_Name: var params = reader.decodeStruct(MediaRouter_OnRouteMessagesReceived_Params); this.onRouteMessagesReceived(params.route_id, params.messages); return true; default: return false; } }; MediaRouterStub.prototype.acceptWithResponder = function(message, responder) { var reader = new codec.MessageReader(message); switch (reader.messageName) { case kMediaRouter_RegisterMediaRouteProvider_Name: var params = reader.decodeStruct(MediaRouter_RegisterMediaRouteProvider_Params); return this.registerMediaRouteProvider(params.media_router_provider).then(function(response) { var responseParams = new MediaRouter_RegisterMediaRouteProvider_ResponseParams(); responseParams.instance_id = response.instance_id; var builder = new codec.MessageWithRequestIDBuilder( kMediaRouter_RegisterMediaRouteProvider_Name, codec.align(MediaRouter_RegisterMediaRouteProvider_ResponseParams.encodedSize), codec.kMessageIsResponse, reader.requestID); builder.encodeStruct(MediaRouter_RegisterMediaRouteProvider_ResponseParams, responseParams); var message = builder.finish(); responder.accept(message); }); default: return Promise.reject(Error("Unhandled message: " + reader.messageName)); } }; function validateMediaRouterRequest(messageValidator) { var message = messageValidator.message; var paramsClass = null; switch (message.getName()) { case kMediaRouter_RegisterMediaRouteProvider_Name: if (message.expectsResponse()) paramsClass = MediaRouter_RegisterMediaRouteProvider_Params; break; case kMediaRouter_OnSinksReceived_Name: if (!message.expectsResponse() && !message.isResponse()) paramsClass = MediaRouter_OnSinksReceived_Params; break; case kMediaRouter_OnIssue_Name: if (!message.expectsResponse() && !message.isResponse()) paramsClass = MediaRouter_OnIssue_Params; break; case kMediaRouter_OnRoutesUpdated_Name: if (!message.expectsResponse() && !message.isResponse()) paramsClass = MediaRouter_OnRoutesUpdated_Params; break; case kMediaRouter_OnSinkAvailabilityUpdated_Name: if (!message.expectsResponse() && !message.isResponse()) paramsClass = MediaRouter_OnSinkAvailabilityUpdated_Params; break; case kMediaRouter_OnPresentationConnectionStateChanged_Name: if (!message.expectsResponse() && !message.isResponse()) paramsClass = MediaRouter_OnPresentationConnectionStateChanged_Params; break; case kMediaRouter_OnPresentationConnectionClosed_Name: if (!message.expectsResponse() && !message.isResponse()) paramsClass = MediaRouter_OnPresentationConnectionClosed_Params; break; case kMediaRouter_OnRouteMessagesReceived_Name: if (!message.expectsResponse() && !message.isResponse()) paramsClass = MediaRouter_OnRouteMessagesReceived_Params; break; } if (paramsClass === null) return validator.validationError.NONE; return paramsClass.validate(messageValidator, messageValidator.message.getHeaderNumBytes()); } function validateMediaRouterResponse(messageValidator) { var message = messageValidator.message; var paramsClass = null; switch (message.getName()) { case kMediaRouter_RegisterMediaRouteProvider_Name: if (message.isResponse()) paramsClass = MediaRouter_RegisterMediaRouteProvider_ResponseParams; break; } if (paramsClass === null) return validator.validationError.NONE; return paramsClass.validate(messageValidator, messageValidator.message.getHeaderNumBytes()); } var MediaRouter = { name: 'media_router::mojom::MediaRouter', ptrClass: MediaRouterPtr, proxyClass: MediaRouterProxy, stubClass: MediaRouterStub, validateRequest: validateMediaRouterRequest, validateResponse: validateMediaRouterResponse, }; MediaRouter.SinkAvailability = {}; MediaRouter.SinkAvailability.UNAVAILABLE = 0; MediaRouter.SinkAvailability.PER_SOURCE = MediaRouter.SinkAvailability.UNAVAILABLE + 1; MediaRouter.SinkAvailability.AVAILABLE = MediaRouter.SinkAvailability.PER_SOURCE + 1; MediaRouter.SinkAvailability.isKnownEnumValue = function(value) { switch (value) { case 0: case 1: case 2: return true; } return false; }; MediaRouter.SinkAvailability.validate = function(enumValue) { var isExtensible = false; if (isExtensible || this.isKnownEnumValue(enumValue)) return validator.validationError.NONE; return validator.validationError.UNKNOWN_ENUM_VALUE; }; MediaRouter.PresentationConnectionState = {}; MediaRouter.PresentationConnectionState.CONNECTING = 0; MediaRouter.PresentationConnectionState.CONNECTED = MediaRouter.PresentationConnectionState.CONNECTING + 1; MediaRouter.PresentationConnectionState.CLOSED = MediaRouter.PresentationConnectionState.CONNECTED + 1; MediaRouter.PresentationConnectionState.TERMINATED = MediaRouter.PresentationConnectionState.CLOSED + 1; MediaRouter.PresentationConnectionState.isKnownEnumValue = function(value) { switch (value) { case 0: case 1: case 2: case 3: return true; } return false; }; MediaRouter.PresentationConnectionState.validate = function(enumValue) { var isExtensible = false; if (isExtensible || this.isKnownEnumValue(enumValue)) return validator.validationError.NONE; return validator.validationError.UNKNOWN_ENUM_VALUE; }; MediaRouter.PresentationConnectionCloseReason = {}; MediaRouter.PresentationConnectionCloseReason.CONNECTION_ERROR = 0; MediaRouter.PresentationConnectionCloseReason.CLOSED = MediaRouter.PresentationConnectionCloseReason.CONNECTION_ERROR + 1; MediaRouter.PresentationConnectionCloseReason.WENT_AWAY = MediaRouter.PresentationConnectionCloseReason.CLOSED + 1; MediaRouter.PresentationConnectionCloseReason.isKnownEnumValue = function(value) { switch (value) { case 0: case 1: case 2: return true; } return false; }; MediaRouter.PresentationConnectionCloseReason.validate = function(enumValue) { var isExtensible = false; if (isExtensible || this.isKnownEnumValue(enumValue)) return validator.validationError.NONE; return validator.validationError.UNKNOWN_ENUM_VALUE; }; MediaRouterStub.prototype.validator = validateMediaRouterRequest; MediaRouterProxy.prototype.validator = validateMediaRouterResponse; var exports = {}; exports.RouteRequestResultCode = RouteRequestResultCode; exports.MediaSink = MediaSink; exports.MediaRoute = MediaRoute; exports.Issue = Issue; exports.RouteMessage = RouteMessage; exports.SinkSearchCriteria = SinkSearchCriteria; exports.MediaRouteProvider = MediaRouteProvider; exports.MediaRouteProviderPtr = MediaRouteProviderPtr; exports.MediaRouter = MediaRouter; exports.MediaRouterPtr = MediaRouterPtr; return exports; });// Copyright 2015 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. var mediaRouter; define('media_router_bindings', [ 'content/public/renderer/frame_interfaces', 'chrome/browser/media/router/mojo/media_router.mojom', 'extensions/common/mojo/keep_alive.mojom', 'mojo/common/time.mojom', 'mojo/public/js/bindings', ], function(frameInterfaces, mediaRouterMojom, keepAliveMojom, timeMojom, bindings) { 'use strict'; /** * Converts a media sink to a MediaSink Mojo object. * @param {!MediaSink} sink A media sink. * @return {!mediaRouterMojom.MediaSink} A Mojo MediaSink object. */ function sinkToMojo_(sink) { return new mediaRouterMojom.MediaSink({ 'name': sink.friendlyName, 'description': sink.description, 'domain': sink.domain, 'sink_id': sink.id, 'icon_type': sinkIconTypeToMojo(sink.iconType), }); } /** * Converts a media sink's icon type to a MediaSink.IconType Mojo object. * @param {!MediaSink.IconType} type A media sink's icon type. * @return {!mediaRouterMojom.MediaSink.IconType} A Mojo MediaSink.IconType * object. */ function sinkIconTypeToMojo(type) { switch (type) { case 'cast': return mediaRouterMojom.MediaSink.IconType.CAST; case 'cast_audio': return mediaRouterMojom.MediaSink.IconType.CAST_AUDIO; case 'cast_audio_group': return mediaRouterMojom.MediaSink.IconType.CAST_AUDIO_GROUP; case 'generic': return mediaRouterMojom.MediaSink.IconType.GENERIC; case 'hangout': return mediaRouterMojom.MediaSink.IconType.HANGOUT; default: console.error('Unknown sink icon type : ' + type); return mediaRouterMojom.MediaSink.IconType.GENERIC; } } /** * Returns a Mojo MediaRoute object given a MediaRoute and a * media sink name. * @param {!MediaRoute} route * @return {!mojo.MediaRoute} */ function routeToMojo_(route) { return new mediaRouterMojom.MediaRoute({ 'media_route_id': route.id, 'media_source': route.mediaSource, 'media_sink_id': route.sinkId, 'description': route.description, 'icon_url': route.iconUrl, 'is_local': route.isLocal, 'custom_controller_path': route.customControllerPath, // Begin newly added properties, followed by the milestone they were // added. The guard should be safe to remove N+2 milestones later. 'for_display': route.forDisplay, // M47 'is_incognito': !!route.offTheRecord, // M50 'is_offscreen_presentation': !!route.isOffscreenPresentation // M56 }); } /** * Converts a route message to a RouteMessage Mojo object. * @param {!RouteMessage} message * @return {!mediaRouterMojom.RouteMessage} A Mojo RouteMessage object. */ function messageToMojo_(message) { if ("string" == typeof message.message) { return new mediaRouterMojom.RouteMessage({ 'type': mediaRouterMojom.RouteMessage.Type.TEXT, 'message': message.message, }); } else { return new mediaRouterMojom.RouteMessage({ 'type': mediaRouterMojom.RouteMessage.Type.BINARY, 'data': message.message, }); } } /** * Converts presentation connection state to Mojo enum value. * @param {!string} state * @return {!mediaRouterMojom.MediaRouter.PresentationConnectionState} */ function presentationConnectionStateToMojo_(state) { var PresentationConnectionState = mediaRouterMojom.MediaRouter.PresentationConnectionState; switch (state) { case 'connecting': return PresentationConnectionState.CONNECTING; case 'connected': return PresentationConnectionState.CONNECTED; case 'closed': return PresentationConnectionState.CLOSED; case 'terminated': return PresentationConnectionState.TERMINATED; default: console.error('Unknown presentation connection state: ' + state); return PresentationConnectionState.TERMINATED; } } /** * Converts presentation connection close reason to Mojo enum value. * @param {!string} reason * @return {!mediaRouterMojom.MediaRouter.PresentationConnectionCloseReason} */ function presentationConnectionCloseReasonToMojo_(reason) { var PresentationConnectionCloseReason = mediaRouterMojom.MediaRouter.PresentationConnectionCloseReason; switch (reason) { case 'error': return PresentationConnectionCloseReason.CONNECTION_ERROR; case 'closed': return PresentationConnectionCloseReason.CLOSED; case 'went_away': return PresentationConnectionCloseReason.WENT_AWAY; default: console.error('Unknown presentation connection close reason : ' + reason); return PresentationConnectionCloseReason.CONNECTION_ERROR; } } /** * Parses the given route request Error object and converts it to the * corresponding result code. * @param {!Error} error * @return {!mediaRouterMojom.RouteRequestResultCode} */ function getRouteRequestResultCode_(error) { return error.errorCode ? error.errorCode : mediaRouterMojom.RouteRequestResultCode.UNKNOWN_ERROR; } /** * Creates and returns a successful route response from given route. * @param {!MediaRoute} route * @return {!Object} */ function toSuccessRouteResponse_(route) { return { route: routeToMojo_(route), result_code: mediaRouterMojom.RouteRequestResultCode.OK }; } /** * Creates and returns a error route response from given Error object. * @param {!Error} error * @return {!Object} */ function toErrorRouteResponse_(error) { return { error_text: error.message, result_code: getRouteRequestResultCode_(error) }; } /** * Creates a new MediaRouter. * Converts a route struct to its Mojo form. * @param {!mediaRouterMojom.MediaRouterPtr} service * @constructor */ function MediaRouter(service) { /** * The Mojo service proxy. Allows extension code to call methods that reside * in the browser. * @type {!mediaRouterMojom.MediaRouterPtr} */ this.service_ = service; /** * The provider manager service delegate. Its methods are called by the * browser-resident Mojo service. * @type {!MediaRouter} */ this.mrpm_ = new MediaRouteProvider(this); /** * Handle to a KeepAlive service object, which prevents the extension from * being suspended as long as it remains in scope. * @type {boolean} */ this.keepAlive_ = null; /** * The bindings to bind the service delegate to the Mojo interface. * Object must remain in scope for the lifetime of the connection to * prevent the connection from closing automatically. * @type {!bindings.Binding} */ this.mediaRouteProviderBinding_ = new bindings.Binding( mediaRouterMojom.MediaRouteProvider, this.mrpm_); } /** * Registers the Media Router Provider Manager with the Media Router. * @return {!Promise} Instance ID for the Media Router. */ MediaRouter.prototype.start = function() { return this.service_.registerMediaRouteProvider( this.mediaRouteProviderBinding_.createInterfacePtrAndBind()).then( function(result) { return result.instance_id; }.bind(this)); } /** * Sets the service delegate methods. * @param {Object} handlers */ MediaRouter.prototype.setHandlers = function(handlers) { this.mrpm_.setHandlers(handlers); } /** * The keep alive status. * @return {boolean} */ MediaRouter.prototype.getKeepAlive = function() { return this.keepAlive_ != null; }; /** * Called by the provider manager when a sink list for a given source is * updated. * @param {!string} sourceUrn * @param {!Array} sinks * @param {!Array} origins */ MediaRouter.prototype.onSinksReceived = function(sourceUrn, sinks, origins) { this.service_.onSinksReceived(sourceUrn, sinks.map(sinkToMojo_), origins); }; /** * Called by the provider manager when a sink is found to notify the MR of the * sink's ID. The actual sink will be returned through the normal sink list * update process, so this helps the MR identify the search result in the * list. * @param {string} pseudoSinkId ID of the pseudo sink that started the * search. * @param {string} sinkId ID of the newly-found sink. */ MediaRouter.prototype.onSearchSinkIdReceived = function( pseudoSinkId, sinkId) { this.service_.onSearchSinkIdReceived(pseudoSinkId, sinkId); }; /** * Called by the provider manager to keep the extension from suspending * if it enters a state where suspension is undesirable (e.g. there is an * active MediaRoute.) * If keepAlive is true, the extension is kept alive. * If keepAlive is false, the extension is allowed to suspend. * @param {boolean} keepAlive */ MediaRouter.prototype.setKeepAlive = function(keepAlive) { if (keepAlive === false && this.keepAlive_) { this.keepAlive_.ptr.reset(); this.keepAlive_ = null; } else if (keepAlive === true && !this.keepAlive_) { this.keepAlive_ = new keepAliveMojom.KeepAlivePtr( frameInterfaces.getInterface(keepAliveMojom.KeepAlive.name)); } }; /** * Called by the provider manager to send an issue from a media route * provider to the Media Router, to show the user. * @param {!Object} issue The issue object. */ MediaRouter.prototype.onIssue = function(issue) { function issueSeverityToMojo_(severity) { switch (severity) { case 'fatal': return mediaRouterMojom.Issue.Severity.FATAL; case 'warning': return mediaRouterMojom.Issue.Severity.WARNING; case 'notification': return mediaRouterMojom.Issue.Severity.NOTIFICATION; default: console.error('Unknown issue severity: ' + severity); return mediaRouterMojom.Issue.Severity.NOTIFICATION; } } function issueActionToMojo_(action) { switch (action) { case 'dismiss': return mediaRouterMojom.Issue.ActionType.DISMISS; case 'learn_more': return mediaRouterMojom.Issue.ActionType.LEARN_MORE; default: console.error('Unknown issue action type : ' + action); return mediaRouterMojom.Issue.ActionType.OK; } } var secondaryActions = (issue.secondaryActions || []).map(function(e) { return issueActionToMojo_(e); }); this.service_.onIssue(new mediaRouterMojom.Issue({ 'route_id': issue.routeId, 'severity': issueSeverityToMojo_(issue.severity), 'title': issue.title, 'message': issue.message, 'default_action': issueActionToMojo_(issue.defaultAction), 'secondary_actions': secondaryActions, 'help_page_id': issue.helpPageId, 'is_blocking': issue.isBlocking })); }; /** * Called by the provider manager when the set of active routes * has been updated. * @param {!Array} routes The active set of media routes. * @param {string=} sourceUrn The sourceUrn associated with this route * query. * @param {Array=} joinableRouteIds The active set of joinable * media routes. */ MediaRouter.prototype.onRoutesUpdated = function(routes, sourceUrn = '', joinableRouteIds = []) { this.service_.onRoutesUpdated( routes.map(routeToMojo_), sourceUrn, joinableRouteIds); }; /** * Called by the provider manager when sink availability has been updated. * @param {!mediaRouterMojom.MediaRouter.SinkAvailability} availability * The new sink availability. */ MediaRouter.prototype.onSinkAvailabilityUpdated = function(availability) { this.service_.onSinkAvailabilityUpdated(availability); }; /** * Called by the provider manager when the state of a presentation connected * to a route has changed. * @param {string} routeId * @param {string} state */ MediaRouter.prototype.onPresentationConnectionStateChanged = function(routeId, state) { this.service_.onPresentationConnectionStateChanged( routeId, presentationConnectionStateToMojo_(state)); }; /** * Called by the provider manager when the state of a presentation connected * to a route has closed. * @param {string} routeId * @param {string} reason * @param {string} message */ MediaRouter.prototype.onPresentationConnectionClosed = function(routeId, reason, message) { this.service_.onPresentationConnectionClosed( routeId, presentationConnectionCloseReasonToMojo_(reason), message); }; /** * @param {string} routeId * @param {!Array} mesages */ MediaRouter.prototype.onRouteMessagesReceived = function(routeId, messages) { this.service_.onRouteMessagesReceived( routeId, messages.map(messageToMojo_)); }; /** * Object containing callbacks set by the provider manager. * * @constructor * @struct */ function MediaRouterHandlers() { /** * @type {function(!string, !string, !string, !string, !number)} */ this.createRoute = null; /** * @type {function(!string, !string, !string, !number)} */ this.joinRoute = null; /** * @type {function(string): Promise} */ this.terminateRoute = null; /** * @type {function(string)} */ this.startObservingMediaSinks = null; /** * @type {function(string)} */ this.stopObservingMediaSinks = null; /** * @type {function(string, string): Promise} */ this.sendRouteMessage = null; /** * @type {function(string, Uint8Array): Promise} */ this.sendRouteBinaryMessage = null; /** * @type {function(string)} */ this.startListeningForRouteMessages = null; /** * @type {function(string)} */ this.stopListeningForRouteMessages = null; /** * @type {function(string)} */ this.detachRoute = null; /** * @type {function()} */ this.startObservingMediaRoutes = null; /** * @type {function()} */ this.stopObservingMediaRoutes = null; /** * @type {function()} */ this.connectRouteByRouteId = null; /** * @type {function()} */ this.enableMdnsDiscovery = null; /** * @type {function()} */ this.updateMediaSinks = null; /** * @type {function(!string, !string, !SinkSearchCriteria): !string} */ this.searchSinks = null; }; /** * Routes calls from Media Router to the provider manager extension. * Registered with the MediaRouter stub. * @param {!MediaRouter} MediaRouter proxy to call into the * Media Router mojo interface. * @constructor */ function MediaRouteProvider(mediaRouter) { /** * Object containing JS callbacks into Provider Manager code. * @type {!MediaRouterHandlers} */ this.handlers_ = new MediaRouterHandlers(); /** * Proxy class to the browser's Media Router Mojo service. * @type {!MediaRouter} */ this.mediaRouter_ = mediaRouter; } /* * Sets the callback handler used to invoke methods in the provider manager. * * @param {!MediaRouterHandlers} handlers */ MediaRouteProvider.prototype.setHandlers = function(handlers) { // TODO(mfoltz): Remove when component that supports this method is // rolled out to all Chrome channels in M56. if (!handlers['onBeforeInvokeHandler']) handlers['onBeforeInvokeHandler'] = () => {}; this.handlers_ = handlers; var requiredHandlers = [ 'stopObservingMediaRoutes', 'startObservingMediaRoutes', 'sendRouteMessage', 'sendRouteBinaryMessage', 'startListeningForRouteMessages', 'stopListeningForRouteMessages', 'detachRoute', 'terminateRoute', 'joinRoute', 'createRoute', 'stopObservingMediaSinks', 'startObservingMediaRoutes', 'connectRouteByRouteId', 'enableMdnsDiscovery', 'updateMediaSinks', 'searchSinks', 'onBeforeInvokeHandler' ]; requiredHandlers.forEach(function(nextHandler) { if (handlers[nextHandler] === undefined) { console.error(nextHandler + ' handler not registered.'); } }); } /** * Starts querying for sinks capable of displaying the media source * designated by |sourceUrn|. Results are returned by calling * OnSinksReceived. * @param {!string} sourceUrn */ MediaRouteProvider.prototype.startObservingMediaSinks = function(sourceUrn) { this.handlers_.onBeforeInvokeHandler(); this.handlers_.startObservingMediaSinks(sourceUrn); }; /** * Stops querying for sinks capable of displaying |sourceUrn|. * @param {!string} sourceUrn */ MediaRouteProvider.prototype.stopObservingMediaSinks = function(sourceUrn) { this.handlers_.onBeforeInvokeHandler(); this.handlers_.stopObservingMediaSinks(sourceUrn); }; /** * Requests that |sinkId| render the media referenced by |sourceUrn|. If the * request is from the Presentation API, then origin and tabId will * be populated. * @param {!string} sourceUrn Media source to render. * @param {!string} sinkId Media sink ID. * @param {!string} presentationId Presentation ID from the site * requesting presentation. TODO(mfoltz): Remove. * @param {!string} origin Origin of site requesting presentation. * @param {!number} tabId ID of tab requesting presentation. * @param {!TimeDelta} timeout If positive, the timeout duration for the * request. Otherwise, the default duration will be used. * @param {!boolean} incognito If true, the route is being requested by * an incognito profile. * @return {!Promise.} A Promise resolving to an object describing * the newly created media route, or rejecting with an error message on * failure. */ MediaRouteProvider.prototype.createRoute = function(sourceUrn, sinkId, presentationId, origin, tabId, timeout, incognito) { this.handlers_.onBeforeInvokeHandler(); return this.handlers_.createRoute( sourceUrn, sinkId, presentationId, origin, tabId, Math.floor(timeout.microseconds / 1000), incognito) .then(function(route) { return toSuccessRouteResponse_(route); }, function(err) { return toErrorRouteResponse_(err); }); }; /** * Handles a request via the Presentation API to join an existing route given * by |sourceUrn| and |presentationId|. |origin| and |tabId| are used for * validating same-origin/tab scope. * @param {!string} sourceUrn Media source to render. * @param {!string} presentationId Presentation ID to join. * @param {!string} origin Origin of site requesting join. * @param {!number} tabId ID of tab requesting join. * @param {!TimeDelta} timeout If positive, the timeout duration for the * request. Otherwise, the default duration will be used. * @param {!boolean} incognito If true, the route is being requested by * an incognito profile. * @return {!Promise.} A Promise resolving to an object describing * the newly created media route, or rejecting with an error message on * failure. */ MediaRouteProvider.prototype.joinRoute = function(sourceUrn, presentationId, origin, tabId, timeout, incognito) { this.handlers_.onBeforeInvokeHandler(); return this.handlers_.joinRoute( sourceUrn, presentationId, origin, tabId, Math.floor(timeout.microseconds / 1000), incognito) .then(function(route) { return toSuccessRouteResponse_(route); }, function(err) { return toErrorRouteResponse_(err); }); }; /** * Handles a request via the Presentation API to join an existing route given * by |sourceUrn| and |routeId|. |origin| and |tabId| are used for * validating same-origin/tab scope. * @param {!string} sourceUrn Media source to render. * @param {!string} routeId Route ID to join. * @param {!string} presentationId Presentation ID to join. * @param {!string} origin Origin of site requesting join. * @param {!number} tabId ID of tab requesting join. * @param {!TimeDelta} timeout If positive, the timeout duration for the * request. Otherwise, the default duration will be used. * @param {!boolean} incognito If true, the route is being requested by * an incognito profile. * @return {!Promise.} A Promise resolving to an object describing * the newly created media route, or rejecting with an error message on * failure. */ MediaRouteProvider.prototype.connectRouteByRouteId = function(sourceUrn, routeId, presentationId, origin, tabId, timeout, incognito) { this.handlers_.onBeforeInvokeHandler(); return this.handlers_.connectRouteByRouteId( sourceUrn, routeId, presentationId, origin, tabId, Math.floor(timeout.microseconds / 1000), incognito) .then(function(route) { return toSuccessRouteResponse_(route); }, function(err) { return toErrorRouteResponse_(err); }); }; /** * Terminates the route specified by |routeId|. * @param {!string} routeId * @return {!Promise} A Promise resolving to an object describing * the result of the terminate operation, or rejecting with an error * message and code if the operation failed. */ MediaRouteProvider.prototype.terminateRoute = function(routeId) { this.handlers_.onBeforeInvokeHandler(); return this.handlers_.terminateRoute(routeId).then( () => ({result_code: mediaRouterMojom.RouteRequestResultCode.OK}), (err) => toErrorRouteResponse_(err)); }; /** * Posts a message to the route designated by |routeId|. * @param {!string} routeId * @param {!string} message * @return {!Promise.} Resolved with true if the message was sent, * or false on failure. */ MediaRouteProvider.prototype.sendRouteMessage = function( routeId, message) { this.handlers_.onBeforeInvokeHandler(); return this.handlers_.sendRouteMessage(routeId, message) .then(function() { return {'sent': true}; }, function() { return {'sent': false}; }); }; /** * Sends a binary message to the route designated by |routeId|. * @param {!string} routeId * @param {!Array} data * @return {!Promise.} Resolved with true if the data was sent, * or false on failure. */ MediaRouteProvider.prototype.sendRouteBinaryMessage = function( routeId, data) { this.handlers_.onBeforeInvokeHandler(); return this.handlers_.sendRouteBinaryMessage(routeId, new Uint8Array(data)) .then(function() { return {'sent': true}; }, function() { return {'sent': false}; }); }; /** * Listen for messages from a route. * @param {!string} routeId */ MediaRouteProvider.prototype.startListeningForRouteMessages = function( routeId) { this.handlers_.onBeforeInvokeHandler(); this.handlers_.startListeningForRouteMessages(routeId); }; /** * @param {!string} routeId */ MediaRouteProvider.prototype.stopListeningForRouteMessages = function( routeId) { this.handlers_.onBeforeInvokeHandler(); this.handlers_.stopListeningForRouteMessages(routeId); }; /** * Indicates that the presentation connection that was connected to |routeId| * is no longer connected to it. * @param {!string} routeId */ MediaRouteProvider.prototype.detachRoute = function( routeId) { this.handlers_.detachRoute(routeId); }; /** * Requests that the provider manager start sending information about active * media routes to the Media Router. * @param {!string} sourceUrn */ MediaRouteProvider.prototype.startObservingMediaRoutes = function(sourceUrn) { this.handlers_.onBeforeInvokeHandler(); this.handlers_.startObservingMediaRoutes(sourceUrn); }; /** * Requests that the provider manager stop sending information about active * media routes to the Media Router. * @param {!string} sourceUrn */ MediaRouteProvider.prototype.stopObservingMediaRoutes = function(sourceUrn) { this.handlers_.onBeforeInvokeHandler(); this.handlers_.stopObservingMediaRoutes(sourceUrn); }; /** * Enables mDNS device discovery. */ MediaRouteProvider.prototype.enableMdnsDiscovery = function() { this.handlers_.onBeforeInvokeHandler(); this.handlers_.enableMdnsDiscovery(); }; /** * Requests that the provider manager update media sinks. * @param {!string} sourceUrn */ MediaRouteProvider.prototype.updateMediaSinks = function(sourceUrn) { this.handlers_.onBeforeInvokeHandler(); this.handlers_.updateMediaSinks(sourceUrn); }; /** * Requests that the provider manager search its providers for a sink matching * |searchCriteria| that is compatible with |sourceUrn|. If a sink is found * that can be used immediately for route creation, its ID is returned. * Otherwise the empty string is returned. * * @param {string} sinkId Sink ID of the pseudo sink generating the request. * @param {string} sourceUrn Media source to be used with the sink. * @param {!SinkSearchCriteria} searchCriteria Search criteria for the route * providers. * @return {!Promise.} A Promise resolving to either the * sink ID of the sink found by the search that can be used for route * creation, or the empty string if no route can be immediately created. */ MediaRouteProvider.prototype.searchSinks = function( sinkId, sourceUrn, searchCriteria) { // TODO(btolsch): Remove this check when we no longer expect old extensions // to be missing this API. if (!this.handlers_.searchSinks) { return Promise.resolve({'sink_id': ''}); } this.handlers_.onBeforeInvokeHandler(); return Promise.resolve({ 'sink_id': this.handlers_.searchSinks(sinkId, sourceUrn, searchCriteria) }); }; mediaRouter = new MediaRouter(new mediaRouterMojom.MediaRouterPtr( frameInterfaces.getInterface(mediaRouterMojom.MediaRouter.name))); return mediaRouter; }); // Copyright 2014 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. define("mojo/common/time.mojom", [ "mojo/public/js/bindings", "mojo/public/js/codec", "mojo/public/js/core", "mojo/public/js/validator", ], function(bindings, codec, core, validator) { function Time(values) { this.initDefaults_(); this.initFields_(values); } Time.prototype.initDefaults_ = function() { }; Time.prototype.initFields_ = function(fields) { for(var field in fields) { if (this.hasOwnProperty(field)) this[field] = fields[field]; } }; Time.validate = function(messageValidator, offset) { var err; err = messageValidator.validateStructHeader(offset, codec.kStructHeaderSize); if (err !== validator.validationError.NONE) return err; var kVersionSizes = [ {version: 0, numBytes: 8} ]; err = messageValidator.validateStructVersion(offset, kVersionSizes); if (err !== validator.validationError.NONE) return err; return validator.validationError.NONE; }; Time.encodedSize = codec.kStructHeaderSize + 0; Time.decode = function(decoder) { var packed; var val = new Time(); var numberOfBytes = decoder.readUint32(); var version = decoder.readUint32(); return val; }; Time.encode = function(encoder, val) { var packed; encoder.writeUint32(Time.encodedSize); encoder.writeUint32(0); }; function TimeDelta(values) { this.initDefaults_(); this.initFields_(values); } TimeDelta.prototype.initDefaults_ = function() { this.microseconds = 0; }; TimeDelta.prototype.initFields_ = function(fields) { for(var field in fields) { if (this.hasOwnProperty(field)) this[field] = fields[field]; } }; TimeDelta.validate = function(messageValidator, offset) { var err; err = messageValidator.validateStructHeader(offset, codec.kStructHeaderSize); if (err !== validator.validationError.NONE) return err; var kVersionSizes = [ {version: 0, numBytes: 16} ]; err = messageValidator.validateStructVersion(offset, kVersionSizes); if (err !== validator.validationError.NONE) return err; return validator.validationError.NONE; }; TimeDelta.encodedSize = codec.kStructHeaderSize + 8; TimeDelta.decode = function(decoder) { var packed; var val = new TimeDelta(); var numberOfBytes = decoder.readUint32(); var version = decoder.readUint32(); val.microseconds = decoder.decodeStruct(codec.Int64); return val; }; TimeDelta.encode = function(encoder, val) { var packed; encoder.writeUint32(TimeDelta.encodedSize); encoder.writeUint32(0); encoder.encodeStruct(codec.Int64, val.microseconds); }; function TimeTicks(values) { this.initDefaults_(); this.initFields_(values); } TimeTicks.prototype.initDefaults_ = function() { }; TimeTicks.prototype.initFields_ = function(fields) { for(var field in fields) { if (this.hasOwnProperty(field)) this[field] = fields[field]; } }; TimeTicks.validate = function(messageValidator, offset) { var err; err = messageValidator.validateStructHeader(offset, codec.kStructHeaderSize); if (err !== validator.validationError.NONE) return err; var kVersionSizes = [ {version: 0, numBytes: 8} ]; err = messageValidator.validateStructVersion(offset, kVersionSizes); if (err !== validator.validationError.NONE) return err; return validator.validationError.NONE; }; TimeTicks.encodedSize = codec.kStructHeaderSize + 0; TimeTicks.decode = function(decoder) { var packed; var val = new TimeTicks(); var numberOfBytes = decoder.readUint32(); var version = decoder.readUint32(); return val; }; TimeTicks.encode = function(encoder, val) { var packed; encoder.writeUint32(TimeTicks.encodedSize); encoder.writeUint32(0); }; var exports = {}; exports.Time = Time; exports.TimeDelta = TimeDelta; exports.TimeTicks = TimeTicks; return exports; });/* * Copyright 2014 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. * * This stylesheet is used to apply Chrome styles to extension pages that opt in * to using them. * * These styles have been copied from ui/webui/resources/css/chrome_shared.css * and ui/webui/resources/css/widgets.css *with CSS class logic removed*, so * that it's as close to a user-agent stylesheet as possible. * * For example, extensions shouldn't be able to set a .link-button class and * have it do anything. * * Other than that, keep this file and chrome_shared.css/widgets.cc in sync as * much as possible. */ body { color: #333; cursor: default; /* Note that the correct font-family and font-size are set in * extension_fonts.css. */ /* This top margin of 14px matches the top padding on the h1 element on * overlays (see the ".overlay .page h1" selector in overlay.css), which * every dialogue has. * * Similarly, the bottom 14px margin matches the bottom padding of the area * which hosts the buttons (see the ".overlay .page * .action-area" selector * in overlay.css). * * Both have a padding left/right of 17px. * * Note that we're putting this here in the Extension content, rather than * the WebUI element which contains the content, so that scrollbars in the * Extension content don't get a 6px margin, which looks quite odd. */ margin: 14px 17px; } p { line-height: 1.8em; } h1, h2, h3 { -webkit-user-select: none; font-weight: normal; /* Makes the vertical size of the text the same for all fonts. */ line-height: 1; } h1 { font-size: 1.5em; } h2 { font-size: 1.3em; margin-bottom: 0.4em; } h3 { color: black; font-size: 1.2em; margin-bottom: 0.8em; } a { color: rgb(17, 85, 204); text-decoration: underline; } a:active { color: rgb(5, 37, 119); } /* Default state **************************************************************/ :-webkit-any(button, input[type='button'], input[type='submit']), select, input[type='checkbox'], input[type='radio'] { -webkit-appearance: none; -webkit-user-select: none; background-image: linear-gradient(#ededed, #ededed 38%, #dedede); border: 1px solid rgba(0, 0, 0, 0.25); border-radius: 2px; box-shadow: 0 1px 0 rgba(0, 0, 0, 0.08), inset 0 1px 2px rgba(255, 255, 255, 0.75); color: #444; font: inherit; margin: 0 1px 0 0; outline: none; text-shadow: 0 1px 0 rgb(240, 240, 240); } :-webkit-any(button, input[type='button'], input[type='submit']), select { min-height: 2em; min-width: 4em; /* The following platform-specific rule is necessary to get adjacent * buttons, text inputs, and so forth to align on their borders while also * aligning on the text's baselines. */ padding-bottom: 1px; } :-webkit-any(button, input[type='button'], input[type='submit']) { -webkit-padding-end: 10px; -webkit-padding-start: 10px; } select { -webkit-appearance: none; -webkit-padding-end: 20px; -webkit-padding-start: 6px; /* OVERRIDE */ background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAAICAQAAACxSAwfAAAAUklEQVQY02P4z0AMRGZGMaShwCisyhITmb8huMzfEhOxKvuvsGAh208Ik+3ngoX/FbBbClcIUcSAw21QhXxfIIrwKAMpfNsEUYRXGVCEFc6CQwBqq4CCCtU4VgAAAABJRU5ErkJggg==), linear-gradient(#ededed, #ededed 38%, #dedede); background-position: right center; background-repeat: no-repeat; } html[dir='rtl'] select { background-position: center left; } input[type='checkbox'] { height: 13px; position: relative; vertical-align: middle; width: 13px; } input[type='radio'] { /* OVERRIDE */ border-radius: 100%; height: 15px; position: relative; vertical-align: middle; width: 15px; } /* TODO(estade): add more types here? */ input[type='number'], input[type='password'], input[type='search'], input[type='text'], input[type='url'], input:not([type]), textarea { border: 1px solid #bfbfbf; border-radius: 2px; box-sizing: border-box; color: #444; font: inherit; margin: 0; /* Use min-height to accommodate addditional padding for touch as needed. */ min-height: 2em; padding: 3px; outline: none; /* For better alignment between adjacent buttons and inputs. */ padding-bottom: 4px; } input[type='search'] { -webkit-appearance: textfield; /* NOTE: Keep a relatively high min-width for this so we don't obscure the end * of the default text in relatively spacious languages (i.e. German). */ min-width: 160px; } /* Checked ********************************************************************/ input[type='checkbox']:checked::before { -webkit-user-select: none; background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAsAAAALCAQAAAADpb+tAAAAaElEQVR4Xl3PIQoCQQCF4Y8JW42D1bDZ4iVEjDbxFpstYhC7eIVBZHkXFGw734sv/TqDQQ8Xb1udja/I8igeIm7Aygj2IpoKTGZnVRNxAHYi4iPiDlA9xX+aNQDFySziqDN6uSp6y7ofEMwZ05uUZRkAAAAASUVORK5CYII=); background-size: 100% 100%; content: ''; display: block; height: 100%; width: 100%; } input[type='radio']:checked::before { background-color: #666; border-radius: 100%; bottom: 3px; content: ''; display: block; left: 3px; position: absolute; right: 3px; top: 3px; } /* Hover **********************************************************************/ :enabled:hover:-webkit-any( select, input[type='checkbox'], input[type='radio'], :-webkit-any( button, input[type='button'], input[type='submit'])) { background-image: linear-gradient(#f0f0f0, #f0f0f0 38%, #e0e0e0); border-color: rgba(0, 0, 0, 0.3); box-shadow: 0 1px 0 rgba(0, 0, 0, 0.12), inset 0 1px 2px rgba(255, 255, 255, 0.95); color: black; } :enabled:hover:-webkit-any(select) { /* OVERRIDE */ background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAAICAQAAACxSAwfAAAAUklEQVQY02P4z0AMRGZGMaShwCisyhITmb8huMzfEhOxKvuvsGAh208Ik+3ngoX/FbBbClcIUcSAw21QhXxfIIrwKAMpfNsEUYRXGVCEFc6CQwBqq4CCCtU4VgAAAABJRU5ErkJggg==), linear-gradient(#f0f0f0, #f0f0f0 38%, #e0e0e0); } /* Active *********************************************************************/ :enabled:active:-webkit-any( select, input[type='checkbox'], input[type='radio'], :-webkit-any( button, input[type='button'], input[type='submit'])) { background-image: linear-gradient(#e7e7e7, #e7e7e7 38%, #d7d7d7); box-shadow: none; text-shadow: none; } :enabled:active:-webkit-any(select) { /* OVERRIDE */ background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAAICAQAAACxSAwfAAAAUklEQVQY02P4z0AMRGZGMaShwCisyhITmb8huMzfEhOxKvuvsGAh208Ik+3ngoX/FbBbClcIUcSAw21QhXxfIIrwKAMpfNsEUYRXGVCEFc6CQwBqq4CCCtU4VgAAAABJRU5ErkJggg==), linear-gradient(#e7e7e7, #e7e7e7 38%, #d7d7d7); } /* Disabled *******************************************************************/ :disabled:-webkit-any( button, input[type='button'], input[type='submit']), select:disabled { background-image: linear-gradient(#f1f1f1, #f1f1f1 38%, #e6e6e6); border-color: rgba(80, 80, 80, 0.2); box-shadow: 0 1px 0 rgba(80, 80, 80, 0.08), inset 0 1px 2px rgba(255, 255, 255, 0.75); color: #aaa; } select:disabled { /* OVERRIDE */ background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAAICAQAAACxSAwfAAAASklEQVQY02P4z0AMRGZGMaShwCisyhITG/4jw8RErMr+KyxYiFC0YOF/BeyWIikEKWLA4Ta4QogiPMpACt82QRThVQYUYYWz4BAAGr6Ii6kEPacAAAAASUVORK5CYII=), linear-gradient(#f1f1f1, #f1f1f1 38%, #e6e6e6); } input:disabled:-webkit-any([type='checkbox'], [type='radio']) { opacity: .75; } input:disabled:-webkit-any([type='password'], [type='search'], [type='text'], [type='url'], :not([type])) { color: #999; } /* Focus **********************************************************************/ :enabled:focus:-webkit-any( select, input[type='checkbox'], input[type='number'], input[type='password'], input[type='radio'], input[type='search'], input[type='text'], input[type='url'], input:not([type]), :-webkit-any( button, input[type='button'], input[type='submit'])) { /* OVERRIDE */ -webkit-transition: border-color 200ms; /* We use border color because it follows the border radius (unlike outline). * This is particularly noticeable on mac. */ border-color: rgb(77, 144, 254); outline: none; } /* Checkbox/radio helpers ****************************************************** * * .checkbox and .radio classes wrap labels. Checkboxes and radios should use * these classes with the markup structure: * *
* *
*/ :-webkit-any(.checkbox, .radio) label { /* Don't expand horizontally: . */ align-items: center; display: inline-flex; padding-bottom: 7px; padding-top: 7px; } :-webkit-any(.checkbox, .radio) label input { flex-shrink: 0; } :-webkit-any(.checkbox, .radio) label input ~ span { -webkit-margin-start: 0.6em; /* Make sure long spans wrap at the same horizontal position they start. */ display: block; } :-webkit-any(.checkbox, .radio) label:hover { color: black; } label > input:disabled:-webkit-any([type='checkbox'], [type='radio']) ~ span { color: #999; } // Copyright (c) 2012 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. // // Copyright (c) 2012 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. /** @typedef {Document|DocumentFragment|Element} */ var ProcessingRoot; /** * @fileoverview This is a simple template engine inspired by JsTemplates * optimized for i18n. * * It currently supports three handlers: * * * i18n-content which sets the textContent of the element. * * * * * i18n-options which generates