/******************************************************************************* * ***** BEGIN LICENSE BLOCK Version: MPL 1.1/GPL 2.0/LGPL 2.1 * * The contents of this file are subject to the Mozilla Public License Version * 1.1 (the "License"); you may not use this file except in compliance with the * License. You may obtain a copy of the License at http://www.mozilla.org/MPL/ * * Software distributed under the License is distributed on an "AS IS" basis, * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for * the specific language governing rights and limitations under the License. * * The Original Code is Bugzilla Tweaks. * * The Initial Developer of the Original Code is Mozilla Foundation. Portions * created by the Initial Developer are Copyright (C) 2010 the Initial * Developer. All Rights Reserved. * * Contributor(s): Johnathan Nightingale Ehsan Akhgari * * * Alternatively, the contents of this file may be used under the terms of * either the GNU General Public License Version 2 or later (the "GPL"), or the * GNU Lesser General Public License Version 2.1 or later (the "LGPL"), in which * case the provisions of the GPL or the LGPL are applicable instead of those * above. If you wish to allow use of your version of this file only under the * terms of either the GPL or the LGPL, and not to allow others to use your * version of this file under the terms of the MPL, indicate your decision by * deleting the provisions above and replace them with the notice and other * provisions required by the GPL or the LGPL. If you do not delete the * provisions above, a recipient may use your version of this file under the * terms of any one of the MPL, the GPL or the LGPL. * * ***** END LICENSE BLOCK ***** */ function TweakOnMessageHandler(msg, nextHandlerList) { switch (msg.cmd) { case "Unhandled": break; case "returnedHistory": processHistory(msg.data); break; default: if (nextHandlerList) { var nextHandler = nextHandlerList.splice(0, 1); if (nextHandler[0]) { nextHandler[0](msg, nextHandlerList); } } else { console.error("Error: unknown RPC call " + msg.toSource()); } break; } } /** * generate XML-RPC call to collect complete history of the bug * * @param XMLRPCURL * URL of the XML-RPC gateway on the particular bugzilla * @returns nothing * * As part of the message is name of the signal "returnedHistory" */ function collectHistory(rpcURL) { // https://bugzilla.redhat.com/docs/en/html/api/Bugzilla\ // /WebService/Bug.html#Bug_Information /* This is a heads up on a change that we will be making soon with Bugzilla. The component, version and target release fields will become multi-select fields. This means a bug will have one or more values for these fields (currently they can only have one value for each field). If you use the RPC scripts, this change will affect the results you send and receive to it. For example, in the Bug.get function, the XML for the target_release field currently is: {{{ target_release release_1 }}} the new XML will read: {{{ target_release release_1 release_1 }}} We plan to release this change on our test server - https://partner-bugzilla.redhat.com/ - this Wednesday April 20th EDT. An announcement will be sent to bugzilla-announce-list redhat com when we make the change. The change will be made to the production servers in early May, with notification to the announce list prior to the change. */ var bugId = getBugNo(); self.postMessage(new Message("MakeJSONRPCall", { url : rpcURL.replace("xmlrpc.cgi","jsonrpc.cgi"), login : getLogin(), method : "Bug.history", params : { "ids": [ bugId ] }, callRPC : "returnedHistory" })); } function tweakBugzilla(things, cData) { var atts = things.attachments; // FIXME compatibility crutch, should be removed // when this rewrite is done viewAttachmentSource(atts); // Mark up history along right hand edge var historyLink = document.querySelector("link[title='Bug Activity']"); if (!historyLink) return; // Add our own style for bugzilla-tweaks var style = document.createElement("style"); style.setAttribute("type", "text/css"); style.appendChild(document.createTextNode( ".bztw_history { border: none; font-weight: normal; width: 58em; margin-left: 5em; }" + ".bztw_inlinehistory { font-weight: normal; width: 56em; }" + ".bztw_history .old, .bztw_inlinehistory .old { text-decoration: line-through; }" + ".bztw_history .sep:before { content: \" \"; }" + ".bztw_unconfirmed { font-style: italic; }" + "tr.bz_tr_obsolete.bztw_plusflag { display: table-row !important; }" + '.bztw_historyitem + .bztw_historyitem:before { content: "; "; }' )); document.getElementsByTagName("head")[0].appendChild(style); style = document.createElement("style"); style.setAttribute("type", "text/css"); style.id = "bztw_cc"; style.appendChild(document.createTextNode( ".bztw_cc { display: none; }" + '.bztw_historyitem.bztw_cc + .bztw_historyitem:before { content: ""; }' + '.bztw_historyitem:not([class~="bztw_cc"]) ~ .bztw_historyitem.bztw_cc + .bztw_historyitem:before { content: "; "; }' )); document.getElementsByTagName("head")[0].appendChild(style); // collect the flag names, FIXME this is exactly the same as // mozFlags._init() function in mozpage.js var flagNames = [], flags = {}, flagOccurrences = {}; var flagRows = document.querySelectorAll("#flags tr"); for (var i = 0; i < flagRows.length; ++i) { var item = flagRows[i].querySelectorAll("td"); if (!item[1]) continue; var name = trimContent(item[1]).replace('\u2011', '-', 'g'); flagNames.push(name); flags[name] = item[1]; } flagRows = document.querySelectorAll(".field_label[id^=field_label_cf_]"); for (var i = 0; i < flagRows.length; ++i) { var name = trimContent(flagRows[i]).replace(/\:$/, '') .replace('\u2011', '-', 'g'); flagNames.push(name); flags[name] = flagRows[i]; } var flagCounter = 1; // ================================================= function findFlag(item) { function lookup(name) { name = name.replace('\u2011', '-', 'g'); for (var i = 0; i < flagNames.length; ++i) { var quotedFlagName = flagNames[i].replace('.', '\\.', 'g') .replace('\u2011', '-', 'g'); if ((new RegExp('^' + quotedFlagName)).test(name)) { return [flagNames[i]]; } } return []; } var base = item[4] ? 2 : 0; // handle normal flags if (trimContent(item[base]) == 'Flag') { var result = []; var tmp = lookup(trimContent(item[base + 1])); if (tmp.length) { result.push(tmp[0]); } tmp = lookup(trimContent(item[base + 2])); if (tmp.length) { result.push(tmp[0]); } return result; } // handle special pseudo-flags return lookup(trimContent(item[base])); } var DataStore = new DataStoreCtor(); var AttachmentFlagHandler = new AttachmentFlagHandlerCtor(); AttachmentFlagHandler.determineInterestingFlags(); var CheckinComment = new CheckinCommentCtor(); CheckinComment.initialize(AttachmentFlagHandler._interestingFlags); if (document.location.hostname in cData.XMLRPCData) { var XMLRPCUrl = cData.XMLRPCData[document.location.hostname].url; collectHistory(XMLRPCUrl); } tbplbotSpamCollapser(); } // =================================================== function processHistory(history) { // FIXME Remove remaining code to special function ... callback // preprocessDuplicateMarkers(document, iframe.contentDocument); /* * This is an example of the history we get: { "version": "1.1", "result": { * "bugs": [ { "history": [ { "when": "2011-04-04T00:19:04Z", "who": * "cebbert@redhat.com", "changes": [ { "removed": "", "added": * "xgl-maint@redhat.com", "field_name": "cc", "field": "CC" }, { "removed": * "kernel", "added": "xorg-x11-drv-ati", "field_name": "component", "field": * "Component" }, { "removed": "kernel-maint@redhat.com", "added": * "xgl-maint@redhat.com", "field_name": "assigned_to", "field": "AssignedTo" } ] }, { * "when": "2011-04-12T22:48:22Z", "who": "mcepl@redhat.com", "changes": [ { * "attachment_id": 488889, "removed": "application/octet-stream", "added": * "text/plain", "field_name": "attachments.mimetype", "field": "Attachment * mime type" } ] }, { "when": "2011-04-13T17:07:04Z", "who": * "mcepl@redhat.com", "changes": [ { "removed": "", "added": * "needinfo?(suckfish@ihug.co.nz)", "field_name": "flagtypes.name", "field": * "Flags" } ] }, { "when": "2011-04-21T12:17:33Z", "who": "mcepl@redhat.com", * "changes": [ { "removed": "xgl-maint@redhat.com", "added": * "jglisse@redhat.com", "field_name": "assigned_to", "field": "AssignedTo" } ] }, { * "when": "2011-04-28T22:53:58Z", "who": "mcepl@redhat.com", "changes": [ { * "attachment_id": 488889, "removed": "text/plain", "added": * "application/octet-stream", "field_name": "attachments.mimetype", "field": * "Attachment mime type" } ] }, { "when": "2011-04-28T22:59:18Z", "who": * "mcepl@redhat.com", "changes": [ { "attachment_id": 488889, "removed": * "application/octet-stream", "added": "text/plain", "field_name": * "attachments.mimetype", "field": "Attachment mime type" } ] } ], "id": * 692250, "alias": [ * ] } ], "faults": [ * ] } } */ // UserNameCache var userNameCache = {}; function getUserName(email) { if (email in userNameCache) { return userNameCache[email]; } var emailLink = document.querySelectorAll("a.email"); for (var i = 0; i < emailLink.length; ++i) { if (emailLink[i].href == "mailto:" + email) { return userNameCache[email] = htmlEncode(trimContent(emailLink[i])); } } return email; } // MC $$$ NOT DONE YET // // Sometimes the history will stack several changes together, // // and we'll want to append the data from the Nth item to the // // div created in N-1 // if (history) { // history.bugs.forEach(function (historyBug) { // historyBug.history.forEach(function (historyItem) { // processHistoryItem(comments, historyItem); // }); // }); // } // handleEmptyCollapsedBoxes(document); // // Set the latest flag links if necessary // for (var flagName in flagOccurrences) { // flags[flagName].innerHTML = '' // + flags[flagName].innerHTML + ''; // } // AttachmentFlagHandler.setupLinks(document); // END OF load event handler } /* { "when": "2011-04-13T17:07:04Z", "who": "mcepl@redhat.com", "changes": [ { "removed": "", "added": "needinfo?(suckfish@ihug.co.nz)", "field_name": "flagtypes.name", "field": "Flags" } ] }, */ function displayCommentActions (comment, historyItem) { if (historyItem.who == comment.who) { } } function processHistoryItem(objects, itemRaw) { console.log("processHistoryItem: itemRaw = " + itemRaw.toSource()); return ; // FIXME just to get rid of this unfinished and unanalyzed function var idx = itemRaw.when; // Given the mid-air protection we could assume // history time to be unique per second. if (idx in objects.attachment) { displayAttachmentActions(objects.attachment[idx], itemRaw) } if (idx in objects.comment) { displayCommentActions(objects.comment[idx], itemRaw) } // FIXME anything else? displayRemainingActions(itemRaw); // ===================================================== var commentHead = commentTimes[j].parentNode; var mainUser = commentHead.querySelector(".bz_comment_user a.email") .href .substr(7); var user = trimContent(item[0]); var mainTime = trimContent(commentTimes[j]); var time = trimContent(item[1]); // ===================================================================== // $$$ FIXME the change is made by commenter? is that it? var inline = (mainUser == user && time == mainTime); // §§§§ This is a function addToInlineHistory or something TODO var currentDiv = document.createElement("div"); var userPrefix = ''; if (inline) { // assume that the change was made by the same user // XXX? §§§ commentHead.appendChild(currentDiv); currentDiv.setAttribute("class", "bztw_inlinehistory"); } else { // the change was made by another user if (!reachedEnd) { var parentDiv = commentHead.parentNode; if (parentDiv.previousElementSibling && parentDiv.previousElementSibling.className.indexOf("bztw_history") >= 0) { currentDiv = parentDiv.previousElementSibling; } else { parentDiv.parentNode.insertBefore(currentDiv, parentDiv); } } else { var parentDiv = commentHead.parentNode; if (parentDiv.nextElementSibling && parentDiv.nextElementSibling.className.indexOf("bztw_history") >= 0) { currentDiv = parentDiv.nextElementSibling; } else { parentDiv.parentNode.appendChild(currentDiv); } } currentDiv.setAttribute("class", "bz_comment bztw_history"); userPrefix += "" + getUserName(trimContent(item[0])) + ": "; } // XXX END OF if (inline) CONSTRUCT // XXX flags // check to see if this is a flag setting flagsFound = findFlag(item); // XXX findFlag call 2 // XXX Add around every flag comment for (var idx = 0; idx < flagsFound.length; ++idx) { var flag = flagsFound[idx]; flagOccurrences[flag] = 'flag' + flagCounter; if (inline) { var anchor = document.createElement("a"); anchor.setAttribute("name", "flag" + flagCounter); commentHead.insertBefore(anchor, commentHead.firstChild); } else { userPrefix += ''; } ++flagCounter; } // XXX attachments var attachmentFlagAnchors = AttachmentFlagHandler.handleItem(user, item); if (inline) { for (var idx = 0; idx < attachmentFlagAnchors.length; ++idx) { var anchor = document.createElement("a"); anchor.setAttribute("name", attachmentFlagAnchors[idx]); commentHead.insertBefore(anchor, commentHead.firstChild); } } else { userPrefix += attachmentFlagAnchors.map(function(name) { return ''; }).join(""); } // XXX just adding/removing sometbody from CC list var ccOnly = (trimContent(item[2]) == 'CC'); var ccPrefix = ccOnly ? '' : '', ccSuffix = ''; var html = userPrefix + ccPrefix + transformType(trimContent(item[2]), trimContent(item[3]), trimContent(item[4])) + ": " + formatTransition(trimContent(item[3]), trimContent(item[4]), trimContent(item[2]), iframe.contentDocument); // var nextItemsCount = item[0].rowSpan; for (var k = 1; k < nextItemsCount; ++k) { // XXX doing once more the same for non-first elements of the imte array. ccOnly = false; item = historyItems[++i].querySelectorAll("td") ccPrefix = (trimContent(item[0]) == 'CC') ? '' : ''; // avoid showing a trailing semicolon if the previous entry // wasn't a CC and this one is var prefix = ccSuffix + ccPrefix; // check to see if this is a flag setting flagsFound = findFlag(item); // TODO findFlag call 1 for (var idx = 0; idx < flagsFound.length; ++idx) { var flag = flagsFound[idx]; flagOccurrences[flag] = 'flag' + flagCounter; if (inline) { var anchor = document.createElement("a"); anchor.setAttribute("name", "flag" + flagCounter); commentHead.insertBefore(anchor, commentHead.firstChild); } else { prefix += ''; } ++flagCounter; } var attachmentFlagAnchors = AttachmentFlagHandler.handleItem(user, item); if (inline) { for (var idx = 0; idx < attachmentFlagAnchors.length; ++idx) { var anchor = document.createElement("a"); anchor.setAttribute("name", attachmentFlagAnchors[idx]); commentHead.insertBefore(anchor, commentHead.firstChild); } } else { prefix += attachmentFlagAnchors.map(function(name) { return ''; }).join(""); } // END OF THE SAME STUFF FOR NON-FIRST ITEMS html += prefix + transformType(trimContent(item[0]), trimContent(item[1]), trimContent(item[2])) + ": " + formatTransition(trimContent(item[1]), trimContent(item[2]), trimContent(item[0]), iframe.contentDocument); } html += ccSuffix; if (ccOnly) { html = '
' + html + '
'; } else { html = '
' + html + '
'; } // Add the new stuff to the place below the top of the comment div currentDiv.innerHTML += html; // FIXME here used to be a break; } // =================================================== var TransformValues = { linkifyURLs: function (str) { return str.replace(/((https?|ftp)\:\/\/[\S]+)/g, '$1'); }, linkifyBugAndCommentNumbers: function (str) { return str.replace(/(bug )(\d+) (comment )(\d+)/gi, '$1\n$2 $3\n$4'); }, linkifyCommentNumbers: function (str) { return str.replace(/(comment (\d+))/gi, '$1'); }, linkifyBugNumbers: function (str) { return str.replace(/(bug (\d+))/gi, '$1'); }, linkifyDependencies: function (str, type, histDoc) { switch (type) { case "Blocks": case "Depends on": case "Duplicate": str = str.replace(/\d+/g, function(str) { var link = histDoc.querySelector("a[href='show_bug.cgi?id=" + str + "']"); if (link) { var class_ = ''; if (/bz_closed/i.test(link.className)) { class_ += 'bz_closed '; } else if (/bztw_unconfirmed/i.test(link.className)) { class_ += 'bztw_unconfirmed '; } var parent = link.parentNode; if (parent) { if (parent.tagName.toLowerCase() == "i") { class_ += 'bztw_unconfirmed '; } if (/bz_closed/i.test(parent.className)) { class_ += 'bz_closed '; } } str = applyClass(class_, '' + htmlEncode(str) + ''); } return str; }); } return str; } }; // =============================================================================== function transform(str, type, histDoc) { for (var funcname in TransformValues) { var func = TransformValues[funcname]; str = func.call(null, str, type, histDoc); } return str } var TransformTypes = { linkifyAttachments: function (str) { return str.replace(/(Attachment #(\d+))/g, function (str, x, id) { var link = document.querySelector("a[href='attachment.cgi?id=" + id + "']"); if (link) { var class_ = ''; if (/bz_obsolete/i.test(link.className)) { class_ += 'bz_obsolete '; } var parent = link.parentNode; if (parent && /bz_obsolete/i.test(parent.className)) { class_ += 'bz_obsolete '; } if (link.querySelector(".bz_obsolete")) { class_ += 'bz_obsolete '; } str = applyClass(class_, '' + htmlEncode(str) + ''); } return str; }); }, changeDependencyLinkTitles: function (str, old, new_) { switch (str) { case "Blocks": case "Depends on": if (old.length && !new_.length) { // if the dependency was removed str = "No longer " + str[0].toLowerCase() + str.substr(1); } break; } return str; } }; // ======================================================================= function transformType(str, old, new_) { for (var funcname in TransformTypes) { var func = TransformTypes[funcname]; str = func.call(null, str, old, new_); } return str; } // new is a keyword, which makes this function uglier than I'd like function formatTransition(old, new_, type, histDoc) { if (old.length) { old = transform(htmlEncode(old), type, histDoc); var setOldStyle = true; switch (type) { case "Blocks": case "Depends on": setOldStyle = false; break; } if (setOldStyle) { old = '' + old + ''; } } if (new_.length) { new_ = '' + transform(htmlEncode(new_), type, histDoc) + ''; } var mid = ''; if (old.length && new_.length) { mid = ' '; } return old + mid + new_; } // ========================================================================= function trimContent(el) { return el.textContent.trim(); } function AttachmentFlag(flag) { for (var name in flag) this[name] = flag[name]; } AttachmentFlag.prototype = { equals: function(flag) { if (this.type != flag.type || this.name != flag.name || this.setter != flag.setter || ("requestee" in this && !("requestee" in flag)) || ("requestee" in flag && !("requestee" in this))) return false; return this.requestee == flag.requestee; } }; var reAttachmentDiff = /attachment\.cgi\?id=(\d+)&action=diff$/i; var reviewBoardUrlBase = "http://reviews.visophyte.org/"; // =============================================================================== function AttachmentFlagHandlerCtor() { this._db = {}; this._interestingFlags = {}; } AttachmentFlagHandlerCtor.prototype = { determineInterestingFlags: function () { var table = document.getElementById("attachment_table"); if (!table) return; var rows = table.querySelectorAll("tr"); for (var i = 0; i < rows.length; ++i) { var item = rows[i].querySelectorAll("td"); if (item.length != 3 || item[1].className.indexOf("bz_attach_flags") < 0 || trimContent(item[1]) == "no flags") continue; // get the ID of the attachment var link = item[0].querySelector("a"); if (!link) continue; var match = this._reAttachmentHref.exec(link.href); if (match) { var attachmentID = match[1]; if (!(attachmentID in this._interestingFlags)) { this._interestingFlags[attachmentID] = []; } for (var el = item[1].firstChild; el.nextSibling; el = el.nextSibling) { if (el.nodeType != el.TEXT_NODE) continue; var text = trimContent(el).replace('\u2011', '-', 'g'); if (!text) continue; match = this._reParseInterestingFlag.exec(text); if (match) { var flag = {}; flag.setter = match[1]; flag.name = match[2]; if (match[4] == "+" || match[4] == "-") { flag.type = match[4]; } else { flag.type = "?"; if (match[7]) { flag.requestee = match[7]; } } // always show the obsolete attachments with a + flag if (flag.type == "+") { var parent = link.parentNode; while (parent) { if (parent.tagName.toLowerCase() == "tr") { if (/bz_tr_obsolete/i.test(parent.className)) { parent.className += " bztw_plusflag"; } break; } parent = parent.parentNode; } } // try to put the flag name and type part in a span // which we will // use in setupLinks to inject links into. match = this._reLinkifyInterestingFlag.exec(text); if (match) { el.textContent = match[1]; if (match[3]) { var textNode = document.createTextNode(match[3]); el.parentNode.insertBefore(textNode, el.nextSibling); } var span = document.createElement("span"); span.textContent = match[2]; el.parentNode.insertBefore(span, el.nextSibling); flag.placeholder = span; } this._interestingFlags[attachmentID].push(new AttachmentFlag(flag)); } } } } }, handleItem: function (name, item) { var anchorsCreated = []; var base = item[4] ? 2 : 0; var what = trimContent(item[base]); var match = this._reAttachmentFlagName.exec(what); if (match) { var id = match[1]; if (!(id in this._db)) { this._db[id] = []; } name = name.split('@')[0]; // convert the name to the fraction // before the @ var added = this._parseData(name, trimContent(item[base + 2])); for (var i = 0; i < added.length; ++i) { var flag = added[i]; if (!(id in this._interestingFlags)) continue; for (var j = 0; j < this._interestingFlags[id].length; ++j) { if (flag.equals(this._interestingFlags[id][j])) { // found an interesting flag this._interestingFlags[id][j].anchor = this.anchorName; anchorsCreated.push(this.anchorName); this._counter++; break; } } } } return anchorsCreated; }, setupLinks: function () { for (var id in this._interestingFlags) { for (var i = 0; i < this._interestingFlags[id].length; ++i) { var flag = this._interestingFlags[id][i]; if ("placeholder" in flag && "anchor" in flag) { var link = document.createElement("a"); link.href = "#" + flag.anchor; link.textContent = flag.placeholder.textContent; flag.placeholder.replaceChild(link, flag.placeholder.firstChild); } } } }, _parseData: function (name, str) { var items = str.replace('\u2011', '-', 'g').split(', '), flags = []; for (var i = 0; i < items.length; ++i) { if (!items[i].length) continue; var match = this._reParseRequest.exec(items[i]); if (match) { var flag = {}; flag.name = match[1]; flag.setter = name; if (match[4]) { flag.requestee = match[4]; } flag.type = match[2]; flags.push(new AttachmentFlag(flag)); } } return flags; }, _counter: 1, get anchorName() { return "attachflag" + this._counter; }, _reParseRequest: /^(.+)([\?\-\+])(\((.+)@.+\))?$/, _reParsePartToLinkify: /^\s*:\s+.+[\-\+\?](\s*\()?\s*$/, _reParseInterestingFlag: /^(.+):\s+(.+)(([\-\+])|\?(\s+(\((.+)\)))?)$/, _reLinkifyInterestingFlag: /^(\s*:\s+)(.+[\-\+\?])(\s*\(\s*)?$/, _reAttachmentHref: /attachment\.cgi\?id=(\d+)$/i, _reAttachmentFlagName: /^Attachment\s+#(\d+)\s+Flags$/i }; function CheckinCommentCtor() { this.bugNumber = null; this.summarySpan = null; this.checkinFlags = ""; } CheckinCommentCtor.prototype = { initialize: function(flags) { this.bugNumber = getBugNo(); var summarySpan = document.getElementById("short_desc_nonedit_display"); if (summarySpan) { this.summary = summarySpan.textContent; } var checkinFlagsMap = {}; for (var id in flags) { for (var i = 0; i < flags[id].length; ++i) { var flag = flags[id][i]; if (flag.type == "+") { var name = flag.name; if (name == "review") { name = "r"; } else if (name == "superreview") { name = "sr"; } else if (name == "ui-review") { name = "ui-r"; } else if (name == "feedback") { name = "f"; } if (!(name in checkinFlagsMap)) { checkinFlagsMap[name] = {}; } checkinFlagsMap[name][flag.setter]++; } } } var flagsOrdered = []; for (var name in checkinFlagsMap) { flagsOrdered.push(name); } flagsOrdered.sort(function (a, b) { function convertToNumber(x) { switch (x) { case "f": return -4; case "r": return -3; case "sr": return -2; case "ui-r": return -1; default: return 0; } } var an = convertToNumber(a); var bn = convertToNumber(b); if (an == 0 && bn == 0) { return a < b ? -1 : (a = b ? 0 : 1); } else { return an - bn; } }); var checkinFlags = []; for (var i = 0; i < flagsOrdered.length; ++i) { var name = flagsOrdered[i]; var flag = name + "="; var setters = []; for (var setter in checkinFlagsMap[name]) { setters.push(setter); } flag += setters.join(","); checkinFlags.push(flag); } this.checkinFlags = checkinFlags.join(" "); if (this.isValid()) { var div = document.createElement("div"); div.setAttribute("style", "display: none;"); div.id = "__bz_tw_checkin_comment"; div.appendChild(document.createTextNode(this.toString())); document.body.appendChild(div); } }, isValid: function() { return this.bugNumber != null && this.summary != null; }, toString: function() { if (!this.isValid()) { return ""; } var message = "Bug " + this.bugNumber + " - " + this.summary; if (this.checkinFlags.length) { message += "; " + this.checkinFlags; } return message; } }; function DataStoreCtor() { this.storage = document.defaultView.localStorage; this.data = {}; this.bugNumber = null; function visualizeStoredData() { var data = ""; for (var i = 0; i < window.localStorage.length; ++i) { var key = window.localStorage.key(i); data += key + ": " + JSON.parse(window.localStorage.getItem(key).toString()).toSource() + "\n"; } open("data:text/html,
" + escape(htmlEncode(data)) + "
"); } function clearStoredData() { var count = window.localStorage.length; if (count > 0) { if (window.confirm("You currently have data stored for " + count + " bugs.\n\n" + "Are you sure you want to clear this data? This action cannot be undone.")) { window.localStorage.clear(); } } else { alert("You don't have any data stored about your bugs"); } } var script = document.createElement("script"); script.appendChild(document.createTextNode(visualizeStoredData.toSource() + clearStoredData.toSource() + htmlEncode.toSource())); document.body.appendChild(script); this.initialize(); } DataStoreCtor.prototype = { initialize: function() { this.bugNumber = getBugNo(); var data = this._ensureEntry(this.bugNumber, this.data); // last visited date data.visitedTime = (new Date()).getTime(); // last comment count data.commentCount = document.querySelectorAll(".bz_comment").length; // last status of bug flags var flags = this._ensureEntry("flags", data); var flagRows = document.querySelectorAll("#flags tr"); for (var i = 0; i < flagRows.length; ++i) { var flagCols = flagRows[i].querySelectorAll("td"); if (flagCols.length != 3) { continue; } var flagName = trimContent(flagCols[1]); var flagValue = flagCols[2].querySelector("select"); if (flagValue) { flagValue = flagValue.value; } else { continue; } flags[flagName] = flagValue; } flagRows = document.querySelectorAll(".field_label[id^=field_label_cf_]"); for (var i = 0; i < flagRows.length; ++i) { var flagName = trimContent(flagRows[i]).replace(/:$/, ""); var flagValue = flagRows[i].parentNode.querySelector("select"); if (flagValue) { flagValue = flagValue.value; } else { continue; } flags[flagName] = flagValue; } // last attachments var attachmentTable = document.getElementById("attachment_table"); var attachmentRows = attachmentTable.querySelectorAll("tr"); for (var i = 0; i < attachmentRows.length; ++i) { var attachmentCells = attachmentRows[i].querySelectorAll("td"); if (attachmentCells.length != 3) { continue; } var link = attachmentCells[0].querySelector("a"); var match = this._reAttachmentHref.exec(link.href); if (match) { var attachmentID = match[1]; var attachment = this._ensureEntry("attachments", data); var attachmentFlags = this._ensureArray(attachmentID, attachment); for (var el = attachmentCells[1].firstChild; el.nextSibling; el = el.nextSibling) { if (el.nodeType != el.TEXT_NODE) { continue; } var text = trimContent(el); if (!text) { continue; } match = this._reParseInterestingFlag.exec(text); if (match) { var flag = {}; flag.setter = match[1]; flag.name = match[2]; if (match[4] == "+" || match[4] == "-") { flag.type = match[4]; } else { flag.type = "?"; if (match[7]) { flag.requestee = match[7]; } } attachmentFlags.push(flag); } } } } // Write data to storage for (var key in this.data) { this._ensure(key, this.storage, JSON.stringify(this.data[key])); } }, _ensure: function(entry, obj, val) { if (obj.toString().indexOf("[object Storage") >= 0) { obj.setItem(entry, val); } else { if (typeof obj[entry] == "undefined") obj[entry] = val; return obj[entry]; } }, _ensureEntry: function(entry, obj) { return this._ensure(entry, obj, {}); }, _ensureArray: function(entry, obj) { return this._ensure(entry, obj, []); }, _reParseInterestingFlag: /^(.+):\s+(.+)(([\-\+])|\?(\s+(\((.+)\)))?)$/, _reAttachmentHref: /attachment\.cgi\?id=(\d+)$/i }; function handleEmptyCollapsedBoxes() { // first, try to get the display style of a CC field (any would do) var historyBoxes = document.querySelectorAll(".bztw_history"); for (var i = 0; i < historyBoxes.length; ++i) { var box = historyBoxes[i]; for (var j = 0; j < box.childNodes.length; ++j) { var child = box.childNodes[j], found = true; if (child.nodeType != child.ELEMENT_NODE) continue; if (child.className == "sep") { // separators are insignificant continue; } else if (!/bztw_cc/.test(child.className)) { found = false; break; } } if (found) { box.className += " bztw_cc"; } } } function applyClass(class_, html) { return '' + html + ''; } function htmlEncode(str) { return str.replace('&', '&', 'g') .replace('<', '<', 'g') .replace('>', '>', 'g') .replace('"', '"', 'g'); } function tbplbotSpamCollapser() { var collapseExpandBox = document.querySelector(".bz_collapse_expand_comments"); if (!collapseExpandBox) { return; } var a = document.createElement("a"); a.href = "#"; a.addEventListener("click", function(e) { e.preventDefault(); var win = document.defaultView.wrappedJSObject; var comments = document.querySelectorAll(".bz_comment"); for (var i = 0; i < comments.length; ++i) { var comment = comments[i]; try { if (comment.querySelector(".bz_comment_user a.email").href.substr(7) == "tbplbot@gmail.com") { win.collapse_comment(comment.querySelector(".bz_collapse_comment"), comment.querySelector(".bz_comment_text")); } } catch (e) { continue; } } return false; }, false); a.appendChild(document.createTextNode("Collapse All tbplbot Comments")); var li = document.createElement("li"); li.appendChild(a); collapseExpandBox.appendChild(li); }