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 });