/data/js/bug-page-mod.js
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%;">⇒</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('&', '&', 'g') 1126 .replace('<', '<', 'g') 1127 .replace('>', '>', 'g') 1128 .replace('"', '"', '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);