1 /* See license.txt for terms of usage */
  2 
  3 define([
  4     "firebug/lib/lib",
  5     "firebug/lib/object",
  6     "firebug/firebug",
  7     "firebug/lib/locale",
  8     "firebug/lib/xpcom",
  9     "firebug/lib/url",
 10     "firebug/lib/string",
 11     "firebug/js/sourceLink",
 12     "firebug/lib/css",
 13     "firebug/lib/system",
 14     "firebug/lib/array",
 15     "firebug/lib/dom",
 16     "firebug/chrome/menu",
 17     "firebug/trace/debug",
 18     "firebug/chrome/firefox"
 19 ],
 20 function(FBL, Obj, Firebug, Locale, Xpcom, Url, Str, SourceLink, Css, System, Arr, Dom,
 21     Menu, Debug, Firefox) {
 22 
 23 // ********************************************************************************************* //
 24 // Constants
 25 
 26 const Cc = Components.classes;
 27 const Ci = Components.interfaces;
 28 
 29 const DirService = Xpcom.CCSV("@mozilla.org/file/directory_service;1",
 30     "nsIDirectoryServiceProvider");
 31 const NS_OS_TEMP_DIR = "TmpD"
 32 const nsIFile = Ci.nsIFile;
 33 const nsISafeOutputStream = Ci.nsISafeOutputStream;
 34 const nsIURI = Ci.nsIURI;
 35 
 36 const prefDomain = "extensions.firebug";
 37 
 38 var editors = [];
 39 var externalEditors = [];
 40 var temporaryFiles = [];
 41 var temporaryDirectory = null;
 42 
 43 // ********************************************************************************************* //
 44 // Module Implementation
 45 
 46 Firebug.ExternalEditors = Obj.extend(Firebug.Module,
 47 {
 48     dispatchName: "externalEditors",
 49 
 50     initializeUI: function()
 51     {
 52         Firebug.Module.initializeUI.apply(this, arguments);
 53 
 54         Firebug.registerUIListener(this)
 55         this.loadExternalEditors();
 56     },
 57 
 58     updateOption: function(name, value)
 59     {
 60         if (name.substr(0, 15) == "externalEditors")
 61             this.loadExternalEditors();
 62     },
 63 
 64     shutdown: function()
 65     {
 66          this.deleteTemporaryFiles();
 67     },
 68 
 69     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
 70 
 71     registerEditor: function()
 72     {
 73         editors.push.apply(editors, arguments);
 74     },
 75 
 76     getRegisteredEditors: function()
 77     {
 78         var newArray = [];
 79 
 80         if (editors.length > 0)
 81         {
 82             newArray.push.apply(newArray, editors);
 83             if (externalEditors.length > 0)
 84                 newArray.push("-");
 85         }
 86 
 87         if (externalEditors.length > 0)
 88             newArray.push.apply(newArray, externalEditors);
 89 
 90         return newArray;
 91     },
 92 
 93     loadExternalEditors: function()
 94     {
 95         const prefName = "externalEditors";
 96         const editorPrefNames = ["label", "executable", "cmdline", "image"];
 97 
 98         externalEditors = [];
 99         var prefDomain = Firebug.Options.getPrefDomain();
100         var list = Firebug.Options.getPref(prefDomain, prefName).split(",");
101 
102         for (var i=0; i<list.length; ++i)
103         {
104             var editorId = list[i];
105             if (!editorId || editorId == "")
106                 continue;
107 
108             var item = { id: editorId };
109             for (var j=0; j<editorPrefNames.length; ++j)
110             {
111                 try
112                 {
113                     item[editorPrefNames[j]] = Firebug.Options.getPref(prefDomain,
114                         prefName + "." + editorId + "." + editorPrefNames[j]);
115                 }
116                 catch(exc)
117                 {
118                 }
119             }
120 
121             if (item.label && item.executable)
122             {
123                 if (!item.image)
124                     item.image = System.getIconURLForFile(item.executable);
125                 externalEditors.push(item);
126             }
127         }
128         return externalEditors;
129     },
130 
131     getDefaultEditor: function()
132     {
133         return externalEditors[0] || editors[0];
134     },
135 
136     getEditor: function(id)
137     {
138         if (typeof id == "object")
139             return id;
140 
141         if (!id)
142             return this.getDefaultEditor();
143 
144         var list = Arr.extendArray(externalEditors, editors);
145         for (var i=0; i<list.length; i++)
146         {
147             var editor = list[i];
148             if (editor.id == id)
149                 return editor;
150         }
151     },
152 
153     count: function()
154     {
155         return externalEditors.length + editors.length;
156     },
157 
158     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
159     // Overlay menu support
160 
161     onEditorsShowing: function(popup)
162     {
163         Dom.eraseNode(popup);
164 
165         var editors = this.getRegisteredEditors();
166         for (var i=0; i<editors.length; ++i)
167         {
168             if (editors[i] == "-")
169             {
170                 Menu.createMenuItem(popup, "-");
171                 continue;
172             }
173 
174             var item = {
175                 label: editors[i].label,
176                 image: editors[i].image,
177                 nol10n: true
178             };
179 
180             var menuitem = Menu.createMenuItem(popup, item);
181             menuitem.value = editors[i].id;
182         }
183 
184         if (editors.length > 0)
185             Menu.createMenuItem(popup, "-");
186 
187         Menu.createMenuItem(popup, {
188             label: Locale.$STR("firebug.Configure_Editors") + "...",
189             nol10n: true,
190             option: "openEditorList"
191         });
192     },
193 
194     openEditorList: function()
195     {
196         var args = {
197             FBL: FBL,
198             prefName: prefDomain + ".externalEditors"
199         };
200 
201         Firefox.openWindow("Firebug:ExternalEditors",
202             "chrome://firebug/content/firefox/external-editors/editors.xul",
203             "", args);
204     },
205 
206     onContextMenu: function(items, object, target, context, panel, popup)
207     {
208         if (!this.count())
209             return
210 
211         if (object instanceof SourceLink.SourceLink)
212         {
213             var sourceLink = object;
214             this.appendContextMenuItem(popup, sourceLink.href, sourceLink.line);
215         }
216         else if (target.id == "fbLocationList")
217         {
218             if (object.href)
219                 this.appendContextMenuItem(popup, object.href, 0);
220         }
221         else if (panel)
222         {
223             var sourceLink = panel.getSourceLink(target, object);
224             if (sourceLink)
225                 this.appendContextMenuItem(popup, sourceLink.href, sourceLink.line);
226         }
227         else if (Css.hasClass(target, "stackFrameLink"))
228         {
229             this.appendContextMenuItem(popup, target.innerHTML,
230                 target.getAttribute("lineNumber"));
231         }
232     },
233 
234     createContextMenuItem: function(doc)
235     {
236         var item = doc.createElement("menu");
237         item.setAttribute("type", "splitmenu");
238         item.setAttribute("iconic", "true");
239 
240         item.addEventListener("command", function(event)
241         {
242             Firebug.ExternalEditors.onContextMenuCommand(event);
243         });
244 
245         var menupopup = doc.createElement("menupopup");
246         menupopup.addEventListener("popupshowing", function(event)
247         {
248             return Firebug.ExternalEditors.onEditorsShowing(this)
249         });
250 
251         item.appendChild(menupopup);
252         return item;
253     },
254 
255     appendContextMenuItem: function(popup, url, line)
256     {
257         var editor = this.getDefaultEditor();
258         var doc = popup.ownerDocument;
259         var item = doc.getElementById("menu_firebug_firebugOpenWithEditor");
260 
261         if (item)
262         {
263             item = item.cloneNode(true);
264             item.hidden = false;
265             item.removeAttribute("openFromContext");
266         }
267         else
268         {
269             item = this.createContextMenuItem(doc);
270         }
271 
272         item.setAttribute("image", editor.image);
273         item.setAttribute("label", editor.label);
274         item.value = editor.id;
275 
276         popup.appendChild(item);
277 
278         this.lastSource={url: url, line: line};
279     },
280 
281     onContextMenuCommand: function(event)
282     {
283         if (event.target.getAttribute("option") == "openEditorList")
284             this.openEditorList();
285         else if (event.currentTarget.hasAttribute("openFromContext"))
286             this.openContext(Firebug.currentContext, event.target.value);
287         else
288             this.open(this.lastSource.url, this.lastSource.line, event.target.value);
289     },
290 
291     openContext: function(context, editorId)
292     {
293         var line = null;
294         var panel = Firebug.chrome.getSelectedPanel();
295         if (panel)
296         {
297             var box = panel.selectedSourceBox;
298             if (box && box.centralLine)
299                 line = box.centralLine;
300         }
301 
302         // if firebug isn't active this will redturn documentURI
303         var url = Firebug.chrome.getSelectedPanelURL();
304         this.open(url, line, editorId, context);
305     },
306 
307     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
308     // main
309 
310     open: function(href, line, editorId, context)
311     {
312         try
313         {
314             if (FBTrace.DBG_EXTERNALEDITORS)
315                 FBTrace.sysout("externalEditors.open; href: " + href + ", line: " + line +
316                     ", editorId: " + editorId + ", context: " + context, context);
317 
318             if (!href)
319                 return;
320 
321             var editor = this.getEditor(editorId);
322             if (!editor)
323                  return;
324 
325             if (editor.handler)
326                 return editor.handler(href, line);
327 
328             var options = {
329                 url: href,
330                 href: href,
331                 line: line,
332                 editor: editor,
333                 cmdline: editor.cmdline
334             }
335 
336             var self = this;
337             this.getLocalFile(options, function(file)
338             {
339                 if (file.exists())
340                 {
341                     if (file.isDirectory())
342                     {
343                         file.reveal();
344                         return;
345                     }
346 
347                     options.file = file.path;
348                 }
349 
350                 var args = self.parseCmdLine(options.cmdline, options);
351 
352                 if (FBTrace.DBG_EXTERNALEDITORS)
353                     FBTrace.sysout("externalEditors.open; launcProgram with args:", args);
354 
355                 System.launchProgram(editor.executable, args);
356             });
357         }
358         catch (exc)
359         {
360             Debug.ERROR(exc);
361         }
362     },
363 
364     getLocalFile: function(options, callback)
365     {
366         var href = options.href;
367         var file = Url.getLocalOrSystemFile(href);
368         if (file)
369             return callback(file);
370 
371         if (this.checkHeaderRe.test(href))
372         {
373             if (FBTrace.DBG_EXTERNALEDITORS)
374                 FBTrace.sysout("externalEditors. connecting server for", href);
375 
376             var req = new XMLHttpRequest;
377             req.open("HEAD", href, true);
378             req.setRequestHeader("X-Line", options.line);
379             req.setRequestHeader("X-Column", options.col);
380             req.onloadend = function()
381             {
382                 var path = req.getResponseHeader("X-Local-File-Path");
383                 if (FBTrace.DBG_EXTERNALEDITORS)
384                     FBTrace.sysout("externalEditors. server says", path);
385 
386                 var file = fixupFilePath(path);
387                 if (file)
388                     callback(file);
389 
390                 // TODO: do we need to notifiy user if path was wrong?
391             }
392 
393             req.send(null);
394             return;
395         }
396 
397         file = this.transformHref(href);
398         if (file)
399             return callback(file);
400 
401         this.saveToTemporaryFile(href, callback);
402     },
403 
404     parseCmdLine: function(cmdLine, options)
405     {
406         cmdLine = cmdLine || "";
407 
408         var lastI = 0, args = [], argIndex = 0, inGroup;
409         var subs = "col|line|file|url".split("|");
410 
411         // do not send argument with bogus line number
412         function checkGroup()
413         {
414             var group = args.slice(argIndex), isValid = null;
415             for (var i=0; i<subs.length; i++)
416             {
417                 var sub = subs[i];
418                 if (group.indexOf("%" + sub) == -1)
419                     continue;
420 
421                 if (options[sub] == undefined)
422                 {
423                     isValid = false;
424                 }
425                 else
426                 {
427                     isValid = true;
428                     break;
429                 }
430             }
431 
432             if (isValid == false)
433                 args = args.slice(0, argIndex);
434 
435             argIndex = args.length;
436         }
437 
438         cmdLine.replace(/(\s+|$)|(?:%([{}]|(%|col|line|file|url)))/g, function(a, b, c, d, i, str)
439         {
440             var skipped = str.substring(lastI, i);
441             lastI = i + a.length;
442             skipped && args.push(skipped);
443 
444             if (b || !a)
445             {
446                 args.push(" ");
447                 if (!inGroup)
448                     checkGroup();
449             }
450             else if (c == "{")
451             {
452                 inGroup = true;
453             }
454             else if (c == "}")
455             {
456                 inGroup = false;
457                 checkGroup();
458             }
459             else if (d)
460             {
461                 args.push(a);
462             }
463         });
464 
465         cmdLine = args.join("");
466 
467         // add %file
468         if (!/%(url|file)/.test(cmdLine))
469             cmdLine += " %file";
470 
471         args = cmdLine.trim().split(" ");
472         args = args.map(function(x)
473         {
474             return x.replace(/(?:%(%|col|line|file|url))/g, function(a, b)
475             {
476                 if (b == "%")
477                     return b;
478                 if (options[b] == null)
479                     return "";
480                 return options[b];
481             });
482         })
483 
484         return args;
485     },
486 
487     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
488 
489     transformHref: function(href)
490     {
491         for (var i=0; i<this.pathTransformations.length; i++)
492         {
493             var transform = this.pathTransformations[i];
494             if (transform.regexp.test(href))
495             {
496                 var path = href.replace(transform.regexp, transform.filePath);
497                 var file = fixupFilePath(path);
498                 if (file && file.exists())
499                 {
500                     if (FBTrace.DBG_EXTERNALEDITORS)
501                         FBTrace.sysout("externalEditors. " + href + " transformed to", file.path);
502                     return file;
503                 }
504             }
505         }
506     },
507 
508     saveToTemporaryFile: function(href, callback)
509     {
510         var data = Firebug.currentContext.sourceCache.loadText(href);
511         var file = this.createTemporaryFile(href, data);
512 
513         callback(file);
514     },
515 
516     createTemporaryFile: function(href, data)
517     {
518         if (!data)
519             return;
520 
521         if (!temporaryDirectory)
522         {
523             var tmpDir = DirService.getFile(NS_OS_TEMP_DIR, {});
524             tmpDir.append("fbtmp");
525             tmpDir.createUnique(nsIFile.DIRECTORY_TYPE, 0775);
526             temporaryDirectory = tmpDir;
527         }
528 
529         var lpath = href.replace(/^[^:]+:\/*/g, "").replace(/\?.*$/g, "")
530             .replace(/[^0-9a-zA-Z\/.]/g, "_");
531 
532         /* dummy comment to workaround eclipse bug */
533         if (!/\.[\w]{1,5}$/.test(lpath))
534         {
535             if (lpath.charAt(lpath.length-1) == "/")
536                 lpath += "index";
537             lpath += ".html";
538         }
539 
540         if (System.getPlatformName() == "WINNT")
541             lpath = lpath.replace(/\//g, "\\");
542 
543         var file = Xpcom.QI(temporaryDirectory.clone(), nsIFile);
544         file.appendRelativePath(lpath);
545         if (!file.exists())
546             file.create(nsIFile.NORMAL_FILE_TYPE, 0664);
547         temporaryFiles.push(file.path);
548 
549         // TODO detect charset from current tab
550         data = Str.convertFromUnicode(data);
551 
552         var stream = Xpcom.CCIN("@mozilla.org/network/safe-file-output-stream;1",
553             "nsIFileOutputStream");
554         stream.init(file, 0x04 | 0x08 | 0x20, 0664, 0); // write, create, truncate
555         stream.write(data, data.length);
556 
557         if (stream instanceof nsISafeOutputStream)
558             stream.finish();
559         else
560             stream.close();
561 
562         return file;
563     },
564 
565     deleteTemporaryFiles: function()  // TODO call on "shutdown" event to modules
566     {
567         try
568         {
569             var file = Xpcom.CCIN("@mozilla.org/file/local;1", "nsIFile");
570             for (var i = 0; i < temporaryFiles.length; ++i)
571             {
572                 file.initWithPath(temporaryFiles[i]);
573                 if (file.exists())
574                     file.remove(false);
575             }
576         }
577         catch(exc)
578         {
579         }
580 
581         try
582         {
583             if (temporaryDirectory && temporaryDirectory.exists())
584                 temporaryDirectory.remove(true);
585         }
586         catch(exc)
587         {
588         }
589     },
590 });
591 
592 // ********************************************************************************************* //
593 // Helpers
594 
595 function fixupFilePath(path)
596 {
597     var file = Url.getLocalOrSystemFile(path);
598     if (!file)
599     {
600         path = "file:///" + path.replace(/[\/\\]+/g, "/");
601         file = Url.getLocalOrSystemFile(path);
602     }
603     return file;
604 }
605 
606 // object.extend doesn't handle getters
607 Firebug.ExternalEditors.__defineGetter__("pathTransformations",
608     lazyLoadUrlMappings.bind(Firebug.ExternalEditors, "pathTransformations"));
609 
610 Firebug.ExternalEditors.__defineGetter__("checkHeaderRe",
611     lazyLoadUrlMappings.bind(Firebug.ExternalEditors, "checkHeaderRe"));
612 
613 function lazyLoadUrlMappings(propName)
614 {
615     delete this.pathTransformations;
616     delete this.checkHeaderRe;
617 
618     var lines = readEntireFile(userFile("urlMappings.txt")).split(/[\n\r]+/);
619     var sp = "=>";
620 
621     function safeRegexp(source)
622     {
623         try
624         {
625             return RegExp(source, "i");
626         }
627         catch(e)
628         {
629         }
630     }
631 
632     this.pathTransformations = [];
633     this.checkHeaderRe = null;
634 
635     for (var i in lines)
636     {
637         var line = lines[i].split("=>");
638 
639         if (!line[1] || !line[0])
640             continue;
641 
642         var start = line[0].trim()
643         var end = line[1].trim();
644 
645         if (start[0] == "/" && start[1] == "/")
646             continue;
647 
648         if (start == "X-Local-File-Path")
649         {
650             this.checkHeaderRe = safeRegexp(end);
651             continue;
652         }
653         var t = {
654             regexp: safeRegexp(start, i),
655             filePath: end
656         }
657         if (t.regexp && t.filePath)
658             this.pathTransformations.push(t)
659     }
660 
661     if (!this.checkHeaderRe)
662         this.checkHeaderRe = /^https?:\/\/(localhost)(\/|:|$)/i;
663 
664     return this[propName];
665 }
666 
667 Firebug.ExternalEditors.saveUrlMappings = function()
668 {
669     var sp = " => ";
670     var text = [
671         "X-Local-File-Path", sp, this.checkHeaderRe.source, "\n\n"
672     ];
673 
674     for (var i = 0; i < this.pathTransformations.length; i++)
675     {
676         var t = this.pathTransformations[i];
677         text.push(t.regexp, sp, t.filePath, "\n");
678     }
679 
680     var file = userFile("urlMappings.txt");
681     writeToFile(file, text.join(""));
682 }
683 
684 // file helpers
685 function userFile(name)
686 {
687     var file = Services.dirsvc.get("ProfD", Ci.nsIFile);
688     file.append("firebug");
689     file.append(name);
690     return file
691 }
692 
693 function readEntireFile(file)
694 {
695     if (!file.exists())
696         return "";
697 
698     var data = "", str = {};
699     var fstream = Cc["@mozilla.org/network/file-input-stream;1"]
700         .createInstance(Ci.nsIFileInputStream);
701     var converter = Cc["@mozilla.org/intl/converter-input-stream;1"]
702         .createInstance(Ci.nsIConverterInputStream);
703 
704     const replacementChar = Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER;
705     fstream.init(file, -1, 0, 0);
706     converter.init(fstream, "UTF-8", 1024, replacementChar);
707 
708     while (converter.readString(4096, str) != 0)
709         data += str.value;
710 
711     converter.close();
712 
713     return data;
714 }
715 
716 function writeToFile(file, text)
717 {
718     var fostream = Cc["@mozilla.org/network/file-output-stream;1"]
719         .createInstance(Ci.nsIFileOutputStream);
720     var converter = Cc["@mozilla.org/intl/converter-output-stream;1"]
721         .createInstance(Ci.nsIConverterOutputStream);
722 
723     if (!file.exists())
724         file.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0664);
725 
726     fostream.init(file, 0x02 | 0x08 | 0x20, 0664, 0); // write, create, truncate
727     converter.init(fostream, "UTF-8", 4096, 0x0000);
728     converter.writeString(text);
729     converter.close();
730 }
731 
732 // ********************************************************************************************* //
733 // Registration
734 
735 Firebug.registerModule(Firebug.ExternalEditors);
736 
737 return Firebug.ExternalEditors;
738 
739 // ********************************************************************************************* //
740 });