PageRenderTime 197ms CodeModel.GetById 93ms app.highlight 92ms RepoModel.GetById 1ms app.codeStats 1ms

/data/js/bug-page-mod.js

https://bitbucket.org/ehsan/bugzilla-tweaks/
JavaScript | 1200 lines | 1053 code | 61 blank | 86 comment | 259 complexity | 6f0e217f50cb9b7c54f1ab9dace63df9 MD5 | raw file
   1/* ***** BEGIN LICENSE BLOCK *****
   2 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
   3 *
   4 * The contents of this file are subject to the Mozilla Public License Version
   5 * 1.1 (the "License"); you may not use this file except in compliance with
   6 * the License. You may obtain a copy of the License at
   7 * http://www.mozilla.org/MPL/
   8 *
   9 * Software distributed under the License is distributed on an "AS IS" basis,
  10 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
  11 * for the specific language governing rights and limitations under the
  12 * License.
  13 *
  14 * The Original Code is Bugzilla Tweaks.
  15 *
  16 * The Initial Developer of the Original Code is Mozilla Foundation.
  17 * Portions created by the Initial Developer are Copyright (C) 2010
  18 * the Initial Developer. All Rights Reserved.
  19 *
  20 * Contributor(s):
  21 *   Johnathan Nightingale <johnath@mozilla.com>
  22 *   Ehsan Akhgari <ehsan@mozilla.com>
  23 *
  24 * Alternatively, the contents of this file may be used under the terms of
  25 * either the GNU General Public License Version 2 or later (the "GPL"), or
  26 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
  27 * in which case the provisions of the GPL or the LGPL are applicable instead
  28 * of those above. If you wish to allow use of your version of this file only
  29 * under the terms of either the GPL or the LGPL, and not to allow others to
  30 * use your version of this file under the terms of the MPL, indicate your
  31 * decision by deleting the provisions above and replace them with the notice
  32 * and other provisions required by the GPL or the LGPL. If you do not delete
  33 * the provisions above, a recipient may use your version of this file under
  34 * the terms of any one of the MPL, the GPL or the LGPL.
  35 *
  36 * ***** END LICENSE BLOCK ***** */
  37
  38function tweakBugzilla(d) {
  39    // run on both bugzilla.m.o and bugzilla-stage-tip.m.o
  40    if (!onBugzillaPage(d.URL))
  41        return;
  42
  43    if (!d.getElementById("comments")) // don't process the mid-air collision pages
  44      return;
  45
  46    // Strip "Bug " from titles for better tab readability
  47    if (/^Bug /.test(d.title))
  48        d.title = d.title.slice(4);
  49
  50    // After POSTing, redirect with a GET back to the same bug
  51    if (/\/(process_bug|attachment|post_bug).cgi$/.test(d.location.href)) {
  52      var bug = getBugNumber(d);
  53      if (bug) {
  54        var url = d.location.href;
  55        url = url.replace("process_bug.cgi", "show_bug.cgi");
  56        url = url.replace("attachment.cgi", "show_bug.cgi");
  57        url = url.replace("post_bug.cgi", "show_bug.cgi");
  58        url += "?id=" + bug;
  59        d.defaultView.history.replaceState(null, "", url);
  60        d.title = bug + " - " + d.getElementById("short_desc_nonedit_display").textContent;
  61      }
  62    }
  63
  64    // Make the comment box bigger
  65    var commentBox = d.querySelector("#comment");
  66    if (commentBox)
  67        commentBox.rows=20;
  68
  69    // Always show obsolete plussed attachments
  70    var style = d.createElement("style");
  71    style.setAttribute("type", "text/css");
  72    style.appendChild(d.createTextNode(
  73        "tr.bz_tr_obsolete.bztw_plusflag { display: table-row !important; }"
  74    ));
  75    d.getElementsByTagName("head")[0].appendChild(style);
  76
  77    addNewLinks(d);
  78
  79    attachmentDiffLinkify(d);
  80
  81    viewAttachmentSource(d);
  82
  83    var userNameCache = {};
  84    function getUserName(email) {
  85        if (email in userNameCache) {
  86            return userNameCache[email];
  87        }
  88        var emailLink = d.querySelectorAll("a.email");
  89        for (var i = 0; i < emailLink.length; ++i) {
  90            if (emailLink[i].href == "mailto:" + email) {
  91                return userNameCache[email] = htmlEncode(trimContent(emailLink[i]));
  92            }
  93        }
  94        return email;
  95    }
  96
  97    // collect the flag names
  98    var flagNames = [], flags = {}, flagOccurrences = {};
  99    var flagRows = d.querySelectorAll("#flags tr");
 100    for (var i = 0; i < flagRows.length; ++i) {
 101        var item = flagRows[i].querySelectorAll("td");
 102        if (!item[1])
 103            continue;
 104        var name = trimContent(item[1]).replace('\u2011', '-', 'g');
 105        flagNames.push(name);
 106        flags[name] = item[1];
 107    }
 108    flagRows = d.querySelectorAll(".field_label[id^=field_label_cf_]");
 109    for (var i = 0; i < flagRows.length; ++i) {
 110        var name = trimContent(flagRows[i]).replace(/\:$/, '')
 111                                           .replace('\u2011', '-', 'g');
 112        flagNames.push(name);
 113        flags[name] = flagRows[i];
 114    }
 115    var flagCounter = 1;
 116    function findFlag(item) {
 117        function lookup(names) {
 118            names = names.split(", ");
 119            var results = [];
 120            for (var j = 0; j < names.length; ++j) {
 121                var name = names[j].replace('\u2011', '-', 'g');
 122                for (var i = 0; i < flagNames.length; ++i) {
 123                    var quotedFlagName = flagNames[i].replace('.', '\\.', 'g')
 124                                                     .replace('\u2011', '-', 'g');
 125                    if ((new RegExp('^' + quotedFlagName)).test(name)) {
 126                        results.push(flagNames[i]);
 127                        break;
 128                    }
 129                }
 130            }
 131            return results;
 132        }
 133        var base = item[4] ? 2 : 0;
 134        // handle normal flags
 135        if (trimContent(item[base]) == 'Flags') {
 136            var result = lookup(trimContent(item[base + 1])).
 137                         concat(lookup(trimContent(item[base + 2])));
 138            return result;
 139        }
 140        // handle special pseudo-flags
 141        return lookup(trimContent(item[base]));
 142    }
 143
 144    var DataStore = new DataStoreCtor(d);
 145
 146    var AttachmentFlagHandler = new AttachmentFlagHandlerCtor();
 147    AttachmentFlagHandler.determineInterestingFlags(d);
 148
 149    var CheckinComment = new CheckinCommentCtor();
 150    CheckinComment.initialize(d, AttachmentFlagHandler._interestingFlags);
 151
 152    tbplbotSpamCollapser(d);
 153
 154    // Mark up history inline
 155    var historyLink = d.querySelector("link[title='Bug Activity']");
 156    if (!historyLink)
 157        return;
 158    if (d.getElementById('inline-history-ext')) // bugzilla inline-history is active
 159      return;
 160    if (d.getElementById('login_link_top')) // not logged in
 161      return;
 162
 163    // Add our own style for inline hisotry
 164    var style = d.createElement("style");
 165    style.setAttribute("type", "text/css");
 166    style.appendChild(d.createTextNode(
 167        ".bztw_history { border: none; font-weight: normal; }" +
 168        ".bztw_inlinehistory { font-weight: normal; width: 56em; }" +
 169        ".bztw_history .old, .bztw_inlinehistory .old { text-decoration: line-through; }" +
 170        ".bztw_history .sep:before { content: \" \"; }" +
 171        ".bztw_unconfirmed { font-style: italic; }" +
 172        '.bztw_historyitem + .bztw_historyitem:before { content: "; "; }'
 173    ));
 174    d.getElementsByTagName("head")[0].appendChild(style);
 175    style = d.createElement("style");
 176    style.setAttribute("type", "text/css");
 177    style.id = "bztw_cc";
 178    style.appendChild(d.createTextNode(
 179        ".bztw_cc { display: none; }" +
 180        '.bztw_historyitem.bztw_cc + .bztw_historyitem:before { content: ""; }' +
 181        '.bztw_historyitem:not([class~="bztw_cc"]) ~ .bztw_historyitem.bztw_cc + .bztw_historyitem:before { content: "; "; }'
 182    ));
 183    d.getElementsByTagName("head")[0].appendChild(style);
 184
 185    var iframe = d.createElement('iframe');
 186    iframe.src = historyLink.href;
 187    iframe.style.display = "none";
 188    iframe.addEventListener("load", function() {
 189        preprocessDuplicateMarkers(d, iframe.contentDocument);
 190
 191        var historyItems = iframe.contentDocument.querySelectorAll('#bugzilla-body tr');
 192        var commentTimes = d.querySelectorAll('.bz_comment_time');
 193
 194        // Sometimes the history will stack several changes together,
 195        // and we'll want to append the data from the Nth item to the
 196        // div created in N-1
 197        var i=0, j=0, flagsFound;
 198        for (; i < historyItems.length; i++) {
 199            var item = historyItems[i].querySelectorAll("td");
 200            if (!item[1])
 201                continue;
 202
 203            var reachedEnd = false;
 204            for (; j < commentTimes.length; j++) {
 205                if (trimContent(item[1]) > trimContent(commentTimes[j])) {
 206                    if (j < commentTimes.length - 1) {
 207                        continue;
 208                    } else {
 209                        reachedEnd = true;
 210                    }
 211                }
 212
 213                var commentHead = commentTimes[j].parentNode;
 214
 215                var mainUser = commentHead.querySelector(".bz_comment_user a.email")
 216                                          .href
 217                                          .substr(7);
 218                var user = trimContent(item[0]);
 219                var mainTime = trimContent(commentTimes[j]);
 220                var time = trimContent(item[1]);
 221                var inline = (mainUser == user && time == mainTime);
 222
 223                var currentDiv = d.createElement("div");
 224                var userPrefix = '';
 225                if (inline) {
 226                    // assume that the change was made by the same user
 227                    commentHead.appendChild(currentDiv);
 228                    currentDiv.setAttribute("class", "bztw_inlinehistory");
 229                } else {
 230                    // the change was made by another user
 231                    if (!reachedEnd) {
 232                        var parentDiv = commentHead.parentNode;
 233                        if (parentDiv.previousElementSibling &&
 234                            parentDiv.previousElementSibling.className.indexOf("bztw_history") >= 0) {
 235                            currentDiv = parentDiv.previousElementSibling;
 236                        } else {
 237                            parentDiv.parentNode.insertBefore(currentDiv, parentDiv);
 238                        }
 239                    } else {
 240                        var parentDiv = commentHead.parentNode;
 241                        if (parentDiv.nextElementSibling &&
 242                            parentDiv.nextElementSibling.className.indexOf("bztw_history") >= 0) {
 243                            currentDiv = parentDiv.nextElementSibling;
 244                        } else {
 245                            parentDiv.parentNode.appendChild(currentDiv);
 246                        }
 247                    }
 248                    currentDiv.setAttribute("class", "bz_comment bztw_history");
 249                    userPrefix += "<a class=\"email\" href=\"mailto:" +
 250                                  htmlEncode(trimContent(item[0])) + "\" title=\"" +
 251                                  htmlEncode(trimContent(item[1])) +"\">" +
 252                                  getUserName(trimContent(item[0])) + "</a>: ";
 253                }
 254                // check to see if this is a flag setting
 255                flagsFound = findFlag(item);
 256                for (var idx = 0; idx < flagsFound.length; ++idx) {
 257                    var flag = flagsFound[idx];
 258                    flagOccurrences[flag] = 'flag' + flagCounter;
 259                    if (inline) {
 260                        var anchor = d.createElement("a");
 261                        anchor.setAttribute("name", "flag" + flagCounter);
 262                        commentHead.insertBefore(anchor, commentHead.firstChild);
 263                    } else {
 264                        userPrefix += '<a name="flag' + flagCounter + '"></a>';
 265                    }
 266                    ++flagCounter;
 267                }
 268
 269                var attachmentFlagAnchors = AttachmentFlagHandler.handleItem(user, item);
 270                if (inline) {
 271                    for (var idx = 0; idx < attachmentFlagAnchors.length; ++idx) {
 272                        var anchor = d.createElement("a");
 273                        anchor.setAttribute("name", attachmentFlagAnchors[idx]);
 274                        commentHead.insertBefore(anchor, commentHead.firstChild);
 275                    }
 276                } else {
 277                    userPrefix += attachmentFlagAnchors.map(function(name) '<a name="' + name + '"></a>').join("");
 278                }
 279
 280                var ccOnly = (trimContent(item[2]) == 'CC');
 281                var ccPrefix = ccOnly ? '<span class="bztw_cc bztw_historyitem">' :
 282                                        '<span class="bztw_historyitem">',
 283                    ccSuffix = '</span>';
 284                var html = userPrefix +
 285                           ccPrefix +
 286                           transformType(trimContent(item[2]), d, trimContent(item[3]),
 287                                         trimContent(item[4])) + ": " +
 288                           formatTransition(trimContent(item[3]), trimContent(item[4]),
 289                                            trimContent(item[2]), d, iframe.contentDocument);
 290
 291                var nextItemsCount = item[0].rowSpan;
 292                for (var k = 1; k < nextItemsCount; ++k) {
 293                    ccOnly = false;
 294                    item = historyItems[++i].querySelectorAll("td")
 295                    ccPrefix = (trimContent(item[0]) == 'CC') ?
 296                        '<span class="bztw_cc bztw_historyitem">' : '<span class="bztw_historyitem">';
 297                    // avoid showing a trailing semicolon if the previous entry wasn't a CC and this one is
 298                    var prefix = ccSuffix + ccPrefix;
 299                    // check to see if this is a flag setting
 300                    flagsFound = findFlag(item);
 301                    for (var idx = 0; idx < flagsFound.length; ++idx) {
 302                        var flag = flagsFound[idx];
 303                        flagOccurrences[flag] = 'flag' + flagCounter;
 304                        if (inline) {
 305                            var anchor = d.createElement("a");
 306                            anchor.setAttribute("name", "flag" + flagCounter);
 307                            commentHead.insertBefore(anchor, commentHead.firstChild);
 308                        } else {
 309                            prefix += '<a name="flag' + flagCounter + '"></a>';
 310                        }
 311                        ++flagCounter;
 312                    }
 313
 314                    var attachmentFlagAnchors = AttachmentFlagHandler.handleItem(user, item);
 315                    if (inline) {
 316                        for (var idx = 0; idx < attachmentFlagAnchors.length; ++idx) {
 317                            var anchor = d.createElement("a");
 318                            anchor.setAttribute("name", attachmentFlagAnchors[idx]);
 319                            commentHead.insertBefore(anchor, commentHead.firstChild);
 320                        }
 321                    } else {
 322                        prefix += attachmentFlagAnchors.map(function(name) '<a name="' + name + '"></a>').join("");
 323                    }
 324
 325                    html += prefix +
 326                            transformType(trimContent(item[0]), d, trimContent(item[1]),
 327                                          trimContent(item[2])) + ": " +
 328                            formatTransition(trimContent(item[1]), trimContent(item[2]),
 329                                             trimContent(item[0]), d, iframe.contentDocument);
 330                }
 331                html += ccSuffix;
 332                if (ccOnly) {
 333                    html = '<div class="bztw_cc">' + html + '</div>';
 334                } else {
 335                    html = '<div>' + html + '</div>';
 336                }
 337                currentDiv.innerHTML += html;
 338                break;
 339            }
 340        }
 341
 342        handleEmptyCollapsedBoxes(d);
 343
 344        // Set the latest flag links if necessary
 345        for (var flagName in flagOccurrences) {
 346            flags[flagName].innerHTML = '<a href="#' + flagOccurrences[flagName] + '">'
 347                + flags[flagName].innerHTML + '</a>';
 348        }
 349
 350        AttachmentFlagHandler.setupLinks(d);
 351    },true);
 352    d.body.appendChild(iframe);
 353}
 354
 355var TransformValues = {
 356    linkifyURLs: function (str) {
 357        return str.replace(/((https?|ftp)\:\/\/[\S]+)/g, '<a href="$1">$1</a>');
 358    },
 359    linkifyBugAndCommentNumbers: function (str) {
 360        return str.replace(/(bug )(\d+) (comment )(\d+)/gi, '<a href="show_bug.cgi?id=$2#c$4">$1\n$2 $3\n$4</a>');
 361    },
 362    linkifyCommentNumbers: function (str) {
 363        return str.replace(/(comment (\d+))/gi, '<a href="#c$2">$1</a>');
 364    },
 365    linkifyBugNumbers: function (str) {
 366        return str.replace(/(bug (\d+))/gi, '<a href="show_bug.cgi?id=$2">$1</a>');
 367    },
 368    linkifyDependencies: function (str, type, doc, histDoc) {
 369        switch (type) {
 370        case "Blocks":
 371        case "Depends on":
 372        case "Duplicate":
 373            str = str.replace(/\d+/g, function(str) {
 374                var link = histDoc.querySelector("a[href='show_bug.cgi?id=" + str + "']");
 375                if (link) {
 376                    var class_ = '';
 377                    if (/bz_closed/i.test(link.className)) {
 378                        class_ += 'bz_closed ';
 379                    } else if (/bztw_unconfirmed/i.test(link.className)) {
 380                        class_ += 'bztw_unconfirmed ';
 381                    }
 382                    var parent = link.parentNode;
 383                    if (parent) {
 384                        if (parent.tagName.toLowerCase() == "i") {
 385                            class_ += 'bztw_unconfirmed ';
 386                        }
 387                        if (/bz_closed/i.test(parent.className)) {
 388                            class_ += 'bz_closed ';
 389                        }
 390                    }
 391                    str = applyClass(class_,
 392                                     '<a title="' + htmlEncode(link.title) + '" href="show_bug.cgi?id=' + htmlEncode(str) + '"' +
 393                                     (link.hasAttribute("name") ? (' name="' + htmlEncode(link.getAttribute("name")) + '"') : '') +
 394                                     '>' + htmlEncode(str) + '</a>');
 395                }
 396                return str;
 397            });
 398        }
 399        return str;
 400    }
 401};
 402
 403function transform(str, type, doc, histDoc) {
 404    for (var funcname in TransformValues) {
 405        var func = TransformValues[funcname];
 406        str = func.call(null, str, type, doc, histDoc);
 407    }
 408    return str
 409}
 410
 411var TransformTypes = {
 412    linkifyAttachments: function (str, doc) {
 413        return str.replace(/(Attachment #(\d+))/g, function (str, x, id) {
 414            var link = doc.querySelector("a[href='attachment.cgi?id=" + id + "']");
 415            if (link) {
 416                var class_ = '';
 417                if (/bz_obsolete/i.test(link.className)) {
 418                    class_ += 'bz_obsolete ';
 419                }
 420                var parent = link.parentNode;
 421                if (parent && /bz_obsolete/i.test(parent.className)) {
 422                    class_ += 'bz_obsolete ';
 423                }
 424                if (link.querySelector(".bz_obsolete")) {
 425                    class_ += 'bz_obsolete ';
 426                }
 427                str = applyClass(class_,
 428                                 '<a title="' + htmlEncode(trimContent(link)) + '" href="attachment.cgi?id=' +
 429                                 htmlEncode(id) + '&action=edit">' + htmlEncode(str) + '</a>');
 430            }
 431            return str;
 432        });
 433    },
 434    changeDependencyLinkTitles: function (str, doc, old, new_) {
 435        switch (str) {
 436        case "Blocks":
 437        case "Depends on":
 438            if (old.length && !new_.length) { // if the dependency was removed
 439                str = "No longer " + str[0].toLowerCase() + str.substr(1);
 440            }
 441            break;
 442        }
 443        return str;
 444    }
 445};
 446
 447function transformType(str, doc, old, new_) {
 448    for (var funcname in TransformTypes) {
 449        var func = TransformTypes[funcname];
 450        str = func.call(null, str, doc, old, new_);
 451    }
 452    return str;
 453}
 454
 455// new is a keyword, which makes this function uglier than I'd like
 456function formatTransition(old, new_, type, doc, histDoc) {
 457    if (old.length) {
 458        old = transform(htmlEncode(old), type, doc, histDoc);
 459        var setOldStyle = true;
 460        switch (type) {
 461        case "Blocks":
 462        case "Depends on":
 463            setOldStyle = false;
 464            break;
 465        }
 466        if (setOldStyle) {
 467            old = '<span class="old">' + old + '</span>';
 468        }
 469    }
 470    if (new_.length) {
 471        new_ = '<span class="new">' + transform(htmlEncode(new_), type, doc, histDoc) + '</span>';
 472    }
 473    var mid = '';
 474    if (old.length && new_.length) {
 475        mid = ' <span style="font-size: 150%;">&rArr;</span> ';
 476    }
 477    return old + mid + new_;
 478}
 479
 480function trimContent(el) {
 481    return el.textContent.trim();
 482}
 483
 484function AttachmentFlag(flag) {
 485    for (var name in flag)
 486        this[name] = flag[name];
 487}
 488AttachmentFlag.prototype = {
 489    equals: function(flag) {
 490        if (this.type != flag.type ||
 491            this.name != flag.name ||
 492            this.setter != flag.setter ||
 493            ("requestee" in this && !("requestee" in flag)) ||
 494            ("requestee" in flag && !("requestee" in this)))
 495            return false;
 496        return this.requestee == flag.requestee;
 497    }
 498};
 499
 500var reAttachmentDiff = /attachment\.cgi\?id=(\d+)&action=diff$/i;
 501var reviewBoardUrlBase = "http://reviews.visophyte.org/";
 502
 503/**
 504 * Whenever we find a patch with a diff, insert an additional link to asuth's
 505 * review board magic.
 506 */
 507function attachmentDiffLinkify(doc) {
 508  var bug_id = getBugNumber(doc);
 509
 510  var table = doc.getElementById("attachment_table");
 511  if (!table)
 512    return;
 513  var rows = table.querySelectorAll("tr");
 514  for (var i = 0; i < rows.length; ++i) {
 515    var item = rows[i].querySelectorAll("td");
 516    if (item.length != 3)
 517      continue;
 518    // get the ID of the attachment
 519    var links = item[2].querySelectorAll("a");
 520    if (links.length != 2)
 521      continue;
 522    var match = reAttachmentDiff.exec(links[1].href);
 523    if (match) {
 524      var attach_id = match[1];
 525      var parentNode = links[1].parentNode;
 526      parentNode.appendChild(doc.createTextNode(" | "));
 527      var linkNode = doc.createElement("a");
 528      linkNode.href = reviewBoardUrlBase + "r/bzpatch/bug" + bug_id + "/attach" + attach_id + "/";
 529      linkNode.textContent = "Review";
 530      parentNode.appendChild(linkNode);
 531    }
 532  }
 533}
 534
 535var reAttachmentType = /,\s+([^ )]*)[;)]/;
 536
 537function viewAttachmentSource(doc) {
 538  function addLink(elem, title, href) {
 539    if (elem.textContent.match(/[\S]/)) {
 540      elem.appendChild(doc.createTextNode(" | "));
 541    }
 542    var link = doc.createElement("a");
 543    link.href = href;
 544    link.textContent = title;
 545    elem.appendChild(link);
 546  }
 547  var table = doc.getElementById("attachment_table");
 548  if (!table)
 549    return;
 550  var rows = table.querySelectorAll("tr");
 551  for (var i = 0; i < rows.length; ++i) {
 552    var items = rows[i].querySelectorAll("td");
 553    if (items.length != 3)
 554      continue;
 555    var links = items[0].querySelectorAll("a");
 556    if (links.length == 0)
 557      continue;
 558    var attachHref = links[0].href;
 559    // get the type of the attachment
 560    var span = items[0].querySelector(".bz_attach_extra_info");
 561    if (!span)
 562      continue;
 563    var typeName = null;
 564    try {
 565      // Match mime type followed by ";" (charset) or ")" (no charset)
 566      typeName = span.textContent.match(reAttachmentType)[1];
 567      typeName = typeName.split(";")[0]; // ignore charset following type
 568    } catch (e) {}
 569    if (typeName == "application/java-archive" ||
 570        typeName == "application/x-jar") {
 571      // Due to the fix for bug 369814, only zip files with this special
 572      // mime type can be used with the jar: protocol.
 573      // http://hg.mozilla.org/mozilla-central/rev/be54f6bb9e1e
 574      addLink(items[2], "JAR Contents", "jar:" + attachHref + "!/");
 575    // https://bugzilla.mozilla.org/show_bug.cgi?id=369814#c5 has more possible mime types for zips?
 576    } else if (typeName == "application/zip" ||
 577               typeName == "application/x-zip-compressed" ||
 578               typeName == "application/x-xpinstall") {
 579      addLink(items[2], "Static ZIP Contents", "jar:" + attachHref + "!/");
 580    } else if (typeName != "text/plain" &&
 581               typeName != "patch" &&
 582               // Other types that Gecko displays like text/plain
 583               // http://mxr.mozilla.org/mozilla-central/source/parser/htmlparser/public/nsIParser.h
 584               typeName != "text/css" &&
 585               typeName != "text/javascript" &&
 586               typeName != "text/ecmascript" &&
 587               typeName != "application/javascript" &&
 588               typeName != "application/ecmascript" &&
 589               typeName != "application/x-javascript" &&
 590               // Binary image types for which the "source" is not useful
 591               typeName != "image/gif" &&
 592               typeName != "image/png" &&
 593               typeName != "image/jpeg") {
 594      addLink(items[2], "Source", "view-source:" + attachHref);
 595    }
 596  }
 597}
 598
 599function AttachmentFlagHandlerCtor() {
 600    this._db = {};
 601    this._interestingFlags = {};
 602}
 603AttachmentFlagHandlerCtor.prototype = {
 604    determineInterestingFlags: function (doc) {
 605        var table = doc.getElementById("attachment_table");
 606        if (!table)
 607            return;
 608        var rows = table.querySelectorAll("tr");
 609        for (var i = 0; i < rows.length; ++i) {
 610            var item = rows[i].querySelectorAll("td");
 611            if (item.length != 3 ||
 612                item[1].className.indexOf("bz_attach_flags") < 0 ||
 613                trimContent(item[1]) == "no flags")
 614                continue;
 615            // get the ID of the attachment
 616            var link = item[0].querySelector("a");
 617            if (!link)
 618                continue;
 619            var match = this._reAttachmentHref.exec(link.href);
 620            if (match) {
 621                var attachmentID = match[1];
 622                if (!(attachmentID in this._interestingFlags)) {
 623                    this._interestingFlags[attachmentID] = [];
 624                }
 625                var text = "";
 626                var previousText = "";
 627                var previousEl = null;
 628                for (var el = item[1].firstChild; el.nextSibling; el = el.nextSibling) {
 629                    var thisText = trimContent(el).replace('\u2011', '-', 'g');
 630                    text += thisText;
 631                    if (this._reParsePartToLinkify.test(thisText)) {
 632                        previousText = thisText;
 633                        previousEl = el;
 634                    }
 635                    if (el.nodeType != el.ELEMENT_NODE ||
 636                        el.localName.toLowerCase() != "br")
 637                        continue;
 638                    match = this._reParseInterestingFlag.exec(text);
 639                    if (match) {
 640                        var flag = {};
 641                        flag.setter = match[1];
 642                        flag.name = match[2];
 643                        if (match[4] == "+" || match[4] == "-") {
 644                            flag.type = match[4];
 645                        } else {
 646                            flag.type = "?";
 647                            if (match[7]) {
 648                                flag.requestee = match[7];
 649                            }
 650                        }
 651
 652                        // always show the obsolete attachments with a + flag
 653                        if (flag.type == "+") {
 654                            var parent = link.parentNode;
 655                            while (parent) {
 656                                if (parent.tagName.toLowerCase() == "tr") {
 657                                    if (/bz_tr_obsolete/i.test(parent.className)) {
 658                                        parent.className += " bztw_plusflag";
 659                                    }
 660                                    break;
 661                                }
 662                                parent = parent.parentNode;
 663                            }
 664                        }
 665
 666                        // try to put the flag name and type part in a span which we will
 667                        // use in setupLinks to inject links into.
 668                        match = this._reLinkifyInterestingFlag.exec(previousText);
 669                        if (match) {
 670                            previousEl.textContent = match[1];
 671                            if (match[3]) {
 672                                var textNode = doc.createTextNode(match[3]);
 673                                previousEl.parentNode.insertBefore(textNode, previousEl.nextSibling);
 674                            }
 675                            var span = doc.createElement("span");
 676                            span.textContent = match[2];
 677                            previousEl.parentNode.insertBefore(span, previousEl.nextSibling);
 678
 679                            flag.placeholder = span;
 680                        }
 681
 682                        this._interestingFlags[attachmentID].push(new AttachmentFlag(flag));
 683                    }
 684                    text = "";
 685                    previousText = "";
 686                    previousEl = null;
 687                }
 688            }
 689        }
 690    },
 691    handleItem: function (name, item) {
 692        var anchorsCreated = [];
 693        var base = item[4] ? 2 : 0;
 694        var what = trimContent(item[base]);
 695        var match = this._reAttachmentFlagName.exec(what);
 696        if (match) {
 697            var id = match[1];
 698            if (!(id in this._db)) {
 699                this._db[id] = [];
 700            }
 701            name = name.split('@')[0]; // convert the name to the fraction before the @
 702            var added = this._parseData(name, trimContent(item[base + 2]));
 703            for (var i = 0; i < added.length; ++i) {
 704                var flag = added[i];
 705                if (!(id in this._interestingFlags))
 706                    continue;
 707                for (var j = 0; j < this._interestingFlags[id].length; ++j) {
 708                    // Take care to not assign an anchor to a flag which already has one
 709                    if (flag.equals(this._interestingFlags[id][j]) &&
 710                        !("anchor" in this._interestingFlags[id][j])) {
 711                        // found an interesting flag
 712                        this._interestingFlags[id][j].anchor = this.anchorName;
 713                        anchorsCreated.push(this.anchorName);
 714                        this._counter++;
 715                        break;
 716                    }
 717                }
 718            }
 719        }
 720        return anchorsCreated;
 721    },
 722    setupLinks: function (doc) {
 723        for (var id in this._interestingFlags) {
 724            for (var i = 0; i < this._interestingFlags[id].length; ++i) {
 725                var flag = this._interestingFlags[id][i];
 726                if ("placeholder" in flag &&
 727                    "anchor" in flag) {
 728                    var link = doc.createElement("a");
 729                    link.href = "#" + flag.anchor;
 730                    link.textContent = flag.placeholder.textContent;
 731                    flag.placeholder.replaceChild(link, flag.placeholder.firstChild);
 732                }
 733            }
 734        }
 735    },
 736    _parseData: function (name, str) {
 737        var items = str.replace('\u2011', '-', 'g').split(', '), flags = [];
 738        for (var i = 0; i < items.length; ++i) {
 739            if (!items[i].length)
 740                continue;
 741
 742            var match = this._reParseRequest.exec(items[i]);
 743            if (match) {
 744                var flag = {};
 745                flag.name = match[1];
 746                flag.setter = name;
 747                if (match[4]) {
 748                    flag.requestee = match[4];
 749                }
 750                flag.type = match[2];
 751                flags.push(new AttachmentFlag(flag));
 752            }
 753        }
 754        return flags;
 755    },
 756    _counter: 1,
 757    get anchorName() {
 758        return "attachflag" + this._counter;
 759    },
 760    _reParseRequest: /^(.+)([\?\-\+])(\((.+)@.+\))?$/,
 761    _reParsePartToLinkify: /^\s*:\s+.+[\-\+\?](\s*\()?\s*$/,
 762    _reParseInterestingFlag: /^(.+):\s+(.+)(([\-\+])|\?(\s+(\((.+)\)))?)$/,
 763    _reLinkifyInterestingFlag: /^(\s*:\s+)(.+[\-\+\?])(\s*\(\s*)?$/,
 764    _reAttachmentHref: /attachment\.cgi\?id=(\d+)$/i,
 765    _reAttachmentFlagName: /^Attachment\s+#(\d+)\s+Flags$/i
 766};
 767
 768function CheckinCommentCtor() {
 769    this.bugNumber = null;
 770    this.summarySpan = null;
 771    this.checkinFlags = "";
 772}
 773CheckinCommentCtor.prototype = {
 774  initialize: function(doc, flags) {
 775    this.bugNumber = getBugNumber(doc);
 776    var summarySpan = doc.getElementById("short_desc_nonedit_display");
 777    if (summarySpan) {
 778        this.summary = summarySpan.textContent;
 779    }
 780    var checkinFlagsMap = {};
 781    for (var id in flags) {
 782        for (var i = 0; i < flags[id].length; ++i) {
 783            var flag = flags[id][i];
 784            if (flag.type == "+") {
 785                var name = flag.name;
 786                if (name == "review") {
 787                    name = "r";
 788                } else if (name == "superreview") {
 789                    name = "sr";
 790                } else if (name == "ui-review") {
 791                    name = "ui-r";
 792                } else if (name == "feedback") {
 793                    name = "f";
 794                }
 795                if (!(name in checkinFlagsMap)) {
 796                    checkinFlagsMap[name] = {};
 797                }
 798                checkinFlagsMap[name][flag.setter]++;
 799            }
 800        }
 801    }
 802    var flagsOrdered = [];
 803    for (var name in checkinFlagsMap) {
 804        flagsOrdered.push(name);
 805    }
 806    flagsOrdered.sort(function (a, b) {
 807        function convertToNumber(x) {
 808            switch (x) {
 809            case "f":
 810                return -4;
 811            case "r":
 812                return -3;
 813            case "sr":
 814                return -2;
 815            case "ui-r":
 816                return -1;
 817            default:
 818                return 0;
 819            }
 820        }
 821        var an = convertToNumber(a);
 822        var bn = convertToNumber(b);
 823        if (an == 0 && bn == 0) {
 824            return a < b ? -1 : (a = b ? 0 : 1);
 825        } else {
 826            return an - bn;
 827        }
 828    });
 829    var checkinFlags = [];
 830    for (var i = 0; i < flagsOrdered.length; ++i) {
 831        var name = flagsOrdered[i];
 832        var flag = name + "=";
 833        var setters = [];
 834        for (var setter in checkinFlagsMap[name]) {
 835            setters.push(setter);
 836        }
 837        flag += setters.join(",");
 838        checkinFlags.push(flag);
 839    }
 840    this.checkinFlags = checkinFlags.join(" ");
 841    if (this.isValid()) {
 842      var div = doc.createElement("div");
 843      div.setAttribute("style", "display: none;");
 844      div.id = "__bz_tw_checkin_comment";
 845      div.appendChild(doc.createTextNode(this.toString()));
 846      doc.body.appendChild(div);
 847    }
 848  },
 849  isValid: function() {
 850      return this.bugNumber != null &&
 851             this.summary != null;
 852  },
 853  toString: function() {
 854    if (!this.isValid()) {
 855        return "";
 856    }
 857    var message = "Bug " + this.bugNumber + " - " + this.summary;
 858    if (this.checkinFlags.length) {
 859        message += "; " + this.checkinFlags;
 860    }
 861    return message;
 862  }
 863};
 864
 865function DataStoreCtor(doc) {
 866  this.storage = doc.defaultView.localStorage;
 867  this.data = {};
 868  this.bugNumber = null;
 869  function visualizeStoredData() {
 870    var data = "";
 871    for (var i = 0; i < window.localStorage.length; ++i) {
 872      var key = window.localStorage.key(i);
 873      data += key + ": " + JSON.parse(window.localStorage.getItem(key).toString()).toSource() + "\n";
 874    }
 875    open("data:text/html,<pre>" + escape(htmlEncode(data)) + "</pre>");
 876  }
 877  function clearStoredData() {
 878    var count = window.localStorage.length;
 879    if (count > 0) {
 880      if (window.confirm("You currently have data stored for " + count + " bugs.\n\n" +
 881                         "Are you sure you want to clear this data?  This action cannot be undone.")) {
 882        window.localStorage.clear();
 883      }
 884    } else {
 885      alert("You don't have any data stored about your bugs");
 886    }
 887  }
 888  var script = doc.createElement("script");
 889  script.appendChild(doc.createTextNode(visualizeStoredData.toSource() +
 890                                        clearStoredData.toSource() +
 891                                        htmlEncode.toSource()));
 892  doc.body.appendChild(script);
 893  this.initialize(doc);
 894}
 895
 896DataStoreCtor.prototype = {
 897  initialize: function(doc) {
 898    this.bugNumber = getBugNumber(doc);
 899    var data = this._ensureEntry(this.bugNumber, this.data);
 900    // last visited date
 901    data.visitedTime = (new Date()).getTime();
 902    // last comment count
 903    data.commentCount = doc.querySelectorAll(".bz_comment").length;
 904    // last status of bug flags
 905    var flags = this._ensureEntry("flags", data);
 906    var flagRows = doc.querySelectorAll("#flags tr");
 907    for (var i = 0; i < flagRows.length; ++i) {
 908      var flagCols = flagRows[i].querySelectorAll("td");
 909      if (flagCols.length != 3) {
 910        continue;
 911      }
 912      var flagName = trimContent(flagCols[1]);
 913      var flagValue = flagCols[2].querySelector("select");
 914      if (flagValue) {
 915        flagValue = flagValue.value;
 916      } else {
 917        continue;
 918      }
 919      flags[flagName] = flagValue;
 920    }
 921    flagRows = doc.querySelectorAll(".field_label[id^=field_label_cf_]");
 922    for (var i = 0; i < flagRows.length; ++i) {
 923      var flagName = trimContent(flagRows[i]).replace(/:$/, "");
 924      var flagValue = flagRows[i].parentNode.querySelector("select");
 925      if (flagValue) {
 926        flagValue = flagValue.value;
 927      } else {
 928        continue;
 929      }
 930      flags[flagName] = flagValue;
 931    }
 932    // last attachments
 933    var attachmentTable = doc.getElementById("attachment_table");
 934    var attachmentRows = attachmentTable.querySelectorAll("tr");
 935    for (var i = 0; i < attachmentRows.length; ++i) {
 936      var attachmentCells = attachmentRows[i].querySelectorAll("td");
 937      if (attachmentCells.length != 3) {
 938        continue;
 939      }
 940      var link = attachmentCells[0].querySelector("a");
 941      var match = this._reAttachmentHref.exec(link.href);
 942      if (match) {
 943        var attachmentID = match[1];
 944        var attachment = this._ensureEntry("attachments", data);
 945        var attachmentFlags = this._ensureArray(attachmentID, attachment);
 946        for (var el = attachmentCells[1].firstChild; el.nextSibling; el = el.nextSibling) {
 947          if (el.nodeType != el.TEXT_NODE) {
 948            continue;
 949          }
 950          var text = trimContent(el);
 951          if (!text) {
 952            continue;
 953          }
 954          match = this._reParseInterestingFlag.exec(text);
 955          if (match) {
 956            var flag = {};
 957            flag.setter = match[1];
 958            flag.name = match[2];
 959            if (match[4] == "+" || match[4] == "-") {
 960                flag.type = match[4];
 961            } else {
 962                flag.type = "?";
 963                if (match[7]) {
 964                    flag.requestee = match[7];
 965                }
 966            }
 967            attachmentFlags.push(flag);
 968          }
 969        }
 970      }
 971    }
 972    // Write data to storage
 973    for (var key in this.data) {
 974      this._ensure(key, this.storage, JSON.stringify(this.data[key]));
 975    }
 976  },
 977  _ensure: function(entry, obj, val) {
 978    if (obj.toString().indexOf("[object Storage") >= 0) {
 979      obj.setItem(entry, val);
 980    } else {
 981      if (typeof obj[entry] == "undefined")
 982        obj[entry]  = val;
 983      return obj[entry];
 984    }
 985  },
 986  _ensureEntry: function(entry, obj) {
 987    return this._ensure(entry, obj, {});
 988  },
 989  _ensureArray: function(entry, obj) {
 990    return this._ensure(entry, obj, []);
 991  },
 992  _reParseInterestingFlag: /^(.+):\s+(.+)(([\-\+])|\?(\s+(\((.+)\)))?)$/,
 993  _reAttachmentHref: /attachment\.cgi\?id=(\d+)$/i
 994};
 995
 996function getBugNumber(doc) {
 997  var idField = doc.querySelector("form[name=changeform] input[name=id]");
 998  if (idField) {
 999    return idField.value;
1000  }
1001  return null;
1002}
1003
1004function getUserName(doc) {
1005  var links = doc.querySelectorAll("#header .links li");
1006  var last = links[links.length - 1];
1007  if (last.innerHTML.indexOf("logout") >= 0) {
1008    return trimContent(last.lastChild);
1009  }
1010  return null;
1011}
1012
1013function preprocessDuplicateMarkers(mainDoc, histDoc) {
1014    var comments = mainDoc.querySelectorAll(".bz_comment");
1015    var reDuplicate = /^\s*\*\*\*\s+Bug\s+(\d+)\s+has\s+been\s+marked\s+as\s+a\s+duplicate\s+of\s+this\s+bug.\s+\*\*\*\s*$/i;
1016    var row = 0;
1017    var rows = histDoc.querySelectorAll("#bugzilla-body tr");
1018    for (var i = 1 /* comment 0 can never be a duplicate marker */;
1019         i < comments.length; ++i) {
1020        var textHolder = comments[i].querySelector(".bz_comment_text");
1021        var match = reDuplicate.exec(trimContent(textHolder));
1022        if (match) {
1023            // construct the table row to be injected in histDoc
1024            var bugID = match[1];
1025            var email = comments[i].querySelector(".bz_comment_user .email")
1026                                   .href
1027                                   .substr(7);
1028            var link = textHolder.querySelector("a");
1029            var title = link.title;
1030            var time = trimContent(comments[i].querySelector(".bz_comment_time"));
1031            var what = 'Duplicate';
1032            var removed = '';
1033            var number = trimContent(comments[i].querySelector(".bz_comment_number")).
1034                         replace(/[^\d]+/g, '');
1035            var class_ = '';
1036            if (/bz_closed/i.test(link.className + " " + link.parentNode.className)) {
1037                class_ += 'bz_closed ';
1038            }
1039            if (link.parentNode.tagName.toLowerCase() == 'i') {
1040                class_ += 'bztw_unconfirmed ';
1041            }
1042            var added = '<a href="show_bug.cgi?id=' + bugID + '" title="' +
1043                        htmlEncode(title) + '" name="c' + number + '" class="' + class_ +
1044                        '">' + bugID + '</a>';
1045
1046            // inject the table row
1047            var reachedEnd = false;
1048            for (; row < rows.length; ++row) {
1049                var cells = rows[row].querySelectorAll("td");
1050                if (cells.length != 5)
1051                    continue;
1052                if (time > trimContent(cells[1])) {
1053                    if (row < rows.length - 1) {
1054                        continue;
1055                    } else {
1056                        reachedEnd = true;
1057                    }
1058                }
1059                if (time == trimContent(cells[1])) {
1060                    cells[0].rowSpan++;
1061                    cells[1].rowSpan++;
1062                    var rowContents = [what, removed, added];
1063                    var tr =  histDoc.createElement("tr");
1064                    rowContents.forEach(function (cellContents) {
1065                        var td = histDoc.createElement("td");
1066                        td.innerHTML = cellContents;
1067                        tr.appendChild(td);
1068                    });
1069                    if (row != rows.length - 1) {
1070                        rows[row].parentNode.insertBefore(tr, rows[row+1]);
1071                    } else {
1072                        rows[row].parentNode.appendChild(tr);
1073                    }
1074                } else {
1075                    var rowContents = [email, time, what, removed, added];
1076                    var tr =  histDoc.createElement("tr");
1077                    rowContents.forEach(function (cellContents) {
1078                        var td = histDoc.createElement("td");
1079                        td.innerHTML = cellContents;
1080                        tr.appendChild(td);
1081                    });
1082                    if (reachedEnd) {
1083                        rows[row].parentNode.appendChild(tr);
1084                    } else {
1085                        rows[row].parentNode.insertBefore(tr, rows[row]);
1086                    }
1087                }
1088                break;
1089            }
1090
1091            // remove the comment from the main doc
1092            comments[i].parentNode.removeChild(comments[i]);
1093        }
1094    }
1095}
1096
1097function handleEmptyCollapsedBoxes(doc) {
1098    // first, try to get the display style of a CC field (any would do)
1099    var historyBoxes = doc.querySelectorAll(".bztw_history");
1100    for (var i = 0; i < historyBoxes.length; ++i) {
1101        var box = historyBoxes[i];
1102        for (var j = 0; j < box.childNodes.length; ++j) {
1103            var child = box.childNodes[j], found = true;
1104            if (child.nodeType != child.ELEMENT_NODE)
1105                continue;
1106            if (child.className == "sep") {
1107                // separators are insignificant
1108                continue;
1109            } else if (!/bztw_cc/.test(child.className)) {
1110                found = false;
1111                break;
1112            }
1113        }
1114        if (found) {
1115            box.className += " bztw_cc";
1116        }
1117    }
1118}
1119
1120function applyClass(class_, html) {
1121    return '<span class="' + class_ + '">' + html + '</span>';
1122}
1123
1124function htmlEncode(str) {
1125    return str.replace('&', '&amp;', 'g')
1126              .replace('<', '&lt;', 'g')
1127              .replace('>', '&gt;', 'g')
1128              .replace('"', '&quot;', 'g');
1129}
1130
1131function addNewLinks(d) {
1132  var product = d.querySelector("#field_container_product option[selected]");
1133  var component = d.querySelector("#component option[selected]");
1134
1135  if (product) {
1136    var label = d.getElementById('field_container_product');
1137    var url = 'enter_bug.cgi?product=' + encodeURIComponent(product.value);
1138    if (label) {
1139      label.appendChild(d.createTextNode("("));
1140      var link = d.createElement('a');
1141      link.href = url;
1142      link.textContent = "new";
1143      link.title = "File a new bug in the same Product";
1144      var span = d.createElement('span');
1145      span.appendChild(link);
1146      label.appendChild(span);
1147      label.appendChild(d.createTextNode(")"));
1148    }
1149  }
1150
1151  if (product && component) {
1152    var select = d.querySelector("select#component");
1153    var label = select.parentNode;
1154    var url = 'enter_bug.cgi?product=' + encodeURIComponent(product.value) + '&component=' + encodeURIComponent(component.value);
1155    if (label) {
1156      label.appendChild(d.createTextNode("("));
1157      var link = d.createElement('a');
1158      link.href = url;
1159      link.textContent = "new";
1160      link.title = "File a new bug in the same Product and Component";
1161      var span = d.createElement('span');
1162      span.appendChild(link);
1163      label.appendChild(span);
1164      label.appendChild(d.createTextNode(")"));
1165    }
1166  }
1167}
1168
1169function tbplbotSpamCollapser(d) {
1170  var collapseExpandBox = d.querySelector(".bz_collapse_expand_comments");
1171  if (!collapseExpandBox) {
1172    return;
1173  }
1174  var a = d.createElement("a");
1175  a.href = "#";
1176  a.addEventListener("click", function(e) {
1177    e.preventDefault();
1178    var win = d.defaultView;
1179    var comments = d.querySelectorAll(".bz_comment");
1180    for (var i = 0; i < comments.length; ++i) {
1181      var comment = comments[i];
1182      try {
1183        if (comment.querySelector(".bz_comment_user a.email").href.substr(7) ==
1184            "tbplbot@gmail.com") {
1185          win.collapse_comment(comment.querySelector(".bz_collapse_comment"),
1186                               comment.querySelector(".bz_comment_text"));
1187        }
1188      } catch (e) {
1189        continue;
1190      }
1191    }
1192    return false;
1193  }, false);
1194  a.appendChild(d.createTextNode("Collapse All tbplbot Comments"));
1195  var li = d.createElement("li");
1196  li.appendChild(a);
1197  collapseExpandBox.appendChild(li);
1198}
1199
1200tweakBugzilla(document);