Wed, 31 Dec 2014 06:09:35 +0100
Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 const Cc = Components.classes;
6 const Ci = Components.interfaces;
7 const Cu = Components.utils;
8 const Cr = Components.results;
9 Cu.import("resource:///modules/devtools/gDevTools.jsm");
10 const {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
11 const {require} = devtools;
12 const {ConnectionManager, Connection} = require("devtools/client/connection-manager");
13 const {AppProjects} = require("devtools/app-manager/app-projects");
14 const {AppValidator} = require("devtools/app-manager/app-validator");
15 const {Services} = Cu.import("resource://gre/modules/Services.jsm");
16 const {FileUtils} = Cu.import("resource://gre/modules/FileUtils.jsm");
17 const {installHosted, installPackaged, getTargetForApp,
18 reloadApp, launchApp, closeApp} = require("devtools/app-actor-front");
19 const {EventEmitter} = Cu.import("resource://gre/modules/devtools/event-emitter.js");
21 const promise = require("devtools/toolkit/deprecated-sync-thenables");
23 const MANIFEST_EDITOR_ENABLED = "devtools.appmanager.manifestEditor.enabled";
25 window.addEventListener("message", function(event) {
26 try {
27 let json = JSON.parse(event.data);
28 if (json.name == "connection") {
29 let cid = parseInt(json.cid);
30 for (let c of ConnectionManager.connections) {
31 if (c.uid == cid) {
32 UI.connection = c;
33 UI.onNewConnection();
34 break;
35 }
36 }
37 }
38 } catch(e) {}
39 });
41 window.addEventListener("unload", function onUnload() {
42 window.removeEventListener("unload", onUnload);
43 UI.destroy();
44 });
46 let UI = {
47 isReady: false,
49 onload: function() {
50 if (Services.prefs.getBoolPref(MANIFEST_EDITOR_ENABLED)) {
51 document.querySelector("#lense").setAttribute("manifest-editable", "");
52 }
54 this.template = new Template(document.body, AppProjects.store, Utils.l10n);
55 this.template.start();
57 AppProjects.load().then(() => {
58 AppProjects.store.object.projects.forEach(UI.validate);
59 this.isReady = true;
60 this.emit("ready");
61 });
62 },
64 destroy: function() {
65 if (this.connection) {
66 this.connection.off(Connection.Events.STATUS_CHANGED, this._onConnectionStatusChange);
67 }
68 this.template.destroy();
69 },
71 onNewConnection: function() {
72 this.connection.on(Connection.Events.STATUS_CHANGED, this._onConnectionStatusChange);
73 this._onConnectionStatusChange();
74 },
76 _onConnectionStatusChange: function() {
77 if (this.connection.status != Connection.Status.CONNECTED) {
78 document.body.classList.remove("connected");
79 this.listTabsResponse = null;
80 } else {
81 document.body.classList.add("connected");
82 this.connection.client.listTabs(
83 response => {this.listTabsResponse = response}
84 );
85 }
86 },
88 get connected() { return !!this.listTabsResponse; },
90 _selectFolder: function() {
91 let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
92 fp.init(window, Utils.l10n("project.filePickerTitle"), Ci.nsIFilePicker.modeGetFolder);
93 let res = fp.show();
94 if (res != Ci.nsIFilePicker.returnCancel)
95 return fp.file;
96 return null;
97 },
99 addPackaged: function(folder) {
100 if (!folder) {
101 folder = this._selectFolder();
102 }
103 if (!folder)
104 return;
105 return AppProjects.addPackaged(folder)
106 .then(function (project) {
107 UI.validate(project);
108 UI.selectProject(project.location);
109 });
110 },
112 addHosted: function() {
113 let form = document.querySelector("#new-hosted-project-wrapper");
114 if (!form.checkValidity())
115 return;
117 let urlInput = document.querySelector("#url-input");
118 let manifestURL = urlInput.value;
119 return AppProjects.addHosted(manifestURL)
120 .then(function (project) {
121 UI.validate(project);
122 UI.selectProject(project.location);
123 });
124 },
126 _getLocalIconURL: function(project, manifest) {
127 let icon;
128 if (manifest.icons) {
129 let size = Object.keys(manifest.icons).sort(function(a, b) b - a)[0];
130 if (size) {
131 icon = manifest.icons[size];
132 }
133 }
134 if (!icon)
135 return "chrome://browser/skin/devtools/app-manager/default-app-icon.png";
136 if (project.type == "hosted") {
137 let manifestURL = Services.io.newURI(project.location, null, null);
138 let origin = Services.io.newURI(manifestURL.prePath, null, null);
139 return Services.io.newURI(icon, null, origin).spec;
140 } else if (project.type == "packaged") {
141 let projectFolder = FileUtils.File(project.location);
142 let folderURI = Services.io.newFileURI(projectFolder).spec;
143 return folderURI + icon.replace(/^\/|\\/, "");
144 }
145 },
147 validate: function(project) {
148 let validation = new AppValidator(project);
149 return validation.validate()
150 .then(function () {
151 if (validation.manifest) {
152 project.icon = UI._getLocalIconURL(project, validation.manifest);
153 project.manifest = validation.manifest;
154 }
156 project.validationStatus = "valid";
158 if (validation.warnings.length > 0) {
159 project.warningsCount = validation.warnings.length;
160 project.warnings = validation.warnings.join(",\n ");
161 project.validationStatus = "warning";
162 } else {
163 project.warnings = "";
164 project.warningsCount = 0;
165 }
167 if (validation.errors.length > 0) {
168 project.errorsCount = validation.errors.length;
169 project.errors = validation.errors.join(",\n ");
170 project.validationStatus = "error";
171 } else {
172 project.errors = "";
173 project.errorsCount = 0;
174 }
176 if (project.warningsCount && project.errorsCount) {
177 project.validationStatus = "error warning";
178 }
180 });
182 },
184 update: function(button, location) {
185 button.disabled = true;
186 let project = AppProjects.get(location);
188 // Update the manifest editor view, in case the manifest was modified
189 // outside of the app manager. This can happen in parallel with the other
190 // steps.
191 this._showManifestEditor(project);
193 this.validate(project)
194 .then(() => {
195 // Install the app to the device if we are connected,
196 // and there is no error
197 if (project.errorsCount == 0 && this.connected) {
198 return this.install(project);
199 }
200 })
201 .then(() => {
202 button.disabled = false;
203 // Finally try to reload the app if it is already opened
204 if (this.connected) {
205 this.reload(project);
206 }
207 },
208 (res) => {
209 button.disabled = false;
210 let message = res.error + ": " + res.message;
211 alert(message);
212 this.connection.log(message);
213 });
214 },
216 saveManifest: function(button) {
217 button.disabled = true;
218 this.manifestEditor.save().then(() => button.disabled = false);
219 },
221 reload: function (project) {
222 if (!this.connected) {
223 return promise.reject();
224 }
225 return reloadApp(this.connection.client,
226 this.listTabsResponse.webappsActor,
227 this._getProjectManifestURL(project)).
228 then(() => {
229 this.connection.log("App reloaded");
230 });
231 },
233 remove: function(location, event) {
234 if (event) {
235 // We don't want the "click" event to be propagated to the project item.
236 // That would trigger `selectProject()`.
237 event.stopPropagation();
238 }
240 let item = document.getElementById(location);
242 let toSelect = document.querySelector(".project-item.selected");
243 toSelect = toSelect ? toSelect.id : "";
245 if (toSelect == location) {
246 toSelect = null;
247 let sibling;
248 if (item.previousElementSibling) {
249 sibling = item.previousElementSibling;
250 } else {
251 sibling = item.nextElementSibling;
252 }
253 if (sibling && !!AppProjects.get(sibling.id)) {
254 toSelect = sibling.id;
255 }
256 }
258 AppProjects.remove(location).then(() => {
259 this.selectProject(toSelect);
260 });
261 },
263 _getProjectManifestURL: function (project) {
264 if (project.type == "packaged") {
265 return "app://" + project.packagedAppOrigin + "/manifest.webapp";
266 } else if (project.type == "hosted") {
267 return project.location;
268 }
269 },
271 install: function(project) {
272 if (!this.connected) {
273 return promise.reject();
274 }
275 this.connection.log("Installing the " + project.manifest.name + " app...");
276 let installPromise;
277 if (project.type == "packaged") {
278 installPromise = installPackaged(this.connection.client, this.listTabsResponse.webappsActor, project.location, project.packagedAppOrigin)
279 .then(({ appId }) => {
280 // If the packaged app specified a custom origin override,
281 // we need to update the local project origin
282 project.packagedAppOrigin = appId;
283 // And ensure the indexed db on disk is also updated
284 AppProjects.update(project);
285 });
286 } else {
287 let manifestURLObject = Services.io.newURI(project.location, null, null);
288 let origin = Services.io.newURI(manifestURLObject.prePath, null, null);
289 let appId = origin.host;
290 let metadata = {
291 origin: origin.spec,
292 manifestURL: project.location
293 };
294 installPromise = installHosted(this.connection.client, this.listTabsResponse.webappsActor, appId, metadata, project.manifest);
295 }
297 installPromise.then(() => {
298 this.connection.log("Install completed.");
299 }, () => {
300 this.connection.log("Install failed.");
301 });
303 return installPromise;
304 },
306 start: function(project) {
307 if (!this.connected) {
308 return promise.reject();
309 }
310 let manifestURL = this._getProjectManifestURL(project);
311 return launchApp(this.connection.client,
312 this.listTabsResponse.webappsActor,
313 manifestURL);
314 },
316 stop: function(location) {
317 if (!this.connected) {
318 return promise.reject();
319 }
320 let project = AppProjects.get(location);
321 let manifestURL = this._getProjectManifestURL(project);
322 return closeApp(this.connection.client,
323 this.listTabsResponse.webappsActor,
324 manifestURL);
325 },
327 debug: function(button, location) {
328 if (!this.connected) {
329 return promise.reject();
330 }
331 button.disabled = true;
332 let project = AppProjects.get(location);
334 let onFailedToStart = (error) => {
335 // If not installed, install and open it
336 if (error == "NO_SUCH_APP") {
337 return this.install(project);
338 } else {
339 throw error;
340 }
341 };
342 let onStarted = () => {
343 // Once we asked the app to launch, the app isn't necessary completely loaded.
344 // launch request only ask the app to launch and immediatly returns.
345 // We have to keep trying to get app tab actors required to create its target.
346 let deferred = promise.defer();
347 let loop = (count) => {
348 // Ensure not looping for ever
349 if (count >= 100) {
350 deferred.reject("Unable to connect to the app");
351 return;
352 }
353 // Also, in case the app wasn't installed yet, we also have to keep asking the
354 // app to launch, as launch request made right after install may race.
355 this.start(project);
356 getTargetForApp(
357 this.connection.client,
358 this.listTabsResponse.webappsActor,
359 this._getProjectManifestURL(project)).
360 then(deferred.resolve,
361 (err) => {
362 if (err == "appNotFound")
363 setTimeout(loop, 500, count + 1);
364 else
365 deferred.reject(err);
366 });
367 };
368 loop(0);
369 return deferred.promise;
370 };
372 // First try to open the app
373 this.start(project)
374 .then(null, onFailedToStart)
375 .then(onStarted)
376 .then((target) =>
377 top.UI.openAndShowToolboxForTarget(target,
378 project.manifest.name,
379 project.icon))
380 .then(() => {
381 // And only when the toolbox is opened, release the button
382 button.disabled = false;
383 },
384 (err) => {
385 button.disabled = false;
386 let message = err.error ? err.error + ": " + err.message : String(err);
387 alert(message);
388 this.connection.log(message);
389 });
390 },
392 reveal: function(location) {
393 let project = AppProjects.get(location);
394 if (project.type == "packaged") {
395 let projectFolder = FileUtils.File(project.location);
396 projectFolder.reveal();
397 } else {
398 // TODO: eventually open hosted apps in firefox
399 // when permissions are correctly supported by firefox
400 }
401 },
403 selectProject: function(location) {
404 let projects = AppProjects.store.object.projects;
405 let idx = 0;
406 for (; idx < projects.length; idx++) {
407 if (projects[idx].location == location) {
408 break;
409 }
410 }
412 let oldButton = document.querySelector(".project-item.selected");
413 if (oldButton) {
414 oldButton.classList.remove("selected");
415 }
417 if (idx == projects.length) {
418 // Not found. Empty lense.
419 let lense = document.querySelector("#lense");
420 lense.setAttribute("template-for", '{"path":"","childSelector":""}');
421 this.template._processFor(lense);
422 return;
423 }
425 let button = document.getElementById(location);
426 button.classList.add("selected");
428 let template = '{"path":"projects.' + idx + '","childSelector":"#lense-template"}';
430 let lense = document.querySelector("#lense");
431 lense.setAttribute("template-for", template);
432 this.template._processFor(lense);
434 let project = projects[idx];
435 this._showManifestEditor(project).then(() => this.emit("project-selected"));
436 },
438 _showManifestEditor: function(project) {
439 if (this.manifestEditor) {
440 this.manifestEditor.destroy();
441 }
442 let editorContainer = document.querySelector("#lense .manifest-editor");
443 this.manifestEditor = new ManifestEditor(project);
444 return this.manifestEditor.show(editorContainer);
445 }
446 };
448 // This must be bound immediately, as it might be used via the message listener
449 // before UI.onload() has been called.
450 UI._onConnectionStatusChange = UI._onConnectionStatusChange.bind(UI);
452 EventEmitter.decorate(UI);