/*******************************************************************************
* ***** 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 <johnath@mozilla.com> Ehsan Akhgari
* <ehsan@mozilla.com>
*
* 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:
{{{
<member>
<name>target_release</name>
<value><string>release_1</string></value>
</member>
}}}
the new XML will read:
{{{
<member><name>target_release</name>
<value><array><data>
<value><string>release_1</string></value>
<value><string>release_1</string></value>
</data></array></value>
</member>
}}}
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(atts, cData) {
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;
}
if (history) {
// console.log("processHistory: history = " + history.toSource());
return ;
}
// var historyItems = iframe.contentDocument.querySelectorAll('#bugzilla-body
// tr');
// var cmtTimes = document.querySelectorAll('.bz_comment_time');
// 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
var i=0, j=0, flagsFound;
if (history) {
Array.forEach(history, function (item) {
processHistoryItem(cmtTimes, item);
});
}
handleEmptyCollapsedBoxes(document);
// // Set the latest flag links if necessary
// for (var flagName in flagOccurrences) {
// flags[flagName].innerHTML = '<a href="#' + flagOccurrences[flagName] + '">'
// + flags[flagName].innerHTML + '</a>';
// }
// AttachmentFlagHandler.setupLinks(document);
// END OF load event handler
}
function processHistoryItem(commentTimes, itemRaw) {
var item = itemRaw.querySelectorAll("td");
if (!item[1])
return;
var reachedEnd = false;
for (; j < commentTimes.length; j++) {
if (trimContent(item[1]) > trimContent(commentTimes[j])) {
if (j < commentTimes.length - 1) {
return;
} else {
reachedEnd = true;
}
}
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]);
var inline = (mainUser == user && time == mainTime);
var currentDiv = document.createElement("div");
var userPrefix = '';
if (inline) {
// assume that the change was made by the same user
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 += "<a class=\"email\" href=\"mailto:" +
htmlEncode(trimContent(item[0])) + "\" title=\"" +
htmlEncode(trimContent(item[1])) +"\">" +
getUserName(trimContent(item[0])) + "</a>: ";
}
// check to see if this is a flag setting
flagsFound = findFlag(item);
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 += '<a name="flag' + flagCounter + '"></a>';
}
++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 {
userPrefix += attachmentFlagAnchors.map(function(name) '<a name="' + name + '"></a>').join("");
}
var ccOnly = (trimContent(item[2]) == 'CC');
var ccPrefix = ccOnly ? '<span class="bztw_cc bztw_historyitem">' :
'<span class="bztw_historyitem">',
ccSuffix = '</span>';
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) {
ccOnly = false;
item = historyItems[++i].querySelectorAll("td")
ccPrefix = (trimContent(item[0]) == 'CC') ?
'<span class="bztw_cc bztw_historyitem">' : '<span class="bztw_historyitem">';
// 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);
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 += '<a name="flag' + flagCounter + '"></a>';
}
++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) '<a name="' + name + '"></a>').join("");
}
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 = '<div class="bztw_cc">' + html + '</div>';
} else {
html = '<div>' + html + '</div>';
}
currentDiv.innerHTML += html;
break;
}
}
// ===================================================
var TransformValues = {
linkifyURLs: function (str) {
return str.replace(/((https?|ftp)\:\/\/[\S]+)/g, '<a href="$1">$1</a>');
},
linkifyBugAndCommentNumbers: function (str) {
return str.replace(/(bug )(\d+) (comment )(\d+)/gi, '<a href="show_bug.cgi?id=$2#c$4">$1\n$2 $3\n$4</a>');
},
linkifyCommentNumbers: function (str) {
return str.replace(/(comment (\d+))/gi, '<a href="#c$2">$1</a>');
},
linkifyBugNumbers: function (str) {
return str.replace(/(bug (\d+))/gi, '<a href="show_bug.cgi?id=$2">$1</a>');
},
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_,
'<a title="' + htmlEncode(link.title) + '" href="show_bug.cgi?id=' + htmlEncode(str) + '"' +
(link.hasAttribute("name") ? (' name="' + htmlEncode(link.getAttribute("name")) + '"') : '') +
'>' + htmlEncode(str) + '</a>');
}
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_,
'<a title="' + htmlEncode(trimContent(link)) + '" href="attachment.cgi?id=' +
htmlEncode(id) + '&action=edit">' + htmlEncode(str) + '</a>');
}
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 = '<span class="old">' + old + '</span>';
}
}
if (new_.length) {
new_ = '<span class="new">' + transform(htmlEncode(new_), type, histDoc) + '</span>';
}
var mid = '';
if (old.length && new_.length) {
mid = ' <span style="font-size: 150%;">⇒</span> ';
}
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,<pre>" + escape(htmlEncode(data)) + "</pre>");
}
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 '<span class="' + class_ + '">' + html + '</span>';
}
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);
}