Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(66)

Side by Side Diff: chrome/browser/resources/gaia_auth/main.js

Issue 134263005: Implement inline signin with iframe (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: fix for various iframe bugs Created 6 years, 10 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. 1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be 2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file. 3 // found in the LICENSE file.
4 4
5 /** 5 /**
6 * Authenticator class wraps the communications between Gaia and its host. 6 * Authenticator class wraps the communications between Gaia and its host.
7 */ 7 */
8 function Authenticator() { 8 function Authenticator() {
9 } 9 }
10 10
(...skipping 18 matching lines...) Expand all
29 Authenticator.prototype = { 29 Authenticator.prototype = {
30 email_: null, 30 email_: null,
31 password_: null, 31 password_: null,
32 attemptToken_: null, 32 attemptToken_: null,
33 33
34 // Input params from extension initialization URL. 34 // Input params from extension initialization URL.
35 inputLang_: undefined, 35 inputLang_: undefined,
36 intputEmail_: undefined, 36 intputEmail_: undefined,
37 37
38 isSAMLFlow_: false, 38 isSAMLFlow_: false,
39 samlSupportChannel_: null, 39 isSAMLEnabled_: false,
40 supportChannel_: null,
40 41
41 GAIA_URL: 'https://accounts.google.com/', 42 GAIA_URL: 'https://accounts.google.com/',
42 GAIA_PAGE_PATH: 'ServiceLogin?skipvpage=true&sarp=1&rm=hide', 43 GAIA_PAGE_PATH: 'ServiceLogin?skipvpage=true&sarp=1&rm=hide',
43 PARENT_PAGE: 'chrome://oobe/', 44 PARENT_PAGE: 'chrome://oobe/',
44 SERVICE_ID: 'chromeoslogin', 45 SERVICE_ID: 'chromeoslogin',
45 CONTINUE_URL: Authenticator.THIS_EXTENSION_ORIGIN + '/success.html', 46 CONTINUE_URL: Authenticator.THIS_EXTENSION_ORIGIN + '/success.html',
46 CONSTRAINED_FLOW_SOURCE: 'chrome', 47 CONSTRAINED_FLOW_SOURCE: 'chrome',
47 48
48 initialize: function() { 49 initialize: function() {
49 var params = getUrlSearchParams(location.search); 50 var params = getUrlSearchParams(location.search);
50 this.parentPage_ = params.parentPage || this.PARENT_PAGE; 51 this.parentPage_ = params.parentPage || this.PARENT_PAGE;
51 this.gaiaUrl_ = params.gaiaUrl || this.GAIA_URL; 52 this.gaiaUrl_ = params.gaiaUrl || this.GAIA_URL;
52 this.gaiaPath_ = params.gaiaPath || this.GAIA_PAGE_PATH; 53 this.gaiaPath_ = params.gaiaPath || this.GAIA_PAGE_PATH;
53 this.inputLang_ = params.hl; 54 this.inputLang_ = params.hl;
54 this.inputEmail_ = params.email; 55 this.inputEmail_ = params.email;
55 this.service_ = params.service || this.SERVICE_ID; 56 this.service_ = params.service || this.SERVICE_ID;
56 this.continueUrl_ = params.continueUrl || this.CONTINUE_URL; 57 this.continueUrl_ = params.continueUrl || this.CONTINUE_URL;
57 this.continueUrlWithoutParams_ = stripParams(this.continueUrl_); 58 this.desktopMode_ = params.desktopMode == '1';
58 this.inlineMode_ = params.inlineMode == '1'; 59 this.isConstrainedWindow_ = params.constrained == '1';
59 this.constrained_ = params.constrained == '1';
60 this.partitionId_ = params.partitionId || '';
61 this.initialFrameUrl_ = params.frameUrl || this.constructInitialFrameUrl_(); 60 this.initialFrameUrl_ = params.frameUrl || this.constructInitialFrameUrl_();
62 this.initialFrameUrlWithoutParams_ = stripParams(this.initialFrameUrl_); 61 this.initialFrameUrlWithoutParams_ = stripParams(this.initialFrameUrl_);
63 this.loaded_ = false;
64 62
65 document.addEventListener('DOMContentLoaded', this.onPageLoad.bind(this)); 63 document.addEventListener('DOMContentLoaded', this.onPageLoad_.bind(this));
66 document.addEventListener('enableSAML', this.onEnableSAML_.bind(this)); 64 document.addEventListener('enableSAML', this.onEnableSAML_.bind(this));
67 }, 65 },
68 66
69 isGaiaMessage_: function(msg) { 67 isGaiaMessage_: function(msg) {
70 // Not quite right, but good enough. 68 // Not quite right, but good enough.
71 return this.gaiaUrl_.indexOf(msg.origin) == 0 || 69 return this.gaiaUrl_.indexOf(msg.origin) == 0 ||
72 this.GAIA_URL.indexOf(msg.origin) == 0; 70 this.GAIA_URL.indexOf(msg.origin) == 0;
73 }, 71 },
74 72
75 isInternalMessage_: function(msg) { 73 isInternalMessage_: function(msg) {
76 return msg.origin == Authenticator.THIS_EXTENSION_ORIGIN; 74 return msg.origin == Authenticator.THIS_EXTENSION_ORIGIN;
77 }, 75 },
78 76
79 isParentMessage_: function(msg) { 77 isParentMessage_: function(msg) {
80 return msg.origin == this.parentPage_; 78 return msg.origin == this.parentPage_;
81 }, 79 },
82 80
83 constructInitialFrameUrl_: function() { 81 constructInitialFrameUrl_: function() {
84 var url = this.gaiaUrl_ + this.gaiaPath_; 82 var url = this.gaiaUrl_ + this.gaiaPath_;
85 83
86 url = appendParam(url, 'service', this.service_); 84 url = appendParam(url, 'service', this.service_);
87 url = appendParam(url, 'continue', this.continueUrl_); 85 url = appendParam(url, 'continue', this.continueUrl_);
88 if (this.inputLang_) 86 if (this.inputLang_)
89 url = appendParam(url, 'hl', this.inputLang_); 87 url = appendParam(url, 'hl', this.inputLang_);
90 if (this.inputEmail_) 88 if (this.inputEmail_)
91 url = appendParam(url, 'Email', this.inputEmail_); 89 url = appendParam(url, 'Email', this.inputEmail_);
92 if (this.constrained_) 90 if (this.isConstrainedWindow_)
93 url = appendParam(url, 'source', this.CONSTRAINED_FLOW_SOURCE); 91 url = appendParam(url, 'source', this.CONSTRAINED_FLOW_SOURCE);
94 return url; 92 return url;
95 }, 93 },
96 94
97 /** Callback when all loads in the gaia webview is complete. */ 95 onPageLoad_: function() {
98 onWebviewLoadstop_: function(gaiaFrame) { 96 window.addEventListener('message', this.onMessage.bind(this), false);
99 if (gaiaFrame.src.lastIndexOf(this.continueUrlWithoutParams_, 0) == 0) { 97
100 // Detect when login is finished by the load stop event of the continue 98 var gaiaFrame = $('gaia-frame');
101 // URL. Cannot reuse the login complete flow in success.html, because 99 gaiaFrame.src = this.initialFrameUrl_;
102 // webview does not support extension pages yet. 100
103 var skipForNow = false; 101 if (this.desktopMode_) {
104 if (this.inlineMode_ && gaiaFrame.src.indexOf('ntp=1') >= 0) { 102 var handler = function() {
105 skipForNow = true; 103 this.onLoginUILoaded_();
104 gaiaFrame.removeEventListener('load', handler);
105
106 this.initDesktopChannel_();
107 }.bind(this);
108 gaiaFrame.addEventListener('load', handler);
109 }
110 },
111
112 initDesktopChannel_: function() {
113 this.supportChannel_ = new Channel();
114 this.supportChannel_.connect('authMain');
115
116 var channelConnected = false;
117 this.supportChannel_.registerMessage('channelConnected', function() {
118 channelConnected = true;
119
120 this.supportChannel_.send({
121 name: 'initDesktopFlow',
122 gaiaUrl: this.gaiaUrl_,
123 continueUrl: stripParams(this.continueUrl_),
124 isConstrainedWindow: this.isConstrainedWindow_
125 });
126 this.supportChannel_.registerMessage(
127 'switchToFullTab', this.switchToFullTab_.bind(this));
128 this.supportChannel_.registerMessage(
129 'completeLogin', this.completeLogin_.bind(this));
130 }.bind(this));
131
132 window.setTimeout(function() {
133 if (!channelConnected) {
134 // Re-initialize the channel if it is not connected properly, e.g.
135 // connect may be called before background script started running.
136 this.initDesktopChannel_();
106 } 137 }
107 msg = { 138 }.bind(this), 200);
108 'method': 'completeLogin',
109 'skipForNow': skipForNow
110 };
111 window.parent.postMessage(msg, this.parentPage_);
112 // Do no report state to the parent for the continue URL, since it is a
113 // blank page.
114 return;
115 }
116
117 // Report the current state to the parent which will then update the
118 // browser history so that later it could respond properly to back/forward.
119 var msg = {
120 'method': 'reportState',
121 'src': gaiaFrame.src
122 };
123 window.parent.postMessage(msg, this.parentPage_);
124
125 if (gaiaFrame.src.lastIndexOf(this.gaiaUrl_, 0) == 0) {
126 gaiaFrame.executeScript({file: 'inline_injected.js'}, function() {
127 // Send an initial message to gaia so that it has an JavaScript
128 // reference to the embedder.
129 gaiaFrame.contentWindow.postMessage('', gaiaFrame.src);
130 });
131 if (this.constrained_) {
132 var preventContextMenu = 'document.addEventListener("contextmenu", ' +
133 'function(e) {e.preventDefault();})';
134 gaiaFrame.executeScript({code: preventContextMenu});
135 }
136 }
137
138 this.loaded_ || this.onLoginUILoaded();
139 }, 139 },
140 140
141 /** 141 /**
142 * Callback when the gaia webview attempts to open a new window. 142 * Invoked when the login UI is initialized or reset.
143 */ 143 */
144 onWebviewNewWindow_: function(gaiaFrame, e) { 144 onLoginUILoaded_: function() {
145 window.open(e.targetUrl, '_blank');
146 e.window.discard();
147 },
148
149 onWebviewRequestCompleted_: function(details) {
150 if (details.url.lastIndexOf(this.continueUrlWithoutParams_, 0) == 0) {
151 return;
152 }
153
154 var headers = details.responseHeaders;
155 for (var i = 0; headers && i < headers.length; ++i) {
156 if (headers[i].name.toLowerCase() == 'google-accounts-embedded') {
157 return;
158 }
159 }
160 var msg = { 145 var msg = {
161 'method': 'switchToFullTab', 146 'method': 'loginUILoaded'
162 'url': details.url
163 }; 147 };
164 window.parent.postMessage(msg, this.parentPage_); 148 window.parent.postMessage(msg, this.parentPage_);
165 }, 149 },
166 150
167 loadFrame_: function() { 151 /**
168 var gaiaFrame = $('gaia-frame'); 152 * Invoked when the background script sends a message to indicate that the
169 gaiaFrame.partition = this.partitionId_; 153 * current content does not fit in a constrained window.
170 gaiaFrame.src = this.initialFrameUrl_; 154 * @param {Object=} opt_extraMsg Optional extra info to send.
171 if (this.inlineMode_) { 155 */
172 gaiaFrame.addEventListener( 156 switchToFullTab_: function(msg) {
173 'loadstop', this.onWebviewLoadstop_.bind(this, gaiaFrame)); 157 var parentMsg = {
174 gaiaFrame.addEventListener( 158 'method': 'switchToFullTab',
175 'newwindow', this.onWebviewNewWindow_.bind(this, gaiaFrame)); 159 'url': msg.url
176 } 160 };
177 if (this.constrained_) { 161 window.parent.postMessage(parentMsg, this.parentPage_);
178 gaiaFrame.request.onCompleted.addListener(
179 this.onWebviewRequestCompleted_.bind(this),
180 {urls: ['<all_urls>'], types: ['main_frame']},
181 ['responseHeaders']);
182 }
183 }, 162 },
184 163
185 completeLogin: function() { 164 /**
165 * Invoked when the signin flow is complete.
166 * @param {Object=} opt_extraMsg Optional extra info to send.
167 */
168 completeLogin_: function(opt_extraMsg) {
186 var msg = { 169 var msg = {
187 'method': 'completeLogin', 170 'method': 'completeLogin',
188 'email': this.email_, 171 'email': (opt_extraMsg && opt_extraMsg.email) || this.email_,
189 'password': this.password_, 172 'password': this.password_,
190 'usingSAML': this.isSAMLFlow_ 173 'usingSAML': this.isSAMLFlow_,
174 'chooseWhatToSync': this.chooseWhatToSync_ || false,
175 'skipForNow': opt_extraMsg && opt_extraMsg.skipForNow,
176 'sessionIndex': opt_extraMsg && opt_extraMsg.sessionIndex
191 }; 177 };
192 window.parent.postMessage(msg, this.parentPage_); 178 window.parent.postMessage(msg, this.parentPage_);
193 if (this.samlSupportChannel_) 179 if (this.isSAMLEnabled_)
194 this.samlSupportChannel_.send({name: 'resetAuth'}); 180 this.supportChannel_.send({name: 'resetAuth'});
195 },
196
197 onPageLoad: function(e) {
198 window.addEventListener('message', this.onMessage.bind(this), false);
199 this.loadFrame_();
200 }, 181 },
201 182
202 /** 183 /**
203 * Invoked when 'enableSAML' event is received to initialize SAML support. 184 * Invoked when 'enableSAML' event is received to initialize SAML support.
204 */ 185 */
205 onEnableSAML_: function() { 186 onEnableSAML_: function() {
187 this.isSAMLEnabled_ = true;
206 this.isSAMLFlow_ = false; 188 this.isSAMLFlow_ = false;
207 189
208 this.samlSupportChannel_ = new Channel(); 190 if (!this.supportChannel_) {
209 this.samlSupportChannel_.connect('authMain'); 191 this.supportChannel_ = new Channel();
210 this.samlSupportChannel_.registerMessage( 192 this.supportChannel_.connect('authMain');
193 }
194
195 this.supportChannel_.registerMessage(
211 'onAuthPageLoaded', this.onAuthPageLoaded_.bind(this)); 196 'onAuthPageLoaded', this.onAuthPageLoaded_.bind(this));
212 this.samlSupportChannel_.registerMessage( 197 this.supportChannel_.registerMessage(
213 'apiCall', this.onAPICall_.bind(this)); 198 'apiCall', this.onAPICall_.bind(this));
214 this.samlSupportChannel_.send({ 199 this.supportChannel_.send({
215 name: 'setGaiaUrl', 200 name: 'setGaiaUrl',
216 gaiaUrl: this.gaiaUrl_ 201 gaiaUrl: this.gaiaUrl_
217 }); 202 });
218 }, 203 },
219 204
220 /** 205 /**
221 * Invoked when the background page sends 'onHostedPageLoaded' message. 206 * Invoked when the background page sends 'onHostedPageLoaded' message.
222 * @param {!Object} msg Details sent with the message. 207 * @param {!Object} msg Details sent with the message.
223 */ 208 */
224 onAuthPageLoaded_: function(msg) { 209 onAuthPageLoaded_: function(msg) {
(...skipping 27 matching lines...) Expand all
252 this.email_ = call.user; 237 this.email_ = call.user;
253 this.password_ = call.password; 238 this.password_ = call.password;
254 } else if (call.method == 'confirm') { 239 } else if (call.method == 'confirm') {
255 if (call.token != this.apiToken_) 240 if (call.token != this.apiToken_)
256 console.error('Authenticator.onAPICall_: token mismatch'); 241 console.error('Authenticator.onAPICall_: token mismatch');
257 } else { 242 } else {
258 console.error('Authenticator.onAPICall_: unknown message'); 243 console.error('Authenticator.onAPICall_: unknown message');
259 } 244 }
260 }, 245 },
261 246
262 onLoginUILoaded: function() {
263 var msg = {
264 'method': 'loginUILoaded'
265 };
266 window.parent.postMessage(msg, this.parentPage_);
267 if (this.inlineMode_) {
268 // TODO(guohui): temporary workaround until webview team fixes the focus
269 // on their side.
270 var gaiaFrame = $('gaia-frame');
271 gaiaFrame.focus();
272 gaiaFrame.onblur = function() {
273 gaiaFrame.focus();
274 };
275 }
276 this.loaded_ = true;
277 },
278
279 onConfirmLogin_: function() { 247 onConfirmLogin_: function() {
280 if (!this.isSAMLFlow_) { 248 if (!this.isSAMLFlow_) {
281 this.completeLogin(); 249 this.completeLogin_();
282 return; 250 return;
283 } 251 }
284 252
285 var apiUsed = !!this.password_; 253 var apiUsed = !!this.password_;
286 254
287 // Retrieve the e-mail address of the user who just authenticated from GAIA. 255 // Retrieve the e-mail address of the user who just authenticated from GAIA.
288 window.parent.postMessage({method: 'retrieveAuthenticatedUserEmail', 256 window.parent.postMessage({method: 'retrieveAuthenticatedUserEmail',
289 attemptToken: this.attemptToken_, 257 attemptToken: this.attemptToken_,
290 apiUsed: apiUsed}, 258 apiUsed: apiUsed},
291 this.parentPage_); 259 this.parentPage_);
292 260
293 if (!apiUsed) { 261 if (!apiUsed) {
294 this.samlSupportChannel_.sendWithCallback( 262 this.supportChannel_.sendWithCallback(
295 {name: 'getScrapedPasswords'}, 263 {name: 'getScrapedPasswords'},
296 function(passwords) { 264 function(passwords) {
297 if (passwords.length == 0) { 265 if (passwords.length == 0) {
298 window.parent.postMessage( 266 window.parent.postMessage(
299 {method: 'noPassword', email: this.email_}, 267 {method: 'noPassword', email: this.email_},
300 this.parentPage_); 268 this.parentPage_);
301 } else { 269 } else {
302 window.parent.postMessage({method: 'confirmPassword', 270 window.parent.postMessage({method: 'confirmPassword',
303 email: this.email_, 271 email: this.email_,
304 passwordCount: passwords.length}, 272 passwordCount: passwords.length},
305 this.parentPage_); 273 this.parentPage_);
306 } 274 }
307 }.bind(this)); 275 }.bind(this));
308 } 276 }
309 }, 277 },
310 278
311 maybeCompleteSAMLLogin_: function() { 279 maybeCompleteSAMLLogin_: function() {
312 // SAML login is complete when the user's e-mail address has been retrieved 280 // SAML login is complete when the user's e-mail address has been retrieved
313 // from GAIA and the user has successfully confirmed the password. 281 // from GAIA and the user has successfully confirmed the password.
314 if (this.email_ !== null && this.password_ !== null) 282 if (this.email_ !== null && this.password_ !== null)
315 this.completeLogin(); 283 this.completeLogin_();
316 }, 284 },
317 285
318 onVerifyConfirmedPassword_: function(password) { 286 onVerifyConfirmedPassword_: function(password) {
319 this.samlSupportChannel_.sendWithCallback( 287 this.supportChannel_.sendWithCallback(
320 {name: 'getScrapedPasswords'}, 288 {name: 'getScrapedPasswords'},
321 function(passwords) { 289 function(passwords) {
322 for (var i = 0; i < passwords.length; ++i) { 290 for (var i = 0; i < passwords.length; ++i) {
323 if (passwords[i] == password) { 291 if (passwords[i] == password) {
324 this.password_ = passwords[i]; 292 this.password_ = passwords[i];
325 this.maybeCompleteSAMLLogin_(); 293 this.maybeCompleteSAMLLogin_();
326 return; 294 return;
327 } 295 }
328 } 296 }
329 window.parent.postMessage( 297 window.parent.postMessage(
330 {method: 'confirmPassword', email: this.email_}, 298 {method: 'confirmPassword', email: this.email_},
331 this.parentPage_); 299 this.parentPage_);
332 }.bind(this)); 300 }.bind(this));
333 }, 301 },
334 302
335 onMessage: function(e) { 303 onMessage: function(e) {
336 var msg = e.data; 304 var msg = e.data;
337 if (msg.method == 'attemptLogin' && this.isGaiaMessage_(e)) { 305 if (msg.method == 'attemptLogin' && this.isGaiaMessage_(e)) {
338 this.email_ = msg.email; 306 this.email_ = msg.email;
339 this.password_ = msg.password; 307 this.password_ = msg.password;
340 this.attemptToken_ = msg.attemptToken; 308 this.attemptToken_ = msg.attemptToken;
309 this.chooseWhatToSync_ = msg.chooseWhatToSync;
341 this.isSAMLFlow_ = false; 310 this.isSAMLFlow_ = false;
342 if (this.samlSupportChannel_) 311 if (this.isSAMLEnabled_)
343 this.samlSupportChannel_.send({name: 'startAuth'}); 312 this.supportChannel_.send({name: 'startAuth'});
344 } else if (msg.method == 'clearOldAttempts' && this.isGaiaMessage_(e)) { 313 } else if (msg.method == 'clearOldAttempts' && this.isGaiaMessage_(e)) {
345 this.email_ = null; 314 this.email_ = null;
346 this.password_ = null; 315 this.password_ = null;
347 this.attemptToken_ = null; 316 this.attemptToken_ = null;
348 this.isSAMLFlow_ = false; 317 this.isSAMLFlow_ = false;
349 this.onLoginUILoaded(); 318 this.onLoginUILoaded_();
350 if (this.samlSupportChannel_) 319 if (this.isSAMLEnabled_)
351 this.samlSupportChannel_.send({name: 'resetAuth'}); 320 this.supportChannel_.send({name: 'resetAuth'});
352 } else if (msg.method == 'setAuthenticatedUserEmail' && 321 } else if (msg.method == 'setAuthenticatedUserEmail' &&
353 this.isParentMessage_(e)) { 322 this.isParentMessage_(e)) {
354 if (this.attemptToken_ == msg.attemptToken) { 323 if (this.attemptToken_ == msg.attemptToken) {
355 this.email_ = msg.email; 324 this.email_ = msg.email;
356 this.maybeCompleteSAMLLogin_(); 325 this.maybeCompleteSAMLLogin_();
357 } 326 }
358 } else if (msg.method == 'confirmLogin' && this.isInternalMessage_(e)) { 327 } else if (msg.method == 'confirmLogin' && this.isInternalMessage_(e)) {
359 if (this.attemptToken_ == msg.attemptToken) 328 if (this.attemptToken_ == msg.attemptToken)
360 this.onConfirmLogin_(); 329 this.onConfirmLogin_();
361 else 330 else
362 console.error('Authenticator.onMessage: unexpected attemptToken!?'); 331 console.error('Authenticator.onMessage: unexpected attemptToken!?');
363 } else if (msg.method == 'verifyConfirmedPassword' && 332 } else if (msg.method == 'verifyConfirmedPassword' &&
364 this.isParentMessage_(e)) { 333 this.isParentMessage_(e)) {
365 this.onVerifyConfirmedPassword_(msg.password); 334 this.onVerifyConfirmedPassword_(msg.password);
366 } else if (msg.method == 'navigate' && 335 } else if (msg.method == 'navigate' &&
367 this.isParentMessage_(e)) { 336 this.isParentMessage_(e)) {
368 $('gaia-frame').src = msg.src; 337 $('gaia-frame').src = msg.src;
369 } else if (msg.method == 'redirectToSignin' && 338 } else if (msg.method == 'redirectToSignin' &&
370 this.isParentMessage_(e)) { 339 this.isParentMessage_(e)) {
371 $('gaia-frame').src = this.constructInitialFrameUrl_(); 340 $('gaia-frame').src = this.constructInitialFrameUrl_();
372 } else { 341 } else {
373 console.error('Authenticator.onMessage: unknown message + origin!?'); 342 console.error('Authenticator.onMessage: unknown message + origin!?');
374 } 343 }
375 } 344 }
376 }; 345 };
377 346
378 Authenticator.getInstance().initialize(); 347 Authenticator.getInstance().initialize();
OLDNEW
« no previous file with comments | « chrome/browser/resources/gaia_auth/inline_main.html ('k') | chrome/browser/resources/gaia_auth/manifest_desktop.json » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698