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

https://bitbucket.org/ehsan/bugzilla-tweaks/ · JavaScript · 1200 lines · 1053 code · 61 blank · 86 comment · 259 complexity · 6f0e217f50cb9b7c54f1ab9dace63df9 MD5 · raw file

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