From 1f30fac936a3b0905e736dd86e559e33caf036ac Mon Sep 17 00:00:00 2001
From: David Lawrence
Date: Wed, 10 Aug 2011 18:26:03 -0400
Subject: Initial checkin of bmo/4.0 extensions. Still todo: port changes to
core Bugzilla code
---
Bugzilla/UserAgent.pm | 247 ++
extensions/BMO/Config.pm | 38 +
extensions/BMO/Extension.pm | 794 +++++++
extensions/BMO/lib/Data.pm | 239 ++
extensions/BMO/lib/FakeBug.pm | 42 +
extensions/BMO/lib/Reports.pm | 476 ++++
extensions/BMO/lib/WebService.pm | 207 ++
.../template/en/default/account/create.html.tmpl | 181 ++
.../default/bug/create/comment-mktgevent.txt.tmpl | 48 +
.../en/default/bug/create/comment-mozlist.txt.tmpl | 44 +
.../en/default/bug/create/comment-mozreps.txt.tmpl | 81 +
.../bug/create/comment-remo-budget.txt.tmpl | 64 +
.../default/bug/create/comment-remo-swag.txt.tmpl | 68 +
.../en/default/bug/create/comment-swag.txt.tmpl | 48 +
.../default/bug/create/create-brownbag.html.tmpl | 220 ++
.../default/bug/create/create-itrequest.html.tmpl | 184 ++
.../en/default/bug/create/create-legal.html.tmpl | 216 ++
.../default/bug/create/create-mktgevent.html.tmpl | 249 ++
.../en/default/bug/create/create-mozlist.html.tmpl | 306 +++
.../en/default/bug/create/create-mozpr.html.tmpl | 654 ++++++
.../en/default/bug/create/create-mozreps.html.tmpl | 204 ++
.../default/bug/create/create-poweredby.html.tmpl | 85 +
.../bug/create/create-presentation.html.tmpl | 217 ++
.../bug/create/create-remo-budget.html.tmpl | 248 ++
.../default/bug/create/create-remo-swag.html.tmpl | 292 +++
.../en/default/bug/create/create-swag.html.tmpl | 221 ++
.../default/bug/create/create-trademark.html.tmpl | 87 +
.../default/bug/create/created-mozreps.html.tmpl | 38 +
.../en/default/bug/create/user-message.html.tmpl | 140 ++
.../en/default/global/choose-product.html.tmpl | 183 ++
.../createformcontents-mimetypes.html.tmpl | 2 +
.../createformcontents-patch_notes.html.tmpl | 1 +
.../hook/bug/comments-comment_banner.html.tmpl | 7 +
.../default/hook/bug/create/create-form.html.tmpl | 18 +
.../hook/bug/create/create-guided-form.html.tmpl | 22 +
.../hook/bug/edit-after_custom_fields.html.tmpl | 82 +
.../en/default/hook/bug/field-help-end.none.tmpl | 113 +
.../en/default/hook/bug/show-header-end.html.tmpl | 27 +
.../en/default/hook/global/footer-outro.html.tmpl | 1 +
.../hook/global/header-additional_header.html.tmpl | 60 +
.../en/default/hook/global/header-start.html.tmpl | 3 +
.../hook/global/setting-descs-settings.none.tmpl | 5 +
.../hook/global/user-error-errors.html.tmpl | 55 +
.../auth_failure/permissions.html.tmpl | 29 +
.../en/default/hook/global/variables-end.none.tmpl | 3 +
.../default/hook/index-additional_links.html.tmpl | 10 +
.../template/en/default/hook/index-intro.html.tmpl | 2 +
.../hook/pages/fields-resolutions.html.tmpl | 13 +
.../en/default/hook/reports/menu-end.html.tmpl | 35 +
.../en/default/list/list.microsummary.tmpl | 28 +
.../template/en/default/list/server-push.html.tmpl | 52 +
.../en/default/pages/bug-writing.html.tmpl | 25 +
.../pages/comment-remo-form-payment.txt.tmpl | 37 +
.../template/en/default/pages/etiquette.html.tmpl | 147 ++
.../template/en/default/pages/get_help.html.tmpl | 42 +
.../en/default/pages/remo-form-payment.html.tmpl | 243 ++
.../en/default/pages/triage_reports.html.tmpl | 199 ++
.../en/default/pages/upgrade-3.6.html.tmpl | 304 +++
.../en/default/pages/user_activity.html.tmpl | 180 ++
.../en/default/search/search-plugin.xml.tmpl | 24 +
extensions/BMO/web/images/background.png | Bin 0 -> 1695 bytes
extensions/BMO/web/images/bugzilla.png | Bin 0 -> 1242 bytes
.../BMO/web/images/groups/bugzilla-approvers.png | Bin 0 -> 829 bytes
.../BMO/web/images/groups/calendar-drivers.png | Bin 0 -> 744 bytes
extensions/BMO/web/images/mozchomp.gif | Bin 0 -> 89485 bytes
extensions/BMO/web/images/presshat.png | Bin 0 -> 23450 bytes
extensions/BMO/web/images/stop-sign.gif | Bin 0 -> 3227 bytes
extensions/BMO/web/js/edit_bug.js | 56 +
extensions/BMO/web/js/form_validate.js | 21 +
extensions/BMO/web/js/sorttable.js | 709 ++++++
extensions/BMO/web/js/swag.js | 60 +
extensions/BMO/web/js/triage_reports.js | 83 +
extensions/BMO/web/producticons/camino.png | Bin 0 -> 6060 bytes
extensions/BMO/web/producticons/dino.png | Bin 0 -> 3375 bytes
extensions/BMO/web/producticons/fennec.png | Bin 0 -> 9023 bytes
extensions/BMO/web/producticons/firefox.png | Bin 0 -> 9395 bytes
extensions/BMO/web/producticons/idea.png | Bin 0 -> 6189 bytes
extensions/BMO/web/producticons/input.png | Bin 0 -> 8333 bytes
extensions/BMO/web/producticons/labs.png | Bin 0 -> 4085 bytes
extensions/BMO/web/producticons/mozilla.png | Bin 0 -> 10808 bytes
extensions/BMO/web/producticons/other.png | Bin 0 -> 6654 bytes
extensions/BMO/web/producticons/seamonkey.png | Bin 0 -> 5255 bytes
extensions/BMO/web/producticons/sunbird.png | Bin 0 -> 10462 bytes
extensions/BMO/web/producticons/thunderbird.png | Bin 0 -> 9939 bytes
extensions/BMO/web/styles/create_account.css | 62 +
extensions/BMO/web/styles/edit_bug.css | 32 +
extensions/BMO/web/styles/moz_reps.css | 44 +
extensions/BMO/web/styles/triage_reports.css | 23 +
extensions/BzAPI/Config.pm | 63 +
extensions/BzAPI/Extension.pm | 71 +
.../BzAPI/template/en/default/config.json.tmpl | 317 +++
extensions/ComponentWatching/Config.pm | 26 +
extensions/ComponentWatching/Extension.pm | 343 +++
.../ComponentWatching/reset-watch-preferences.pl | 75 +
.../account/prefs/component_watch.html.tmpl | 148 ++
.../account/prefs/email-relationships.html.tmpl | 22 +
.../hook/account/prefs/prefs-tabs.html.tmpl | 27 +
.../default/hook/global/reason-descs-end.none.tmpl | 22 +
extensions/GuidedBugEntry/Config.pm | 33 +
extensions/GuidedBugEntry/Extension.pm | 123 +
.../en/default/bug/create/comment-guided.txt.tmpl | 25 +
.../template/en/default/guided/guided.html.tmpl | 534 +++++
.../template/en/default/guided/products.html.tmpl | 63 +
.../en/default/pages/guided_products.js.tmpl | 36 +
extensions/GuidedBugEntry/web/images/advanced.png | Bin 0 -> 720 bytes
extensions/GuidedBugEntry/web/images/help.png | Bin 0 -> 786 bytes
extensions/GuidedBugEntry/web/images/idea.png | Bin 0 -> 2799 bytes
extensions/GuidedBugEntry/web/images/input.png | Bin 0 -> 5545 bytes
extensions/GuidedBugEntry/web/images/message.png | Bin 0 -> 1497 bytes
.../GuidedBugEntry/web/images/products/camino.png | Bin 0 -> 6060 bytes
.../GuidedBugEntry/web/images/products/core.png | Bin 0 -> 7497 bytes
.../GuidedBugEntry/web/images/products/dino.png | Bin 0 -> 3375 bytes
.../GuidedBugEntry/web/images/products/fennec.png | Bin 0 -> 9023 bytes
.../GuidedBugEntry/web/images/products/firefox.png | Bin 0 -> 9395 bytes
.../GuidedBugEntry/web/images/products/labs.png | Bin 0 -> 4085 bytes
.../GuidedBugEntry/web/images/products/mozilla.png | Bin 0 -> 10808 bytes
.../GuidedBugEntry/web/images/products/other.png | Bin 0 -> 6654 bytes
.../web/images/products/seamonkey.png | Bin 0 -> 5255 bytes
.../GuidedBugEntry/web/images/products/sunbird.png | Bin 0 -> 10462 bytes
.../web/images/products/thunderbird.png | Bin 0 -> 9939 bytes
extensions/GuidedBugEntry/web/images/sumo.png | Bin 0 -> 3517 bytes
extensions/GuidedBugEntry/web/images/support.png | Bin 0 -> 2409 bytes
extensions/GuidedBugEntry/web/images/throbber.gif | Bin 0 -> 723 bytes
extensions/GuidedBugEntry/web/images/warning.png | Bin 0 -> 1428 bytes
extensions/GuidedBugEntry/web/js/guided.js | 824 +++++++
extensions/GuidedBugEntry/web/js/products.js | 117 +
extensions/GuidedBugEntry/web/style/guided.css | 231 ++
.../GuidedBugEntry/web/yui-history-iframe.txt | 0
extensions/InlineImages/Config.pm | 33 +
extensions/InlineImages/Extension.pm | 63 +
extensions/InlineImages/disabled | 0
.../hook/bug/comments-aftercomments.html.tmpl | 111 +
extensions/LimitedEmail/Config.pm | 38 +
extensions/LimitedEmail/Extension.pm | 60 +
extensions/LimitedEmail/disabled | 0
extensions/Profanivore/Config.pm | 35 +
extensions/Profanivore/Extension.pm | 86 +
extensions/Profanivore/README | 14 +
extensions/SecureMail/Config.pm | 41 +
extensions/SecureMail/Extension.pm | 326 +++
extensions/SecureMail/README | 8 +
.../account/email/encryption-required.txt.tmpl | 15 +
.../en/default/account/prefs/securemail.html.tmpl | 34 +
.../hook/account/prefs/prefs-tabs.html.tmpl | 28 +
.../hook/admin/groups/create-field.html.tmpl | 25 +
.../default/hook/admin/groups/edit-field.html.tmpl | 27 +
.../hook/global/user-error-errors.html.tmpl | 27 +
.../en/default/pages/securemail/help.html.tmpl | 99 +
extensions/SiteMapIndex/Config.pm | 36 +
extensions/SiteMapIndex/Extension.pm | 156 ++
extensions/SiteMapIndex/lib/Constants.pm | 47 +
extensions/SiteMapIndex/lib/Util.pm | 205 ++
extensions/SiteMapIndex/robots.txt | 9 +
.../hook/global/header-additional_header.html.tmpl | 23 +
.../hook/global/messages-messages.html.tmpl | 37 +
extensions/Splinter/Config.pm | 5 +
extensions/Splinter/Extension.pm | 137 ++
extensions/Splinter/lib/Config.pm | 46 +
extensions/Splinter/lib/Util.pm | 143 ++
.../en/default/admin/params/splinter.html.tmpl | 38 +
.../default/hook/attachment/edit-action.html.tmpl | 31 +
.../default/hook/attachment/list-action.html.tmpl | 31 +
.../hook/global/user-error-errors.html.tmpl | 5 +
.../hook/request/email-after_summary.txt.tmpl | 6 +
.../hook/request/queue-after_column.html.tmpl | 8 +
.../template/en/default/pages/splinter.html.tmpl | 264 +++
.../en/default/pages/splinter/help.html.tmpl | 153 ++
extensions/Splinter/web/splinter.css | 402 ++++
extensions/Splinter/web/splinter.js | 2479 ++++++++++++++++++++
extensions/TagNewUsers/Config.pm | 33 +
extensions/TagNewUsers/Extension.pm | 226 ++
.../hook/bug/comments-comment_banner.html.tmpl | 25 +
.../en/default/hook/bug/comments-user.html.tmpl | 37 +
extensions/TagNewUsers/web/style.css | 16 +
extensions/TypeSniffer/Config.pm | 40 +
extensions/TypeSniffer/Extension.pm | 74 +
t/001compile.t | 5 +
t/008filter.t | 2 +-
t/012throwables.t | 4 +-
t/Support/Files.pm | 5 +-
180 files changed, 17874 insertions(+), 4 deletions(-)
create mode 100644 Bugzilla/UserAgent.pm
create mode 100644 extensions/BMO/Config.pm
create mode 100644 extensions/BMO/Extension.pm
create mode 100644 extensions/BMO/lib/Data.pm
create mode 100644 extensions/BMO/lib/FakeBug.pm
create mode 100644 extensions/BMO/lib/Reports.pm
create mode 100644 extensions/BMO/lib/WebService.pm
create mode 100644 extensions/BMO/template/en/default/account/create.html.tmpl
create mode 100644 extensions/BMO/template/en/default/bug/create/comment-mktgevent.txt.tmpl
create mode 100644 extensions/BMO/template/en/default/bug/create/comment-mozlist.txt.tmpl
create mode 100644 extensions/BMO/template/en/default/bug/create/comment-mozreps.txt.tmpl
create mode 100644 extensions/BMO/template/en/default/bug/create/comment-remo-budget.txt.tmpl
create mode 100644 extensions/BMO/template/en/default/bug/create/comment-remo-swag.txt.tmpl
create mode 100644 extensions/BMO/template/en/default/bug/create/comment-swag.txt.tmpl
create mode 100644 extensions/BMO/template/en/default/bug/create/create-brownbag.html.tmpl
create mode 100644 extensions/BMO/template/en/default/bug/create/create-itrequest.html.tmpl
create mode 100644 extensions/BMO/template/en/default/bug/create/create-legal.html.tmpl
create mode 100644 extensions/BMO/template/en/default/bug/create/create-mktgevent.html.tmpl
create mode 100644 extensions/BMO/template/en/default/bug/create/create-mozlist.html.tmpl
create mode 100644 extensions/BMO/template/en/default/bug/create/create-mozpr.html.tmpl
create mode 100644 extensions/BMO/template/en/default/bug/create/create-mozreps.html.tmpl
create mode 100644 extensions/BMO/template/en/default/bug/create/create-poweredby.html.tmpl
create mode 100644 extensions/BMO/template/en/default/bug/create/create-presentation.html.tmpl
create mode 100644 extensions/BMO/template/en/default/bug/create/create-remo-budget.html.tmpl
create mode 100644 extensions/BMO/template/en/default/bug/create/create-remo-swag.html.tmpl
create mode 100644 extensions/BMO/template/en/default/bug/create/create-swag.html.tmpl
create mode 100644 extensions/BMO/template/en/default/bug/create/create-trademark.html.tmpl
create mode 100644 extensions/BMO/template/en/default/bug/create/created-mozreps.html.tmpl
create mode 100644 extensions/BMO/template/en/default/bug/create/user-message.html.tmpl
create mode 100644 extensions/BMO/template/en/default/global/choose-product.html.tmpl
create mode 100644 extensions/BMO/template/en/default/hook/attachment/createformcontents-mimetypes.html.tmpl
create mode 100644 extensions/BMO/template/en/default/hook/attachment/createformcontents-patch_notes.html.tmpl
create mode 100644 extensions/BMO/template/en/default/hook/bug/comments-comment_banner.html.tmpl
create mode 100644 extensions/BMO/template/en/default/hook/bug/create/create-form.html.tmpl
create mode 100644 extensions/BMO/template/en/default/hook/bug/create/create-guided-form.html.tmpl
create mode 100644 extensions/BMO/template/en/default/hook/bug/edit-after_custom_fields.html.tmpl
create mode 100644 extensions/BMO/template/en/default/hook/bug/field-help-end.none.tmpl
create mode 100644 extensions/BMO/template/en/default/hook/bug/show-header-end.html.tmpl
create mode 100644 extensions/BMO/template/en/default/hook/global/footer-outro.html.tmpl
create mode 100644 extensions/BMO/template/en/default/hook/global/header-additional_header.html.tmpl
create mode 100644 extensions/BMO/template/en/default/hook/global/header-start.html.tmpl
create mode 100644 extensions/BMO/template/en/default/hook/global/setting-descs-settings.none.tmpl
create mode 100644 extensions/BMO/template/en/default/hook/global/user-error-errors.html.tmpl
create mode 100644 extensions/BMO/template/en/default/hook/global/user-error.html.tmpl/auth_failure/permissions.html.tmpl
create mode 100644 extensions/BMO/template/en/default/hook/global/variables-end.none.tmpl
create mode 100644 extensions/BMO/template/en/default/hook/index-additional_links.html.tmpl
create mode 100644 extensions/BMO/template/en/default/hook/index-intro.html.tmpl
create mode 100644 extensions/BMO/template/en/default/hook/pages/fields-resolutions.html.tmpl
create mode 100644 extensions/BMO/template/en/default/hook/reports/menu-end.html.tmpl
create mode 100644 extensions/BMO/template/en/default/list/list.microsummary.tmpl
create mode 100644 extensions/BMO/template/en/default/list/server-push.html.tmpl
create mode 100644 extensions/BMO/template/en/default/pages/bug-writing.html.tmpl
create mode 100644 extensions/BMO/template/en/default/pages/comment-remo-form-payment.txt.tmpl
create mode 100644 extensions/BMO/template/en/default/pages/etiquette.html.tmpl
create mode 100644 extensions/BMO/template/en/default/pages/get_help.html.tmpl
create mode 100644 extensions/BMO/template/en/default/pages/remo-form-payment.html.tmpl
create mode 100644 extensions/BMO/template/en/default/pages/triage_reports.html.tmpl
create mode 100644 extensions/BMO/template/en/default/pages/upgrade-3.6.html.tmpl
create mode 100644 extensions/BMO/template/en/default/pages/user_activity.html.tmpl
create mode 100644 extensions/BMO/template/en/default/search/search-plugin.xml.tmpl
create mode 100644 extensions/BMO/web/images/background.png
create mode 100644 extensions/BMO/web/images/bugzilla.png
create mode 100644 extensions/BMO/web/images/groups/bugzilla-approvers.png
create mode 100644 extensions/BMO/web/images/groups/calendar-drivers.png
create mode 100644 extensions/BMO/web/images/mozchomp.gif
create mode 100644 extensions/BMO/web/images/presshat.png
create mode 100644 extensions/BMO/web/images/stop-sign.gif
create mode 100644 extensions/BMO/web/js/edit_bug.js
create mode 100644 extensions/BMO/web/js/form_validate.js
create mode 100644 extensions/BMO/web/js/sorttable.js
create mode 100644 extensions/BMO/web/js/swag.js
create mode 100644 extensions/BMO/web/js/triage_reports.js
create mode 100644 extensions/BMO/web/producticons/camino.png
create mode 100644 extensions/BMO/web/producticons/dino.png
create mode 100644 extensions/BMO/web/producticons/fennec.png
create mode 100644 extensions/BMO/web/producticons/firefox.png
create mode 100644 extensions/BMO/web/producticons/idea.png
create mode 100644 extensions/BMO/web/producticons/input.png
create mode 100644 extensions/BMO/web/producticons/labs.png
create mode 100644 extensions/BMO/web/producticons/mozilla.png
create mode 100644 extensions/BMO/web/producticons/other.png
create mode 100644 extensions/BMO/web/producticons/seamonkey.png
create mode 100644 extensions/BMO/web/producticons/sunbird.png
create mode 100644 extensions/BMO/web/producticons/thunderbird.png
create mode 100644 extensions/BMO/web/styles/create_account.css
create mode 100644 extensions/BMO/web/styles/edit_bug.css
create mode 100644 extensions/BMO/web/styles/moz_reps.css
create mode 100644 extensions/BMO/web/styles/triage_reports.css
create mode 100644 extensions/BzAPI/Config.pm
create mode 100644 extensions/BzAPI/Extension.pm
create mode 100644 extensions/BzAPI/template/en/default/config.json.tmpl
create mode 100644 extensions/ComponentWatching/Config.pm
create mode 100644 extensions/ComponentWatching/Extension.pm
create mode 100755 extensions/ComponentWatching/reset-watch-preferences.pl
create mode 100644 extensions/ComponentWatching/template/en/default/account/prefs/component_watch.html.tmpl
create mode 100644 extensions/ComponentWatching/template/en/default/hook/account/prefs/email-relationships.html.tmpl
create mode 100644 extensions/ComponentWatching/template/en/default/hook/account/prefs/prefs-tabs.html.tmpl
create mode 100644 extensions/ComponentWatching/template/en/default/hook/global/reason-descs-end.none.tmpl
create mode 100644 extensions/GuidedBugEntry/Config.pm
create mode 100644 extensions/GuidedBugEntry/Extension.pm
create mode 100644 extensions/GuidedBugEntry/template/en/default/bug/create/comment-guided.txt.tmpl
create mode 100644 extensions/GuidedBugEntry/template/en/default/guided/guided.html.tmpl
create mode 100644 extensions/GuidedBugEntry/template/en/default/guided/products.html.tmpl
create mode 100644 extensions/GuidedBugEntry/template/en/default/pages/guided_products.js.tmpl
create mode 100644 extensions/GuidedBugEntry/web/images/advanced.png
create mode 100644 extensions/GuidedBugEntry/web/images/help.png
create mode 100644 extensions/GuidedBugEntry/web/images/idea.png
create mode 100644 extensions/GuidedBugEntry/web/images/input.png
create mode 100644 extensions/GuidedBugEntry/web/images/message.png
create mode 100644 extensions/GuidedBugEntry/web/images/products/camino.png
create mode 100644 extensions/GuidedBugEntry/web/images/products/core.png
create mode 100644 extensions/GuidedBugEntry/web/images/products/dino.png
create mode 100644 extensions/GuidedBugEntry/web/images/products/fennec.png
create mode 100644 extensions/GuidedBugEntry/web/images/products/firefox.png
create mode 100644 extensions/GuidedBugEntry/web/images/products/labs.png
create mode 100644 extensions/GuidedBugEntry/web/images/products/mozilla.png
create mode 100644 extensions/GuidedBugEntry/web/images/products/other.png
create mode 100644 extensions/GuidedBugEntry/web/images/products/seamonkey.png
create mode 100644 extensions/GuidedBugEntry/web/images/products/sunbird.png
create mode 100644 extensions/GuidedBugEntry/web/images/products/thunderbird.png
create mode 100644 extensions/GuidedBugEntry/web/images/sumo.png
create mode 100644 extensions/GuidedBugEntry/web/images/support.png
create mode 100644 extensions/GuidedBugEntry/web/images/throbber.gif
create mode 100644 extensions/GuidedBugEntry/web/images/warning.png
create mode 100644 extensions/GuidedBugEntry/web/js/guided.js
create mode 100644 extensions/GuidedBugEntry/web/js/products.js
create mode 100644 extensions/GuidedBugEntry/web/style/guided.css
create mode 100644 extensions/GuidedBugEntry/web/yui-history-iframe.txt
create mode 100644 extensions/InlineImages/Config.pm
create mode 100644 extensions/InlineImages/Extension.pm
create mode 100644 extensions/InlineImages/disabled
create mode 100644 extensions/InlineImages/template/en/default/hook/bug/comments-aftercomments.html.tmpl
create mode 100644 extensions/LimitedEmail/Config.pm
create mode 100644 extensions/LimitedEmail/Extension.pm
create mode 100644 extensions/LimitedEmail/disabled
create mode 100644 extensions/Profanivore/Config.pm
create mode 100644 extensions/Profanivore/Extension.pm
create mode 100644 extensions/Profanivore/README
create mode 100644 extensions/SecureMail/Config.pm
create mode 100644 extensions/SecureMail/Extension.pm
create mode 100644 extensions/SecureMail/README
create mode 100644 extensions/SecureMail/template/en/default/account/email/encryption-required.txt.tmpl
create mode 100644 extensions/SecureMail/template/en/default/account/prefs/securemail.html.tmpl
create mode 100644 extensions/SecureMail/template/en/default/hook/account/prefs/prefs-tabs.html.tmpl
create mode 100644 extensions/SecureMail/template/en/default/hook/admin/groups/create-field.html.tmpl
create mode 100644 extensions/SecureMail/template/en/default/hook/admin/groups/edit-field.html.tmpl
create mode 100644 extensions/SecureMail/template/en/default/hook/global/user-error-errors.html.tmpl
create mode 100644 extensions/SecureMail/template/en/default/pages/securemail/help.html.tmpl
create mode 100644 extensions/SiteMapIndex/Config.pm
create mode 100644 extensions/SiteMapIndex/Extension.pm
create mode 100644 extensions/SiteMapIndex/lib/Constants.pm
create mode 100644 extensions/SiteMapIndex/lib/Util.pm
create mode 100644 extensions/SiteMapIndex/robots.txt
create mode 100644 extensions/SiteMapIndex/template/en/default/hook/global/header-additional_header.html.tmpl
create mode 100644 extensions/SiteMapIndex/template/en/default/hook/global/messages-messages.html.tmpl
create mode 100644 extensions/Splinter/Config.pm
create mode 100644 extensions/Splinter/Extension.pm
create mode 100644 extensions/Splinter/lib/Config.pm
create mode 100644 extensions/Splinter/lib/Util.pm
create mode 100644 extensions/Splinter/template/en/default/admin/params/splinter.html.tmpl
create mode 100644 extensions/Splinter/template/en/default/hook/attachment/edit-action.html.tmpl
create mode 100644 extensions/Splinter/template/en/default/hook/attachment/list-action.html.tmpl
create mode 100644 extensions/Splinter/template/en/default/hook/global/user-error-errors.html.tmpl
create mode 100644 extensions/Splinter/template/en/default/hook/request/email-after_summary.txt.tmpl
create mode 100644 extensions/Splinter/template/en/default/hook/request/queue-after_column.html.tmpl
create mode 100644 extensions/Splinter/template/en/default/pages/splinter.html.tmpl
create mode 100644 extensions/Splinter/template/en/default/pages/splinter/help.html.tmpl
create mode 100644 extensions/Splinter/web/splinter.css
create mode 100644 extensions/Splinter/web/splinter.js
create mode 100644 extensions/TagNewUsers/Config.pm
create mode 100644 extensions/TagNewUsers/Extension.pm
create mode 100644 extensions/TagNewUsers/template/en/default/hook/bug/comments-comment_banner.html.tmpl
create mode 100644 extensions/TagNewUsers/template/en/default/hook/bug/comments-user.html.tmpl
create mode 100644 extensions/TagNewUsers/web/style.css
create mode 100644 extensions/TypeSniffer/Config.pm
create mode 100644 extensions/TypeSniffer/Extension.pm
diff --git a/Bugzilla/UserAgent.pm b/Bugzilla/UserAgent.pm
new file mode 100644
index 000000000..c0cda2586
--- /dev/null
+++ b/Bugzilla/UserAgent.pm
@@ -0,0 +1,247 @@
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+#
+# 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 the Bugzilla Bug Tracking System.
+#
+# The Initial Developer of the Original Code is the Mozilla Foundation
+# Portions created by the Initial Developer are Copyright (C) 2011 the
+# Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+# Terry Weissman
+# Dave Miller
+# Joe Robins
+# Gervase Markham
+# Shane H. W. Travis
+# Nitish Bezzala
+# Byron Jones
+
+package Bugzilla::UserAgent;
+
+use strict;
+use base qw(Exporter);
+our @EXPORT = qw(detect_platform detect_op_sys);
+
+use Bugzilla::Field;
+use List::MoreUtils qw(natatime);
+
+use constant DEFAULT_VALUE => 'Other';
+
+use constant PLATFORMS_MAP => (
+ # PowerPC
+ qr/\(.*PowerPC.*\)/i => ["PowerPC", "Macintosh"],
+ # AMD64, Intel x86_64
+ qr/\(.*[ix0-9]86 (?:on |\()x86_64.*\)/ => ["IA32", "x86", "PC"],
+ qr/\(.*amd64.*\)/ => ["AMD64", "x86_64", "PC"],
+ qr/\(.*x86_64.*\)/ => ["AMD64", "x86_64", "PC"],
+ # Intel IA64
+ qr/\(.*IA64.*\)/ => ["IA64", "PC"],
+ # Intel x86
+ qr/\(.*Intel.*\)/ => ["IA32", "x86", "PC"],
+ qr/\(.*[ix0-9]86.*\)/ => ["IA32", "x86", "PC"],
+ # Versions of Windows that only run on Intel x86
+ qr/\(.*Win(?:dows |)[39M].*\)/ => ["IA32", "x86", "PC"],
+ qr/\(.*Win(?:dows |)16.*\)/ => ["IA32", "x86", "PC"],
+ # Sparc
+ qr/\(.*sparc.*\)/ => ["Sparc", "Sun"],
+ qr/\(.*sun4.*\)/ => ["Sparc", "Sun"],
+ # Alpha
+ qr/\(.*AXP.*\)/i => ["Alpha", "DEC"],
+ qr/\(.*[ _]Alpha.\D/i => ["Alpha", "DEC"],
+ qr/\(.*[ _]Alpha\)/i => ["Alpha", "DEC"],
+ # MIPS
+ qr/\(.*IRIX.*\)/i => ["MIPS", "SGI"],
+ qr/\(.*MIPS.*\)/i => ["MIPS", "SGI"],
+ # 68k
+ qr/\(.*68K.*\)/ => ["68k", "Macintosh"],
+ qr/\(.*680[x0]0.*\)/ => ["68k", "Macintosh"],
+ # HP
+ qr/\(.*9000.*\)/ => ["PA-RISC", "HP"],
+ # ARM
+ qr/\(.*ARM.*\)/ => ["ARM", "PocketPC"],
+ # PocketPC intentionally before PowerPC
+ qr/\(.*Windows CE.*PPC.*\)/ => ["ARM", "PocketPC"],
+ # PowerPC
+ qr/\(.*PPC.*\)/ => ["PowerPC", "Macintosh"],
+ qr/\(.*AIX.*\)/ => ["PowerPC", "Macintosh"],
+ # Stereotypical and broken
+ qr/\(.*Windows CE.*\)/ => ["ARM", "PocketPC"],
+ qr/\(.*Macintosh.*\)/ => ["68k", "Macintosh"],
+ qr/\(.*Mac OS [89].*\)/ => ["68k", "Macintosh"],
+ qr/\(.*WOW64.*\)/ => ["x86_64"],
+ qr/\(.*Win64.*\)/ => ["IA64"],
+ qr/\(Win.*\)/ => ["IA32", "x86", "PC"],
+ qr/\(.*Win(?:dows[ -])NT.*\)/ => ["IA32", "x86", "PC"],
+ qr/\(.*OSF.*\)/ => ["Alpha", "DEC"],
+ qr/\(.*HP-?UX.*\)/i => ["PA-RISC", "HP"],
+ qr/\(.*IRIX.*\)/i => ["MIPS", "SGI"],
+ qr/\(.*(SunOS|Solaris).*\)/ => ["Sparc", "Sun"],
+ # Braindead old browsers who didn't follow convention:
+ qr/Amiga/ => ["68k", "Macintosh"],
+ qr/WinMosaic/ => ["IA32", "x86", "PC"],
+);
+
+use constant OS_MAP => (
+ # Sun
+ qr/\(.*Solaris.*\)/ => ["Solaris"],
+ qr/\(.*SunOS 5.11.*\)/ => [("OpenSolaris", "Opensolaris", "Solaris 11")],
+ qr/\(.*SunOS 5.10.*\)/ => ["Solaris 10"],
+ qr/\(.*SunOS 5.9.*\)/ => ["Solaris 9"],
+ qr/\(.*SunOS 5.8.*\)/ => ["Solaris 8"],
+ qr/\(.*SunOS 5.7.*\)/ => ["Solaris 7"],
+ qr/\(.*SunOS 5.6.*\)/ => ["Solaris 6"],
+ qr/\(.*SunOS 5.5.*\)/ => ["Solaris 5"],
+ qr/\(.*SunOS 5.*\)/ => ["Solaris"],
+ qr/\(.*SunOS.*sun4u.*\)/ => ["Solaris"],
+ qr/\(.*SunOS.*i86pc.*\)/ => ["Solaris"],
+ qr/\(.*SunOS.*\)/ => ["SunOS"],
+ # BSD
+ qr/\(.*BSD\/(?:OS|386).*\)/ => ["BSDI"],
+ qr/\(.*FreeBSD.*\)/ => ["FreeBSD"],
+ qr/\(.*OpenBSD.*\)/ => ["OpenBSD"],
+ qr/\(.*NetBSD.*\)/ => ["NetBSD"],
+ # Misc POSIX
+ qr/\(.*IRIX.*\)/ => ["IRIX"],
+ qr/\(.*OSF.*\)/ => ["OSF/1"],
+ qr/\(.*Linux.*\)/ => ["Linux"],
+ qr/\(.*BeOS.*\)/ => ["BeOS"],
+ qr/\(.*AIX.*\)/ => ["AIX"],
+ qr/\(.*OS\/2.*\)/ => ["OS/2"],
+ qr/\(.*QNX.*\)/ => ["Neutrino"],
+ qr/\(.*VMS.*\)/ => ["OpenVMS"],
+ qr/\(.*HP-?UX.*\)/ => ["HP-UX"],
+ # Windows
+ qr/\(.*Windows XP.*\)/ => ["Windows XP"],
+ qr/\(.*Windows NT 6\.1.*\)/ => ["Windows 7"],
+ qr/\(.*Windows NT 6\.0.*\)/ => ["Windows Vista"],
+ qr/\(.*Windows NT 5\.2.*\)/ => ["Windows Server 2003"],
+ qr/\(.*Windows NT 5\.1.*\)/ => ["Windows XP"],
+ qr/\(.*Windows 2000.*\)/ => ["Windows 2000"],
+ qr/\(.*Windows NT 5.*\)/ => ["Windows 2000"],
+ qr/\(.*Win.*9[8x].*4\.9.*\)/ => ["Windows ME"],
+ qr/\(.*Win(?:dows |)M[Ee].*\)/ => ["Windows ME"],
+ qr/\(.*Win(?:dows |)98.*\)/ => ["Windows 98"],
+ qr/\(.*Win(?:dows |)95.*\)/ => ["Windows 95"],
+ qr/\(.*Win(?:dows |)16.*\)/ => ["Windows 3.1"],
+ qr/\(.*Win(?:dows[ -]|)NT.*\)/ => ["Windows NT"],
+ qr/\(.*Windows.*NT.*\)/ => ["Windows NT"],
+ # OS X
+ qr/\(.*Mac OS X (?:|Mach-O |\()10.6.*\)/ => ["Mac OS X 10.6"],
+ qr/\(.*Mac OS X (?:|Mach-O |\()10.5.*\)/ => ["Mac OS X 10.5"],
+ qr/\(.*Mac OS X (?:|Mach-O |\()10.4.*\)/ => ["Mac OS X 10.4"],
+ qr/\(.*Mac OS X (?:|Mach-O |\()10.3.*\)/ => ["Mac OS X 10.3"],
+ qr/\(.*Mac OS X (?:|Mach-O |\()10.2.*\)/ => ["Mac OS X 10.2"],
+ qr/\(.*Mac OS X (?:|Mach-O |\()10.1.*\)/ => ["Mac OS X 10.1"],
+ # Unfortunately, OS X 10.4 was the first to support Intel. This is fallback
+ # support because some browsers refused to include the OS Version.
+ qr/\(.*Intel.*Mac OS X.*\)/ => ["Mac OS X 10.4"],
+ # OS X 10.3 is the most likely default version of PowerPC Macs
+ # OS X 10.0 is more for configurations which didn't setup 10.x versions
+ qr/\(.*Mac OS X.*\)/ => [("Mac OS X 10.3", "Mac OS X 10.0", "Mac OS X")],
+ qr/\(.*Mac OS 9.*\)/ => [("Mac System 9.x", "Mac System 9.0")],
+ qr/\(.*Mac OS 8\.6.*\)/ => [("Mac System 8.6", "Mac System 8.5")],
+ qr/\(.*Mac OS 8\.5.*\)/ => ["Mac System 8.5"],
+ qr/\(.*Mac OS 8\.1.*\)/ => [("Mac System 8.1", "Mac System 8.0")],
+ qr/\(.*Mac OS 8\.0.*\)/ => ["Mac System 8.0"],
+ qr/\(.*Mac OS 8[^.].*\)/ => ["Mac System 8.0"],
+ qr/\(.*Mac OS 8.*\)/ => ["Mac System 8.6"],
+ qr/\(.*Darwin.*\)/ => [("Mac OS X 10.0", "Mac OS X")],
+ # Silly
+ qr/\(.*Mac.*PowerPC.*\)/ => ["Mac System 9.x"],
+ qr/\(.*Mac.*PPC.*\)/ => ["Mac System 9.x"],
+ qr/\(.*Mac.*68k.*\)/ => ["Mac System 8.0"],
+ # Evil
+ qr/Amiga/i => ["Other"],
+ qr/WinMosaic/ => ["Windows 95"],
+ qr/\(.*32bit.*\)/ => ["Windows 95"],
+ qr/\(.*16bit.*\)/ => ["Windows 3.1"],
+ qr/\(.*PowerPC.*\)/ => ["Mac System 9.x"],
+ qr/\(.*PPC.*\)/ => ["Mac System 9.x"],
+ qr/\(.*68K.*\)/ => ["Mac System 8.0"],
+);
+
+sub detect_platform {
+ my $userAgent = $ENV{'HTTP_USER_AGENT'};
+ my @detected;
+ my $iterator = natatime(2, PLATFORMS_MAP);
+ while (my($re, $ra) = $iterator->()) {
+ if ($userAgent =~ $re) {
+ push @detected, @$ra;
+ }
+ }
+ return _pick_valid_field_value('rep_platform', @detected);
+}
+
+sub detect_op_sys {
+ my $userAgent = $ENV{'HTTP_USER_AGENT'};
+ my @detected;
+ my $iterator = natatime(2, OS_MAP);
+ while (my($re, $ra) = $iterator->()) {
+ if ($userAgent =~ $re) {
+ push @detected, @$ra;
+ }
+ }
+ push(@detected, "Windows") if grep(/^Windows /, @detected);
+ push(@detected, "Mac OS") if grep(/^Mac /, @detected);
+ return _pick_valid_field_value('op_sys', @detected);
+}
+
+# Takes the name of a field and a list of possible values for that field.
+# Returns the first value in the list that is actually a valid value for that
+# field.
+# Returns 'Other' if none of the values match.
+sub _pick_valid_field_value {
+ my ($field, @values) = @_;
+ foreach my $value (@values) {
+ return $value if check_field($field, $value, undef, 1);
+ }
+ return DEFAULT_VALUE;
+}
+
+1;
+
+__END__
+
+=head1 NAME
+
+Bugzilla::UserAgent - UserAgent utilities for Bugzilla
+
+=head1 SYNOPSIS
+
+ use Bugzilla::UserAgent;
+ printf "platform: %s op-sys: %s\n", detect_platform(), detect_op_sys();
+
+=head1 DESCRIPTION
+
+The functions exported by this module all return information derived from the
+remote client's user agent.
+
+=head1 FUNCTIONS
+
+=over 4
+
+=item C
+
+This function attempts to detect the remote client's platform from the
+presented user-agent. If a suitable value on the I field is found,
+that field value will be returned. If no suitable value is detected,
+C returns I.
+
+=item C
+
+This function attempts to detect the remote client's operating system from the
+presented user-agent. If a suitable value on the I field is found, that
+field value will be returned. If no suitable value is detected,
+C returns I.
+
+=back
+
diff --git a/extensions/BMO/Config.pm b/extensions/BMO/Config.pm
new file mode 100644
index 000000000..0ad817768
--- /dev/null
+++ b/extensions/BMO/Config.pm
@@ -0,0 +1,38 @@
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+#
+# 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 the BMO Bugzilla Extension.
+#
+# The Initial Developer of the Original Code is Gervase Markham
+# Portions created by the Initial Developer are Copyright (C) 2010 the
+# Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+# Gervase Markham
+
+package Bugzilla::Extension::BMO;
+use strict;
+
+use constant NAME => 'BMO';
+
+use constant REQUIRED_MODULES => [
+ {
+ package => 'Tie-IxHash',
+ module => 'Tie::IxHash',
+ version => 0
+ }
+];
+
+use constant OPTIONAL_MODULES => [
+];
+
+__PACKAGE__->NAME;
diff --git a/extensions/BMO/Extension.pm b/extensions/BMO/Extension.pm
new file mode 100644
index 000000000..f9ee38563
--- /dev/null
+++ b/extensions/BMO/Extension.pm
@@ -0,0 +1,794 @@
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+#
+# 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 the BMO Bugzilla Extension.
+#
+# The Initial Developer of the Original Code is Gervase Markham.
+# Portions created by the Initial Developer are Copyright (C) 2010 the
+# Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+# Gervase Markham
+# David Lawrence
+# Byron Jones
+
+package Bugzilla::Extension::BMO;
+use strict;
+use base qw(Bugzilla::Extension);
+
+use Bugzilla::Field;
+use Bugzilla::Constants;
+use Bugzilla::Status;
+use Bugzilla::User;
+use Bugzilla::User::Setting;
+use Bugzilla::Util qw(html_quote trick_taint trim datetime_from detaint_natural);
+use Bugzilla::Token;
+use Bugzilla::Error;
+use Bugzilla::Mailer;
+
+use Scalar::Util qw(blessed);
+use Date::Parse;
+use DateTime;
+
+use Bugzilla::Extension::BMO::FakeBug;
+use Bugzilla::Extension::BMO::Data qw($cf_visible_in_products
+ $cf_flags
+ %group_to_cc_map
+ $blocking_trusted_setters
+ $blocking_trusted_requesters
+ $status_trusted_wanters
+ $status_trusted_setters
+ $other_setters
+ %always_fileable_group
+ %product_sec_groups);
+use Bugzilla::Extension::BMO::Reports qw(user_activity_report
+ triage_reports);
+
+our $VERSION = '0.1';
+
+#
+# Monkey-patched methods
+#
+
+BEGIN {
+ *Bugzilla::Bug::last_closed_date = \&_last_closed_date;
+}
+
+sub template_before_process {
+ my ($self, $args) = @_;
+ my $file = $args->{'file'};
+ my $vars = $args->{'vars'};
+
+ $vars->{'cf_hidden_in_product'} = \&cf_hidden_in_product;
+
+ if ($file =~ /^list\/list/) {
+ # Purpose: enable correct sorting of list table
+ # Matched to changes in list/table.html.tmpl
+ my %db_order_column_name_map = (
+ 'map_components.name' => 'component',
+ 'map_products.name' => 'product',
+ 'map_reporter.login_name' => 'reporter',
+ 'map_assigned_to.login_name' => 'assigned_to',
+ 'delta_ts' => 'opendate',
+ 'creation_ts' => 'changeddate',
+ );
+
+ my @orderstrings = split(/,\s*/, $vars->{'order'});
+
+ # contains field names of the columns being used to sort the table.
+ my @order_columns;
+ foreach my $o (@orderstrings) {
+ $o =~ s/bugs.//;
+ $o = $db_order_column_name_map{$o} if
+ grep($_ eq $o, keys(%db_order_column_name_map));
+ next if (grep($_ eq $o, @order_columns));
+ push(@order_columns, $o);
+ }
+
+ $vars->{'order_columns'} = \@order_columns;
+
+ # fields that have a custom sortkey. (So they are correctly sorted
+ # when using js)
+ my @sortkey_fields = qw(bug_status resolution bug_severity priority
+ rep_platform op_sys);
+
+ my %columns_sortkey;
+ foreach my $field (@sortkey_fields) {
+ $columns_sortkey{$field} = _get_field_values_sort_key($field);
+ }
+ $columns_sortkey{'target_milestone'} = _get_field_values_sort_key('milestones');
+
+ $vars->{'columns_sortkey'} = \%columns_sortkey;
+ }
+ elsif ($file =~ /^bug\/create\/create[\.-]/) {
+ if (!$vars->{'cloned_bug_id'}) {
+ # Allow status whiteboard values to be bookmarked
+ $vars->{'status_whiteboard'} =
+ Bugzilla->cgi->param('status_whiteboard') || "";
+ }
+
+ # Purpose: for pretty product chooser
+ $vars->{'format'} = Bugzilla->cgi->param('format');
+
+ # Data needed for "this is a security bug" checkbox
+ $vars->{'sec_groups'} = \%product_sec_groups;
+ }
+
+
+ if ($file =~ /^list\/list/ || $file =~ /^bug\/create\/create[\.-]/) {
+ # hack to allow the bug entry templates to use check_can_change_field
+ # to see if various field values should be available to the current user.
+ $vars->{'default'} = Bugzilla::Extension::BMO::FakeBug->new($vars->{'default'} || {});
+ }
+}
+
+sub page_before_template {
+ my ($self, $args) = @_;
+ my $page = $args->{'page_id'};
+ my $vars = $args->{'vars'};
+
+ if ($page eq 'user_activity.html') {
+ user_activity_report($vars);
+
+ } elsif ($page eq 'triage_reports.html') {
+ triage_reports($vars);
+
+ } elsif ($page eq 'upgrade-3.6.html') {
+ $vars->{'bzr_history'} = sub {
+ return `cd /data/www/bugzilla.mozilla.org; /usr/bin/bzr log -n0 -rlast:10..`;
+ };
+ }
+ elsif ($page eq 'fields.html') {
+ $vars->{'fields_page'} = 1;
+ }
+ elsif ($page eq 'remo-form-payment.html') {
+ _remo_form_payment($vars);
+ }
+}
+
+sub _get_field_values_sort_key {
+ my ($field) = @_;
+ my $dbh = Bugzilla->dbh;
+ my $fields = $dbh->selectall_arrayref(
+ "SELECT value, sortkey FROM $field
+ ORDER BY sortkey, value");
+
+ my %field_values;
+ foreach my $field (@$fields) {
+ my ($value, $sortkey) = @$field;
+ $field_values{$value} = $sortkey;
+ }
+ return \%field_values;
+}
+
+sub cf_hidden_in_product {
+ my ($field_name, $product_name, $component_name, $custom_flag_mode) = @_;
+
+ # If used in buglist.cgi, we pass in one_product which is a Bugzilla::Product
+ # elsewhere, we just pass the name of the product.
+ $product_name = blessed($product_name) ? $product_name->name
+ : $product_name;
+
+ # Also in buglist.cgi, we pass in a list of components instead
+ # of a single compoent name everywhere else.
+ my $component_list = ref $component_name ? $component_name
+ : [ $component_name ];
+
+ if ($custom_flag_mode) {
+ if ($custom_flag_mode == 1) {
+ # skip custom flags
+ foreach my $flag_re (@$cf_flags) {
+ return 1 if $field_name =~ $flag_re;
+ }
+ } elsif ($custom_flag_mode == 2) {
+ # custom flags only
+ my $found = 0;
+ foreach my $flag_re (@$cf_flags) {
+ if ($field_name =~ $flag_re) {
+ $found = 1;
+ last;
+ }
+ }
+ return 1 unless $found;
+ }
+ }
+
+ foreach my $field_re (keys %$cf_visible_in_products) {
+ if ($field_name =~ $field_re) {
+ # If no product given, for example more than one product
+ # in buglist.cgi, then hide field by default
+ return 1 if !$product_name;
+
+ my $products = $cf_visible_in_products->{$field_re};
+ foreach my $product (keys %$products) {
+ my $components = $products->{$product};
+
+ my $found_component = 0;
+ if (@$components) {
+ foreach my $component (@$components) {
+ if (grep($_ eq $component, @$component_list)) {
+ $found_component = 1;
+ last;
+ }
+ }
+ }
+
+ # If product matches and at at least one component matches
+ # from component_list (if a matching component was required),
+ # we allow the field to be seen
+ if ($product eq $product_name && (!@$components || $found_component)) {
+ return 0;
+ }
+ }
+
+ return 1;
+ }
+ }
+
+ return 0;
+}
+
+# Purpose: CC certain email addresses on bugmail when a bug is added or
+# removed from a particular group.
+sub bugmail_recipients {
+ my ($self, $args) = @_;
+ my $bug = $args->{'bug'};
+ my $recipients = $args->{'recipients'};
+ my $diffs = $args->{'diffs'};
+
+ if (@$diffs) {
+ # Changed bug
+ foreach my $ref (@$diffs) {
+ my ($who, $whoname, $what, $when,
+ $old, $new, $attachid, $fieldname) = (@$ref);
+
+ if ($fieldname eq "bug_group") {
+ _cc_if_special_group($old, $recipients);
+ _cc_if_special_group($new, $recipients);
+ }
+ }
+ } else {
+ # Determine if it's a new bug, or a comment without a field change
+ my $comment_count = scalar @{$bug->comments};
+ if ($comment_count == 1) {
+ # New bug
+ foreach my $group (@{ $bug->groups_in }) {
+ _cc_if_special_group($group->{'name'}, $recipients);
+ }
+ }
+ }
+}
+
+sub _cc_if_special_group {
+ my ($group, $recipients) = @_;
+
+ return if !$group;
+
+ if ($group_to_cc_map{$group}) {
+ my $id = login_to_id($group_to_cc_map{$group});
+ $recipients->{$id}->{+REL_CC} = Bugzilla::BugMail::BIT_DIRECT();
+ }
+}
+
+sub _check_trusted {
+ my ($field, $trusted, $priv_results) = @_;
+
+ my $needed_group = $trusted->{'_default'} || "";
+ foreach my $dfield (keys %$trusted) {
+ if ($field =~ $dfield) {
+ $needed_group = $trusted->{$dfield};
+ }
+ }
+ if ($needed_group && !Bugzilla->user->in_group($needed_group)) {
+ push (@$priv_results, PRIVILEGES_REQUIRED_EMPOWERED);
+ }
+}
+
+sub _is_field_set {
+ my $value = shift;
+ return $value ne '---' && $value ne '?';
+}
+
+sub bug_check_can_change_field {
+ my ($self, $args) = @_;
+ my $bug = $args->{'bug'};
+ my $field = $args->{'field'};
+ my $new_value = $args->{'new_value'};
+ my $old_value = $args->{'old_value'};
+ my $priv_results = $args->{'priv_results'};
+ my $user = Bugzilla->user;
+
+ # Only users in the appropriate drivers group can change the
+ # cf_blocking_* fields or cf_tracking_* fields
+
+ if ($field =~ /^cf_(?:blocking|tracking)_/) {
+ # 0 -> 1 is used by show_bug, always allow so we skip this whole part
+ if (!($old_value eq '0' && $new_value eq '1')) {
+ # require privileged access to set a flag
+ if (_is_field_set($new_value)) {
+ _check_trusted($field, $blocking_trusted_setters, $priv_results);
+ }
+
+ # require editbugs to clear or re-nominate a set flag
+ elsif (_is_field_set($old_value)
+ && !$user->in_group('editbugs', $bug->{'product_id'}))
+ {
+ push (@$priv_results, PRIVILEGES_REQUIRED_EMPOWERED);
+ }
+ }
+
+ if ($new_value eq '?') {
+ _check_trusted($field, $blocking_trusted_requesters, $priv_results);
+ }
+ if ($user->id) {
+ push (@$priv_results, PRIVILEGES_REQUIRED_NONE);
+ }
+
+ } elsif ($field =~ /^cf_status_/) {
+ # Only drivers can set wanted.
+ if ($new_value eq 'wanted') {
+ _check_trusted($field, $status_trusted_wanters, $priv_results);
+ } elsif (_is_field_set($new_value)) {
+ _check_trusted($field, $status_trusted_setters, $priv_results);
+ }
+ if ($user->id) {
+ push (@$priv_results, PRIVILEGES_REQUIRED_NONE);
+ }
+
+ } elsif ($field =~ /^cf/ && !@$priv_results && $new_value ne '---') {
+ # "other" custom field setters restrictions
+ if (exists $other_setters->{$field}) {
+ my $in_group = 0;
+ foreach my $group (@{$other_setters->{$field}}) {
+ if ($user->in_group($group, $bug->product_id)) {
+ $in_group = 1;
+ last;
+ }
+ }
+ if (!$in_group) {
+ push (@$priv_results, PRIVILEGES_REQUIRED_EMPOWERED);
+ }
+ }
+
+ } elsif ($field eq 'resolution' && $new_value eq 'EXPIRED') {
+ # The EXPIRED resolution should only be settable by gerv.
+ if ($user->login ne 'gerv@mozilla.org') {
+ push (@$priv_results, PRIVILEGES_REQUIRED_EMPOWERED);
+ }
+
+ } elsif ($field eq 'resolution' && $new_value eq 'FIXED') {
+ # You need at least canconfirm to mark a bug as FIXED
+ if (!$user->in_group('canconfirm', $bug->{'product_id'})) {
+ push (@$priv_results, PRIVILEGES_REQUIRED_EMPOWERED);
+ }
+
+ } elsif ($user->in_group('canconfirm', $bug->{'product_id'})) {
+ # Canconfirm is really "cantriage"; users with canconfirm can also mark
+ # bugs as DUPLICATE, WORKSFORME, and INCOMPLETE.
+ if ($field eq 'bug_status'
+ && is_open_state($old_value)
+ && !is_open_state($new_value))
+ {
+ push (@$priv_results, PRIVILEGES_REQUIRED_NONE);
+ }
+ elsif ($field eq 'resolution' &&
+ ($new_value eq 'DUPLICATE' ||
+ $new_value eq 'WORKSFORME' ||
+ $new_value eq 'INCOMPLETE'))
+ {
+ push (@$priv_results, PRIVILEGES_REQUIRED_NONE);
+ }
+
+ } elsif ($field eq 'bug_status') {
+ # Disallow reopening of bugs which have been resolved for > 1 year
+ if (is_open_state($new_value)
+ && !is_open_state($old_value)
+ && $bug->resolution eq 'FIXED')
+ {
+ my $days_ago = DateTime->now(time_zone => Bugzilla->local_timezone);
+ $days_ago->subtract(days => 365);
+ my $last_closed = datetime_from($bug->last_closed_date);
+ if ($last_closed lt $days_ago) {
+ push (@$priv_results, PRIVILEGES_REQUIRED_EMPOWERED);
+ }
+ }
+ }
+}
+
+# Purpose: link up various Mozilla-specific strings.
+sub _link_uuid {
+ my $args = shift;
+ my $match = html_quote($args->{matches}->[0]);
+
+ return qq{bp-$match};
+}
+
+sub _link_cve {
+ my $args = shift;
+ my $match = html_quote($args->{matches}->[0]);
+
+ return qq{$match};
+}
+
+sub _link_svn {
+ my $args = shift;
+ my $match = html_quote($args->{matches}->[0]);
+
+ return qq{r$match};
+}
+
+sub _link_hg {
+ my $args = shift;
+ my $text = html_quote($args->{matches}->[0]);
+ my $repo = html_quote($args->{matches}->[1]);
+ my $id = html_quote($args->{matches}->[2]);
+
+ return qq{$text};
+}
+
+sub bug_format_comment {
+ my ($self, $args) = @_;
+ my $regexes = $args->{'regexes'};
+
+ # Only match if not already in an URL using the negative lookbehind (? qr/(? \&_link_uuid
+ });
+
+ push (@$regexes, {
+ match => qr/(? \&_link_cve
+ });
+
+ push (@$regexes, {
+ match => qr/\b((?:CVE|CAN)-\d{4}-\d{4})\b/,
+ replace => \&_link_cve
+ });
+
+ push (@$regexes, {
+ match => qr/\br(\d{4,})\b/,
+ replace => \&_link_svn
+ });
+
+ # Note: for grouping in this regexp, always use non-capturing parentheses.
+ my $hgrepos = join('|', qw!(?:releases/)?comm-[\w.]+
+ (?:releases/)?mozilla-[\w.]+
+ (?:releases/)?mobile-[\w.]+
+ tracemonkey
+ tamarin-[\w.]+
+ camino!);
+
+ push (@$regexes, {
+ match => qr/\b(($hgrepos)\s+changeset:?\s+(?:\d+:)?([0-9a-fA-F]{12}))\b/,
+ replace => \&_link_hg
+ });
+}
+
+# Purpose: make it always possible to file bugs in certain groups.
+sub bug_check_groups {
+ my ($self, $args) = @_;
+ my $group_names = $args->{'group_names'};
+ my $add_groups = $args->{'add_groups'};
+
+ $group_names = ref $group_names
+ ? $group_names
+ : [ map { trim($_) } split(',', $group_names) ];
+
+ foreach my $name (@$group_names) {
+ if ($always_fileable_group{$name}) {
+ my $group = new Bugzilla::Group({ name => $name }) or next;
+ $add_groups->{$group->id} = $group;
+ }
+ }
+}
+
+# Purpose: generically handle generating pretty blocking/status "flags" from
+# custom field names.
+sub quicksearch_map {
+ my ($self, $args) = @_;
+ my $map = $args->{'map'};
+
+ foreach my $name (keys %$map) {
+ if ($name =~ /^cf_(blocking|tracking|status)_([a-z]+)?(\d+)?$/) {
+ my $type = $1;
+ my $product = $2;
+ my $version = $3;
+
+ if ($version) {
+ $version = join('.', split(//, $version));
+ }
+
+ my $pretty_name = $type;
+ if ($product) {
+ $pretty_name .= "-" . $product;
+ }
+ if ($version) {
+ $pretty_name .= $version;
+ }
+
+ $map->{$pretty_name} = $name;
+ }
+ elsif ($name =~ /cf_crash_signature$/) {
+ $map->{'sig'} = $name;
+ }
+ }
+}
+
+# Restrict content types attachable by non-privileged people
+my @mimetype_whitelist = ('^image\/', 'application\/pdf');
+
+sub object_end_of_create_validators {
+ my ($self, $args) = @_;
+ my $class = $args->{'class'};
+
+ if ($class->isa('Bugzilla::Attachment')) {
+ my $params = $args->{'params'};
+ my $bug = $params->{'bug'};
+ if (!Bugzilla->user->in_group('editbugs', $bug->product_id)) {
+ my $mimetype = $params->{'mimetype'};
+ if (!grep { $mimetype =~ /$_/ } @mimetype_whitelist ) {
+ # Need to neuter MIME type to something non-executable
+ if ($mimetype =~ /^text\//) {
+ $params->{'mimetype'} = "text/plain";
+ }
+ else {
+ $params->{'mimetype'} = "application/octet-stream";
+ }
+ }
+ }
+ }
+}
+
+sub install_before_final_checks {
+ my ($self, $args) = @_;
+
+ # Add product chooser setting (although it was added long ago, so add_setting
+ # will just return every time).
+ add_setting('product_chooser',
+ ['pretty_product_chooser', 'full_product_chooser'],
+ 'pretty_product_chooser');
+
+ # Migrate from 'gmail_threading' setting to 'bugmail_new_prefix'
+ my $dbh = Bugzilla->dbh;
+ if ($dbh->selectrow_array("SELECT 1 FROM setting WHERE name='gmail_threading'")) {
+ $dbh->bz_start_transaction();
+ $dbh->do("UPDATE profile_setting
+ SET setting_value='on-temp'
+ WHERE setting_name='gmail_threading' AND setting_value='Off'");
+ $dbh->do("UPDATE profile_setting
+ SET setting_value='off'
+ WHERE setting_name='gmail_threading' AND setting_value='On'");
+ $dbh->do("UPDATE profile_setting
+ SET setting_value='on'
+ WHERE setting_name='gmail_threading' AND setting_value='on-temp'");
+ $dbh->do("UPDATE profile_setting
+ SET setting_name='bugmail_new_prefix'
+ WHERE setting_name='gmail_threading'");
+ $dbh->do("DELETE FROM setting WHERE name='gmail_threading'");
+ $dbh->bz_commit_transaction();
+ }
+}
+
+# Migrate old is_active stuff to new patch (is in core in 4.2), The old column
+# name was 'is_active', the new one is 'isactive' (no underscore).
+sub install_update_db {
+ my $dbh = Bugzilla->dbh;
+
+ if ($dbh->bz_column_info('milestones', 'is_active')) {
+ $dbh->do("UPDATE milestones SET isactive = 0 WHERE is_active = 0;");
+ $dbh->bz_drop_column('milestones', 'is_active');
+ $dbh->bz_drop_column('milestones', 'is_searchable');
+ }
+}
+
+sub _remo_form_payment {
+ my ($vars) = @_;
+ my $input = Bugzilla->input_params;
+
+ my $user = Bugzilla->login(LOGIN_REQUIRED);
+
+ if ($input->{'action'} eq 'commit') {
+ my $template = Bugzilla->template;
+ my $cgi = Bugzilla->cgi;
+ my $dbh = Bugzilla->dbh;
+
+ my $bug_id = $input->{'bug_id'};
+ detaint_natural($bug_id);
+ my $bug = Bugzilla::Bug->check($bug_id);
+
+ # Detect if the user already used the same form to submit again
+ my $token = trim($input->{'token'});
+ if ($token) {
+ my ($creator_id, $date, $old_attach_id) = Bugzilla::Token::GetTokenData($token);
+ if (!$creator_id
+ || $creator_id != $user->id
+ || $old_attach_id !~ "^remo_form_payment:")
+ {
+ # The token is invalid.
+ ThrowUserError('token_does_not_exist');
+ }
+
+ $old_attach_id =~ s/^remo_form_payment://;
+ if ($old_attach_id) {
+ ThrowUserError('remo_payment_cancel_dupe',
+ { bugid => $bug_id, attachid => $old_attach_id });
+ }
+ }
+
+ # Make sure the user can attach to this bug
+ if (!$bug->user->{'canedit'}) {
+ ThrowUserError("remo_payment_bug_edit_denied",
+ { bug_id => $bug->id });
+ }
+
+ # Make sure the bug is under the correct product/component
+ if ($bug->product ne 'Mozilla Reps'
+ || $bug->component ne 'Budget Requests')
+ {
+ ThrowUserError('remo_payment_invalid_product');
+ }
+
+ my ($timestamp) = $dbh->selectrow_array("SELECT NOW()");
+
+ $dbh->bz_start_transaction;
+
+ # Create the comment to be added based on the form fields from rep-payment-form
+ my $comment;
+ $template->process("pages/comment-remo-form-payment.txt.tmpl", $vars, \$comment)
+ || ThrowTemplateError($template->error());
+ $bug->add_comment($comment, { isprivate => 0 });
+
+ # Attach expense report
+ # FIXME: Would be nice to be able to have the above prefilled comment and
+ # the following attachments all show up under a single comment. But the longdescs
+ # table can only handle one attach_id per comment currently. At least only one
+ # email is sent the way it is done below.
+ my $attachment;
+ if (defined $cgi->upload('expenseform')) {
+ # Determine content-type
+ my $content_type = $cgi->uploadInfo($cgi->param('expenseform'))->{'Content-Type'};
+
+ $attachment = Bugzilla::Attachment->create(
+ { bug => $bug,
+ creation_ts => $timestamp,
+ data => $cgi->upload('expenseform'),
+ description => 'Expense Form',
+ filename => scalar $cgi->upload('expenseform'),
+ ispatch => 0,
+ isprivate => 0,
+ isurl => 0,
+ mimetype => $content_type,
+ store_in_file => 0,
+ });
+
+ # Insert comment for attachment
+ $bug->add_comment('', { isprivate => 0,
+ type => CMT_ATTACHMENT_CREATED,
+ extra_data => $attachment->id });
+ }
+
+ # Attach receipts file
+ if (defined $cgi->upload("receipts")) {
+ # Determine content-type
+ my $content_type = $cgi->uploadInfo($cgi->param("receipts"))->{'Content-Type'};
+
+ $attachment = Bugzilla::Attachment->create(
+ { bug => $bug,
+ creation_ts => $timestamp,
+ data => $cgi->upload('receipts'),
+ description => "Receipts",
+ filename => scalar $cgi->upload("receipts"),
+ ispatch => 0,
+ isprivate => 0,
+ isurl => 0,
+ mimetype => $content_type,
+ store_in_file => 0,
+ });
+
+ # Insert comment for attachment
+ $bug->add_comment('', { isprivate => 0,
+ type => CMT_ATTACHMENT_CREATED,
+ extra_data => $attachment->id });
+ }
+
+ $bug->update($timestamp);
+
+ if ($token) {
+ trick_taint($token);
+ $dbh->do('UPDATE tokens SET eventdata = ? WHERE token = ?', undef,
+ ("remo_form_payment:" . $attachment->id, $token));
+ }
+
+ $dbh->bz_commit_transaction;
+
+ # Define the variables and functions that will be passed to the UI template.
+ $vars->{'attachment'} = $attachment;
+ $vars->{'bugs'} = [ new Bugzilla::Bug($bug_id) ];
+ $vars->{'header_done'} = 1;
+ $vars->{'contenttypemethod'} = 'autodetect';
+
+ my $recipients = { 'changer' => $user };
+ $vars->{'sent_bugmail'} = Bugzilla::BugMail::Send($bug_id, $recipients);
+
+ print $cgi->header();
+ # Generate and return the UI (HTML page) from the appropriate template.
+ $template->process("attachment/created.html.tmpl", $vars)
+ || ThrowTemplateError($template->error());
+ exit;
+ }
+ else {
+ $vars->{'token'} = issue_session_token('remo_form_payment:');
+ }
+}
+
+sub _last_closed_date {
+ my ($self) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ return $self->{'last_closed_date'} if defined $self->{'last_closed_date'};
+
+ my $closed_statuses = "'" . join("','", map { $_->name } closed_bug_statuses()) . "'";
+ my $status_field_id = get_field_id('bug_status');
+
+ $self->{'last_closed_date'} = $dbh->selectrow_array("
+ SELECT bugs_activity.bug_when
+ FROM bugs_activity
+ WHERE bugs_activity.fieldid = ?
+ AND bugs_activity.added IN ($closed_statuses)
+ AND bugs_activity.bug_id = ?
+ ORDER BY bugs_activity.bug_when DESC " . $dbh->sql_limit(1),
+ undef, $status_field_id, $self->id
+ );
+
+ return $self->{'last_closed_date'};
+}
+
+sub field_end_of_create {
+ my ($self, $args) = @_;
+ my $field = $args->{'field'};
+
+ # email mozilla's DBAs so they can update the grants for metrics
+ # this really should create a bug in mozilla.org/Server Operations: Database
+
+ if (Bugzilla->params->{'urlbase'} ne 'https://bugzilla.mozilla.org/') {
+ return;
+ }
+
+ if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) {
+ print "Emailing notification to infra-dbnotices\@mozilla.com\n";
+ }
+
+ my $name = $field->name;
+ my @message;
+ push @message, 'To: infra-dbnotices@mozilla.com';
+ push @message, "Subject: custom field '$name' added to bugzilla.mozilla.org";
+ push @message, 'From: ' . Bugzilla->params->{mailfrom};
+ push @message, '';
+ push @message, "The custom field '$name' has been added to the BMO database.";
+ push @message, '';
+ push @message, 'Please run the following on tm-bugs01-master01:';
+ push @message, " GRANT SELECT ON `bugs`.`$name` TO 'metrics'\@'10.2.70.20_';";
+ push @message, " GRANT SELECT ($name) ON `bugs`.`bugs` TO 'metrics'\@'10.2.70.20_';";
+ push @message, '';
+ MessageToMTA(join("\n", @message));
+}
+
+sub webservice {
+ my ($self, $args) = @_;
+
+ my $dispatch = $args->{dispatch};
+ $dispatch->{BMO} = "Bugzilla::Extension::BMO::WebService";
+}
+
+__PACKAGE__->NAME;
diff --git a/extensions/BMO/lib/Data.pm b/extensions/BMO/lib/Data.pm
new file mode 100644
index 000000000..17b84a37e
--- /dev/null
+++ b/extensions/BMO/lib/Data.pm
@@ -0,0 +1,239 @@
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+#
+# 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 the BMO Bugzilla Extension.
+#
+# The Initial Developer of the Original Code is the Mozilla Foundation.
+# Portions created by the Initial Developer are Copyright (C) 2010 the
+# Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+# Gervase Markham
+# Reed Loden
+
+package Bugzilla::Extension::BMO::Data;
+use strict;
+
+use base qw(Exporter);
+use Tie::IxHash;
+
+our @EXPORT_OK = qw($cf_visible_in_products
+ $cf_flags
+ %group_to_cc_map
+ $blocking_trusted_setters
+ $blocking_trusted_requesters
+ $status_trusted_wanters
+ $status_trusted_setters
+ $other_setters
+ %always_fileable_group
+ %product_sec_groups);
+
+# Which custom fields are visible in which products and components.
+#
+# By default, custom fields are visible in all products. However, if the name
+# of the field matches any of these regexps, it is only visible if the
+# product (and component if necessary) is a member of the attached hash. []
+# for component means "all".
+#
+# IxHash keeps them in insertion order, and so we get regexp priorities right.
+our $cf_visible_in_products;
+tie(%$cf_visible_in_products, "Tie::IxHash",
+ qr/^cf_blocking_fennec/ => {
+ "addons.mozilla.org" => [],
+ "AUS" => [],
+ "Core" => [],
+ "Fennec" => [],
+ "mozilla.org" => ["Release Engineering"],
+ "Mozilla Services" => [],
+ "NSPR" => [],
+ "support.mozilla.com" => [],
+ "Toolkit" => [],
+ "Tech Evangelism" => [],
+ "Mozilla Localizations" => [],
+ },
+ qr/^cf_tracking_thunderbird|cf_blocking_thunderbird|cf_status_thunderbird/ => {
+ "support.mozillamessaging.com" => [],
+ "Thunderbird" => [],
+ "MailNews Core" => [],
+ "Mozilla Messaging" => [],
+ "Websites" => ["www.mozillamessaging.com"],
+ },
+ qr/^(cf_(blocking|tracking)_seamonkey|cf_status_seamonkey)/ => {
+ "Composer" => [],
+ "MailNews Core" => [],
+ "Mozilla Localizations" => [],
+ "Other Applications" => [],
+ "SeaMonkey" => [],
+ },
+ qr/^cf_blocking_|cf_tracking_|cf_status/ => {
+ "Add-on SDK" => [],
+ "addons.mozilla.org" => [],
+ "AUS" => [],
+ "Camino" => [],
+ "Core Graveyard" => [],
+ "Core" => [],
+ "Directory" => [],
+ "Fennec" => [],
+ "Firefox" => [],
+ "MailNews Core" => [],
+ "mozilla.org" => ["Release Engineering"],
+ "Mozilla Localizations" => [],
+ "Mozilla Services" => [],
+ "NSPR" => [],
+ "NSS" => [],
+ "Other Applications" => [],
+ "SeaMonkey" => [],
+ "support.mozilla.com" => [],
+ "Tech Evangelism" => [],
+ "Testing" => [],
+ "Toolkit" => [],
+ "Websites" => ["getpersonas.com"],
+ "Webtools" => [],
+ "Plugins" => [],
+ },
+ qr/^cf_colo_site$/ => {
+ "mozilla.org" => [
+ "Server Operations",
+ "Server Operations: Projects",
+ "Server Operations: RelEng",
+ "Server Operations: Security",
+ ],
+ },
+ qr/^cf_crash_signature$/ => {
+ "addons.mozilla.org" => [],
+ "Add-on SDK" => [],
+ "Calendar" => [],
+ "Camino" => [],
+ "Composer" => [],
+ "Fennec" => [],
+ "Firefox" => [],
+ "Mozilla Localizations" => [],
+ "Mozilla Services" => [],
+ "Other Applications" => [],
+ "Penelope" => [],
+ "SeaMonkey" => [],
+ "Thunderbird" => [],
+ "Core" => [],
+ "Directory" => [],
+ "JSS" => [],
+ "MailNews Core" => [],
+ "NSPR" => [],
+ "NSS" => [],
+ "Plugins" => [],
+ "Rhino" => [],
+ "Tamarin" => [],
+ "Testing" => [],
+ "Toolkit" => [],
+ "Mozilla Labs" => [],
+ "mozilla.org" => [],
+ "Tech Evangelism" => [],
+ },
+);
+
+# Which custom fields are acting as flags (ie. custom flags)
+our $cf_flags = [
+ qr/^cf_(?:blocking|tracking|status)_/,
+];
+
+# Who to CC on particular bugmails when certain groups are added or removed.
+our %group_to_cc_map = (
+ 'bugzilla-security' => 'security@bugzilla.org',
+ 'client-services-security' => 'amo-admins@mozilla.org',
+ 'core-security' => 'security@mozilla.org',
+ 'tamarin-security' => 'tamarinsecurity@adobe.com',
+ 'websites-security' => 'website-drivers@mozilla.org',
+ 'webtools-security' => 'webtools-security@mozilla.org',
+);
+
+# Only users in certain groups can change certain custom fields in
+# certain ways.
+#
+# Who can set cf_blocking_* or cf_tracking_* to +/-
+our $blocking_trusted_setters = {
+ 'cf_blocking_fennec' => 'fennec-drivers',
+ 'cf_blocking_20' => 'mozilla-next-drivers',
+ qr/^cf_tracking_firefox/ => 'mozilla-next-drivers',
+ qr/^cf_blocking_thunderbird/ => 'thunderbird-drivers',
+ qr/^cf_tracking_thunderbird/ => 'thunderbird-drivers',
+ qr/^cf_tracking_seamonkey/ => 'seamonkey-council',
+ qr/^cf_blocking_seamonkey/ => 'seamonkey-council',
+ '_default' => 'mozilla-stable-branch-drivers',
+};
+
+# Who can request cf_blocking_* or cf_tracking_*
+our $blocking_trusted_requesters = {
+ qr/^cf_blocking_thunderbird/ => 'thunderbird-trusted-requesters',
+ '_default' => 'everyone',
+};
+
+# Who can set cf_status_* to "wanted"?
+our $status_trusted_wanters = {
+ 'cf_status_20' => 'mozilla-next-drivers',
+ qr/^cf_status_thunderbird/ => 'thunderbird-drivers',
+ qr/^cf_status_seamonkey/ => 'seamonkey-council',
+ '_default' => 'mozilla-stable-branch-drivers',
+};
+
+# Who can set cf_status_* to values other than "wanted"?
+our $status_trusted_setters = {
+ qr/^cf_status_thunderbird/ => 'editbugs',
+ '_default' => 'canconfirm',
+};
+
+# Who can set other custom flags (use full field names only, not regex's)
+our $other_setters = {
+ 'cf_colo_site' => ['infra', 'build'],
+};
+
+# Groups in which you can always file a bug, whoever you are.
+our %always_fileable_group = (
+ 'bugzilla-security' => 1,
+ 'client-services-security' => 1,
+ 'consulting' => 1,
+ 'core-security' => 1,
+ 'infra' => 1,
+ 'marketing-private' => 1,
+ 'mozilla-confidential' => 1,
+ 'mozilla-corporation-confidential' => 1,
+ 'mozilla-messaging-confidential' => 1,
+ 'tamarin-security' => 1,
+ 'websites-security' => 1,
+ 'webtools-security' => 1,
+);
+
+# Mapping of products to their security bits
+our %product_sec_groups = (
+ "mozilla.org" => 'mozilla-confidential',
+ "Webtools" => 'webtools-security',
+ "Marketing" => 'marketing-private',
+ "addons.mozilla.org" => 'client-services-security',
+ "AUS" => 'client-services-security',
+ "Mozilla Services" => 'client-services-security',
+ "Mozilla Corporation" => 'mozilla-corporation-confidential',
+ "Mozilla Metrics" => 'metrics-private',
+ "Legal" => 'legal',
+ "Mozilla Messaging" => 'mozilla-messaging-confidential',
+ "Websites" => 'websites-security',
+ "Mozilla Developer Network" => 'websites-security',
+ "support.mozilla.com" => 'websites-security',
+ "quality.mozilla.org" => 'websites-security',
+ "Skywriter" => 'websites-security',
+ "support.mozillamessaging.com" => 'websites-security',
+ "Bugzilla" => 'bugzilla-security',
+ "bugzilla.mozilla.org" => 'bugzilla-security',
+ "Testopia" => 'bugzilla-security',
+ "Tamarin" => 'tamarin-security',
+ "Mozilla PR" => 'pr-private',
+ "_default" => 'core-security'
+);
+
+1;
diff --git a/extensions/BMO/lib/FakeBug.pm b/extensions/BMO/lib/FakeBug.pm
new file mode 100644
index 000000000..d8cebe379
--- /dev/null
+++ b/extensions/BMO/lib/FakeBug.pm
@@ -0,0 +1,42 @@
+package Bugzilla::Extension::BMO::FakeBug;
+
+use strict;
+
+# hack to allow the bug entry templates to use check_can_change_field to see if
+# various field values should be available to the current user
+
+use Bugzilla::Bug;
+
+our $AUTOLOAD;
+
+sub new {
+ my $class = shift;
+ my $self = shift;
+ bless $self, $class;
+ return $self;
+}
+
+sub AUTOLOAD {
+ my $self = shift;
+ my $name = $AUTOLOAD;
+ $name =~ s/.*://;
+ return exists $self->{$name} ? $self->{$name} : undef;
+}
+
+sub check_can_change_field {
+ my $self = shift;
+ return Bugzilla::Bug::check_can_change_field($self, @_)
+}
+
+sub _changes_everconfirmed {
+ my $self = shift;
+ return Bugzilla::Bug::_changes_everconfirmed($self, @_)
+}
+
+sub everconfirmed {
+ my $self = shift;
+ return ($self->{'status'} == 'UNCONFIRMED') ? 0 : 1;
+}
+
+1;
+
diff --git a/extensions/BMO/lib/Reports.pm b/extensions/BMO/lib/Reports.pm
new file mode 100644
index 000000000..d1f979beb
--- /dev/null
+++ b/extensions/BMO/lib/Reports.pm
@@ -0,0 +1,476 @@
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+#
+# 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 the BMO Bugzilla Extension.
+#
+# The Initial Developer of the Original Code is Byron Jones. Portions created
+# by the Initial Developer are Copyright (C) 2011 the Mozilla Foundation. All
+# Rights Reserved.
+#
+# Contributor(s):
+# Byron Jones
+
+package Bugzilla::Extension::BMO::Reports;
+use strict;
+
+use Bugzilla::User;
+use Bugzilla::Util qw(trim detaint_natural);
+use Bugzilla::Error;
+use Bugzilla::Constants;
+
+use Date::Parse;
+use DateTime;
+
+use base qw(Exporter);
+
+our @EXPORT_OK = qw(user_activity_report
+ triage_reports);
+
+sub user_activity_report {
+ my ($vars) = @_;
+ my $dbh = Bugzilla->dbh;
+ my $input = Bugzilla->input_params;
+
+ my @who = ();
+ my $from = trim($input->{'from'});
+ my $to = trim($input->{'to'});
+
+ if ($input->{'action'} eq 'run') {
+ if ($input->{'who'} eq '') {
+ ThrowUserError('user_activity_missing_username');
+ }
+ Bugzilla::User::match_field({ 'who' => {'type' => 'multi'} });
+
+ ThrowUserError('user_activity_missing_from_date') unless $from;
+ my $from_time = str2time($from)
+ or ThrowUserError('user_activity_invalid_date', { date => $from });
+ my $from_dt = DateTime->from_epoch(epoch => $from_time)
+ ->set_time_zone('local')
+ ->truncate(to => 'day');
+ $from = $from_dt->ymd();
+
+ ThrowUserError('user_activity_missing_to_date') unless $to;
+ my $to_time = str2time($to)
+ or ThrowUserError('user_activity_invalid_date', { date => $to });
+ my $to_dt = DateTime->from_epoch(epoch => $to_time)
+ ->set_time_zone('local')
+ ->truncate(to => 'day');
+ $to = $to_dt->ymd();
+ # add one day to include all activity that happened on the 'to' date
+ $to_dt->add(days => 1);
+
+ my ($activity_joins, $activity_where) = ('', '');
+ my ($attachments_joins, $attachments_where) = ('', '');
+ if (Bugzilla->params->{"insidergroup"}
+ && !Bugzilla->user->in_group(Bugzilla->params->{'insidergroup'}))
+ {
+ $activity_joins = "LEFT JOIN attachments
+ ON attachments.attach_id = bugs_activity.attach_id";
+ $activity_where = "AND COALESCE(attachments.isprivate, 0) = 0";
+ $attachments_where = $activity_where;
+ }
+
+ my @who_bits;
+ foreach my $who (
+ ref $input->{'who'}
+ ? @{$input->{'who'}}
+ : $input->{'who'}
+ ) {
+ push @who, $who;
+ push @who_bits, '?';
+ }
+ my $who_bits = join(',', @who_bits);
+
+ if (!@who) {
+ my $template = Bugzilla->template;
+ my $cgi = Bugzilla->cgi;
+ my $vars = {};
+ $vars->{'script'} = $cgi->url(-relative => 1);
+ $vars->{'fields'} = {};
+ $vars->{'matches'} = [];
+ $vars->{'matchsuccess'} = 0;
+ $vars->{'matchmultiple'} = 1;
+ print $cgi->header();
+ $template->process("global/confirm-user-match.html.tmpl", $vars)
+ || ThrowTemplateError($template->error());
+ exit;
+ }
+
+ $from_dt = $from_dt->ymd() . ' 00:00:00';
+ $to_dt = $to_dt->ymd() . ' 23:59:59';
+ my @params;
+ for (1..4) {
+ push @params, @who;
+ push @params, ($from_dt, $to_dt);
+ }
+
+ my $comment_filter = '';
+ if (!Bugzilla->user->is_insider) {
+ $comment_filter = 'AND longdescs.isprivate = 0';
+ }
+
+ my $query = "
+ SELECT
+ fielddefs.name,
+ bugs_activity.bug_id,
+ bugs_activity.attach_id,
+ ".$dbh->sql_date_format('bugs_activity.bug_when', '%Y.%m.%d %H:%i:%s')." AS ts,
+ bugs_activity.removed,
+ bugs_activity.added,
+ profiles.login_name,
+ bugs_activity.comment_id,
+ bugs_activity.bug_when
+ FROM bugs_activity
+ $activity_joins
+ LEFT JOIN fielddefs
+ ON bugs_activity.fieldid = fielddefs.id
+ INNER JOIN profiles
+ ON profiles.userid = bugs_activity.who
+ WHERE profiles.login_name IN ($who_bits)
+ AND bugs_activity.bug_when >= ? AND bugs_activity.bug_when <= ?
+ $activity_where
+
+ UNION ALL
+
+ SELECT
+ 'bug_id' AS name,
+ bugs.bug_id,
+ NULL AS attach_id,
+ ".$dbh->sql_date_format('bugs.creation_ts', '%Y.%m.%d %H:%i:%s')." AS ts,
+ '(new bug)' AS removed,
+ bugs.short_desc AS added,
+ profiles.login_name,
+ NULL AS comment_id,
+ bugs.creation_ts AS bug_when
+ FROM bugs
+ INNER JOIN profiles
+ ON profiles.userid = bugs.reporter
+ WHERE profiles.login_name IN ($who_bits)
+ AND bugs.creation_ts >= ? AND bugs.creation_ts <= ?
+
+ UNION ALL
+
+ SELECT
+ 'longdesc' AS name,
+ longdescs.bug_id,
+ NULL AS attach_id,
+ DATE_FORMAT(longdescs.bug_when, '%Y.%m.%d %H:%i:%s') AS ts,
+ '' AS removed,
+ '' AS added,
+ profiles.login_name,
+ longdescs.comment_id AS comment_id,
+ longdescs.bug_when
+ FROM longdescs
+ INNER JOIN profiles
+ ON profiles.userid = longdescs.who
+ WHERE profiles.login_name IN ($who_bits)
+ AND longdescs.bug_when >= ? AND longdescs.bug_when <= ?
+ $comment_filter
+
+ UNION ALL
+
+ SELECT
+ 'attachments.filename' AS name,
+ attachments.bug_id,
+ attachments.attach_id,
+ ".$dbh->sql_date_format('attachments.creation_ts', '%Y.%m.%d %H:%i:%s')." AS ts,
+ '' AS removed,
+ attachments.description AS added,
+ profiles.login_name,
+ NULL AS comment_id,
+ attachments.creation_ts AS bug_when
+ FROM attachments
+ INNER JOIN profiles
+ ON profiles.userid = attachments.submitter_id
+ WHERE profiles.login_name IN ($who_bits)
+ AND attachments.creation_ts >= ? AND attachments.creation_ts <= ?
+ $attachments_where
+
+ ORDER BY bug_when ";
+
+ my $list = $dbh->selectall_arrayref($query, undef, @params);
+
+ my @operations;
+ my $operation = {};
+ my $changes = [];
+ my $incomplete_data = 0;
+
+ foreach my $entry (@$list) {
+ my ($fieldname, $bugid, $attachid, $when, $removed, $added, $who,
+ $comment_id) = @$entry;
+ my %change;
+ my $activity_visible = 1;
+
+ next unless Bugzilla->user->can_see_bug($bugid);
+
+ # check if the user should see this field's activity
+ if ($fieldname eq 'remaining_time'
+ || $fieldname eq 'estimated_time'
+ || $fieldname eq 'work_time'
+ || $fieldname eq 'deadline')
+ {
+ $activity_visible = Bugzilla->user->is_timetracker;
+ }
+ elsif ($fieldname eq 'longdescs.isprivate'
+ && !Bugzilla->user->is_insider
+ && $added)
+ {
+ $activity_visible = 0;
+ }
+ else {
+ $activity_visible = 1;
+ }
+
+ if ($activity_visible) {
+ # Check for the results of an old Bugzilla data corruption bug
+ if (($added eq '?' && $removed eq '?')
+ || ($added =~ /^\? / || $removed =~ /^\? /)) {
+ $incomplete_data = 1;
+ }
+
+ # An operation, done by 'who' at time 'when', has a number of
+ # 'changes' associated with it.
+ # If this is the start of a new operation, store the data from the
+ # previous one, and set up the new one.
+ if ($operation->{'who'}
+ && ($who ne $operation->{'who'}
+ || $when ne $operation->{'when'}))
+ {
+ $operation->{'changes'} = $changes;
+ push (@operations, $operation);
+ $operation = {};
+ $changes = [];
+ }
+
+ $operation->{'bug'} = $bugid;
+ $operation->{'who'} = $who;
+ $operation->{'when'} = $when;
+
+ $change{'fieldname'} = $fieldname;
+ $change{'attachid'} = $attachid;
+ $change{'removed'} = $removed;
+ $change{'added'} = $added;
+
+ if ($comment_id) {
+ $change{'comment'} = Bugzilla::Comment->new($comment_id);
+ next if $change{'comment'}->count == 0;
+ }
+
+ push (@$changes, \%change);
+ }
+ }
+
+ if ($operation->{'who'}) {
+ $operation->{'changes'} = $changes;
+ push (@operations, $operation);
+ }
+
+ $vars->{'incomplete_data'} = $incomplete_data;
+ $vars->{'operations'} = \@operations;
+
+ } else {
+
+ if ($from eq '') {
+ my ($yy, $mm) = (localtime)[5, 4];
+ $from = sprintf("%4d-%02d-01", $yy + 1900, $mm + 1);
+ }
+ if ($to eq '') {
+ my ($yy, $mm, $dd) = (localtime)[5, 4, 3];
+ $to = sprintf("%4d-%02d-%02d", $yy + 1900, $mm + 1, $dd);
+ }
+ }
+
+ $vars->{'action'} = $input->{'action'};
+ $vars->{'who'} = join(',', @who);
+ $vars->{'from'} = $from;
+ $vars->{'to'} = $to;
+}
+
+sub triage_reports {
+ my ($vars, $filter) = @_;
+ my $dbh = Bugzilla->dbh;
+ my $input = Bugzilla->input_params;
+ my $user = Bugzilla->user;
+
+ if ($input->{'action'} eq 'run' && $input->{'product'}) {
+
+ # load product and components from input
+
+ my $product = Bugzilla::Product->new({ name => $input->{'product'} });
+
+ my @component_ids;
+ if ($input->{'component'} ne '') {
+ my $ra_components = ref($input->{'component'})
+ ? $input->{'component'} : [ $input->{'component'} ];
+ foreach my $component_name (@$ra_components) {
+ my $component = Bugzilla::Component->new({ name => $component_name, product => $product });
+ push @component_ids, $component->id;
+ }
+ }
+
+ # determine which comment filters to run
+
+ my $filter_commenter = $input->{'filter_commenter'};
+ my $filter_commenter_on = $input->{'commenter'};
+ my $filter_commenter_id;
+ if ($filter_commenter && $filter_commenter_on eq 'is') {
+ Bugzilla::User::match_field({ 'commenter_is' => {'type' => 'single'} });
+ my $user = Bugzilla::User->new({ name => $input->{'commenter_is'} });
+ $filter_commenter_id = $user ? $user->id : 0;
+ }
+
+ my $filter_last = $input->{'filter_last'};
+ my $filter_last_period = $input->{'last'};
+ my $filter_last_time;
+ if ($filter_last) {
+ if ($filter_last_period eq 'is') {
+ $filter_last_period = -1;
+ $filter_last_time = str2time($input->{'last_is'} . " 00:00:00") || 0;
+ } else {
+ detaint_natural($filter_last_period);
+ $filter_last_period = 14 if $filter_last_period < 14;
+ }
+ }
+ my $now = (time);
+ $filter_commenter = 1 unless $filter_commenter || $filter_last;
+
+ # form sql queries
+
+ my $bugs_sql = "
+ SELECT bug_id, short_desc, reporter, creation_ts
+ FROM bugs
+ WHERE product_id = ?
+ AND bug_status = 'UNCONFIRMED'";
+ if (@component_ids) {
+ $bugs_sql .= " AND component_id IN (" . join(',', @component_ids) . ")";
+ }
+ $bugs_sql .= "
+ ORDER BY creation_ts
+ ";
+
+ my $comment_count_sql = "
+ SELECT COUNT(*)
+ FROM longdescs
+ WHERE bug_id = ?
+ ";
+
+ my $comment_sql = "
+ SELECT who, bug_when, type, thetext, extra_data
+ FROM longdescs
+ WHERE bug_id = ?
+ ";
+ if (!Bugzilla->user->is_insider) {
+ $comment_sql .= " AND isprivate = 0 ";
+ }
+ $comment_sql .= "
+ ORDER BY bug_when DESC
+ LIMIT 1
+ ";
+
+ my $attach_sql = "
+ SELECT description, isprivate
+ FROM attachments
+ WHERE attach_id = ?
+ ";
+
+ # work on an initial list of bugs
+
+ my $list = $dbh->selectall_arrayref($bugs_sql, undef, $product->id);
+ my @bugs;
+
+ foreach my $entry (@$list) {
+ my ($bug_id, $summary, $reporter_id, $creation_ts) = @$entry;
+
+ next unless $user->can_see_bug($bug_id);
+
+ # get last comment information
+
+ my ($comment_count) = $dbh->selectrow_array($comment_count_sql, undef, $bug_id);
+ my ($commenter_id, $comment_ts, $type, $comment, $extra)
+ = $dbh->selectrow_array($comment_sql, undef, $bug_id);
+ my $commenter = 0;
+
+ # apply selected filters
+
+ if ($filter_commenter) {
+ next if $comment_count <= 1;
+
+ if ($filter_commenter_on eq 'reporter') {
+ next if $commenter_id != $reporter_id;
+
+ } elsif ($filter_commenter_on eq 'noconfirm') {
+ $commenter = Bugzilla::User->new($commenter_id);
+ next if $commenter_id != $reporter_id
+ || $commenter->in_group('canconfirm');
+
+ } elsif ($filter_commenter_on eq 'is') {
+ next if $commenter_id != $filter_commenter_id;
+ }
+ } else {
+ $input->{'commenter'} = '';
+ $input->{'commenter_is'} = '';
+ }
+
+ if ($filter_last) {
+ my $comment_time = str2time($comment_ts)
+ or next;
+ if ($filter_last_period == -1) {
+ next if $comment_time >= $filter_last_time;
+ } else {
+ next if $now - $comment_time <= 60 * 60 * 24 * $filter_last_period;
+ }
+ } else {
+ $input->{'last'} = '';
+ $input->{'last_is'} = '';
+ }
+
+ # get data for attachment comments
+
+ if ($comment eq '' && $type == CMT_ATTACHMENT_CREATED) {
+ my ($description, $is_private) = $dbh->selectrow_array($attach_sql, undef, $extra);
+ next if $is_private && !Bugzilla->user->is_insider;
+ $comment = "(Attachment) " . $description;
+ }
+
+ # truncate long comments
+
+ if (length($comment) > 80) {
+ $comment = substr($comment, 0, 80) . '...';
+ }
+
+ # build bug hash for template
+
+ my $bug = {};
+ $bug->{id} = $bug_id;
+ $bug->{summary} = $summary;
+ $bug->{reporter} = Bugzilla::User->new($reporter_id);
+ $bug->{creation_ts} = $creation_ts;
+ $bug->{commenter} = $commenter || Bugzilla::User->new($commenter_id);
+ $bug->{comment_ts} = $comment_ts;
+ $bug->{comment} = $comment;
+ $bug->{comment_count} = $comment_count;
+ push @bugs, $bug;
+ }
+
+ @bugs = sort { $b->{comment_ts} cmp $a->{comment_ts} } @bugs;
+
+ $vars->{bugs} = \@bugs;
+ } else {
+ $input->{action} = '';
+ }
+
+ if (!$input->{filter_commenter} && !$input->{filter_last}) {
+ $input->{filter_commenter} = 1;
+ }
+
+ $vars->{'input'} = $input;
+}
+
+1;
diff --git a/extensions/BMO/lib/WebService.pm b/extensions/BMO/lib/WebService.pm
new file mode 100644
index 000000000..95bdcbdf3
--- /dev/null
+++ b/extensions/BMO/lib/WebService.pm
@@ -0,0 +1,207 @@
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+#
+# 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 the BMO Bugzilla Extension.
+#
+# The Initial Developer of the Original Code is Mozilla Foundation. Portions created
+# by the Initial Developer are Copyright (C) 2011 the Mozilla Foundation. All
+# Rights Reserved.
+#
+# Contributor(s):
+# Dave Lawrence
+
+package Bugzilla::Extension::BMO::WebService;
+
+use strict;
+use warnings;
+
+use base qw(Bugzilla::WebService);
+
+use Bugzilla::Constants;
+use Bugzilla::Error;
+use Bugzilla::WebService::Util qw(validate);
+use Bugzilla::Field;
+
+sub getBugsConfirmer {
+ my ($self, $params) = validate(@_, 'names');
+ my $dbh = Bugzilla->dbh;
+
+ defined($params->{names})
+ || ThrowCodeError('params_required',
+ { function => 'BMO.getBugsConfirmer', params => ['names'] });
+
+ my @user_objects = map { Bugzilla::User->check($_) } @{ $params->{names} };
+
+ # start filtering to remove duplicate user ids
+ @user_objects = values %{{ map { $_->id => $_ } @user_objects }};
+
+ my $fieldid = get_field_id('bug_status');
+
+ my $query = "SELECT DISTINCT bugs_activity.bug_id
+ FROM bugs_activity
+ LEFT JOIN bug_group_map
+ ON bugs_activity.bug_id = bug_group_map.bug_id
+ WHERE bugs_activity.fieldid = ?
+ AND bugs_activity.added = 'NEW'
+ AND bugs_activity.removed = 'UNCONFIRMED'
+ AND bugs_activity.who = ?
+ AND bug_group_map.bug_id IS NULL
+ ORDER BY bugs_activity.bug_id";
+
+ my %users;
+ foreach my $user (@user_objects) {
+ my $bugs = $dbh->selectcol_arrayref($query, undef, $fieldid, $user->id);
+ $users{$user->login} = $bugs;
+ }
+
+ return \%users;
+}
+
+sub getBugsVerifier {
+ my ($self, $params) = validate(@_, 'names');
+ my $dbh = Bugzilla->dbh;
+
+ defined($params->{names})
+ || ThrowCodeError('params_required',
+ { function => 'BMO.getBugsVerifier', params => ['names'] });
+
+ my @user_objects = map { Bugzilla::User->check($_) } @{ $params->{names} };
+
+ # start filtering to remove duplicate user ids
+ @user_objects = values %{{ map { $_->id => $_ } @user_objects }};
+
+ my $fieldid = get_field_id('bug_status');
+
+ my $query = "SELECT DISTINCT bugs_activity.bug_id
+ FROM bugs_activity
+ LEFT JOIN bug_group_map
+ ON bugs_activity.bug_id = bug_group_map.bug_id
+ WHERE bugs_activity.fieldid = ?
+ AND bugs_activity.removed = 'RESOLVED'
+ AND bugs_activity.added = 'VERIFIED'
+ AND bugs_activity.who = ?
+ AND bug_group_map.bug_id IS NULL
+ ORDER BY bugs_activity.bug_id";
+
+ my %users;
+ foreach my $user (@user_objects) {
+ my $bugs = $dbh->selectcol_arrayref($query, undef, $fieldid, $user->id);
+ $users{$user->login} = $bugs;
+ }
+
+ return \%users;
+}
+
+1;
+
+__END__
+
+=head1 NAME
+
+Bugzilla::Extension::BMO::Webservice - The BMO WebServices API
+
+=head1 DESCRIPTION
+
+This module contains API methods that are useful to user's of bugzilla.mozilla.org.
+
+=head1 METHODS
+
+See L for a description of how parameters are passed,
+and what B, B, and B mean.
+
+=head2 getBugsConfirmer
+
+B
+
+=over
+
+=item B
+
+This method returns public bug ids that a given user has confirmed (changed from
+C to C).
+
+=item B
+
+You pass a field called C that is a list of Bugzilla login names to find bugs for.
+
+=over
+
+=item C (array) - An array of strings representing Bugzilla login names.
+
+=back
+
+=item B
+
+=over
+
+A hash of Bugzilla login names. Each name points to an array of bug ids that the user has confirmed.
+
+=back
+
+=item B
+
+=over
+
+=back
+
+=item B
+
+=over
+
+=item Added in BMO Bugzilla B<4.0>.
+
+=back
+
+=back
+
+=head2 getBugsVerifier
+
+B
+
+=over
+
+=item B
+
+This method returns public bug ids that a given user has verified (changed from
+C to C).
+
+=item B
+
+You pass a field called C that is a list of Bugzilla login names to find bugs for.
+
+=over
+
+=item C (array) - An array of strings representing Bugzilla login names.
+
+=back
+
+=item B
+
+=over
+
+A hash of Bugzilla login names. Each name points to an array of bug ids that the user has verified.
+
+=back
+
+=item B
+
+=over
+
+=back
+
+=item B
+
+=over
+
+=item Added in BMO Bugzilla B<4.0>.
+
+=back
+
+=back
diff --git a/extensions/BMO/template/en/default/account/create.html.tmpl b/extensions/BMO/template/en/default/account/create.html.tmpl
new file mode 100644
index 000000000..275df01f8
--- /dev/null
+++ b/extensions/BMO/template/en/default/account/create.html.tmpl
@@ -0,0 +1,181 @@
+[%# 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 the Bugzilla Bug Tracking System.
+ #
+ # The Initial Developer of the Original Code is Netscape Communications
+ # Corporation. Portions created by Netscape are
+ # Copyright (C) 1998 Netscape Communications Corporation. All
+ # Rights Reserved.
+ #
+ # Contributor(s): Gervase Markham
+ # Byron Jones
+ #%]
+
+[%# INTERFACE
+ # none
+ #
+ # Param("maintainer") is used to display the maintainer's email.
+ # Param("emailsuffix") is used to pre-fill the email field.
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+
+[% title = BLOCK %]
+ Create a new [% terms.Bugzilla %] account
+[% END %]
+
+[% PROCESS global/header.html.tmpl
+ title = title
+ style_urls = [ 'extensions/BMO/web/styles/create_account.css' ]
+%]
+
+
+
+
+
+
+
+
+
I need help using a Mozilla Product
+
+
+ [% INCLUDE product
+ icon = "firefox"
+ name = "Firefox Support"
+ url = "http://support.mozilla.com/"
+ desc = "Support for the Firefox web browser."
+ %]
+ [% INCLUDE product
+ icon = "firefox"
+ name = "Firefox for Mobile Support"
+ url = "http://support.mozilla.com/mobile"
+ desc = "Support for the Firefox Mobile web browser."
+ %]
+ [% INCLUDE product
+ icon = "thunderbird"
+ name = "Thunderbird Support"
+ url = "http://www.mozillamessaging.com/support/"
+ desc = "Support for Thunderbird email client."
+ %]
+ [% INCLUDE product
+ icon = "other"
+ name = "Support for other products"
+ url = "http://www.mozilla.org/support/"
+ desc = "Support for products not listed here."
+ %]
+ [% INCLUDE product
+ icon = "input"
+ name = "Feedback"
+ url = "http://input.mozilla.com/feedback"
+ desc = "Report issues with web site you use, or provide quick feedback for Firefox."
+ %]
+ [% INCLUDE product
+ icon = "idea"
+ name = "Ideas"
+ url = "http://input.mozilla.com/idea"
+ desc = "Offer us ideas on how to enhance Firefox."
+ %]
+
+ [% terms.Bugzilla %] is a public place, so what you type and your email address will be visible
+ to all logged-in users. Some people use an
+ alternative email address
+ for this reason.
+
+
+ Please give us an email address you want to use. Once we confirm that it works,
+ you'll be asked to set a password and then you can start filing [% terms.bugs %] and helping fix them.
+
+
+
+
+
Create an account
+
+
+
+
+
+
+
+
+
+ If you think there's something wrong with [% terms.Bugzilla %], you can
+ send an email to the admins, but
+ remember, they can't file [% terms.bugs %] for you, or solve tech support problems.
+
+[% END %]
+
diff --git a/extensions/BMO/template/en/default/bug/create/comment-mktgevent.txt.tmpl b/extensions/BMO/template/en/default/bug/create/comment-mktgevent.txt.tmpl
new file mode 100644
index 000000000..216f2c53a
--- /dev/null
+++ b/extensions/BMO/template/en/default/bug/create/comment-mktgevent.txt.tmpl
@@ -0,0 +1,48 @@
+[%# 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 the Bugzilla Bug Tracking System.
+ #
+ # The Initial Developer of the Original Code is Netscape Communications
+ # Corporation. Portions created by Netscape are
+ # Copyright (C) 1998 Netscape Communications Corporation. All
+ # Rights Reserved.
+ #
+ # Contributor(s): Gervase Markham
+ #%]
+[%# INTERFACE:
+ # This template has no interface.
+ #
+ # Form variables from a bug submission (i.e. the fields on a template from
+ # enter_bug.cgi) can be access via Bugzilla.cgi.param. It can be used to
+ # pull out various custom fields and format an initial Description entry
+ # from them.
+ #%]
+[% USE Bugzilla %]
+[% cgi = Bugzilla.cgi %]
+Requester: [% cgi.param('firstname') %] [%+ cgi.param('lastname') %]
+Email: [% cgi.param('email') %]
+Event Name: [% cgi.param('eventname') %]
+Event Date: [% cgi.param("date") %]
+Event Location: [% cgi.param("locations") %]
+Event Website: [% cgi.param("website") %]
+Event Description and Objectives:
+[%+ cgi.param("goals") %]
+
+Attendees: [% IF cgi.param('attendees') != "--Please Select--" %][% cgi.param('attendees') %][% END %]
+Target Audience: [% IF cgi.param('audience') != "--Please Select--" %][% cgi.param('audience') %][% END %]
+
+We'll be doing: [% cgi.param("doing").join(", ") %][% IF cgi.param('doing-other-what') %]: [% cgi.param('doing-other-what') %][% END %]
+
+Success will be measured by:
+[%+ cgi.param('successmeasure') %]
+
+[%+ cgi.param("comment") IF cgi.param("comment") %]
+
diff --git a/extensions/BMO/template/en/default/bug/create/comment-mozlist.txt.tmpl b/extensions/BMO/template/en/default/bug/create/comment-mozlist.txt.tmpl
new file mode 100644
index 000000000..c62461d42
--- /dev/null
+++ b/extensions/BMO/template/en/default/bug/create/comment-mozlist.txt.tmpl
@@ -0,0 +1,44 @@
+[%# 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 the Bugzilla Bug Tracking System.
+ #
+ # The Initial Developer of the Original Code is Netscape Communications
+ # Corporation. Portions created by Netscape are
+ # Copyright (C) 1998 Netscape Communications Corporation. All
+ # Rights Reserved.
+ #
+ # Contributor(s): Gervase Markham
+ #%]
+[%# INTERFACE:
+ # This template has no interface.
+ #
+ # Form variables from a bug submission (i.e. the fields on a template from
+ # enter_bug.cgi) can be access via Bugzilla.cgi.param. It can be used to
+ # pull out various custom fields and format an initial Description entry
+ # from them.
+ #%]
+[% USE Bugzilla %]
+[% cgi = Bugzilla.cgi %]
+List Name: [% cgi.param("listName") %]
+List Admin: [% cgi.param("listAdmin") %]
+
+Short Description:
+[%+ cgi.param("listShortDesc") %]
+
+[% IF cgi.param("listType") != "mozilla.com" %]
+Long Description:
+[%+ cgi.param("listLongDesc") %]
+[% END %]
+
+Justification / Special Instructions:
+
+[%+ cgi.param("comment") IF cgi.param("comment") %]
+
diff --git a/extensions/BMO/template/en/default/bug/create/comment-mozreps.txt.tmpl b/extensions/BMO/template/en/default/bug/create/comment-mozreps.txt.tmpl
new file mode 100644
index 000000000..6c9d7c6b7
--- /dev/null
+++ b/extensions/BMO/template/en/default/bug/create/comment-mozreps.txt.tmpl
@@ -0,0 +1,81 @@
+[%# 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 the BMO Bugzilla Extension.
+ #
+ # The Initial Developer of the Original Code is the Mozilla Foundation
+ # Portions created by the Initial Developers are Copyright (C) 2011 the
+ # Initial Developer. All Rights Reserved.
+ #
+ # Contributor(s): Byron Jones
+ #%]
+[% USE Bugzilla %]
+[% cgi = Bugzilla.cgi %]
+First Name:
+[%+ cgi.param('first_name') %]
+
+Last Name:
+[%+ cgi.param('last_name') %]
+
+Under 18 years old:
+[%+ IF cgi.param('underage') %]Yes[% ELSE %]No[% END %]
+
+Sex:
+[%+ cgi.param('sex') %]
+
+City:
+[%+ cgi.param('city') %]
+
+Country:
+[%+ cgi.param('country') %]
+
+Local Community:
+[% IF cgi.param('community') %]
+[%+ cgi.param('community') %]
+[% ELSE %]
+-
+[% END %]
+
+IM:
+[% IF cgi.param('im') %]
+[%+ cgi.param('im') %]
+[% ELSE %]
+-
+[% END %]
+
+References:
+[% IF cgi.param('references') %]
+[%+ cgi.param('references') %]
+[% ELSE %]
+-
+[% END %]
+
+Currently Involved with Mozilla:
+[% IF cgi.param('involved') %]
+[%+ cgi.param('involved') %]
+[% ELSE %]
+-
+[% END %]
+
+Languages Spoken:
+[%+ cgi.param('languages') %]
+
+How did you lean about Mozilla Reps:
+[%+ cgi.param('learn') %]
+
+What motivates you most about joining Mozilla Reps:
+[%+ cgi.param('motivation') %]
+
+Comments:
+[% IF cgi.param('comments') %]
+[%+ cgi.param('comments') %]
+[% ELSE %]
+-
+[% END %]
diff --git a/extensions/BMO/template/en/default/bug/create/comment-remo-budget.txt.tmpl b/extensions/BMO/template/en/default/bug/create/comment-remo-budget.txt.tmpl
new file mode 100644
index 000000000..9486c56fe
--- /dev/null
+++ b/extensions/BMO/template/en/default/bug/create/comment-remo-budget.txt.tmpl
@@ -0,0 +1,64 @@
+[%# 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 the Bugzilla Bug Tracking System.
+ #
+ # The Initial Developer of the Original Code is Netscape Communications
+ # Corporation. Portions created by Netscape are
+ # Copyright (C) 1998 Netscape Communications Corporation. All
+ # Rights Reserved.
+ #
+ # Contributor(s): Gervase Markham
+ #%]
+[%# INTERFACE:
+ # This template has no interface.
+ #
+ # Form variables from a bug submission (i.e. the fields on a template from
+ # enter_bug.cgi) can be access via Bugzilla.cgi.param. It can be used to
+ # pull out various custom fields and format an initial Description entry
+ # from them.
+ #%]
+[% USE Bugzilla %]
+[% cgi = Bugzilla.cgi %]
+
+Requester info:
+
+Requester: [% cgi.param('firstname') %] [%+ cgi.param('lastname') %]
+Wiki user profile: [% cgi.param('wikiprofile') %]
+Event wiki page: [% cgi.param('wikipage') %]
+Advance payment needed: [% IF cgi.param('advancepayment') %]Yes[% ELSE %]No[% END %]
+
+Budget breakdown:
+
+Total amount requested in $USD: [% cgi.param('budgettotal') %]
+Costs per service:
+Service 1: [% cgi.param('service1') %] Cost: [% cgi.param('cost1') %]
+Service 2: [% cgi.param('service2') %] Cost: [% cgi.param('cost2') %]
+Service 3: [% cgi.param('service3') %] Cost: [% cgi.param('cost3') %]
+Service 4: [% cgi.param('service4') %] Cost: [% cgi.param('cost4') %]
+Service 5: [% cgi.param('service5') %] Cost: [% cgi.param('cost5') %]
+
+Additional costs: (add comment box)
+[% cgi.param('costadditional') %]
+
+Success measurement:
+
+How will the event help push the Mozilla project forward?
+[%+ cgi.param('successmeasure') %]
+
+Metric 1: [% cgi.param('metric1') %] Success scenario: [% cgi.param('success1') %]
+Metric 2: [% cgi.param('metric2') %] Success scenario: [% cgi.param('success2') %]
+Metric 3: [% cgi.param('metric3') %] Success scenario: [% cgi.param('success3') %]
+
+Additional information:
+[%+ cgi.param('successadditional') %]
+
+[%+ cgi.param("comment") IF cgi.param("comment") %]
+
diff --git a/extensions/BMO/template/en/default/bug/create/comment-remo-swag.txt.tmpl b/extensions/BMO/template/en/default/bug/create/comment-remo-swag.txt.tmpl
new file mode 100644
index 000000000..0b98178b2
--- /dev/null
+++ b/extensions/BMO/template/en/default/bug/create/comment-remo-swag.txt.tmpl
@@ -0,0 +1,68 @@
+[%# 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 the Bugzilla Bug Tracking System.
+ #
+ # The Initial Developer of the Original Code is Netscape Communications
+ # Corporation. Portions created by Netscape are
+ # Copyright (C) 1998 Netscape Communications Corporation. All
+ # Rights Reserved.
+ #
+ # Contributor(s): Gervase Markham
+ #%]
+[%# INTERFACE:
+ # This template has no interface.
+ #
+ # Form variables from a bug submission (i.e. the fields on a template from
+ # enter_bug.cgi) can be access via Bugzilla.cgi.param. It can be used to
+ # pull out various custom fields and format an initial Description entry
+ # from them.
+ #%]
+[% USE Bugzilla %]
+[% cgi = Bugzilla.cgi %]
+
+Requester info:
+
+Requester name: [% cgi.param('firstname') %][% " " %][% cgi.param('lastname') %]
+Wiki user profile: [% cgi.param('wikiprofile') %]
+Event wiki page: [% cgi.param('wikipage') %]
+Estimated Attendance: [% cgi.param('attendance') %]
+
+Shipping details:
+
+Ship swag before: [% cgi.param('shipdate') %]
+
+[%+ cgi.param("shiptofirstname") +%] [%+ cgi.param("shiptolastname") +%]
+[%+ cgi.param("shiptoaddress") +%]
+[%+ cgi.param("shiptoaddress2") +%]
+[%+ cgi.param("shiptocity") +%] [%+ cgi.param("shiptostate") +%] [%+ cgi.param("shiptopcode") +%]
+[%+ cgi.param("shiptocountry") %]
+
+Phone: [% cgi.param("shiptophone") %]
+[%+ IF cgi.param("shiptoidrut") %]Personal ID/RUT: [% cgi.param("shiptoidrut") %][% END %]
+
+Addition information for delivery person:
+[%+ cgi.param('shipadditional') %]
+
+Swag requested:
+
+Stickers: [% IF cgi.param('stickers') %]Yes[% ELSE %]No[% END %]
+Buttons: [% IF cgi.param('buttons') %]Yes[% ELSE %]No[% END %]
+Posters: [% IF cgi.param('posters') %]Yes[% ELSE %]No[% END %]
+Lanyards: [% IF cgi.param('lanyards') %]Yes[% ELSE %]No[% END %]
+T-shirts: [% IF cgi.param('tshirts') %]Yes[% ELSE %]No[% END %]
+Roll-up banners: [% IF cgi.param('rollupbanners') %]Yes[% ELSE %]No[% END %]
+Horizontal banners: [% IF cgi.param('horizontalbanners') %]Yes[% ELSE %]No[% END %]
+Booth cloth: [% IF cgi.param('boothcloth') %]Yes[% ELSE %]No[% END %]
+Pens: [% IF cgi.param('pens') %]Yes[% ELSE %]No[% END %]
+Other: [% IF cgi.param('otherswag') %][% cgi.param('otherswag') %][% ELSE %]No[% END %]
+
+[%+ cgi.param("comment") IF cgi.param("comment") %]
+
diff --git a/extensions/BMO/template/en/default/bug/create/comment-swag.txt.tmpl b/extensions/BMO/template/en/default/bug/create/comment-swag.txt.tmpl
new file mode 100644
index 000000000..0ec7687d4
--- /dev/null
+++ b/extensions/BMO/template/en/default/bug/create/comment-swag.txt.tmpl
@@ -0,0 +1,48 @@
+[%# 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 the Bugzilla Bug Tracking System.
+ #
+ # The Initial Developer of the Original Code is Netscape Communications
+ # Corporation. Portions created by Netscape are
+ # Copyright (C) 1998 Netscape Communications Corporation. All
+ # Rights Reserved.
+ #
+ # Contributor(s): Gervase Markham
+ #%]
+[%# INTERFACE:
+ # This template has no interface.
+ #
+ # Form variables from a bug submission (i.e. the fields on a template from
+ # enter_bug.cgi) can be access via Bugzilla.cgi.param. It can be used to
+ # pull out various custom fields and format an initial Description entry
+ # from them.
+ #%]
+[% USE Bugzilla %]
+[% cgi = Bugzilla.cgi %]
+Requester: [% cgi.param('firstname') %] [% cgi.param('lastname') %]
+Email: [% cgi.param('email') %]
+
+Additional Swag: [% cgi.param("additional") %]
+
+Ship to:
+[%+ cgi.param("shiptofirstname") +%] [%+ cgi.param("shiptolastname") +%]
+[%+ cgi.param("shiptoaddress") +%]
+[%+ cgi.param("shiptoaddress2") +%]
+[%+ cgi.param("shiptocity") +%] [%+ cgi.param("shiptostate") +%] [%+ cgi.param("shiptopcode") +%]
+[%+ cgi.param("shiptocountry") %]
+
+Phone: [% cgi.param("shiptophone") %]
+[%+ IF cgi.param("shiptoidrut") %]Personal ID/RUT: [% cgi.param("shiptoidrut") %][% END %]
+
+Additional comments:
+
+[%+ cgi.param("comment") IF cgi.param("comment") %]
+
diff --git a/extensions/BMO/template/en/default/bug/create/create-brownbag.html.tmpl b/extensions/BMO/template/en/default/bug/create/create-brownbag.html.tmpl
new file mode 100644
index 000000000..72d520dd4
--- /dev/null
+++ b/extensions/BMO/template/en/default/bug/create/create-brownbag.html.tmpl
@@ -0,0 +1,220 @@
+[%# 1.0@bugzilla.org %]
+[%# 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 the Bugzilla Bug Tracking System.
+ #
+ # The Initial Developer of the Original Code is Mozilla Corporation.
+ # Portions created by Mozilla are Copyright (C) 2008 Mozilla
+ # Corporation. All Rights Reserved.
+ #
+ # Contributor(s): Reed Loden
+ # David Tran
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+
+[% PROCESS global/header.html.tmpl
+ title = "Mozilla Corporation Brownbag Requests"
+ style_urls = [ 'skins/standard/attachment.css', 'skins/custom/calendar.css' ]
+ style = ".yui-skin-sam .yui-calcontainer { z-index: 1; }"
+ javascript_urls = [ 'js/attachment.js', 'js/field.js', 'js/util.js' ]
+ yui = [ 'autocomplete', 'calendar' ]
+%]
+
+[% USE Bugzilla %]
+
+
Brownbag Request
+
+
Brownbag Request: Please use this form if you plan on hosting a brownbag so that IT will be able to properly provide support.
+
+
Process:
+
+
Complete and submit request below.
+
Your request will be reviewed and assigned to the appropriate person in IT.
+
+
+
These requests will only be visible internally in all cases and only to the
+person who submitted the request and any persons designated in the CC line.
+
+
+
+
+
+
Thanks for contacting us.
+ You will be notified by email of any progress made in resolving your request.
+
+
+
+
+
+[% PROCESS global/footer.html.tmpl %]
diff --git a/extensions/BMO/template/en/default/bug/create/create-itrequest.html.tmpl b/extensions/BMO/template/en/default/bug/create/create-itrequest.html.tmpl
new file mode 100644
index 000000000..d2f30475d
--- /dev/null
+++ b/extensions/BMO/template/en/default/bug/create/create-itrequest.html.tmpl
@@ -0,0 +1,184 @@
+[%# 1.0@bugzilla.org %]
+[%# 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 the Bugzilla Bug Tracking System.
+ #
+ # The Initial Developer of the Original Code is Netscape Communications
+ # Corporation. Portions created by Netscape are
+ # Copyright (C) 1998 Netscape Communications Corporation. All
+ # Rights Reserved.
+ #
+ # Contributor(s): Gervase Markham
+ # Ville Skyttä
+ # John Hoogstrate
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+
+[% PROCESS global/header.html.tmpl
+ title = "Mozilla Corporation/Foundation IT Requests"
+ javascript_urls = [ 'js/field.js' ]
+ yui = [ 'autocomplete' ]
+%]
+
+[% USE Bugzilla %]
+
+
Thanks for contacting us.
+ You will be notified by email of any progress made in resolving your
+ request.
+
+
+[% PROCESS global/footer.html.tmpl %]
diff --git a/extensions/BMO/template/en/default/bug/create/create-legal.html.tmpl b/extensions/BMO/template/en/default/bug/create/create-legal.html.tmpl
new file mode 100644
index 000000000..c690370fe
--- /dev/null
+++ b/extensions/BMO/template/en/default/bug/create/create-legal.html.tmpl
@@ -0,0 +1,216 @@
+[%# 1.0@bugzilla.org %]
+[%# 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 the Bugzilla Bug Tracking System.
+ #
+ # The Initial Developer of the Original Code is Mozilla Corporation.
+ # Portions created by Mozilla are Copyright (C) 2008 Mozilla
+ # Corporation. All Rights Reserved.
+ #
+ # Contributor(s): Mark Smith
+ # Reed Loden
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+
+[% PROCESS global/header.html.tmpl
+ title = "Mozilla Corporation Legal Requests"
+ style_urls = [ 'skins/standard/attachment.css' ]
+ javascript_urls = [ 'js/attachment.js', 'js/field.js' ]
+ yui = [ 'autocomplete' ]
+%]
+
+[% IF user.in_group("mozilla-corporation-confidential")
+ OR user.in_group("mozilla-messaging-confidential") %]
+
+
MoLegal
+
+
Welcome to MoLegal. If you need legal assistance please complete the form below. To best service
+your request, it is essential that you give us complete information so we can respond
+properly and in a timely fashion. Please use this form only for requests for legal
+assistance related to the Mozilla Corporation. If you’ve been arrested, do not use
+this form or contact us.
+
+
Process:
+
+
Complete and submit request below.
+
Your request will be reviewed and assigned to the appropriate legal service provider as necessary.
+
We will contact you to confirm receipt, gather any additional information, and agree on a completion timeline.
+
+
+
These requests will only be visible internally and in all cases and only to the
+person who submitted the request and any persons designated in the CC line.
+
+
All Submissions, And Information Provided In Response To This Request,
+Are Confidential And Subject To The Attorney-Client Privilege And Work Product Doctrine.
+
+
+
+
+
+
Thanks for contacting us.
+ You will be notified by email of any progress made in resolving your request.
+
+
+[% ELSE %]
+
+
Sorry, you do not have access to this page.
+
+[% END %]
+
+[% PROCESS global/footer.html.tmpl %]
diff --git a/extensions/BMO/template/en/default/bug/create/create-mktgevent.html.tmpl b/extensions/BMO/template/en/default/bug/create/create-mktgevent.html.tmpl
new file mode 100644
index 000000000..92354eac3
--- /dev/null
+++ b/extensions/BMO/template/en/default/bug/create/create-mktgevent.html.tmpl
@@ -0,0 +1,249 @@
+[%# 1.0@bugzilla.org %]
+[%# 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 the Bugzilla Bug Tracking System.
+ #
+ # The Initial Developer of the Original Code is Mozilla Corporation.
+ # Portions created by Mozilla are Copyright (C) 2008 Mozilla
+ # Corporation. All Rights Reserved.
+ #
+ # Contributor(s): Reed Loden
+ # David Tran
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+
+[% PROCESS global/header.html.tmpl
+ title = "Event Request Form"
+ javascript_urls = [ 'extensions/BMO/web/js/form_validate.js',
+ 'js/field.js',
+ 'js/util.js' ]
+ style = ".yui-skin-sam .yui-calcontainer { z-index: 1; }"
+ yui = [ 'autocomplete', 'calendar' ]
+%]
+
+
Event Request Form
+
+
Event Request: Please use this form to file a request for an event. This form is not a request for swag.
+
+
Process:
+
+
+
Complete and submit request below.
+
Your request will be reviewed by the appropriate person in the Engagement team.
+
+
+
These requests will only be visible to the person who submitted the request,
+any persons designated in the CC line, and authorized members of the Mozilla
+Engagement team.
+
+
+
+
+
+
Step 0 - Make sure you are in the right place
+
+ [% END %]
+
+
+The product you have chosen is for [% terms.bug %] reports and enhancement requests for the
+[% terms.Bugzilla %] [% terms.bug %] tracking software only.
+If your [% terms.bug %] is not reporting that [% terms.Bugzilla %] is broken or that you'd like
+a new feature in [% terms.Bugzilla %], your [% terms.bug %] report does not belong in this product.
+[% IF format == "guided" %]
+ See the instructions next to the stop sign above.
+[% ELSE %]
+ Please choose a different product.
+[% END %]
+
+
+
We WILL NOT accept [% terms.bug %] reports for [% terms.Bugzilla %]
+installed via the Debian packaging system. If you obtained [% terms.Bugzilla %] from Debian,
+please visit the Debian Support page,
+or file a [% terms.bug %] on the Debian
+[% terms.Bug %] Tracker. The Debian package maintainer will then determine whether the [% terms.bug %]
+is specific to the package or not, and can move the [% terms.bug %] "upstream" if needed.
+
+
+[% terms.Bugs %] specific to bugzilla.mozilla.org, rather than the [% terms.Bugzilla %] software in
+general (which is used by many sites), should be filed in the
+mozilla.org product.
+
+ This form prompts you for the information required to
+ file a good [% terms.bug %] report (or enhancement request.)
+ It may seem lengthy, but developers need all this information
+ to understand and reproduce the [% terms.bug %], which is the first step towards
+ fixing it. Please note that we do not accept [% terms.bug %] reports by
+ email - please do not email developers or mozilla.org staff
+ with [% terms.bug %] reports. Also, please do not
+ file [% terms.bugs %] on browser/email software older than two weeks - first,
+ download a newer build of
+ Firefox,
+ Thunderbird,
+ SeaMonkey,
+ or
+ Camino
+ and check that the problem is still present.
+
+
+ [% END %]
+[% END %]
diff --git a/extensions/BMO/template/en/default/global/choose-product.html.tmpl b/extensions/BMO/template/en/default/global/choose-product.html.tmpl
new file mode 100644
index 000000000..c957edca7
--- /dev/null
+++ b/extensions/BMO/template/en/default/global/choose-product.html.tmpl
@@ -0,0 +1,183 @@
+[%# 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 the Bugzilla Bug Tracking System.
+ #
+ # The Initial Developer of the Original Code is Netscape Communications
+ # Corporation. Portions created by Netscape are
+ # Copyright (C) 1998 Netscape Communications Corporation. All
+ # Rights Reserved.
+ #
+ # Contributor(s): Gervase Markham
+ #%]
+
+[%# INTERFACE:
+ # classifications: array of hashes, with an 'object' key representing a
+ # classification object and 'products' the list of
+ # product objects the user can enter bugs into.
+ # target: the script that displays this template.
+ # cloned_bug_id: ID of the bug being cloned.
+ # format: the desired format to display the target.
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+
+[% IF target == "enter_bug.cgi" %]
+ [% title = "Enter $terms.Bug" %]
+ [% h2 = BLOCK %]First, you must pick a product on which to enter [% terms.abug %]. [% END %]
+[% ELSIF target == "describecomponents.cgi" %]
+ [% title = "Browse" %]
+ [% h2 = "Please specify the product whose components you want described." %]
+[% END %]
+
+[% DEFAULT title = "Choose a Product" %]
+[% PROCESS global/header.html.tmpl %]
+
+
+
+
Looking for technical support or help getting your site to work with Mozilla? Visit the
+mozilla.org support page before filing [% terms.bugs %].
+
+
+
+
+[% USE Bugzilla %]
+[% cgi = Bugzilla.cgi %]
+[% SET classification = cgi.param('classification') %]
+[% IF NOT ((cgi.param("full")) OR (user.settings.product_chooser.value == 'full_product_chooser')) %]
+[% IF target == "enter_bug.cgi" %]
+
Which product is affected by the problem you would like to report?
+[% END %]
+
+[% INCLUDE easyproduct
+ name="Core"
+ icon="dino.png"
+%]
+[% INCLUDE easyproduct
+ name="Firefox"
+ icon="firefox.png"
+%]
+[% INCLUDE easyproduct
+ name="Thunderbird"
+ icon="thunderbird.png"
+%]
+[% INCLUDE easyproduct
+ name="Calendar"
+ icon="sunbird.png"
+%]
+[% INCLUDE easyproduct
+ name="Camino"
+ icon="camino.png"
+%]
+[% INCLUDE easyproduct
+ name="SeaMonkey"
+ icon="seamonkey.png"
+%]
+[% INCLUDE easyproduct
+ name="Fennec"
+ icon="fennec.png"
+%]
+[% INCLUDE easyproduct
+ name="Mozilla Localizations"
+ icon="dino.png"
+%]
+[% INCLUDE easyproduct
+ name="Mozilla Labs"
+ icon="labs.png"
+%]
+[% INCLUDE easyproduct
+ name="Mozilla Services"
+ icon="dino.png"
+%]
+
diff --git a/extensions/BMO/template/en/default/hook/bug/edit-after_custom_fields.html.tmpl b/extensions/BMO/template/en/default/hook/bug/edit-after_custom_fields.html.tmpl
new file mode 100644
index 000000000..ba6eeb78c
--- /dev/null
+++ b/extensions/BMO/template/en/default/hook/bug/edit-after_custom_fields.html.tmpl
@@ -0,0 +1,82 @@
+[%# ***** BEGIN LICENSE BLOCK *****
+ # Version: MPL 1.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 the BMO Bugzilla Extension;
+ #
+ # The Initial Developer of the Original Code is the Mozilla Foundation.
+ # Portions created by the Initial Developer are Copyright (C) 2011 the
+ # Initial Developer. All Rights Reserved.
+ #
+ # Contributor(s):
+ # Byron Jones
+ #
+ # ***** END LICENSE BLOCK *****
+ #%]
+
+[% show_custom_flags = 0 %]
+[% FOREACH field = Bugzilla.active_custom_fields %]
+ [% NEXT IF NOT user.id AND bug.${field.name} == "---" %]
+ [% NEXT IF cf_hidden_in_product(field.name, bug.product, bug.component, 2) %]
+ [% show_custom_flags = 1 %]
+ [% LAST %]
+[% END %]
+
+[% IF show_custom_flags %]
+ [% custom_flags = [] %]
+
+ [% FOREACH field = Bugzilla.active_custom_fields %]
+ [% NEXT IF NOT user.id AND field.value == "---" %]
+ [% NEXT IF cf_hidden_in_product(field.name, bug.product, bug.component, 2) %]
+ [% custom_flags.push(field.name) %]
+
+
+
+
+
+
+ [% PROCESS bug/field.html.tmpl value = bug.${field.name}
+ editable = user.id
+ no_tds = 1 %]
+ [% IF user.id %]
+
+ [% bug.${field.name} FILTER html %]
+
+ [% END %]
+
+
+ [% END %]
+
+
+
+
+[% END %]
diff --git a/extensions/BMO/template/en/default/hook/bug/field-help-end.none.tmpl b/extensions/BMO/template/en/default/hook/bug/field-help-end.none.tmpl
new file mode 100644
index 000000000..d95d1b606
--- /dev/null
+++ b/extensions/BMO/template/en/default/hook/bug/field-help-end.none.tmpl
@@ -0,0 +1,113 @@
+[%# 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 the BMO Extension
+ #
+ # The Initial Developer of the Original Code is the Mozilla Foundation
+ # Portions created by the Initial Developers are Copyright (C) 2011 the
+ # Initial Developer. All Rights Reserved.
+ #
+ # Contributor(s):
+ # Dave Lawrence
+ #%]
+
+[% IF fields_page %]
+ [% filtered_severity_blocker = display_value("bug_severity", "blocker") FILTER html %]
+ [% filtered_severity_critical = display_value("bug_severity", "critical") FILTER html %]
+ [% filtered_severity_major = display_value("bug_severity", "major") FILTER html %]
+ [% filtered_severity_normal = display_value("bug_severity", "normal") FILTER html %]
+ [% filtered_severity_minor = display_value("bug_severity", "minor") FILTER html %]
+ [% filtered_severity_trivial = display_value("bug_severity", "trivial") FILTER html %]
+ [% filtered_severity_enhancement = display_value("bug_severity", "enhancement") FILTER html %]
+
+ [% filtered_platform_all = display_value("rep_platform", "All") FILTER html %]
+ [% filtered_platform_x86_64 = display_value("rep_platform", "x86_64") FILTER html %]
+ [% filtered_platform_arm = display_value("rep_platform", "ARM") FILTER html %]
+
+ [% filtered_opsys_all = display_value("op_sys", "All") FILTER html %]
+ [% filtered_opsys_windows = display_value("op_sys", "Windows 7") FILTER html %]
+ [% filtered_opsys_mac = display_value("op_sys", "Mac OS X") FILTER html %]
+ [% filtered_opsys_linux = display_value("op_sys", "Linux") FILTER html %]
+
+ [% filtered_status_new = display_value("bug_status", "NEW") FILTER html %]
+
+ [%
+ help_html.priority =
+ "This field describes the importance and order in which $terms.abug
+ should be fixed compared to other ${terms.bugs}. This field is utilized
+ by the programmers/engineers to prioritize their work to be done."
+
+ help_html.bug_severity =
+ "This field describes the impact of ${terms.abug}.
+
+
+
$filtered_severity_blocker
+
Blocks development and/or testing work
+
+
+
$filtered_severity_critical
+
crashes, loss of data, severe memory leak
+
+
+
$filtered_severity_major
+
major loss of function
+
+
+
$filtered_severity_normal
+
regular issue, some loss of functionality under specific circumstances
+
+
+
$filtered_severity_minor
+
minor loss of function, or other problem where easy
+ workaround is present
+
+
+
$filtered_severity_trivial
+
cosmetic problem like misspelled words or misaligned
+ text
+
+
+
$filtered_severity_enhancement
+
Request for enhancement
+
"
+
+ help_html.rep_platform =
+ "This is the hardware platform against which the $terms.bug was reported.
+ Legal platforms include:
+
+
$filtered_platform_all (happens on all platforms; cross-platform ${terms.bug})
+
$filtered_platform_x86_64
+
$filtered_platform_arm
+
+ Note: When searching, selecting the option
+ $filtered_platform_all does not
+ select $terms.bugs assigned against any platform. It merely selects
+ $terms.bugs that are marked as occurring on all platforms, i.e. are
+ designated $filtered_platform_all.",
+
+ help_html.op_sys =
+ "This is the operating system against which the $terms.bug was
+ reported. Legal operating systems include:
+
+
$filtered_opsys_all (happens on all operating systems; cross-platform ${terms.bug})
+
$filtered_opsys_windows
+
$filtered_opsys_mac
+
$filtered_opsys_linux
+
+ Sometimes the operating system implies the platform, but not
+ always. For example, Linux can run on x86_64, ARM, and others.",
+
+ help_html.assigned_to =
+ "This is the person in charge of resolving the ${terms.bug}. Every time
+ this field changes, the status changes to
+ $filtered_status_new to make it
+ easy to see which new $terms.bugs have appeared on a person's list.",
+ %]
+[% END %]
diff --git a/extensions/BMO/template/en/default/hook/bug/show-header-end.html.tmpl b/extensions/BMO/template/en/default/hook/bug/show-header-end.html.tmpl
new file mode 100644
index 000000000..55e428e01
--- /dev/null
+++ b/extensions/BMO/template/en/default/hook/bug/show-header-end.html.tmpl
@@ -0,0 +1,27 @@
+[%# ***** BEGIN LICENSE BLOCK *****
+ # Version: MPL 1.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 the BMO Bugzilla Extension;
+ #
+ # The Initial Developer of the Original Code is the Mozilla Foundation.
+ # Portions created by the Initial Developer are Copyright (C) 2011 the
+ # Initial Developer. All Rights Reserved.
+ #
+ # Contributor(s):
+ # Byron Jones
+ #
+ # ***** END LICENSE BLOCK *****
+ #%]
+
+[% style_urls.push('extensions/BMO/web/styles/edit_bug.css') %]
+[% javascript_urls.push('extensions/BMO/web/js/edit_bug.js') %]
diff --git a/extensions/BMO/template/en/default/hook/global/footer-outro.html.tmpl b/extensions/BMO/template/en/default/hook/global/footer-outro.html.tmpl
new file mode 100644
index 000000000..b5bb4719c
--- /dev/null
+++ b/extensions/BMO/template/en/default/hook/global/footer-outro.html.tmpl
@@ -0,0 +1 @@
+Privacy Policy
diff --git a/extensions/BMO/template/en/default/hook/global/header-additional_header.html.tmpl b/extensions/BMO/template/en/default/hook/global/header-additional_header.html.tmpl
new file mode 100644
index 000000000..05276d5f7
--- /dev/null
+++ b/extensions/BMO/template/en/default/hook/global/header-additional_header.html.tmpl
@@ -0,0 +1,60 @@
+[%#
+ # 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 the BMOHeader Bugzilla Extension.
+ #
+ # The Initial Developer of the Original Code is Reed Loden.
+ # Portions created by the Initial Developer are Copyright (C) 2010 the
+ # Initial Developer. All Rights Reserved.
+ #
+ # Contributor(s):
+ # Reed Loden
+ #%]
+
+
+[% IF bug %]
+
+[% END %]
+
+
+
+[%# *** Bug List Navigation *** %]
+[% IF bug %]
+ [% SET my_search = user.recent_search_for(bug) %]
+ [% IF my_search %]
+ [% SET last_bug_list = my_search.bug_list %]
+ [% SET this_bug_idx = lsearch(last_bug_list, bug.id) %]
+
+
+
+ [% IF this_bug_idx > 0 %]
+ [% prev_bug = this_bug_idx - 1 %]
+
+ [% END %]
+ [% IF this_bug_idx + 1 < last_bug_list.size %]
+ [% next_bug = this_bug_idx + 1 %]
+
+ [% END %]
+ [% END %]
+[% END %]
diff --git a/extensions/BMO/template/en/default/hook/global/header-start.html.tmpl b/extensions/BMO/template/en/default/hook/global/header-start.html.tmpl
new file mode 100644
index 000000000..3c2f90e19
--- /dev/null
+++ b/extensions/BMO/template/en/default/hook/global/header-start.html.tmpl
@@ -0,0 +1,3 @@
+[% IF template.name == 'list/list.html.tmpl' %]
+ [% javascript_urls.push('extensions/BMO/web/js/sorttable.js') %]
+[% END %]
\ No newline at end of file
diff --git a/extensions/BMO/template/en/default/hook/global/setting-descs-settings.none.tmpl b/extensions/BMO/template/en/default/hook/global/setting-descs-settings.none.tmpl
new file mode 100644
index 000000000..666621d8b
--- /dev/null
+++ b/extensions/BMO/template/en/default/hook/global/setting-descs-settings.none.tmpl
@@ -0,0 +1,5 @@
+[%
+ setting_descs.product_chooser = "Product chooser to use when entering bugs",
+ setting_descs.pretty_product_chooser = "Pretty chooser with common products and icons",
+ setting_descs.full_product_chooser = "Full chooser with all products",
+%]
diff --git a/extensions/BMO/template/en/default/hook/global/user-error-errors.html.tmpl b/extensions/BMO/template/en/default/hook/global/user-error-errors.html.tmpl
new file mode 100644
index 000000000..5a3e2bed6
--- /dev/null
+++ b/extensions/BMO/template/en/default/hook/global/user-error-errors.html.tmpl
@@ -0,0 +1,55 @@
+[%# 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 the BMO Extension
+ #
+ # The Initial Developer of the Original Code is the Mozilla Foundation
+ # Portions created by the Initial Developers are Copyright (C) 2011 the
+ # Initial Developer. All Rights Reserved.
+ #
+ # Contributor(s):
+ # Byron Jones
+ #%]
+
+[% IF error == "user_activity_missing_username" %]
+ [% title = "Missing Username" %]
+ You must provide at least one email address to report on.
+
+[% ELSIF error == "user_activity_missing_from_date" %]
+ [% title = "Missing Date" %]
+ You must provided the period start date.
+
+[% ELSIF error == "user_activity_missing_to_date" %]
+ [% title = "Missing Date" %]
+ You must provided the period end date.
+
+[% ELSIF error == "user_activity_invalid_date" %]
+ [% title = "Invalid Date" %]
+ The date '[% date FILTER html %]' is invalid.
+
+[% ELSIF error == "remo_payment_invalid_product" %]
+ [% title = "Mozilla Reps Payment Invalid $terms.Bug" %]
+ You can only attach budget payment information to [% terms.bugs %] under
+ the product 'Mozilla Reps' and component 'Budget Requests'.
+
+[% ELSIF error == "remo_payment_bug_edit_denied" %]
+ [% title = "Mozilla Reps Payment $terms.Bug Edit Denied" %]
+ You do not have permission to edit [% terms.bug %] '[% bug_id FILTER html %]'.
+
+[% ELSIF error == "remo_payment_cancel_dupe" %]
+ [% title = "Already filed payment request" %]
+ You already used the form to file
+
+ attachment [% attachid FILTER uri %].
+
+ You can either
+ create a new payment request or [% "go back to $terms.bug $bugid" FILTER bug_link(bugid) FILTER none %].
+
+[% END %]
diff --git a/extensions/BMO/template/en/default/hook/global/user-error.html.tmpl/auth_failure/permissions.html.tmpl b/extensions/BMO/template/en/default/hook/global/user-error.html.tmpl/auth_failure/permissions.html.tmpl
new file mode 100644
index 000000000..346e02373
--- /dev/null
+++ b/extensions/BMO/template/en/default/hook/global/user-error.html.tmpl/auth_failure/permissions.html.tmpl
@@ -0,0 +1,29 @@
+
+[%# 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 the Bugzilla Bug Tracking System.
+ #
+ # The Initial Developer of the Original Code is Netscape Communications
+ # Corporation. Portions created by Netscape are
+ # Copyright (C) 1998 Netscape Communications Corporation. All
+ # Rights Reserved.
+ #
+ # Contributor(s): Gervase Markham
+ # Reed Loden
+ #%]
+
+[% IF (group == "canconfirm" OR group == "editbugs") AND !reason %]
+
diff --git a/extensions/BMO/template/en/default/hook/index-intro.html.tmpl b/extensions/BMO/template/en/default/hook/index-intro.html.tmpl
new file mode 100644
index 000000000..d81d91491
--- /dev/null
+++ b/extensions/BMO/template/en/default/hook/index-intro.html.tmpl
@@ -0,0 +1,2 @@
+Get Help
\ No newline at end of file
diff --git a/extensions/BMO/template/en/default/hook/pages/fields-resolutions.html.tmpl b/extensions/BMO/template/en/default/hook/pages/fields-resolutions.html.tmpl
new file mode 100644
index 000000000..4d12ab345
--- /dev/null
+++ b/extensions/BMO/template/en/default/hook/pages/fields-resolutions.html.tmpl
@@ -0,0 +1,13 @@
+
+ [% display_value("resolution", "INCOMPLETE") FILTER html %]
+
+
+ The problem is vaguely described with no steps to reproduce,
+ or is a support request. The reporter should be directed to the
+ product's support page for help diagnosing the issue. If there
+ are only a few comments in the [% terms.bug %], it may be reopened only if
+ the original reporter provides more info, or confirms someone
+ else's steps to reproduce. If the [% terms.bug %] is long, when enough info
+ is provided a new [% terms.bug %] should be filed and the original [% terms.bug %]
+ marked as a duplicate of it.
+
diff --git a/extensions/BMO/template/en/default/hook/reports/menu-end.html.tmpl b/extensions/BMO/template/en/default/hook/reports/menu-end.html.tmpl
new file mode 100644
index 000000000..b42ff8d2a
--- /dev/null
+++ b/extensions/BMO/template/en/default/hook/reports/menu-end.html.tmpl
@@ -0,0 +1,35 @@
+[%# 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 the BMO Extension
+ #
+ # The Initial Developer of the Original Code is the Mozilla Foundation
+ # Portions created by the Initial Developers are Copyright (C) 2011 the
+ # Initial Developer. All Rights Reserved.
+ #
+ # Contributor(s):
+ # Byron Jones
+ #%]
+
+
Other Reports
+
+
+
+ User Changes - Show changes
+ made by an individual user.
+
+
+ Triage Report - Report
+ on UNCONFIRMED [% terms.bugs %] to assist triage.
+
+
+
diff --git a/extensions/BMO/template/en/default/list/list.microsummary.tmpl b/extensions/BMO/template/en/default/list/list.microsummary.tmpl
new file mode 100644
index 000000000..a095a7e4d
--- /dev/null
+++ b/extensions/BMO/template/en/default/list/list.microsummary.tmpl
@@ -0,0 +1,28 @@
+[%# 1.0@bugzilla.org %]
+[%# 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 the Bugzilla Bug Tracking System.
+ #
+ # The Initial Developer of the Original Code is Netscape Communications
+ # Corporation. Portions created by Netscape are
+ # Copyright (C) 1998 Netscape Communications Corporation. All
+ # Rights Reserved.
+ #
+ # Contributor(s): Ronaldo Maia
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+
+[% IF searchname %]
+ [% searchname FILTER html %] ([% bugs.size %])
+[% ELSE %]
+ [% terms.Bug %] List ([% bugs.size %])
+[% END %]
diff --git a/extensions/BMO/template/en/default/list/server-push.html.tmpl b/extensions/BMO/template/en/default/list/server-push.html.tmpl
new file mode 100644
index 000000000..1c1f3cf36
--- /dev/null
+++ b/extensions/BMO/template/en/default/list/server-push.html.tmpl
@@ -0,0 +1,52 @@
+[%# 1.0@bugzilla.org %]
+[%# 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 the Bugzilla Bug Tracking System.
+ #
+ # The Initial Developer of the Original Code is Netscape Communications
+ # Corporation. Portions created by Netscape are
+ # Copyright (C) 1998 Netscape Communications Corporation. All
+ # Rights Reserved.
+ #
+ # Contributor(s): Myk Melez
+ #%]
+
+[%# INTERFACE:
+ # debug: boolean. True if we want the search displayed while we wait.
+ # query: string. The SQL query which makes the buglist.
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+
+
+
+ [% terms.Bugzilla %] is pondering your search
+
+
+
+
+
Please wait while your [% terms.bugs %] are retrieved.
+
+
+ [% IF debug %]
+
+ [% FOREACH debugline = debugdata %]
+ [% debugline FILTER html %]
+ [% END %]
+
+
+ [% query FILTER html %]
+
+ [% END %]
+
+
+
diff --git a/extensions/BMO/template/en/default/pages/bug-writing.html.tmpl b/extensions/BMO/template/en/default/pages/bug-writing.html.tmpl
new file mode 100644
index 000000000..f326d1821
--- /dev/null
+++ b/extensions/BMO/template/en/default/pages/bug-writing.html.tmpl
@@ -0,0 +1,25 @@
+[%# 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 the Bugzilla Bug Tracking System.
+ #
+ # The Initial Developer of the Original Code is Netscape Communications
+ # Corporation. Portions created by Netscape are
+ # Copyright (C) 1998 Netscape Communications Corporation. All
+ # Rights Reserved.
+ #
+ # Contributor(s): David Lawrence
+ #%]
+
+
+
+
+
+
diff --git a/extensions/BMO/template/en/default/pages/comment-remo-form-payment.txt.tmpl b/extensions/BMO/template/en/default/pages/comment-remo-form-payment.txt.tmpl
new file mode 100644
index 000000000..c43a92ae7
--- /dev/null
+++ b/extensions/BMO/template/en/default/pages/comment-remo-form-payment.txt.tmpl
@@ -0,0 +1,37 @@
+[%# 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 the BMO Extension
+ #
+ # The Initial Developer of the Original Code is the Mozilla Foundation
+ # Portions created by the Initial Developers are Copyright (C) 2011 the
+ # Initial Developer. All Rights Reserved.
+ #
+ # Contributor(s):
+ # Dave Lawrence
+ #%]
+
+[% USE Bugzilla %]
+[% cgi = Bugzilla.cgi %]
+
+Mozilla Reps Payment Request
+----------------------------
+
+Requester info:
+
+First name: [% cgi.param('firstname') %]
+Last name: [% cgi.param('lastname') %]
+Wiki user profile: [% cgi.param('wikiprofile') %]
+Event wiki page: [% cgi.param('wikipage') %]
+Budget request [% terms.bug %]: [% cgi.param('bug_id') %]
+Have you already received payment for this event? [% IF cgi.param('receivedpayment') %]Yes[% ELSE %]No[% END %]
+
+[%+ cgi.param("comment") IF cgi.param("comment") %]
+
diff --git a/extensions/BMO/template/en/default/pages/etiquette.html.tmpl b/extensions/BMO/template/en/default/pages/etiquette.html.tmpl
new file mode 100644
index 000000000..8bccaea9d
--- /dev/null
+++ b/extensions/BMO/template/en/default/pages/etiquette.html.tmpl
@@ -0,0 +1,147 @@
+
+[%# 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 the Bugzilla Bug Tracking System.
+ #
+ # The Initial Developer of the Original Code is Netscape Communications
+ # Corporation. Portions created by Netscape are
+ # Copyright (C) 1998 Netscape Communications Corporation. All
+ # Rights Reserved.
+ #
+ # Contributor(s): Stefan Seifert
+ # Gervase Markham
+ #%]
+
+[% INCLUDE global/header.html.tmpl
+ title = "$terms.Bugzilla Etiquette"
+ style = "li { margin: 5px } .heading { font-weight: bold }" %]
+
+
+ There's a number of faux pas you can commit when using
+ [% terms.Bugzilla %]. At the very
+ least, these will make Mozilla contributors upset at you; if committed enough
+ times they will cause those contributors to demand the disabling of your
+ [% terms.Bugzilla %] account. So, ignore this advice at your peril.
+
+
+
+ That said, Mozilla developers are generally a friendly bunch, and will be
+ friendly towards you as long as you follow these guidelines.
+
+
+
1. Commenting
+
+
+ This is the most important section.
+
+
+
+
+ No pointless comments.
+ Unless you have something constructive and helpful to say, do not add a
+ comment to a [% terms.bug %]. In [% terms.bugs %] where there is a heated debate going on, you
+ should be even more
+ inclined not to add a comment. Unless you have something new to contribute,
+ then the [% terms.bug %] owner is aware of all the issues, and will make a judgement
+ as to what to do. If you agree the [% terms.bug %] should be fixed, vote for it.
+ Additional "I see this too" or "It works for me" comments are unnecessary
+ unless they are on a different platform or a significantly different build.
+ Constructive and helpful thoughts unrelated to the topic of the [% terms.bug %]
+ should go in the appropriate
+ newsgroup.
+
+
+
+ No obligation.
+ "Open Source" is not the same as "the developers must do my bidding."
+ Everyone here wants to help, but the only person who has any
+ obligation to fix the [% terms.bugs %] you want fixed is you. Therefore, you
+ should not act as if you expect someone to fix a [% terms.bug %] by a particular date
+ or release. Aggressive or repeated demands will not be received
+ well and will almost certainly diminish the impact and interest in your
+ suggestions.
+
+
+
+ No abusing people.
+ Constant and intense critique is one of the reasons we build great products.
+ It's harder to fall into group-think if there is always a healthy amount of
+ dissent. We want to encourage vibrant debate inside of the Mozilla
+ community, we want you to disagree with us, and we want you to effectively
+ argue your case. However, we require that in the process, you attack
+ things, not people. Examples of things include: interfaces,
+ algorithms, and schedules. Examples of people include: developers,
+ designers and users. Attacking a person may result in you being banned
+ from [% terms.Bugzilla %].
+
+
+
+ No private email.
+ Unless the [% terms.bug %] owner or another respected project contributor has asked you
+ to email them with specific information, please place all information
+ relating to [% terms.bugs %]
+ in the [% terms.bug %] itself. Do not send them by private email; no-one else can read
+ them if you do that, and they'll probably just get ignored. If a file
+ is too big for [% terms.Bugzilla %], add a comment giving the file size and contents
+ and ask what to do.
+
+
+
+
2. Changing Fields
+
+
+
+ No messing with other people's [% terms.bugs %].
+ Unless you are the [% terms.bug %] assignee, or have some say over the use of their
+ time, never change the Priority or Target Milestone fields. If in doubt,
+ do not change the fields of [% terms.bugs %] you do not own - add a comment
+ instead, suggesting the change.
+
+
+
+ No whining about decisions.
+ If a respected project contributor has marked a [% terms.bug %] as INVALID, then it is
+ invalid. Someone filing another duplicate of it does not change this. Unless
+ you have further important evidence, do not post a comment arguing that an
+ INVALID or WONTFIX [% terms.bug %] should be reopened.
+
+
+
+
+
3. Applicability
+
+
+
+ Some of these rules may not apply to you. If they do not, you will know
+ exactly which ones do not, and why they do not apply. If you are not
+ sure, then they definitely all apply to you.
+
+
+
+
+ If you see someone not following these rules, the first step is, as an exception
+ to guideline 1.4, to make them aware of this document by private mail.
+ Flaming people publically in [% terms.bugs %] violates guidelines 1.1 and 1.3. In the case of
+ persistent offending you should report the matter to
+ Gerv.
+
+
+
+ This entire document can be summed up in one sentence:
+ do unto others as you would have them do unto you.
+
+
+[% INCLUDE global/footer.html.tmpl %]
diff --git a/extensions/BMO/template/en/default/pages/get_help.html.tmpl b/extensions/BMO/template/en/default/pages/get_help.html.tmpl
new file mode 100644
index 000000000..70ff0a12b
--- /dev/null
+++ b/extensions/BMO/template/en/default/pages/get_help.html.tmpl
@@ -0,0 +1,42 @@
+[%# 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 the Bugzilla Bug Tracking System.
+ #
+ # The Initial Developer of the Original Code is Netscape Communications
+ # Corporation. Portions created by Netscape are
+ # Copyright (C) 1998 Netscape Communications Corporation. All
+ # Rights Reserved.
+ #
+ # Contributor(s): David Miller
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+[% INCLUDE global/header.html.tmpl title = "Get Help with Mozilla Products" %]
+
+
+
+
+
+[% INCLUDE global/footer.html.tmpl %]
diff --git a/extensions/BMO/template/en/default/pages/remo-form-payment.html.tmpl b/extensions/BMO/template/en/default/pages/remo-form-payment.html.tmpl
new file mode 100644
index 000000000..ae4ca6f2e
--- /dev/null
+++ b/extensions/BMO/template/en/default/pages/remo-form-payment.html.tmpl
@@ -0,0 +1,243 @@
+[%# 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 the BMO Extension
+ #
+ # The Initial Developer of the Original Code is the Mozilla Foundation
+ # Portions created by the Initial Developers are Copyright (C) 2011 the
+ # Initial Developer. All Rights Reserved.
+ #
+ # Contributor(s):
+ # Dave Lawrence
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+
+[% PROCESS global/header.html.tmpl
+ title = "Mozilla Reps Payment Form"
+ style_urls = [ 'extensions/BMO/web/styles/moz_reps.css' ]
+ javascript_urls = [ 'extensions/BMO/web/js/form_validate.js',
+ 'js/util.js',
+ 'js/field.js' ]
+ yui = ['connection', 'json']
+%]
+
+
+
+
Mozilla Reps - Payment Form
+
+
+
+
+
+
+
+
+
+
First Name: *
+
+
+
+
+
+
+
Last Name: *
+
+
+
+
+
+
+
Wiki user profile:*
+
+
+
+
+
+
+
Event wiki page: *
+
+
+
+
+
+
+
Budget request [% terms.bug %]: *
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Have you already received payment for this event?
+
+
+ Please send an email to William at mozilla.com with all the information below:
+
+ Payment information:
+ Bank name:
+ Bank address:
+ IBAN:
+ Swift code/BIC:
+ Additional bank details (if necessary):
+
+
+
+
+
+
+ Expense form and scanned receipts/invoices:
+
+
+
+
+
Expense Form: *
+
+
+
+
+
Receipts File: *
+
+
+
+ Please black out any bank account information included
+ on receipts before attaching them.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ * - Required field
+ Thanks for contacting us.
+
+
+[% PROCESS global/footer.html.tmpl %]
diff --git a/extensions/BMO/template/en/default/pages/triage_reports.html.tmpl b/extensions/BMO/template/en/default/pages/triage_reports.html.tmpl
new file mode 100644
index 000000000..a7f26e86d
--- /dev/null
+++ b/extensions/BMO/template/en/default/pages/triage_reports.html.tmpl
@@ -0,0 +1,199 @@
+[%# 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 the BMO Extension
+ #
+ # The Initial Developer of the Original Code is the Mozilla Foundation
+ # Portions created by the Initial Developers are Copyright (C) 2011 the
+ # Initial Developer. All Rights Reserved.
+ #
+ # Contributor(s):
+ # Byron Jones
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+
+[% js_data = BLOCK %]
+var useclassification = false;
+var first_load = true;
+var last_sel = [];
+var cpts = new Array();
+[% n = 1 %]
+[% FOREACH p = user.get_selectable_products %]
+ cpts['[% n FILTER js %]'] = [
+ [%- FOREACH c = p.components %]'[% c.name FILTER js %]'[% ", " UNLESS loop.last %] [%- END -%] ];
+ [% n = n+1 %]
+[% END %]
+
+var selected_components = [
+ [%- FOREACH c = input.component %]'[% c FILTER js %]'
+ [%- ',' UNLESS loop.last %] [%- END ~%] ];
+
+[% END %]
+
+[% INCLUDE global/header.html.tmpl
+ title = "Triage Reports"
+ yui = [ 'autocomplete', 'calendar' ]
+ javascript = js_data
+ javascript_urls = [ "js/util.js", "js/field.js", "js/productform.js",
+ "extensions/BMO/web/js/triage_reports.js" ]
+ style_urls = [ "skins/standard/buglist.css",
+ "extensions/BMO/web/styles/triage_reports.css" ]
+%]
+
+
+
+[% PROCESS "global/field-descs.none.tmpl" %]
+
+
+ [% INCLUDE global/user.html.tmpl who = bug.reporter %]
+ [% IF bug.commenter.id != bug.reporter.id %]
+ [% INCLUDE global/user.html.tmpl who = bug.commenter %]
+ [% END %]
+
+
+ [% bug.comment_ts FILTER html FILTER no_break %]
+
+[% END %]
+
+[% END %]
+
+[% INCLUDE global/footer.html.tmpl %]
diff --git a/extensions/BMO/template/en/default/pages/upgrade-3.6.html.tmpl b/extensions/BMO/template/en/default/pages/upgrade-3.6.html.tmpl
new file mode 100644
index 000000000..8fa944ae6
--- /dev/null
+++ b/extensions/BMO/template/en/default/pages/upgrade-3.6.html.tmpl
@@ -0,0 +1,304 @@
+[%# 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 the Bugzilla Bug Tracking System.
+ #
+ # The Initial Developer of the Original Code is Netscape Communications
+ # Corporation. Portions created by Netscape are
+ # Copyright (C) 1998 Netscape Communications Corporation. All
+ # Rights Reserved.
+ #
+ # Contributor(s): David Miller
+ # Reed Loden
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+[% INCLUDE global/header.html.tmpl
+ title = "Bugzilla 3.6 Upgrade"
+%]
+[% USE date %]
+
+
Last Updated: [% date.format(template.modtime, "%d-%b-%Y %H:%M %Z") %]
+
+
On Friday, July 9, 2010, at 11:40pm PDT (0640 UTC), bugzilla.mozilla.org was
+ upgraded to Bugzilla 3.6.1+. Please
+ file
+ any regressions for tracking purposes.
+
+
Known Issues
+
+
The following is a list of issues which are known to be broken or incomplete with this upgrade so far.
A scientific
+ usability study was done on [% terms.Bugzilla %] by researchers
+ from Carnegie-Mellon University. As a result of this study,
+ several
+ usability issues were prioritized to be fixed, based on specific data
+ from the study.
+
+
As a result, you will see many small improvements in [% terms.Bugzilla %]'s
+ usability, such as using Javascript to validate certain forms before
+ they are submitted, standardizing the words that we use in the user interface,
+ being clearer about what [% terms.Bugzilla %] needs from the user,
+ and other changes, all of which are also listed individually in this New
+ Features section.
+
+
Work continues on improving usability for the next release of
+ [%+ terms.Bugzilla %], but the results of the research have already
+ had an impact on this 3.6 release.
+
+
Improved Quicksearch
+
+
The "quicksearch" box that appears on the front page of
+ [%+ terms.Bugzilla %] and in the header/footer of every page
+ is now simplified and made more powerful. There is a
+ [?] link next to the box that will take you to
+ the simplified Quicksearch Help,
+ which describes every single feature of the system in a simple layout,
+ including new features such as the ability to use partial field names
+ when searching.
+
+
Quicksearch should also be much faster than it was before, particularly
+ on large installations.
+
+
Note that in order to implement the new quicksearch, certain old
+ and rarely-used features had to be removed:
+
+
+
+ as a prefix to mean "search additional resolutions", and
+ + as a prefix to mean "search just the summary". You can
+ instead use summary: to explicitly search summaries.
+
Searching the Severity field if you type something that matches
+ the first few characters of a severity. You can explicitly search
+ the Severity field if you want to find [% terms.bugs %] by severity.
+
Searching the Priority field if you typed something that exactly
+ matched the name of a priority. You can explicitly search the
+ Priority field if you want to find [% terms.bugs %] by priority.
+
Searching the Platform and OS fields if you typed in one of a
+ certain hard-coded list of strings (like "pc", "windows", etc.).
+ You can explicitly search these fields, instead, if you want to
+ find [% terms.bugs %] with a specific Platform or OS set.
+
+
+
Simple "Browse" Interface
+
+
There is now a "Browse" link in the header of each [% terms.Bugzilla %]
+ page that presents a very basic interface that allows users to simply
+ browse through all open [% terms.bugs %] in particular components.
+
+
JSON-RPC Interface
+
+
[% terms.Bugzilla %] now has support for the
+ JSON-RPC WebServices protocol via
+ jsonrpc.cgi.
+ The JSON-RPC interface is experimental in this release--if you want any
+ fundamental changes in how it works,
+ let us
+ know, for the next release of [% terms.Bugzilla %].
+
+
New Features
+
+
Enhancements for Users
+
+
+
[% terms.Bug %] Filing: When filing [% terms.abug %],
+ [%+ terms.Bugzilla %] now visually indicates which fields are
+ mandatory.
+
[% terms.Bug %] Filing: "Bookmarkable templates" now
+ support the "alias" and "estimated hours" fields.
+
+
[% terms.Bug %] Editing: In previous versions of
+ [%+ terms.Bugzilla %], if you added a private comment to [% terms.abug %],
+ then none of the changes that you made at that time were
+ sent to users who couldn't see the private comment. Now, for users
+ who can't see private comments, public changes are sent, but the private
+ comment is excluded from their email notification.
+
[% terms.Bug %] Editing: The controls for groups now
+ appear to the right of the attachment and time-tracking tables,
+ when editing [% terms.abug %].
+
[% terms.Bug %] Editing: The "Collapse All Comments"
+ and "Expand All Comments" links now appear to the right of the
+ comment list instead of above it.
+
[% terms.Bug %] Editing: The See Also field now supports
+ URLs for Google Code Issues and the Debian B[% %]ug-Tracking System.
+
[% terms.Bug %] Editing: There have been significant performance
+ improvements in show_bug.cgi (the script that displays the
+ [% terms.bug %]-editing form), particularly for [% terms.bugs %] that
+ have lots of comments or attachments.
+
+
Attachments: The "Details" page of an attachment
+ now displays itself as uneditable if you can't edit the fields
+ there.
+
Attachments: We now make sure that there is
+ a Description specified for an attachment, using JavaScript, before
+ the form is submitted.
+
Attachments: There is now a link back to the [% terms.bug %]
+ at the bottom of the "Details" page for an attachment.
+
Attachments: When you click on an "attachment 12345" link
+ in a comment, if the attachment is a patch, you will now see the
+ formatted "Diff" view instead of the raw patch.
+
Attachments: For text attachments, we now let the browser
+ auto-detect the character encoding, instead of forcing the browser to
+ always assume the attachment is in UTF-8.
+
+
Search: You can now display [% terms.bug %] flags as a column
+ in search results.
+
Search: When viewing search results, you can see which columns are
+ being sorted on, and which direction the sort is on, as indicated
+ by arrows next to the column headers.
+
Search: You can now search the Deadline field using relative
+ dates (like "1d", "2w", etc.).
+
Search: The iCalendar format of search results now includes
+ a PRIORITY field.
+
Search: It is no longer an error to enter an invalid search
+ order in a search URL--[% terms.Bugzilla %] will simply warn you that
+ some of your order options are invalid.
+
Search: When there are no search results, some helpful
+ links are displayed, offering actions you might want to take.
+
Search: For those who like to make their own
+ buglist.cgi URLs (and for people working on customizations),
+ buglist.cgi now accepts nearly every valid field in
+ [%+ terms.Bugzilla %] as a direct URL parameter, like
+ &field=value.
+
+
Requests: When viewing the "My Requests" page, you can now
+ see the lists as a normal search result by clicking a link at the
+ bottom of each table.
+
Requests: When viewing the "My Requests" page, if you are
+ using Classifications, the Product drop-down will be grouped by
+ Classification.
+
+
If there are multiple languages available for your
+ [%+ terms.Bugzilla %], you can now select what language you want
+ [%+ terms.Bugzilla %] displayed in using links at the top of every
+ page.
+
When creating a new account, you will be automatically logged in
+ after setting your password.
+
There is no longer a maximum password length for accounts.
+
In the Dusk skin, it's now easier to see links.
+
In the Whining system, you can now choose to receive emails even
+ if there are no [% terms.bugs %] that match your searches.
+
The arrows in dependency graphs now point the other way, so that
+ [%+ terms.bugs %] point at their dependencies.
+
+
New Charts: You can now convert an existing Saved Search
+ into a data series for New Charts.
+
New Charts: There is now an interface that allows you to
+ delete data series.
+
New Charts: When deleting a product, you now have the option
+ to delete the data series that are associated with that product.
+
+
+
Enhancements for Administrators and Developers
+
+
+
Depending on how your workflow is set up, it is now possible to
+ have both UNCONFIRMED and REOPENED show up as status choices for
+ a closed [% terms.bug %]. If you only want one or the other to
+ show up, you should edit your status workflow appropriately
+ (possibly by removing or disabling the REOPENED status).
+
You can now "disable" field values so that they don't show
+ up as choices on [% terms.abug %] unless they are already set as
+ the value for that [% terms.bug %]. This doesn't work for the
+ per-product field values (component, target_milestone, and version)
+ yet, though.
+
Users are now locked out of their accounts for 30 minutes after
+ trying five bad passwords in a row during login. Every time a
+ user is locked out like this, the user in the "maintainer" parameter
+ will get an email.
+
The minimum length allowed for a password is now 6 characters.
+
The UNCONFIRMED status being enabled in a product
+ is now unrelated to the voting parameters. Instead, there is a checkbox
+ to enable the UNCONFIRMED status in a product.
+
Information about duplicates is now stored in the database instead
+ of being stored in the data/ directory. On large installations
+ this could save several hundred megabytes of disk space.
+
+
When editing a group, you can now specify that members of a group
+ are allowed to grant others membership in that group itself.
+
The ability to compress BMP attachments to PNGs is now an Extension.
+ To enable the feature, remove the file
+ extensions/BmpConvert/disabled and then run checksetup.pl.
+
The default list of values for the Priority field are now clear English
+ words instead of P1, P2, etc.
+
config.cgi now returns an ETag header and understands
+ the If-None-Match header in HTTP requests.
+
The XML format of show_bug.cgi now returns more information:
+ the numeric id of each comment, whether an attachment is a URL,
+ the modification time of an attachment, the numeric id of a flag,
+ and the numeric id of a flag's type.
+
+
+
WebService Changes
+
+
+
The WebService now returns all dates and times in the UTC timezone.
+ B[% %]ugzilla.time now acts as though the [% terms.Bugzilla %]
+ server were in the UTC timezone, always. If you want to write clients
+ that are compatible across all [% terms.Bugzilla %] versions,
+ check the timezone from B[% %]ugzilla.timezone or
+ B[% %]ugzilla.time, and always input times in that timezone
+ and expect times to be returned in that format.
+
You can now log in by passing Bugzilla_login and
+ Bugzilla_password as arguments to any WebService function.
+ See the
+ Bugzilla::WebService
+ documentation for details.
+
New Method:
+ B[% %]ug.attachments
+ which allows getting information about attachments.
+
New Method:
+ B[% %]ug.fields,
+ which gets information about all the fields that [% terms.abug %] can have
+ in [% terms.Bugzilla %], include custom fields and legal values for
+ all fields. The B[% %]ug.legal_values method is now deprecated.
+
In the B[% %]ug.add_comment method, the "private" parameter
+ has been renamed to "is_private" (for consistency with other methods).
+ You can still use "private", though, for backwards-compatibility.
+
The WebService now has Perl's "taint mode" turned on. This means that
+ it validates all data passed in before sending it to the database.
+ Also, all parameter names are validated, and if you pass in a parameter
+ whose name contains anything other than letters, numbers, or underscores,
+ that parameter will be ignored. Mostly this just affects
+ customizers--[% terms.Bugzilla %]'s WebService is not functionally
+ affected by these changes.
+
In previous versions of [% terms.Bugzilla %], error messages were
+ sent word-wrapped to the client, from the WebService. Error messages
+ are now sent as one unbroken line.
+
+
+
Last Ten Commits
+
+
[% bzr_history.join('') FILTER html %]
+
+
+
+[% INCLUDE global/footer.html.tmpl %]
diff --git a/extensions/BMO/template/en/default/pages/user_activity.html.tmpl b/extensions/BMO/template/en/default/pages/user_activity.html.tmpl
new file mode 100644
index 000000000..904f0ba62
--- /dev/null
+++ b/extensions/BMO/template/en/default/pages/user_activity.html.tmpl
@@ -0,0 +1,180 @@
+[%# 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 the BMO Extension
+ #
+ # The Initial Developer of the Original Code is the Mozilla Foundation
+ # Portions created by the Initial Developers are Copyright (C) 2011 the
+ # Initial Developer. All Rights Reserved.
+ #
+ # Contributor(s):
+ # Byron Jones
+ #%]
+
+[% INCLUDE global/header.html.tmpl
+ title = "User Activity Report"
+ yui = [ 'autocomplete', 'calendar' ]
+ javascript_urls = [ "js/util.js", "js/field.js" ]
+%]
+
+
+
+[% PROCESS "global/field-descs.none.tmpl" %]
+[% PROCESS bug/time.html.tmpl %]
+
+
+
+
+
+
+
+ Who:
+
+
+ [% INCLUDE global/userselect.html.tmpl
+ id => "who"
+ name => "who"
+ value => who
+ size => 40
+ emptyok => 0
+ title => "One or more email address (comma delimited)"
+ %]
+
+
+
+ Period:
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+[% IF action == 'run' %]
+
+[% IF incomplete_data %]
+
+ There used to be an issue in [% terms.Bugzilla %]
+ which caused activity data to be lost if there were a large number of cc's
+ or dependencies. That has been fixed, but some data was already lost in
+ your activity table that could not be regenerated. The changes that
+ could not reliably determine are prefixed by '?'.
+
+
+[% END %]
+
diff --git a/extensions/ComponentWatching/template/en/default/hook/account/prefs/email-relationships.html.tmpl b/extensions/ComponentWatching/template/en/default/hook/account/prefs/email-relationships.html.tmpl
new file mode 100644
index 000000000..cab637967
--- /dev/null
+++ b/extensions/ComponentWatching/template/en/default/hook/account/prefs/email-relationships.html.tmpl
@@ -0,0 +1,22 @@
+[%# 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 the Component Watching Extension
+ #
+ # The Initial Developer of the Original Code is the Mozilla Foundation
+ # Portions created by the Initial Developers are Copyright (C) 2011 the
+ # Initial Developer. All Rights Reserved.
+ #
+ # Contributor(s):
+ # Byron Jones
+ #%]
+
+[% relationships.push({ id = constants.REL_COMPONENT_WATCHER, description = "Component" }) %]
+[% no_added_removed.push(constants.REL_COMPONENT_WATCHER) %]
diff --git a/extensions/ComponentWatching/template/en/default/hook/account/prefs/prefs-tabs.html.tmpl b/extensions/ComponentWatching/template/en/default/hook/account/prefs/prefs-tabs.html.tmpl
new file mode 100644
index 000000000..7a615a8ac
--- /dev/null
+++ b/extensions/ComponentWatching/template/en/default/hook/account/prefs/prefs-tabs.html.tmpl
@@ -0,0 +1,27 @@
+[%#
+ # 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 the Component Watching Extension
+ #
+ # The Initial Developer of the Original Code is the Mozilla Foundation
+ # Portions created by the Initial Developers are Copyright (C) 2011 the
+ # Initial Developer. All Rights Reserved.
+ #
+ # Contributor(s):
+ # Byron Jones
+ #%]
+
+[% tabs = tabs.import([{
+ name => "component_watch",
+ label => "Component Watching",
+ link => "userprefs.cgi?tab=component_watch",
+ saveable => 1
+ }]) %]
diff --git a/extensions/ComponentWatching/template/en/default/hook/global/reason-descs-end.none.tmpl b/extensions/ComponentWatching/template/en/default/hook/global/reason-descs-end.none.tmpl
new file mode 100644
index 000000000..2621f85e4
--- /dev/null
+++ b/extensions/ComponentWatching/template/en/default/hook/global/reason-descs-end.none.tmpl
@@ -0,0 +1,22 @@
+[%# 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 the Component Watching Extension
+ #
+ # The Initial Developer of the Original Code is the Mozilla Foundation
+ # Portions created by the Initial Developers are Copyright (C) 2011 the
+ # Initial Developer. All Rights Reserved.
+ #
+ # Contributor(s):
+ # Byron Jones
+ #%]
+
+[% watch_reason_descs.${constants.REL_COMPONENT_WATCHER} =
+ "You are watching the component for the ${terms.bug}." %]
diff --git a/extensions/GuidedBugEntry/Config.pm b/extensions/GuidedBugEntry/Config.pm
new file mode 100644
index 000000000..924fe568c
--- /dev/null
+++ b/extensions/GuidedBugEntry/Config.pm
@@ -0,0 +1,33 @@
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+#
+# 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 the GuidedBugEntry Bugzilla Extension.
+#
+# The Initial Developer of the Original Code is the Mozilla Foundation
+# Portions created by the Initial Developer are Copyright (C) 2011 the
+# Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+# Byron Jones
+
+package Bugzilla::Extension::GuidedBugEntry;
+use strict;
+
+use constant NAME => 'GuidedBugEntry';
+
+use constant REQUIRED_MODULES => [
+];
+
+use constant OPTIONAL_MODULES => [
+];
+
+__PACKAGE__->NAME;
diff --git a/extensions/GuidedBugEntry/Extension.pm b/extensions/GuidedBugEntry/Extension.pm
new file mode 100644
index 000000000..977aecfb6
--- /dev/null
+++ b/extensions/GuidedBugEntry/Extension.pm
@@ -0,0 +1,123 @@
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+#
+# 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 the GuidedBugEntry Bugzilla Extension.
+#
+# The Initial Developer of the Original Code is the Mozilla Foundation
+# Portions created by the Initial Developer are Copyright (C) 2011 the
+# Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+# Byron Jones
+
+package Bugzilla::Extension::GuidedBugEntry;
+use strict;
+use base qw(Bugzilla::Extension);
+
+use Bugzilla::Token;
+use Bugzilla::Error;
+use Bugzilla::Status;
+use Bugzilla::Util 'url_quote';
+use Bugzilla::UserAgent;
+
+our $VERSION = '1';
+
+sub enter_bug_start {
+ my ($self, $args) = @_;
+ my $vars = $args->{vars};
+ my $template = Bugzilla->template;
+ my $cgi = Bugzilla->cgi;
+ my $user = Bugzilla->user;
+
+ # hack for skipping old guided code when enabled
+ $vars->{'disable_guided'} = 1;
+
+ # force guided format for new users
+ my $format = $cgi->param('format') || '';
+ if (
+ $format eq 'guided' ||
+ (
+ $format eq '' &&
+ !$user->in_group('canconfirm')
+ )
+ ) {
+ # skip the first step if a product is provided
+ if ($cgi->param('product')) {
+ print $cgi->redirect('enter_bug.cgi?format=guided#h=dupes|' .
+ url_quote($cgi->param('product')));
+ exit;
+ }
+
+ $self->_init_vars($vars);
+ print $cgi->header();
+ $template->process('guided/guided.html.tmpl', $vars)
+ || ThrowTemplateError($template->error());
+ exit;
+ }
+
+ # we use the __default__ format to bypass the guided entry
+ # it isn't understood upstream, so remove it once a product
+ # has been selected.
+ $cgi->delete('format')
+ if $cgi->param('format')
+ && $cgi->param('format') eq "__default__"
+ && $cgi->param('product') ne '';
+}
+
+sub _init_vars {
+ my ($self, $vars) = @_;
+ my $user = Bugzilla->user;
+
+ my @enterable_products = @{$user->get_enterable_products};
+ ThrowUserError('no_products') unless scalar(@enterable_products);
+
+ my @classifications = ({object => undef, products => \@enterable_products});
+
+ my $class;
+ foreach my $product (@enterable_products) {
+ $class->{$product->classification_id}->{'object'} ||=
+ new Bugzilla::Classification($product->classification_id);
+ push(@{$class->{$product->classification_id}->{'products'}}, $product);
+ }
+ @classifications =
+ sort {
+ $a->{'object'}->sortkey <=> $b->{'object'}->sortkey
+ || lc($a->{'object'}->name) cmp lc($b->{'object'}->name)
+ } (values %$class);
+ $vars->{'classifications'} = \@classifications;
+
+ my @open_states = BUG_STATE_OPEN();
+ $vars->{'open_states'} = \@open_states;
+
+ $vars->{'token'} = issue_session_token('createbug:');
+
+ $vars->{'platform'} = detect_platform();
+ $vars->{'op_sys'} = detect_op_sys();
+}
+
+sub page_before_template {
+ my ($self, $args) = @_;
+ my $page = $args->{'page_id'};
+ my $vars = $args->{'vars'};
+
+ return unless $page eq 'guided_products.js';
+
+ # import product -> security group mappings from the BMO ext
+
+ our %product_sec_groups;
+ eval q#use Bugzilla::Extension::BMO::Data '%product_sec_groups'#;
+ return if $@;
+
+ $vars->{'products'} = \%product_sec_groups;
+}
+
+__PACKAGE__->NAME;
diff --git a/extensions/GuidedBugEntry/template/en/default/bug/create/comment-guided.txt.tmpl b/extensions/GuidedBugEntry/template/en/default/bug/create/comment-guided.txt.tmpl
new file mode 100644
index 000000000..6b0de9466
--- /dev/null
+++ b/extensions/GuidedBugEntry/template/en/default/bug/create/comment-guided.txt.tmpl
@@ -0,0 +1,25 @@
+[% USE Bugzilla %]
+[% cgi = Bugzilla.cgi %]
+User Agent: [% cgi.param('user_agent') %]
+[% IF cgi.param('build_id') %]
+Build ID: [% cgi.param('build_id') %][% END %]
+
+[% IF cgi.param('bug_steps') %]
+Steps to reproduce:
+
+[%+ cgi.param('bug_steps') %]
+[% END %]
+
+[% IF cgi.param('actual') %]
+
+Actual results:
+
+[%+ cgi.param('actual') %]
+[% END %]
+
+[% IF cgi.param('expected') %]
+
+Expected results:
+
+[%+ cgi.param('expected') %]
+[% END %]
diff --git a/extensions/GuidedBugEntry/template/en/default/guided/guided.html.tmpl b/extensions/GuidedBugEntry/template/en/default/guided/guided.html.tmpl
new file mode 100644
index 000000000..403b63a6e
--- /dev/null
+++ b/extensions/GuidedBugEntry/template/en/default/guided/guided.html.tmpl
@@ -0,0 +1,534 @@
+[%# ***** BEGIN LICENSE BLOCK *****
+ # Version: MPL 1.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 the GuidedBugEntry Bugzilla Extension.
+ #
+ # The Initial Developer of the Original Code is
+ # the Mozilla Foundation.
+ # Portions created by the Initial Developer are Copyright (C) 2011
+ # the Initial Developer. All Rights Reserved.
+ #
+ # Contributor(s):
+ # Byron Jones
+ #
+ # ***** END LICENSE BLOCK ***** */
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+
+[% PROCESS global/header.html.tmpl
+ title = "Enter A $terms.Bug"
+ javascript_urls = [
+ 'extensions/GuidedBugEntry/web/js/products.js',
+ 'extensions/GuidedBugEntry/web/js/guided.js',
+ 'js/field.js', 'js/TUI.js', 'js/bug.js' ]
+ style_urls = [ 'extensions/GuidedBugEntry/web/style/guided.css',
+ 'js/yui/assets/skins/sam/container.css' ]
+ yui = [ 'history', 'datatable', 'container' ]
+%]
+
+
+
+
+
+
+
+Please wait...
+
+
+
+
+[% INCLUDE product_step %]
+[% INCLUDE otherProducts_step %]
+[% INCLUDE dupes_step %]
+[% INCLUDE bugForm_step %]
+
Please fill out this form clearly, precisely and in as much detail as you can manage.
+
Please report only a single problem at a time.
+
These guidelines
+explain how to write effective [% terms.bug %] reports.
+
+
+
+
+
+
Summary:
+
+
+
+
+ [% PROCESS help id="summary_help" %]
+
+ A sentence which summarises the problem. Please be descriptive and use lots of keywords.
+
+ Bad example: mail crashed
+ Good example: crash if I close the mail window while checking for new POP mail
+
+ The area where the problem occurs.
+
+ If you are unsure which component to use, select a 'General' component.
+
+
+
+
+
What did you do?
+
+ [% PROCESS help id="steps_help" %]
+
+ Please be as specific as possible about what what you did
+ to cause the problem. Providing step-by-step instructions
+ would be ideal.
+
+ Include any relevant URLs and special setup steps.
+
+ Bad example: Mozilla crashed. You suck!
+ Good example: After a crash which happened
+ when I was sorting in the Bookmark Manager, all of my top-level bookmark
+ folders beginning with the letters Q to Z are no longer present.
+
+
+
+
+
+
+
+
+
+
What happened?
+
+ [% PROCESS help id="actual_help" %]
+
+ What happened after you performed the steps above?
+
+
+
+
+
+
+
+
+
What should have happened?
+
+ [% PROCESS help id="expected_help" %]
+
+ What should the software have done instead?
+
+
+
+
+
+
+
+
+
Attach a file:
+
+
+
+
+
+
+ [% PROCESS help id="file_help" %]
+
+ If a file helps explain the issue better, such as a screenshot, please
+ attach one here.
+
+
+
+
+
File Description:
+
+
+
+
+
+
Security:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+[% END %]
+
+[%############################################################################%]
+[%# help block #%]
+[%############################################################################%]
+
+[% BLOCK help %]
+
+[% END %]
diff --git a/extensions/GuidedBugEntry/template/en/default/guided/products.html.tmpl b/extensions/GuidedBugEntry/template/en/default/guided/products.html.tmpl
new file mode 100644
index 000000000..23d73d61f
--- /dev/null
+++ b/extensions/GuidedBugEntry/template/en/default/guided/products.html.tmpl
@@ -0,0 +1,63 @@
+[%# ***** BEGIN LICENSE BLOCK *****
+ # Version: MPL 1.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 the GuidedBugEntry Bugzilla Extension.
+ #
+ # The Initial Developer of the Original Code is
+ # the Mozilla Foundation.
+ # Portions created by the Initial Developer are Copyright (C) 2011
+ # the Initial Developer. All Rights Reserved.
+ #
+ # Contributor(s):
+ # Byron Jones
+ #
+ # ***** END LICENSE BLOCK ***** */
+ #%]
+
+[% INCLUDE product_block
+ name="Firefox"
+ icon="firefox.png"
+%]
+[% INCLUDE product_block
+ name="Fennec"
+ caption="Firefox for Mobile (Fennec)"
+ icon="fennec.png"
+%]
+[% INCLUDE product_block
+ name="Thunderbird"
+ icon="thunderbird.png"
+%]
+[% INCLUDE product_block
+ name="Mozilla Services"
+ icon="dino.png"
+%]
+[% INCLUDE product_block
+ name="SeaMonkey"
+ icon="seamonkey.png"
+%]
+[% INCLUDE product_block
+ name="Mozilla Localizations"
+ icon="dino.png"
+%]
+[% INCLUDE product_block
+ name="Mozilla Labs"
+ icon="labs.png"
+%]
+[% INCLUDE product_block
+ name="Calendar"
+ icon="sunbird.png"
+%]
+[% INCLUDE product_block
+ name="Core"
+ icon="core.png"
+%]
diff --git a/extensions/GuidedBugEntry/template/en/default/pages/guided_products.js.tmpl b/extensions/GuidedBugEntry/template/en/default/pages/guided_products.js.tmpl
new file mode 100644
index 000000000..e8697a5a1
--- /dev/null
+++ b/extensions/GuidedBugEntry/template/en/default/pages/guided_products.js.tmpl
@@ -0,0 +1,36 @@
+[%# ***** BEGIN LICENSE BLOCK *****
+ # Version: MPL 1.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 the GuidedBugEntry Bugzilla Extension.
+ #
+ # The Initial Developer of the Original Code is
+ # the Mozilla Foundation.
+ # Portions created by the Initial Developer are Copyright (C) 2011
+ # the Initial Developer. All Rights Reserved.
+ #
+ # Contributor(s):
+ # Byron Jones
+ #
+ # ***** END LICENSE BLOCK ***** */
+ #%]
+
+[%# this file allows us to pull in data defined in the BMO ext %]
+
+[% IF products %]
+ [% FOREACH product = products %]
+ if (!products['[% product.key FILTER js %]'])
+ products['[% product.key FILTER js %]'] = {};
+ products['[% product.key FILTER js %]'].secgroup = '[% product.value FILTER js %]';
+ [% END %]
+[% END %]
+
diff --git a/extensions/GuidedBugEntry/web/images/advanced.png b/extensions/GuidedBugEntry/web/images/advanced.png
new file mode 100644
index 000000000..71a3fcb78
Binary files /dev/null and b/extensions/GuidedBugEntry/web/images/advanced.png differ
diff --git a/extensions/GuidedBugEntry/web/images/help.png b/extensions/GuidedBugEntry/web/images/help.png
new file mode 100644
index 000000000..5c870176d
Binary files /dev/null and b/extensions/GuidedBugEntry/web/images/help.png differ
diff --git a/extensions/GuidedBugEntry/web/images/idea.png b/extensions/GuidedBugEntry/web/images/idea.png
new file mode 100644
index 000000000..0a0ce6c79
Binary files /dev/null and b/extensions/GuidedBugEntry/web/images/idea.png differ
diff --git a/extensions/GuidedBugEntry/web/images/input.png b/extensions/GuidedBugEntry/web/images/input.png
new file mode 100644
index 000000000..34c10e989
Binary files /dev/null and b/extensions/GuidedBugEntry/web/images/input.png differ
diff --git a/extensions/GuidedBugEntry/web/images/message.png b/extensions/GuidedBugEntry/web/images/message.png
new file mode 100644
index 000000000..55b6add19
Binary files /dev/null and b/extensions/GuidedBugEntry/web/images/message.png differ
diff --git a/extensions/GuidedBugEntry/web/images/products/camino.png b/extensions/GuidedBugEntry/web/images/products/camino.png
new file mode 100644
index 000000000..c833b4d04
Binary files /dev/null and b/extensions/GuidedBugEntry/web/images/products/camino.png differ
diff --git a/extensions/GuidedBugEntry/web/images/products/core.png b/extensions/GuidedBugEntry/web/images/products/core.png
new file mode 100644
index 000000000..b9c5053f6
Binary files /dev/null and b/extensions/GuidedBugEntry/web/images/products/core.png differ
diff --git a/extensions/GuidedBugEntry/web/images/products/dino.png b/extensions/GuidedBugEntry/web/images/products/dino.png
new file mode 100644
index 000000000..9e0470a07
Binary files /dev/null and b/extensions/GuidedBugEntry/web/images/products/dino.png differ
diff --git a/extensions/GuidedBugEntry/web/images/products/fennec.png b/extensions/GuidedBugEntry/web/images/products/fennec.png
new file mode 100644
index 000000000..ebad7e358
Binary files /dev/null and b/extensions/GuidedBugEntry/web/images/products/fennec.png differ
diff --git a/extensions/GuidedBugEntry/web/images/products/firefox.png b/extensions/GuidedBugEntry/web/images/products/firefox.png
new file mode 100644
index 000000000..582a6952a
Binary files /dev/null and b/extensions/GuidedBugEntry/web/images/products/firefox.png differ
diff --git a/extensions/GuidedBugEntry/web/images/products/labs.png b/extensions/GuidedBugEntry/web/images/products/labs.png
new file mode 100644
index 000000000..346e0ef06
Binary files /dev/null and b/extensions/GuidedBugEntry/web/images/products/labs.png differ
diff --git a/extensions/GuidedBugEntry/web/images/products/mozilla.png b/extensions/GuidedBugEntry/web/images/products/mozilla.png
new file mode 100644
index 000000000..e506328bc
Binary files /dev/null and b/extensions/GuidedBugEntry/web/images/products/mozilla.png differ
diff --git a/extensions/GuidedBugEntry/web/images/products/other.png b/extensions/GuidedBugEntry/web/images/products/other.png
new file mode 100644
index 000000000..e436c22ae
Binary files /dev/null and b/extensions/GuidedBugEntry/web/images/products/other.png differ
diff --git a/extensions/GuidedBugEntry/web/images/products/seamonkey.png b/extensions/GuidedBugEntry/web/images/products/seamonkey.png
new file mode 100644
index 000000000..fcb261ae1
Binary files /dev/null and b/extensions/GuidedBugEntry/web/images/products/seamonkey.png differ
diff --git a/extensions/GuidedBugEntry/web/images/products/sunbird.png b/extensions/GuidedBugEntry/web/images/products/sunbird.png
new file mode 100644
index 000000000..6b15c257d
Binary files /dev/null and b/extensions/GuidedBugEntry/web/images/products/sunbird.png differ
diff --git a/extensions/GuidedBugEntry/web/images/products/thunderbird.png b/extensions/GuidedBugEntry/web/images/products/thunderbird.png
new file mode 100644
index 000000000..f3523183a
Binary files /dev/null and b/extensions/GuidedBugEntry/web/images/products/thunderbird.png differ
diff --git a/extensions/GuidedBugEntry/web/images/sumo.png b/extensions/GuidedBugEntry/web/images/sumo.png
new file mode 100644
index 000000000..d5773647c
Binary files /dev/null and b/extensions/GuidedBugEntry/web/images/sumo.png differ
diff --git a/extensions/GuidedBugEntry/web/images/support.png b/extensions/GuidedBugEntry/web/images/support.png
new file mode 100644
index 000000000..2320ea74a
Binary files /dev/null and b/extensions/GuidedBugEntry/web/images/support.png differ
diff --git a/extensions/GuidedBugEntry/web/images/throbber.gif b/extensions/GuidedBugEntry/web/images/throbber.gif
new file mode 100644
index 000000000..bc4fa6561
Binary files /dev/null and b/extensions/GuidedBugEntry/web/images/throbber.gif differ
diff --git a/extensions/GuidedBugEntry/web/images/warning.png b/extensions/GuidedBugEntry/web/images/warning.png
new file mode 100644
index 000000000..86bed170d
Binary files /dev/null and b/extensions/GuidedBugEntry/web/images/warning.png differ
diff --git a/extensions/GuidedBugEntry/web/js/guided.js b/extensions/GuidedBugEntry/web/js/guided.js
new file mode 100644
index 000000000..f600dbb21
--- /dev/null
+++ b/extensions/GuidedBugEntry/web/js/guided.js
@@ -0,0 +1,824 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.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 the GuidedBugEntry Bugzilla Extension.
+ *
+ * The Initial Developer of the Original Code is
+ * the Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Byron Jones
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+// global
+
+var Dom = YAHOO.util.Dom;
+var Event = YAHOO.util.Event;
+var History = YAHOO.util.History;
+
+var guided = {
+ _currentStep: '',
+ detectedPlatform: '',
+ detectedOpSys: '',
+ currentUser: '',
+ openStates: [],
+
+ setStep: function(newStep, noSetHistory) {
+ // initialise new step
+ eval(newStep + '.onShow()');
+
+ // change visibility of _step div
+ if (this._currentStep)
+ Dom.addClass(this._currentStep + '_step', 'hidden');
+ this._currentStep = newStep;
+ Dom.removeClass(this._currentStep + '_step', 'hidden');
+
+ // scroll to top of page to mimic real navigation
+ scroll(0,0);
+
+ // update history
+ if (History && !noSetHistory) {
+ History.navigate('h', newStep + "|" + product.getName());
+ }
+ },
+
+ init: function() {
+ // init history manager
+ try {
+ History.register('h', History.getBookmarkedState('h') || 'product',
+ this._onStateChange);
+ History.initialize("yui-history-field", "yui-history-iframe");
+ History.onReady(function () {
+ guided._onStateChange(History.getCurrentState('h'), true);
+ });
+ } catch(err) {
+ History = false;
+ }
+
+ // init steps
+ product.onInit();
+ dupes.onInit();
+ bugForm.onInit();
+ },
+
+ _onStateChange: function(state, noSetHistory) {
+ state = state.split("|");
+ product.setName(state[1] || '');
+ guided.setStep(state[0], noSetHistory);
+ },
+
+ setAdvancedLink: function() {
+ href = 'enter_bug.cgi?format=__default__' +
+ '&product=' + escape(product.getName()) +
+ '&short_desc=' + escape(dupes.getSummary());
+ Dom.get('advanced_img').href = href;
+ Dom.get('advanced_link').href = href;
+ }
+};
+
+// product step
+
+var product = {
+ details: false,
+ _counter: 0,
+ _loaded: '',
+
+ onInit: function() { },
+
+ onShow: function() {
+ Dom.removeClass('advanced', 'hidden');
+ },
+
+ select: function(productName) {
+ // called when a product is selected
+ this.setName(productName);
+ dupes.reset();
+ guided.setStep('dupes');
+ },
+
+ getName: function() {
+ return Dom.get('product').value;
+ },
+
+ _getNameAndRelated: function() {
+ var result = [];
+
+ var name = this.getName();
+ result.push(name);
+
+ if (products[name] && products[name].related) {
+ for (var i = 0, n = products[name].related.length; i < n; i++) {
+ result.push(products[name].related[i]);
+ }
+ }
+
+ return result;
+ },
+
+ setName: function(productName) {
+ if (productName == this.getName() && this.details)
+ return;
+
+ // display the product name
+ Dom.get('product').value = productName;
+ Dom.get('product_label').innerHTML = YAHOO.lang.escapeHTML(productName);
+ Dom.get('dupes_product_name').innerHTML = YAHOO.lang.escapeHTML(productName);
+ Dom.get('list_comp').href = 'describecomponents.cgi?product=' + escape(productName);
+ guided.setAdvancedLink();
+
+ if (productName == '') {
+ Dom.addClass("product_support", "hidden");
+ return;
+ }
+
+ // use the correct security group
+ if (products[productName] && products[productName].secgroup) {
+ Dom.get('groups').value = products[productName].secgroup;
+ } else {
+ Dom.get('groups').value = products['_default'].secgroup;
+ }
+
+ // use the correct platform & op_sys
+ if (products[productName] && products[productName].detectPlatform) {
+ Dom.get('rep_platform').value = guided.detectedPlatform;
+ Dom.get('op_sys').value = guided.detectedOpSys;
+ } else {
+ Dom.get('rep_platform').value = 'All';
+ Dom.get('op_sys').value = 'All';
+ }
+
+ // show support message
+ if (products[productName] && products[productName].support) {
+ Dom.get("product_support_message").innerHTML =
+ YAHOO.lang.escapeHTML(products[productName].support);
+ Dom.removeClass("product_support", "hidden");
+ } else {
+ Dom.addClass("product_support", "hidden");
+ }
+
+ if (this._loaded == productName)
+ return;
+
+ // grab the product information
+ this.details = false;
+ this._loaded = productName;
+ YAHOO.util.Connect.setDefaultPostHeader('text/plain; charset=UTF-8');
+ YAHOO.util.Connect.asyncRequest(
+ 'POST',
+ 'jsonrpc.cgi',
+ {
+ success: function(res) {
+ try {
+ data = YAHOO.lang.JSON.parse(res.responseText);
+ if (data.error)
+ throw(data.error.message);
+ product.details = data.result.products[0];
+ bugForm.onProductUpdated();
+ } catch (err) {
+ product.details = false;
+ bugForm.onProductUpdated();
+ if (err) {
+ alert('Failed to retreive components for product "' +
+ productName + '":' + "\n\n" + err);
+ if (console)
+ console.error(err);
+ }
+ }
+ },
+ failure: function(res) {
+ this._loaded = '';
+ product.details = false;
+ bugForm.onProductUpdated();
+ if (res.responseText) {
+ alert('Failed to retreive components for product "' +
+ productName + '":' + "\n\n" + res.responseText);
+ if (console)
+ console.error(res);
+ }
+ }
+ },
+ YAHOO.lang.JSON.stringify({
+ version: "1.1",
+ method: "Product.get",
+ id: ++this._counter,
+ params: {
+ names: [productName],
+ exclude_fields: ['internals', 'milestones']
+ }
+ }
+ )
+ );
+ }
+};
+
+// other products step
+
+var otherProducts = {
+ onInit: function() { },
+
+ onShow: function() {
+ Dom.removeClass('advanced', 'hidden');
+ }
+};
+
+// duplicates step
+
+var dupes = {
+ _counter: 0,
+ _dataTable: null,
+ _dataTableColumns: null,
+ _elSummary: null,
+ _elSearch: null,
+ _elList: null,
+ _currentSearchQuery: '',
+
+ onInit: function() {
+ this._elSummary = Dom.get('dupes_summary');
+ this._elSearch = Dom.get('dupes_search');
+ this._elList = Dom.get('dupes_list');
+
+ Event.onBlur(this._elSummary, this._onSummaryBlur);
+ Event.addListener(this._elSummary, 'keydown', this._onSummaryKeyDown);
+ Event.addListener(this._elSummary, 'keyup', this._onSummaryKeyUp);
+ Event.addListener(this._elSearch, 'click', this._doSearch);
+ },
+
+ setLabels: function(labels) {
+ this._dataTableColumns = [
+ { key: "id", label: labels.id, formatter: this._formatId },
+ { key: "summary", label: labels.summary, formatter: "text" },
+ { key: "status", label: labels.status, formatter: this._formatStatus },
+ { key: "update_token", label: '', formatter: this._formatCc }
+ ];
+ },
+
+ _initDataTable: function() {
+ var dataSource = new YAHOO.util.XHRDataSource("jsonrpc.cgi");
+ dataSource.connTimeout = 15000;
+ dataSource.connMethodPost = true;
+ dataSource.connXhrMode = "cancelStaleRequests";
+ dataSource.maxCacheEntries = 3;
+ dataSource.responseSchema = {
+ resultsList : "result.bugs",
+ metaFields : { error: "error", jsonRpcId: "id" }
+ };
+ // DataSource can't understand a JSON-RPC error response, so
+ // we have to modify the result data if we get one.
+ dataSource.doBeforeParseData =
+ function(oRequest, oFullResponse, oCallback) {
+ if (oFullResponse.error) {
+ oFullResponse.result = {};
+ oFullResponse.result.bugs = [];
+ if (console)
+ console.error("JSON-RPC error:", oFullResponse.error);
+ }
+ return oFullResponse;
+ };
+ dataSource.subscribe('dataErrorEvent',
+ function() {
+ dupes._currentSearchQuery = '';
+ }
+ );
+
+ this._dataTable = new YAHOO.widget.DataTable(
+ 'dupes_list',
+ this._dataTableColumns,
+ dataSource,
+ {
+ initialLoad: false,
+ MSG_EMPTY: 'No similar issues found.',
+ MSG_ERROR: 'An error occurred while searching for similar issues,' +
+ ' please try again.'
+ }
+ );
+ },
+
+ _formatId: function(el, oRecord, oColumn, oData) {
+ el.innerHTML = '' + oData + '';
+ },
+
+ _formatStatus: function(el, oRecord, oColumn, oData) {
+ var resolution = oRecord.getData('resolution');
+ var bug_status = display_value('bug_status', oData);
+ if (resolution) {
+ el.innerHTML = bug_status + ' ' +
+ display_value('resolution', resolution);
+ } else {
+ el.innerHTML = bug_status;
+ }
+ },
+
+ _formatCc: function(el, oRecord, oColumn, oData) {
+ var cc = oRecord.getData('cc');
+ var isCCed = false;
+ for (var i = 0, n = cc.length; i < n; i++) {
+ if (cc[i] == guided.currentUser) {
+ isCCed = true;
+ break;
+ }
+ }
+ dupes._buildCcHTML(el, oRecord.getData('id'), oRecord.getData('status'),
+ isCCed);
+ },
+
+ _buildCcHTML: function(el, id, bugStatus, isCCed) {
+ while (el.childNodes.length > 0)
+ el.removeChild(el.firstChild);
+
+ var isOpen = false;
+ for (var i = 0, n = guided.openStates.length; i < n; i++) {
+ if (guided.openStates[i] == bugStatus) {
+ isOpen = true;
+ break;
+ }
+ }
+
+ if (!isOpen && !isCCed) {
+ // you can't cc yourself to a closed bug here
+ return;
+ }
+
+ var button = document.createElement('button');
+ button.setAttribute('type', 'button');
+ if (isCCed) {
+ button.innerHTML = 'Stop following';
+ button.onclick = function() {
+ dupes.updateFollowing(el, id, bugStatus, button, false); return false;
+ };
+ } else {
+ button.innerHTML = 'Follow bug';
+ button.onclick = function() {
+ dupes.updateFollowing(el, id, bugStatus, button, true); return false;
+ };
+ }
+ el.appendChild(button);
+ },
+
+ updateFollowing: function(el, bugID, bugStatus, button, follow) {
+ button.disabled = true;
+ button.innerHTML = 'Updating...';
+
+ var ccObject;
+ if (follow) {
+ ccObject = { add: [ guided.currentUser ] };
+ } else {
+ ccObject = { remove: [ guided.currentUser ] };
+ }
+
+ YAHOO.util.Connect.setDefaultPostHeader('text/plain; charset=UTF-8');
+ YAHOO.util.Connect.asyncRequest(
+ 'POST',
+ 'jsonrpc.cgi',
+ {
+ success: function(res) {
+ data = YAHOO.lang.JSON.parse(res.responseText);
+ if (data.error)
+ throw(data.error.message);
+ dupes._buildCcHTML(el, bugID, bugStatus, follow);
+ },
+ failure: function(res) {
+ dupes._buildCcHTML(el, bugID, bugStatus, !follow);
+ if (res.responseText)
+ alert("Update failed:\n\n" + res.responseText);
+ }
+ },
+ YAHOO.lang.JSON.stringify({
+ version: "1.1",
+ method: "Bug.update",
+ id: ++this._counter,
+ params: {
+ ids: [ bugID ],
+ cc : ccObject
+ }
+ })
+ );
+ },
+
+ reset: function() {
+ this._elSummary.value = '';
+ Dom.addClass(this._elList, 'hidden');
+ Dom.addClass('dupes_continue', 'hidden');
+ this._elList.innerHTML = '';
+ this._showProductSupport();
+ this._currentSearchQuery = '';
+ },
+
+ _showProductSupport: function() {
+ var elSupport = Dom.get('product_support_' +
+ product.getName().replace(' ', '_').toLowerCase());
+ var supportElements = Dom.getElementsByClassName('product_support');
+ for (var i = 0, n = supportElements.length; i < n; i++) {
+ if (supportElements[i] == elSupport) {
+ Dom.removeClass(elSupport, 'hidden');
+ } else {
+ Dom.addClass(supportElements[i], 'hidden');
+ }
+ }
+ },
+
+ onShow: function() {
+ this._showProductSupport();
+ this._onSummaryBlur();
+
+ // hide the advanced form and top continue button entry until
+ // a search has happened
+ Dom.addClass('advanced', 'hidden');
+ Dom.addClass('dupes_continue_button_top', 'hidden');
+
+ if (!this._elSearch.disabled && this.getSummary().length >= 4) {
+ // do an immediate search after a page refresh if there's a query
+ this._doSearch();
+
+ } else {
+ // prepare for a search
+ this.reset();
+ }
+ },
+
+ _onSummaryBlur: function() {
+ dupes._elSearch.disabled = dupes._elSummary.value == '';
+ guided.setAdvancedLink();
+ },
+
+ _onSummaryKeyDown: function(e) {
+ // map to doSearch()
+ if (e && (e.keyCode == 13)) {
+ dupes._doSearch();
+ Event.stopPropagation(e);
+ }
+ },
+
+ _onSummaryKeyUp: function(e) {
+ // disable search button until there's a query
+ dupes._elSearch.disabled = YAHOO.lang.trim(dupes._elSummary.value) == '';
+ },
+
+ _doSearch: function() {
+ if (dupes.getSummary().length < 4) {
+ alert('The summary must be at least 4 characters long.');
+ return;
+ }
+ dupes._elSummary.blur();
+
+ // don't query if we already have the results (or they are pending)
+ if (dupes._currentSearchQuery == dupes.getSummary())
+ return;
+ dupes._currentSearchQuery = dupes.getSummary();
+
+ // initialise the datatable as late as possible
+ dupes._initDataTable();
+
+ try {
+ // run the search
+ Dom.removeClass(dupes._elList, 'hidden');
+
+ dupes._dataTable.showTableMessage(
+ 'Searching for similar issues... ' +
+ '',
+ YAHOO.widget.DataTable.CLASS_LOADING
+ );
+ var json_object = {
+ version: "1.1",
+ method: "Bug.possible_duplicates",
+ id: ++dupes._counter,
+ params: {
+ product: product._getNameAndRelated(),
+ summary: dupes.getSummary(),
+ limit: 12,
+ include_fields: [ "id", "summary", "status", "resolution",
+ "update_token", "cc" ]
+ }
+ };
+
+ dupes._dataTable.getDataSource().sendRequest(
+ YAHOO.lang.JSON.stringify(json_object),
+ {
+ success: dupes._onDupeResults,
+ failure: dupes._onDupeResults,
+ scope: dupes._dataTable,
+ argument: dupes._dataTable.getState()
+ }
+ );
+
+ Dom.get('dupes_continue_button_top').disabled = true;
+ Dom.get('dupes_continue_button_bottom').disabled = true;
+ Dom.removeClass('dupes_continue', 'hidden');
+ } catch(err) {
+ if (console)
+ console.error(err.message);
+ }
+ },
+
+ _onDupeResults: function(sRequest, oResponse, oPayload) {
+ Dom.removeClass('advanced', 'hidden');
+ Dom.removeClass('dupes_continue_button_top', 'hidden');
+ Dom.get('dupes_continue_button_top').disabled = false;
+ Dom.get('dupes_continue_button_bottom').disabled = false;
+ dupes._dataTable.onDataReturnInitializeTable(sRequest, oResponse,
+ oPayload);
+ },
+
+ getSummary: function() {
+ var summary = YAHOO.lang.trim(this._elSummary.value);
+ // work around chrome bug
+ if (summary == dupes._elSummary.getAttribute('placeholder')) {
+ return '';
+ } else {
+ return summary;
+ }
+ }
+};
+
+// bug form step
+
+var bugForm = {
+ _visibleHelpPanel: null,
+ _mandatoryFields: [ 'short_desc', 'version_select' ],
+
+ onInit: function() {
+ Dom.get('user_agent').value = navigator.userAgent;
+ if (navigator.buildID && navigator.buildID != navigator.userAgent) {
+ Dom.get('build_id').value = navigator.buildID;
+ }
+ Event.addListener(Dom.get('short_desc'), 'blur', function() {
+ Dom.get('dupes_summary').value = Dom.get('short_desc').value;
+ guided.setAdvancedLink();
+ });
+ },
+
+ onShow: function() {
+ Dom.removeClass('advanced', 'hidden');
+ // default the summary to the dupes query
+ Dom.get('short_desc').value = dupes.getSummary();
+ this.resetSubmitButton();
+ if (Dom.get('component_select').length == 0)
+ this.onProductUpdated();
+ this.onFileChange();
+ for (var i = 0, n = this._mandatoryFields.length; i < n; i++) {
+ Dom.removeClass(this._mandatoryFields[i], 'missing');
+ }
+ },
+
+ resetSubmitButton: function() {
+ Dom.get('submit').disabled = false;
+ Dom.get('submit').value = 'Submit Bug';
+ },
+
+ onProductUpdated: function() {
+ var productName = product.getName();
+
+ // init
+ var elComponents = Dom.get('component_select');
+ Dom.addClass('component_description', 'hidden');
+ elComponents.options.length = 0;
+
+ var elVersions = Dom.get('version_select');
+ elVersions.length = 0;
+
+ // product not loaded yet, bail out
+ if (!product.details) {
+ Dom.addClass('versionTH', 'hidden');
+ Dom.addClass('versionTD', 'hidden');
+ Dom.get('productTD').colSpan = 2;
+ Dom.get('submit').disabled = true;
+ return;
+ }
+ Dom.get('submit').disabled = false;
+
+ // build components
+ // if there's a general component in it, make it selected by default
+ var defaultComponent = false;
+ for (var i = 0, n = product.details.components.length; i < n; i++) {
+ var component = product.details.components[i];
+ if (component.is_active == '1') {
+ elComponents.options[elComponents.options.length] =
+ new Option(component.name, component.name);
+ if (!defaultComponent && component.name.match(/general/i)) {
+ defaultComponent = component.name;
+ }
+ }
+ }
+
+ var elComponent = Dom.get('component');
+ if (elComponent.value == '' && defaultComponent)
+ elComponent.value = defaultComponent;
+ if (elComponent.value != '') {
+ elComponents.value = elComponent.value;
+ this.onComponentChange(elComponent.value);
+ }
+
+ // build versions
+ var defaultVersion = '';
+ var currentVersion = Dom.get('version').value;
+ for (var i = 0, n = product.details.versions.length; i < n; i++) {
+ var version = product.details.versions[i];
+ if (version.is_active == '1') {
+ elVersions.options[elVersions.options.length] =
+ new Option(version.name, version.name);
+ if (currentVersion == version.name)
+ defaultVersion = version.name;
+ }
+ }
+
+ if (!defaultVersion) {
+ // try to detect version on a per-product basis
+ if (products[productName] && products[productName].version) {
+ var detectedVersion = products[productName].version();
+ var options = elVersions.options;
+ for (var i = 0, n = options.length; i < n; i++) {
+ if (options[i].value == detectedVersion) {
+ defaultVersion = detectedVersion;
+ break;
+ }
+ }
+ }
+ }
+ if (!defaultVersion) {
+ // load last selected version
+ defaultVersion = YAHOO.util.Cookie.get('VERSION-' + productName);
+ }
+
+ if (elVersions.length > 1) {
+ // more than one version, show select
+ Dom.get('productTD').colSpan = 1;
+ Dom.removeClass('versionTH', 'hidden');
+ Dom.removeClass('versionTD', 'hidden');
+
+ } else {
+ // if there's only one version, we don't need to ask the user
+ Dom.addClass('versionTH', 'hidden');
+ Dom.addClass('versionTD', 'hidden');
+ Dom.get('productTD').colSpan = 2;
+ defaultVersion = elVersions.options[0].value;
+ }
+
+ if (defaultVersion) {
+ elVersions.value = defaultVersion;
+
+ } else {
+ // no default version, select an empty value to force a decision
+ var opt = new Option('', '');
+ try {
+ // standards
+ elVersions.add(opt, elVersions.options[0]);
+ } catch(ex) {
+ // ie only
+ elVersions.add(opt, 0);
+ }
+ elVersions.value = '';
+ }
+ bugForm.onVersionChange(elVersions.value);
+ },
+
+ onComponentChange: function(componentName) {
+ // show the component description
+ Dom.get('component').value = componentName;
+ var elComponentDesc = Dom.get('component_description');
+ elComponentDesc.innerHTML = '';
+ for (var i = 0, n = product.details.components.length; i < n; i++) {
+ var component = product.details.components[i];
+ if (component.name == componentName) {
+ elComponentDesc.innerHTML = component.description;
+ break;
+ }
+ }
+ Dom.removeClass(elComponentDesc, 'hidden');
+ },
+
+ onVersionChange: function(version) {
+ Dom.get('version').value = version;
+ },
+
+ onFileChange: function() {
+ // toggle ui enabled when a file is uploaded or cleared
+ var elFile = Dom.get('data');
+ var elReset = Dom.get('reset_data');
+ var elDescription = Dom.get('data_description');
+ var filename = bugForm._getFilename();
+ if (filename) {
+ elReset.disabled = false;
+ elDescription.value = filename;
+ elDescription.disabled = false;
+ } else {
+ elReset.disabled = true;
+ elDescription.value = '';
+ elDescription.disabled = true;
+ }
+ },
+
+ onFileClear: function() {
+ Dom.get('data').value = '';
+ this.onFileChange();
+ return false;
+ },
+
+ _getFilename: function() {
+ var filename = Dom.get('data').value;
+ if (!filename)
+ return '';
+ filename = filename.replace(/^.+[\\\/]/, '');
+ return filename;
+ },
+
+ _mandatoryCheck: function() {
+ result = true;
+ for (var i = 0, n = this._mandatoryFields.length; i < n; i++ ) {
+ id = this._mandatoryFields[i];
+ el = Dom.get(id);
+
+ if (el.type.toString() == "checkbox") {
+ value = el.checked;
+ } else {
+ value = el.value.replace(/^\s\s*/, '').replace(/\s\s*$/, '');
+ el.value = value;
+ }
+
+ if (value == '') {
+ Dom.addClass(id, 'missing');
+ result = false;
+ } else {
+ Dom.removeClass(id, 'missing');
+ }
+ }
+ return result;
+ },
+
+ validate: function() {
+
+ // check mandatory fields
+
+ if (!bugForm._mandatoryCheck()) {
+ if (Dom.hasClass('short_desc', 'missing')
+ && Dom.hasClass('version_select', 'missing')) {
+ alert('Please enter the summary, and select the relevant version.');
+ } else if (Dom.hasClass('short_desc', 'missing')) {
+ alert('Please enter the summary.');
+ } else {
+ alert('Please select the relevant version.\n\n' +
+ 'If you are unsure select "unspecified".');
+ }
+
+ return false;
+ }
+
+ if (Dom.get('data').value && !Dom.get('data_description').value)
+ Dom.get('data_description').value = bugForm._getFilename();
+
+ Dom.get('submit').disabled = true;
+ Dom.get('submit').value = 'Submitting Bug...';
+
+ return true;
+ },
+
+ _initHelp: function(el) {
+ var help_id = el.getAttribute('helpid');
+ if (!el.panel) {
+ if (!el.id)
+ el.id = help_id + '_parent';
+ el.panel = new YAHOO.widget.Panel(
+ help_id,
+ {
+ width: "320px",
+ visible: false,
+ close: false,
+ context: [el.id, 'tl', 'tr', null, [5, 0]]
+ }
+ );
+ el.panel.render();
+ Dom.removeClass(help_id, 'hidden');
+ }
+ },
+
+ showHelp: function(el) {
+ this._initHelp(el);
+ if (this._visibleHelpPanel)
+ this._visibleHelpPanel.hide();
+ el.panel.show();
+ this._visibleHelpPanel = el.panel;
+ },
+
+ hideHelp: function(el) {
+ if (!el.panel)
+ return;
+ if (this._visibleHelpPanel)
+ this._visibleHelpPanel.hide();
+ el.panel.hide();
+ this._visibleHelpPanel = null;
+ }
+}
+
diff --git a/extensions/GuidedBugEntry/web/js/products.js b/extensions/GuidedBugEntry/web/js/products.js
new file mode 100644
index 000000000..cdab695fc
--- /dev/null
+++ b/extensions/GuidedBugEntry/web/js/products.js
@@ -0,0 +1,117 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.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 the GuidedBugEntry Bugzilla Extension.
+ *
+ * The Initial Developer of the Original Code is
+ * the Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Byron Jones
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/* Product-specifc configuration for guided bug entry
+ *
+ * related: array of product names which will also be searched for duplicates
+ * version: function which returns a version (eg. detected from UserAgent)
+ * support: string which is displayed at the top of the duplicates page
+ * secgroup: the group to place confidential bugs into
+ * detectPlatform: when true the platform and op_sys will be set from the
+ * browser's user agent. when false, these will be set to All
+ */
+
+var products = {
+
+ "Firefox": {
+ related: [ "Core", "Toolkit" ],
+ version: function() {
+ var re = /Firefox\/(\d+)\.(\d+)/i;
+ var match = re.exec(navigator.userAgent);
+ if (match) {
+ var maj = match[1];
+ var min = match[2];
+ if (maj * 1 >= 5) {
+ return maj + " Branch";
+ } else {
+ return maj + "." + min + " Branch";
+ }
+ } else {
+ return false;
+ }
+ },
+ detectPlatform: true,
+ support:
+ 'If you are new to Firefox or Bugzilla, please consider checking ' +
+ '' +
+ '' +
+ ' Firefox Help instead of creating a bug.'
+ },
+
+ "Fennec": {
+ related: [ "Core", "Toolkit" ],
+ detectPlatform: true,
+ support:
+ 'If you are new to Firefox or Bugzilla, please consider checking ' +
+ '' +
+ '' +
+ ' Firefox Help instead of creating a bug.'
+ },
+
+ "SeaMonkey": {
+ related: [ "Core", "Toolkit" ],
+ detectPlatform: true,
+ version: function() {
+ var re = /SeaMonkey\/(\d+)\.(\d+)/i;
+ var match = re.exec(navigator.userAgent);
+ if (match) {
+ var maj = match[1];
+ var min = match[2];
+ return "SeaMonkey " + maj + "." + min + " Branch";
+ } else {
+ return false;
+ }
+ }
+ },
+
+ "Camino": {
+ related: [ "Core", "Toolkit" ],
+ detectPlatform: true
+ },
+
+ "Core": {
+ detectPlatform: true
+ },
+
+ "Thunderbird": {
+ related: [ "Core", "Toolkit", "MailNews Core" ],
+ detectPlatform: true
+ },
+
+ "Penelope": {
+ related: [ "Core", "Toolkit", "MailNews Core" ]
+ },
+
+ "Bugzilla": {
+ support:
+ 'Please use Bugzilla Landfill to file "test bugs".'
+ },
+
+ "bugzilla.mozilla.org": {
+ related: [ "Bugzilla" ],
+ support:
+ 'Please use Bugzilla Landfill to file "test bugs".'
+ }
+}
diff --git a/extensions/GuidedBugEntry/web/style/guided.css b/extensions/GuidedBugEntry/web/style/guided.css
new file mode 100644
index 000000000..f4d8ec90d
--- /dev/null
+++ b/extensions/GuidedBugEntry/web/style/guided.css
@@ -0,0 +1,231 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.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 the GuidedBugEntry Bugzilla Extension.
+ *
+ * The Initial Developer of the Original Code is
+ * the Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Byron Jones
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/* global */
+
+#page_title {
+}
+
+#page_title h2 {
+ margin-bottom: 0px;
+}
+
+#page_title h3 {
+ margin-top: 0px;
+}
+
+.hidden {
+ display: none;
+}
+
+#yui-history-iframe {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 1px;
+ height: 1px;
+ visibility: hidden;
+}
+
+.step {
+ margin-left: 20px;
+ margin-bottom: 25px;
+}
+
+#steps a img {
+ border: none;
+}
+
+#advanced {
+ margin-top: 50px;
+}
+
+#advanced img {
+ vertical-align: middle;
+}
+
+#advanced a {
+ cursor: pointer;
+}
+
+/* remove the shaded background from data_table header
+ it looks out of place */
+.yui-skin-sam .yui-dt th {
+ background: #f0f0f0;
+}
+
+/* products and other_products step */
+
+.exits {
+ width: 600px;
+ margin-bottom: 10px;
+}
+
+.exits td {
+ padding: 5px;
+}
+
+.exits h2 {
+ margin: 0px;
+ font-size: 90%;
+}
+
+.exit_img {
+ width: 64px;
+ text-align: right;
+}
+
+#products {
+ width: 600px;
+}
+
+#products td {
+ padding: 5px;
+ padding-bottom: 10px;
+}
+
+#products h2 {
+ margin-bottom: 0px;
+}
+
+#products p {
+ margin-top: 0px;
+}
+
+.product_img {
+ width: 64px;
+}
+
+#other_products .classification {
+ font-weight: bold;
+}
+
+#other_products .classification th {
+ font-size: large;
+}
+
+/* duplicates step */
+
+#dupes_summary {
+ width: 500px;
+}
+
+#dupes_list {
+ margin-top: 1em;
+ margin-bottom: 1em;
+}
+
+#product_support {
+ border: 1px solid #dddddd;
+}
+
+/* bug form step */
+
+#bugForm {
+ width: 600px;
+ border: 4px solid #e0e0e0;
+ -moz-border-radius: 5px;
+ border-radius: 5px;
+}
+
+#bugForm th, #bugForm td {
+ padding: 5px;
+}
+
+#bugForm .even th, #bugForm .even td {
+ background: #e0e0e0;
+}
+
+#bugForm .label {
+ text-align: left;
+ font-weight: bold;
+ white-space: nowrap
+}
+
+#bugzilla-body #bugForm th {
+ vertical-align: middle;
+}
+
+#bugForm .textInput {
+ width: 450px;
+}
+
+#bugForm textarea {
+ font-family: Verdana, sans-serif;
+ font-size: small;
+ width: 590px;
+}
+
+#bugForm .mandatory_mark {
+ color: red;
+ font-size: 80%;
+}
+
+#bugForm .mandatory {
+}
+
+#bugForm .textInput[disabled] {
+ background: transparent;
+ border: 1px solid #dddddd;
+}
+
+#versionTD {
+ text-align: right;
+ white-space: nowrap
+}
+
+#component_select {
+ width: 450px;
+}
+
+#component_description {
+ padding: 5px;
+}
+
+#bugForm .missing {
+ border: 1px solid red;
+ box-shadow: 0px 0px 4px #ff0000;
+ -webkit-box-shadow: 0px 0px 4px #ff0000;
+ -moz-box-shadow: 0px 0px 4px #ff0000;
+}
+
+#submitTD {
+ text-align: right;
+}
+
+.help {
+ position: absolute;
+ background: #ffffff;
+ padding: 2px;
+ cursor: default;
+}
+
+.help-bad {
+ color: #990000;
+}
+
+.help-good {
+ color: #009900;
+}
diff --git a/extensions/GuidedBugEntry/web/yui-history-iframe.txt b/extensions/GuidedBugEntry/web/yui-history-iframe.txt
new file mode 100644
index 000000000..e69de29bb
diff --git a/extensions/InlineImages/Config.pm b/extensions/InlineImages/Config.pm
new file mode 100644
index 000000000..77a1b09de
--- /dev/null
+++ b/extensions/InlineImages/Config.pm
@@ -0,0 +1,33 @@
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+#
+# 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 the InlineImages Bugzilla Extension.
+#
+# The Initial Developer of the Original Code is Guy Pyrzak
+# Portions created by the Initial Developer are Copyright (C) 2010 the
+# Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+# Guy Pyrzak
+
+package Bugzilla::Extension::InlineImages;
+use strict;
+
+use constant NAME => 'InlineImages';
+
+use constant REQUIRED_MODULES => [
+];
+
+use constant OPTIONAL_MODULES => [
+];
+
+__PACKAGE__->NAME;
diff --git a/extensions/InlineImages/Extension.pm b/extensions/InlineImages/Extension.pm
new file mode 100644
index 000000000..dcfd76e1b
--- /dev/null
+++ b/extensions/InlineImages/Extension.pm
@@ -0,0 +1,63 @@
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+#
+# 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 the InlineImages Bugzilla Extension.
+#
+# The Initial Developer of the Original Code is Guy Pyrzak
+# Portions created by the Initial Developer are Copyright (C) 2010 the
+# Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+# Guy Pyrzak
+# Gervase Markham
+
+package Bugzilla::Extension::InlineImages;
+use strict;
+use base qw(Bugzilla::Extension);
+use Bugzilla::Template;
+
+use constant NAME => 'InlineImages';
+
+our $VERSION = '0.2';
+
+sub bug_format_comment {
+ my ($self, $args) = @_;
+ my $regexes = $args->{'regexes'};
+
+ push(@$regexes, {
+ match => qr~\b(attachment\s*\#?\s*(\d+))~,
+ replace => \&_inlineAttachments
+ });
+}
+
+sub _inlineAttachments {
+ my $args = shift @_;
+ my $attachment_id = $args->{matches}->[1];
+ my $attachment_string = $args->{matches}->[0];
+
+ # We need to call get_attachment_link because otherwise it will be skipped
+ my $msg = Bugzilla::Template::get_attachment_link($attachment_id,
+ $attachment_string);
+
+ my $dbh = Bugzilla->dbh;
+ my ($mimetype) =
+ $dbh->selectrow_array('SELECT mimetype
+ FROM attachments WHERE attach_id = ?',
+ undef, $attachment_id);
+ if ($mimetype =~ /^image\/(gif|png|jpeg)$/) {
+ $msg =~ s/(?=name="attach_)/ class="is_image" /;
+ }
+
+ return $msg;
+};
+
+__PACKAGE__->NAME;
diff --git a/extensions/InlineImages/disabled b/extensions/InlineImages/disabled
new file mode 100644
index 000000000..e69de29bb
diff --git a/extensions/InlineImages/template/en/default/hook/bug/comments-aftercomments.html.tmpl b/extensions/InlineImages/template/en/default/hook/bug/comments-aftercomments.html.tmpl
new file mode 100644
index 000000000..531c18981
--- /dev/null
+++ b/extensions/InlineImages/template/en/default/hook/bug/comments-aftercomments.html.tmpl
@@ -0,0 +1,111 @@
+[%#
+# 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 the InlineImages Bugzilla Extension.
+#
+# The Initial Developer of the Original Code is Guy Pyrzak
+# Portions created by the Initial Developer are Copyright (C) 2010 the
+# Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+# Guy Pyrzak
+# Gervase Markham
+#%]
+
+[% IF Param("allow_attachment_display") %]
+
+[% END %]
diff --git a/extensions/LimitedEmail/Config.pm b/extensions/LimitedEmail/Config.pm
new file mode 100644
index 000000000..4622f26bc
--- /dev/null
+++ b/extensions/LimitedEmail/Config.pm
@@ -0,0 +1,38 @@
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+#
+# 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 the LimitedEmail Extension.
+
+#
+# The Initial Developer of the Original Code is the Mozilla Foundation
+# Portions created by the Initial Developers are Copyright (C) 2011 the
+# Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+# Byron Jones
+
+package Bugzilla::Extension::LimitedEmail;
+
+use strict;
+use constant NAME => 'LimitedEmail';
+use constant REQUIRED_MODULES => [ ];
+use constant OPTIONAL_MODULES => [ ];
+
+use constant FILTERS => [
+ qr/^(glob|dkl|justdave)\@mozilla\.com$/i,
+ qr/^gerv\@mozilla\.org$/i,
+ qr/^reed\@reedloden\.com/i,
+];
+
+use constant BLACK_HOLE => 'nobody@mozilla.org';
+
+__PACKAGE__->NAME;
diff --git a/extensions/LimitedEmail/Extension.pm b/extensions/LimitedEmail/Extension.pm
new file mode 100644
index 000000000..253c3d900
--- /dev/null
+++ b/extensions/LimitedEmail/Extension.pm
@@ -0,0 +1,60 @@
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+#
+# 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 the LimitedEmail Extension.
+
+#
+# The Initial Developer of the Original Code is the Mozilla Foundation
+# Portions created by the Initial Developers are Copyright (C) 2011 the
+# Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+# Byron Jones
+
+package Bugzilla::Extension::LimitedEmail;
+use strict;
+use base qw(Bugzilla::Extension);
+
+our $VERSION = '1';
+
+use Bugzilla::User;
+
+sub bugmail_recipients {
+ my ($self, $args) = @_;
+ foreach my $user_id (keys %{$args->{recipients}}) {
+ my $user = Bugzilla::User->new($user_id);
+ if (!deliver_to($user->email)) {
+ delete $args->{recipients}{$user_id};
+ }
+ }
+}
+
+sub mailer_before_send {
+ my ($self, $args) = @_;
+ my $email = $args->{email};
+ if (!deliver_to($email->{header}->header('to'))) {
+ $email->{header}->header_set(to => Bugzilla::Extension::LimitedEmail::BLACK_HOLE);
+ }
+}
+
+sub deliver_to {
+ my $email = shift;
+ my $ra_filters = Bugzilla::Extension::LimitedEmail::FILTERS;
+ foreach my $re (@$ra_filters) {
+ if ($email =~ $re) {
+ return 1;
+ }
+ }
+ return 0;
+}
+
+__PACKAGE__->NAME;
diff --git a/extensions/LimitedEmail/disabled b/extensions/LimitedEmail/disabled
new file mode 100644
index 000000000..e69de29bb
diff --git a/extensions/Profanivore/Config.pm b/extensions/Profanivore/Config.pm
new file mode 100644
index 000000000..778301fbb
--- /dev/null
+++ b/extensions/Profanivore/Config.pm
@@ -0,0 +1,35 @@
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+#
+# 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 the Profanivore Bugzilla Extension.
+#
+# The Initial Developer of the Original Code is the Mozilla Foundation.
+# Portions created by the Initial Developer are Copyright (C) 2010 the
+# Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+# Gervase Markham
+
+package Bugzilla::Extension::Profanivore;
+use strict;
+
+use constant NAME => 'Profanivore';
+
+use constant REQUIRED_MODULES => [
+ {
+ package => 'Regexp-Common',
+ module => 'Regexp::Common',
+ version => 0
+ }
+];
+
+__PACKAGE__->NAME;
\ No newline at end of file
diff --git a/extensions/Profanivore/Extension.pm b/extensions/Profanivore/Extension.pm
new file mode 100644
index 000000000..266ab9ff0
--- /dev/null
+++ b/extensions/Profanivore/Extension.pm
@@ -0,0 +1,86 @@
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+#
+# 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 the Profanivore Bugzilla Extension.
+#
+# The Initial Developer of the Original Code is the Mozilla Foundation.
+# Portions created by the Initial Developer are Copyright (C) 2010 the
+# Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+# Gervase Markham
+
+package Bugzilla::Extension::Profanivore;
+use strict;
+use base qw(Bugzilla::Extension);
+
+use Regexp::Common 'RE_ALL';
+
+our $VERSION = '0.01';
+
+sub bug_format_comment {
+ my ($self, $args) = @_;
+ my $regexes = $args->{'regexes'};
+ my $comment = $args->{'comment'};
+
+ # Censor profanities if the comment author is not reasonably trusted.
+ # However, allow people to see their own profanities, which might stop
+ # them immediately noticing and trying to go around the filter. (I.e.
+ # it tries to stop an arms race starting.)
+ if ($comment &&
+ !$comment->author->in_group('editbugs') &&
+ $comment->author->id != Bugzilla->user->id)
+ {
+ push (@$regexes, {
+ match => RE_profanity(),
+ replace => \&_replace_profanity
+ });
+ }
+}
+
+sub _replace_profanity {
+ # We don't have access to the actual profanity.
+ return "****";
+}
+
+sub mailer_before_send {
+ my ($self, $args) = @_;
+ my $email = $args->{'email'};
+
+ my $author = $email->header("X-Bugzilla-Who");
+ my $recipient = $email->header("To");
+
+ if ($author && $recipient) {
+ my $email_suffix = Bugzilla->params->{'emailsuffix'};
+ if ($email_suffix ne '') {
+ $recipient =~ s/\Q$email_suffix\E$//;
+ $author =~ s/\Q$email_suffix\E$//;
+ }
+
+ $author = new Bugzilla::User({ name => $author });
+ $recipient = new Bugzilla::User({ name => $recipient });
+
+ if ($author->id &&
+ !$author->in_group('editbugs') &&
+ $author->id ne $recipient->id)
+ {
+ my $body = $email->body_str();
+
+ my $offensive = RE_profanity();
+ $body =~ s/$offensive/****/g;
+
+ $email->body_str_set($body);
+ }
+ }
+}
+
+__PACKAGE__->NAME;
diff --git a/extensions/Profanivore/README b/extensions/Profanivore/README
new file mode 100644
index 000000000..5ccab103f
--- /dev/null
+++ b/extensions/Profanivore/README
@@ -0,0 +1,14 @@
+Profanivore 'eats' English profanities in comments, leaving behind instead a
+trail of droppings ('****'). It finds its food using a standard library Perl
+regexp. The profanity is only eaten where the comment was written by a user
+who does not have the global 'editbugs' privilege. The digestion happens at
+display time, so the comment in the database is unaltered.
+
+However, it does not eat profanities when showing people their own comments;
+the aim here is to prevent people immediately noticing they are being
+censored, and getting 'creative'.
+
+The purpose of Profanivore is to make it a little harder for trolls to
+vandalise public Bugzilla installations.
+
+It does not currently affect fields other than comments.
diff --git a/extensions/SecureMail/Config.pm b/extensions/SecureMail/Config.pm
new file mode 100644
index 000000000..436b4753d
--- /dev/null
+++ b/extensions/SecureMail/Config.pm
@@ -0,0 +1,41 @@
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+#
+# 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 the Bugzilla SecureMail Extension
+#
+# The Initial Developer of the Original Code is Mozilla.
+# Portions created by Mozilla are Copyright (C) 2008 Mozilla Corporation.
+# All Rights Reserved.
+#
+# Contributor(s): Max Kanat-Alexander
+# Gervase Markham
+
+package Bugzilla::Extension::SecureMail;
+use strict;
+use constant NAME => 'SecureMail';
+
+use constant REQUIRED_MODULES => [
+ {
+ package => 'Crypt-OpenPGP',
+ module => 'Crypt::OpenPGP',
+ # 1.02 added the ability for new() to take KeyRing objects for the
+ # PubRing argument.
+ version => '1.02',
+ },
+ {
+ package => 'Crypt-SMIME',
+ module => 'Crypt::SMIME',
+ version => 0,
+ },
+];
+
+__PACKAGE__->NAME;
diff --git a/extensions/SecureMail/Extension.pm b/extensions/SecureMail/Extension.pm
new file mode 100644
index 000000000..ca5516c0a
--- /dev/null
+++ b/extensions/SecureMail/Extension.pm
@@ -0,0 +1,326 @@
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+#
+# 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 the Bugzilla SecureMail Extension
+#
+# The Initial Developer of the Original Code is the Mozilla Foundation.
+# Portions created by Mozilla are Copyright (C) 2008 Mozilla Foundation.
+# All Rights Reserved.
+#
+# Contributor(s): Max Kanat-Alexander
+# Gervase Markham
+
+package Bugzilla::Extension::SecureMail;
+use strict;
+use base qw(Bugzilla::Extension);
+
+use Bugzilla::Group;
+use Bugzilla::Object;
+use Bugzilla::User;
+use Bugzilla::Util qw(correct_urlbase trim trick_taint);
+use Bugzilla::Error;
+use Crypt::OpenPGP::KeyRing;
+use Crypt::OpenPGP;
+use Crypt::SMIME;
+
+our $VERSION = '0.4';
+
+##############################################################################
+# Creating new columns
+#
+# secure_mail boolean in the 'groups' table - whether to send secure mail
+# public_key text in the 'profiles' table - stores public key
+##############################################################################
+sub install_update_db {
+ my ($self, $args) = @_;
+
+ my $dbh = Bugzilla->dbh;
+ $dbh->bz_add_column('groups', 'secure_mail',
+ {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 0});
+ $dbh->bz_add_column('profiles', 'public_key', { TYPE => 'LONGTEXT' });
+}
+
+##############################################################################
+# Maintaining new columns
+##############################################################################
+# Make sure generic functions know about the additional fields in the user
+# and group objects.
+sub object_columns {
+ my ($self, $args) = @_;
+ my $class = $args->{'class'};
+ my $columns = $args->{'columns'};
+
+ if ($class->isa('Bugzilla::Group')) {
+ push(@$columns, 'secure_mail');
+ }
+ elsif ($class->isa('Bugzilla::User')) {
+ push(@$columns, 'public_key');
+ }
+}
+
+# Plug appropriate validators so we can check the validity of the two
+# fields created by this extension, when new values are submitted.
+sub object_validators {
+ my ($self, $args) = @_;
+ my %args = %{ $args };
+ my ($invocant, $validators) = @args{qw(class validators)};
+
+ if ($invocant->isa('Bugzilla::Group')) {
+ $validators->{'secure_mail'} = \&Bugzilla::Object::check_boolean;
+ }
+ elsif ($invocant->isa('Bugzilla::User')) {
+ $validators->{'public_key'} = sub {
+ my ($self, $value) = @_;
+ $value = trim($value) || '';
+
+ return $value if $value eq '';
+
+ if ($value =~ /PUBLIC KEY/) {
+ # PGP keys must be ASCII-armoured.
+ my $ring = new Crypt::OpenPGP::KeyRing(Data => $value);
+ $ring->read if $ring;
+ if (!defined $ring || !scalar $ring->blocks) {
+ ThrowUserError('securemail_invalid_key');
+ }
+ }
+ elsif ($value =~ /BEGIN CERTIFICATE/) {
+ # S/MIME Keys must be in PEM format (Base64-encoded X.509)
+ #
+ # Crypt::SMIME seems not to like tainted values - it claims
+ # they aren't scalars!
+ trick_taint($value);
+
+ my $smime = Crypt::SMIME->new();
+
+ eval {
+ $smime->setPublicKey([$value]);
+ };
+ if ($@) {
+ ThrowUserError('securemail_invalid_key');
+ }
+ }
+ else {
+ ThrowUserError('securemail_invalid_key');
+ }
+
+ return $value;
+ };
+ }
+}
+
+# When creating a 'group' object, set up the secure_mail field appropriately.
+sub object_before_create {
+ my ($self, $args) = @_;
+ my $class = $args->{'class'};
+ my $params = $args->{'params'};
+
+ if ($class->isa('Bugzilla::Group')) {
+ $params->{secure_mail} = Bugzilla->cgi->param('secure_mail');
+ }
+}
+
+# On update, make sure the updating process knows about our new columns.
+sub object_update_columns {
+ my ($self, $args) = @_;
+ my $object = $args->{'object'};
+ my $columns = $args->{'columns'};
+
+ if ($object->isa('Bugzilla::Group')) {
+ # This seems like a convenient moment to extract this value...
+ $object->set('secure_mail', Bugzilla->cgi->param('secure_mail'));
+
+ push(@$columns, 'secure_mail');
+ }
+ elsif ($object->isa('Bugzilla::User')) {
+ push(@$columns, 'public_key');
+ }
+}
+
+# Handle the setting and changing of the public key.
+sub user_preferences {
+ my ($self, $args) = @_;
+ my $tab = $args->{'current_tab'};
+ my $save = $args->{'save_changes'};
+ my $handled = $args->{'handled'};
+ my $vars = $args->{'vars'};
+
+ return unless $tab eq 'securemail';
+
+ # Create a new user object so we don't mess with the main one, as we
+ # don't know where it's been...
+ my $user = new Bugzilla::User(Bugzilla->user->id);
+
+ if ($save) {
+ my $public_key = Bugzilla->input_params->{'public_key'};
+ $user->set('public_key', $public_key);
+ $user->update();
+ }
+
+ $vars->{'public_key'} = $user->{'public_key'};
+
+ # Set the 'handled' scalar reference to true so that the caller
+ # knows the panel name is valid and that an extension took care of it.
+ $$handled = 1;
+}
+
+##############################################################################
+# Encrypting the email
+##############################################################################
+sub mailer_before_send {
+ my ($self, $args) = @_;
+
+ my $email = $args->{'email'};
+
+ # Decide whether to make secure.
+ # This is a bit of a hack; it would be nice if it were more clear
+ # what sort a particular email is.
+ my $is_bugmail = $email->header('X-Bugzilla-Status');
+ my $is_passwordmail = !$is_bugmail && ($email->body =~ /cfmpw.*cxlpw/s);
+
+ if ($is_bugmail || $is_passwordmail) {
+ # Convert the email's To address into a User object
+ my $login = $email->header('To');
+ my $emailsuffix = Bugzilla->params->{'emailsuffix'};
+ $login =~ s/$emailsuffix$//;
+ my $user = new Bugzilla::User({ name => $login });
+
+ # Default to secure. (Of course, this means if this extension has a
+ # bug, lots of people are going to get bugmail falsely claiming their
+ # bugs are secure and they need to add a key...)
+ my $make_secure = 1;
+
+ if ($is_bugmail) {
+ # This is also a bit of a hack, but there's no header with the
+ # bug ID in. So we take the first number in the subject.
+ my ($bug_id) = ($email->header('Subject') =~ /^[^\d]+(\d+)/);
+ my $bug = new Bugzilla::Bug($bug_id);
+ if ($bug && !grep($_->{secure_mail}, @{ $bug->groups_in })) {
+ $make_secure = 0;
+ }
+ }
+ elsif ($is_passwordmail) {
+ # Mail is made unsecure only if the user does not have a public
+ # key and is not in any security groups. So specifying a public
+ # key OR being in a security group means the mail is kept secure
+ # (but, as noted above, the check is the other way around because
+ # we default to secure).
+ if ($user &&
+ !$user->{'public_key'} &&
+ !grep($_->{secure_mail}, @{ $user->groups }))
+ {
+ $make_secure = 0;
+ }
+ }
+
+ # If finding the user fails for some reason, but we determine we
+ # should be encrypting, we want to make the mail safe. An empty key
+ # does that.
+ my $public_key = $user ? $user->{'public_key'} : '';
+
+ if ($make_secure) {
+ _make_secure($email, $public_key, $is_bugmail);
+ }
+ }
+}
+
+sub _make_secure {
+ my ($email, $key, $is_bugmail) = @_;
+
+ my $bug_id = undef;
+ my $subject = $email->header('Subject');
+
+ # We only change the subject if it's a bugmail; password mails don't have
+ # confidential information in the subject.
+ if ($is_bugmail) {
+ $subject =~ /^[^\d]+(\d+)/;
+ $bug_id = $1;
+
+ my $new_subject = $subject;
+ # This is designed to still work if the admin changes the word
+ # 'bug' to something else. However, it could break if they change
+ # the format of the subject line in another way.
+ $new_subject =~ s/($bug_id\])\s+(.*)$/$1 (Secure bug updated)/;
+ $email->header_set('Subject', $new_subject);
+ }
+
+ if ($key && $key =~ /PUBLIC KEY/) {
+ ##################
+ # PGP Encryption #
+ ##################
+ my $body = $email->body;
+ if ($is_bugmail) {
+ # Subject gets placed in the body so it can still be read
+ $body = "Subject: $subject\n\n" . $body;
+ }
+
+ my $pubring = new Crypt::OpenPGP::KeyRing(Data => $key);
+ my $pgp = new Crypt::OpenPGP(PubRing => $pubring);
+
+ # "@" matches every key in the public key ring, which is fine,
+ # because there's only one key in our keyring.
+ #
+ # We use the CAST5 cipher because the Rijndael (AES) module doesn't
+ # like us for some reason I don't have time to debug fully.
+ # ("key must be an untainted string scalar")
+ my $encrypted = $pgp->encrypt(Data => $body,
+ Recipients => "@",
+ Cipher => 'CAST5',
+ Armour => 1);
+ if (defined $encrypted) {
+ $email->body_set($encrypted);
+ }
+ else {
+ $email->body_set('Error during Encryption: ' . $pgp->errstr);
+ }
+ }
+ elsif ($key && $key =~ /BEGIN CERTIFICATE/) {
+ #####################
+ # S/MIME Encryption #
+ #####################
+ my $smime = Crypt::SMIME->new();
+ my $encrypted;
+
+ eval {
+ $smime->setPublicKey([$key]);
+ $encrypted = $smime->encrypt($email->as_string());
+ };
+
+ if (!$@) {
+ # We can't replace the Email::MIME object, so we have to swap
+ # out its component parts.
+ my $enc_obj = new Email::MIME($encrypted);
+ $email->header_obj_set($enc_obj->header_obj());
+ $email->body_set($enc_obj->body());
+ }
+ else {
+ $email->body_set('Error during Encryption: ' . $@);
+ }
+ }
+ else {
+ # No encryption key provided; send a generic, safe email.
+ my $template = Bugzilla->template;
+ my $message;
+ my $vars = {
+ 'urlbase' => correct_urlbase(),
+ 'bug_id' => $bug_id,
+ 'maintainer' => Bugzilla->params->{'maintainer'}
+ };
+
+ $template->process('account/email/encryption-required.txt.tmpl',
+ $vars, \$message)
+ || ThrowTemplateError($template->error());
+
+ $email->body_set($message);
+ }
+}
+
+__PACKAGE__->NAME;
diff --git a/extensions/SecureMail/README b/extensions/SecureMail/README
new file mode 100644
index 000000000..ac3484291
--- /dev/null
+++ b/extensions/SecureMail/README
@@ -0,0 +1,8 @@
+This extension should be placed in a directory called "SecureMail" in the
+Bugzilla extensions/ directory. After installing it, remove the file
+"disabled" (if present) and then run checksetup.pl.
+
+Instructions for user key formats:
+
+S/MIME Keys must be in PEM format - i.e. Base64-encoded text, with BEGIN CERTIFICATE
+PGP keys must be ASCII-armoured - i.e. text, with BEGIN PGP PUBLIC KEY.
diff --git a/extensions/SecureMail/template/en/default/account/email/encryption-required.txt.tmpl b/extensions/SecureMail/template/en/default/account/email/encryption-required.txt.tmpl
new file mode 100644
index 000000000..7341992c8
--- /dev/null
+++ b/extensions/SecureMail/template/en/default/account/email/encryption-required.txt.tmpl
@@ -0,0 +1,15 @@
+This email would have contained sensitive information, and you have not set
+a PGP/GPG key or SMIME certificate in the "Secure Mail" section of your user
+preferences.
+
+[% IF bug_id %]
+In order to receive the full text of similar mails in the future, please
+go to:
+[%+ urlbase %]userprefs.cgi?tab=securemail
+and provide a key or certificate.
+
+You can see this bug's current state at:
+[%+ urlbase %]show_bug.cgi?id=[% bug_id %]
+[% ELSE %]
+You will have to contact [% maintainer %] to reset your password.
+[% END %]
diff --git a/extensions/SecureMail/template/en/default/account/prefs/securemail.html.tmpl b/extensions/SecureMail/template/en/default/account/prefs/securemail.html.tmpl
new file mode 100644
index 000000000..2b643c961
--- /dev/null
+++ b/extensions/SecureMail/template/en/default/account/prefs/securemail.html.tmpl
@@ -0,0 +1,34 @@
+[%# 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 the Bugzilla Bug Tracking System.
+ #
+ # The Initial Developer of the Original Code is the Mozilla Corporation.
+ # Portions created by the Initial Developer are Copyright (C) 2008 the
+ # Initial Developer. All Rights Reserved.
+ #
+ # Contributor(s): Max Kanat-Alexander
+ #%]
+
+
Some [% terms.bugs %] in this [% terms.Bugzilla %] are in groups the administrator has
+deemed 'secure'. This means emails containing information about those [% terms.bugs %]
+will only be sent encrypted. Enter your PGP/GPG public key or
+SMIME certificate here to receive full update emails for such [% terms.bugs %].
+
+
If you are a member of a secure group, or if you enter a key here, your password reset email
+will also be sent to you encrypted. If you are a member of a secure group and do not enter a key,
+you will not be able to reset your password without the assistance of an administrator.
+
+[% Hook.process('moreinfo') %]
+
+
diff --git a/extensions/SecureMail/template/en/default/hook/account/prefs/prefs-tabs.html.tmpl b/extensions/SecureMail/template/en/default/hook/account/prefs/prefs-tabs.html.tmpl
new file mode 100644
index 000000000..70a40e592
--- /dev/null
+++ b/extensions/SecureMail/template/en/default/hook/account/prefs/prefs-tabs.html.tmpl
@@ -0,0 +1,28 @@
+[%# -*- Mode: perl; indent-tabs-mode: nil -*-
+ #
+ # 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 the Bugzilla SecureMail Extension
+ #
+ # The Initial Developer of the Original Code is Mozilla.
+ # Portions created by Mozilla are Copyright (C) 2008 Mozilla Corporation.
+ # All Rights Reserved.
+ #
+ # Contributor(s): Max Kanat-Alexander
+ # Gervase Markham
+ #%]
+
+[% tabs = tabs.import([{
+ name => "securemail",
+ label => "Secure Mail",
+ link => "userprefs.cgi?tab=securemail",
+ saveable => 1
+ }]) %]
diff --git a/extensions/SecureMail/template/en/default/hook/admin/groups/create-field.html.tmpl b/extensions/SecureMail/template/en/default/hook/admin/groups/create-field.html.tmpl
new file mode 100644
index 000000000..27c644d02
--- /dev/null
+++ b/extensions/SecureMail/template/en/default/hook/admin/groups/create-field.html.tmpl
@@ -0,0 +1,25 @@
+[%# 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 the Bugzilla Bug Tracking System.
+ #
+ # The Initial Developer of the Original Code is the Mozilla Corporation.
+ # Portions created by the Initial Developer are Copyright (C) 2008 the
+ # Initial Developer. All Rights Reserved.
+ #
+ # Contributor(s): Max Kanat-Alexander
+ #%]
+
+
Secure Bugmail:
+
+
+
+
diff --git a/extensions/SecureMail/template/en/default/hook/admin/groups/edit-field.html.tmpl b/extensions/SecureMail/template/en/default/hook/admin/groups/edit-field.html.tmpl
new file mode 100644
index 000000000..81436a46c
--- /dev/null
+++ b/extensions/SecureMail/template/en/default/hook/admin/groups/edit-field.html.tmpl
@@ -0,0 +1,27 @@
+[%# 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 the Bugzilla Bug Tracking System.
+ #
+ # The Initial Developer of the Original Code is the Mozilla Corporation.
+ # Portions created by the Initial Developer are Copyright (C) 2008 the
+ # Initial Developer. All Rights Reserved.
+ #
+ # Contributor(s): Max Kanat-Alexander
+ #%]
+[% IF group.is_bug_group %]
+
+
Secure Bugmail:
+
+
+
+
+[% END %]
diff --git a/extensions/SecureMail/template/en/default/hook/global/user-error-errors.html.tmpl b/extensions/SecureMail/template/en/default/hook/global/user-error-errors.html.tmpl
new file mode 100644
index 000000000..f48a5389f
--- /dev/null
+++ b/extensions/SecureMail/template/en/default/hook/global/user-error-errors.html.tmpl
@@ -0,0 +1,27 @@
+[%# 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 the Bugzilla Bug Tracking System.
+ #
+ # The Initial Developer of the Original Code is the Mozilla Corporation.
+ # Portions created by the Initial Developer are Copyright (C) 2008 the
+ # Initial Developer. All Rights Reserved.
+ #
+ # Contributor(s): Max Kanat-Alexander
+ #%]
+
+[% IF error == "securemail_invalid_key" %]
+ [% title = "Invalid Public Key" %]
+ We were unable to read the public key that you entered. Make sure
+ that you are entering either an ASCII-armored PGP/GPG public key,
+ including the "BEGIN PGP PUBLIC KEY BLOCK" and "END PGP PUBLIC KEY BLOCK"
+ lines, or a PEM format (Base64-encoded X.509) S/MIME key, including the
+ BEGIN CERTIFICATE and END CERTIFICATE lines.
+[% END %]
\ No newline at end of file
diff --git a/extensions/SecureMail/template/en/default/pages/securemail/help.html.tmpl b/extensions/SecureMail/template/en/default/pages/securemail/help.html.tmpl
new file mode 100644
index 000000000..4e1a5c577
--- /dev/null
+++ b/extensions/SecureMail/template/en/default/pages/securemail/help.html.tmpl
@@ -0,0 +1,99 @@
+[%#
+ # 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 the Bugzilla SecureMail Extension.
+ #
+ # The Initial Developer of the Original Code is the Mozilla Foundation.
+ # Portions created by Mozilla are Copyright (C) 2008 Mozilla Foundation.
+ # All Rights Reserved.
+ #
+ # Contributor(s): Max Kanat-Alexander
+ # Gervase Markham
+ # Dave Lawrence
+ #%]
+
+[% PROCESS global/header.html.tmpl
+ title = "SecureMail Help"
+%]
+
+[% terms.Bugzilla %] considers certain groups as "secure". If a [% terms.bug %] is in one of those groups, [% terms.Bugzilla %]
+will not send unencrypted email about it. To receive encrypted email rather than just a "something changed" placeholder, you must provide either
+a S/MIME or a GPG/PGP key on the SecureMail preferences tab.
+
+In addition, if you have uploaded a S/MIME or GPG/PGP key using the
+SecureMail preferences tab, if you request your password to be reset, [% terms.Bugzilla %] will send the reset email encrypted and you will
+be required to decrypt it to view the reset instructions.
+
+
S/MIME
+
+S/MIME Keys must be in PEM format - i.e. Base64-encoded text, with the first line containing BEGIN CERTIFICATE.
+
+
+Open the .pem file in a text editor. You can recognise the public key because it starts "BEGIN CERTIFICATE" and ends "END CERTIFICATE" and
+has an appropriate friendly name (e.g. "StartCom Free Certificate Member's StartCom Ltd. ID"). It is not the section beginning
+"BEGIN RSA PRIVATE KEY", and it is not any of the intermediate certificates or root certificates.
+
+
+Note: the .pem file has your private key in plaintext. Delete it once you have copied the public key out of it!
+
+
PGP
+
+PGP keys must be ASCII-armoured - i.e. text, with the first line containing BEGIN PGP PUBLIC KEY.
+
+
+If you already have your own PGP key in a keyring, skip straight to step 3. Otherwise:
+
+
+
+
Install the GPG suite of utilities for your operating system, either using your package manager or downloaded from gnupg.org.
+
+
Generate a private key.
+
+
gpg --gen-key
+
+
+You’ll have to answer several questions:
+
+
+
+
What kind and size of key you want; the defaults are probably good enough.
+
How long the key should be valid; you can safely choose a non-expiring key.
+
Your real name and e-mail address; these are necessary for identifying your key in a larger set of keys.
+
A comment for your key; the comment can be empty.
+
A passphrase. Whatever you do, don’t forget it! Your key, and all your encrypted files, will be useless if you do.
Paste the contents of pubkey.txt into the SecureMail text field in [% terms.Bugzilla %].
+
+
Configure your email client to use your associated private key to decrypt the encrypted emails. For Thunderbird, you need the Enigmail extension.
+
+
+
+Further reading: GPG Quickstart.
+
+[% PROCESS global/footer.html.tmpl %]
+
+
diff --git a/extensions/SiteMapIndex/Config.pm b/extensions/SiteMapIndex/Config.pm
new file mode 100644
index 000000000..e10d6ec8b
--- /dev/null
+++ b/extensions/SiteMapIndex/Config.pm
@@ -0,0 +1,36 @@
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+#
+# 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 the Sitemap Bugzilla Extension.
+#
+# The Initial Developer of the Original Code is Everything Solved, Inc.
+# Portions created by the Initial Developer are Copyright (C) 2010 the
+# Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+# Max Kanat-Alexander
+# Dave Lawrence
+
+package Bugzilla::Extension::SiteMapIndex;
+use strict;
+
+use constant NAME => 'SiteMapIndex';
+
+use constant REQUIRED_MODULES => [
+ {
+ package => 'IO-Compress-Gzip',
+ module => 'IO::Compress::Gzip',
+ version => 0,
+ }
+];
+
+__PACKAGE__->NAME;
diff --git a/extensions/SiteMapIndex/Extension.pm b/extensions/SiteMapIndex/Extension.pm
new file mode 100644
index 000000000..f0b2a57d8
--- /dev/null
+++ b/extensions/SiteMapIndex/Extension.pm
@@ -0,0 +1,156 @@
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+#
+# 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 the Sitemap Bugzilla Extension.
+#
+# The Initial Developer of the Original Code is Everything Solved, Inc.
+# Portions created by the Initial Developer are Copyright (C) 2010 the
+# Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+# Max Kanat-Alexander
+# Dave Lawrence
+
+package Bugzilla::Extension::SiteMapIndex;
+use strict;
+use base qw(Bugzilla::Extension);
+
+our $VERSION = '1.0';
+
+use Bugzilla::Constants qw(bz_locations ON_WINDOWS);
+use Bugzilla::Util qw(correct_urlbase get_text);
+use Bugzilla::Install::Filesystem;
+
+use Bugzilla::Extension::SiteMapIndex::Constants;
+use Bugzilla::Extension::SiteMapIndex::Util;
+
+use DateTime;
+use IO::File;
+use POSIX;
+
+#########
+# Pages #
+#########
+
+sub template_before_process {
+ my ($self, $args) = @_;
+ my ($vars, $file) = @$args{qw(vars file)};
+
+ return if !$file eq 'global/header.html.tmpl';
+ return unless (exists $vars->{bug} or exists $vars->{bugs});
+ my $bugs = exists $vars->{bugs} ? $vars->{bugs} : [$vars->{bug}];
+ return if !ref $bugs eq 'ARRAY';
+
+ foreach my $bug (@$bugs) {
+ if (!bug_is_ok_to_index($bug)) {
+ $vars->{sitemap_noindex} = 1;
+ last;
+ }
+ }
+}
+
+sub page_before_template {
+ my ($self, $args) = @_;
+ my $page = $args->{page_id};
+
+ if ($page =~ m{^sitemap/sitemap\.}) {
+ my $map = generate_sitemap(__PACKAGE__->NAME);
+ print Bugzilla->cgi->header('text/xml');
+ print $map;
+ exit;
+ }
+}
+
+################
+# Installation #
+################
+
+sub install_before_final_checks {
+ my ($self) = @_;
+ if (!correct_urlbase()) {
+ print STDERR get_text('sitemap_no_urlbase'), "\n";
+ return;
+ }
+ if (Bugzilla->params->{'requirelogin'}) {
+ print STDERR get_text('sitemap_requirelogin'), "\n";
+ return;
+ }
+
+ $self->_fix_robots_txt();
+}
+
+sub install_filesystem {
+ my ($self, $args) = @_;
+ my $create_dirs = $args->{'create_dirs'};
+ my $recurse_dirs = $args->{'recurse_dirs'};
+ my $htaccess = $args->{'htaccess'};
+
+ # Create the sitemap directory to store the index and sitemap files
+ my $sitemap_path = bz_locations->{'datadir'} . "/" . __PACKAGE__->NAME;
+
+ $create_dirs->{$sitemap_path} = Bugzilla::Install::Filesystem::DIR_CGI_WRITE
+ | Bugzilla::Install::Filesystem::DIR_ALSO_WS_SERVE;
+
+ $recurse_dirs->{$sitemap_path} = {
+ files => Bugzilla::Install::Filesystem::WS_SERVE,
+ dirs => Bugzilla::Install::Filesystem::DIR_CGI_WRITE
+ | Bugzilla::Install::Filesystem::DIR_ALSO_WS_SERVE
+ };
+
+ # Create a htaccess file that allows the sitemap files to be served out
+ $htaccess->{"$sitemap_path/.htaccess"} = {
+ perms => Bugzilla::Install::Filesystem::WS_SERVE,
+ contents => <
+ Allow from all
+
+Deny from all
+EOT
+ };
+}
+
+sub _fix_robots_txt {
+ my ($self) = @_;
+ my $cgi_path = bz_locations()->{'cgi_path'};
+ my $robots_file = "$cgi_path/robots.txt";
+ my $current_fh = new IO::File("$cgi_path/robots.txt", 'r');
+ if (!$current_fh) {
+ warn "$robots_file: $!";
+ return;
+ }
+
+ my $current_contents;
+ { local $/; $current_contents = <$current_fh> }
+ $current_fh->close();
+
+ return if $current_contents =~ m{^Allow: \/\*show_bug\.cgi}ms;
+ my $backup_name = "$cgi_path/robots.txt.old";
+ print get_text('sitemap_fixing_robots', { current => $robots_file,
+ backup => $backup_name }), "\n";
+ rename $robots_file, $backup_name or die "backup failed: $!";
+
+ my $new_fh = new IO::File($self->package_dir . '/robots.txt', 'r');
+ $new_fh || die "Could not open new robots.txt template file: $!";
+ my $new_contents;
+ { local $/; $new_contents = <$new_fh> }
+ $new_fh->close() || die "Could not close new robots.txt template file: $!";
+
+ my $sitemap_url = correct_urlbase() . SITEMAP_URL;
+ $new_contents =~ s/SITEMAP_URL/$sitemap_url/;
+ $new_fh = new IO::File("$cgi_path/robots.txt", 'w');
+ $new_fh || die "Could not open new robots.txt file: $!";
+ print $new_fh $new_contents;
+ $new_fh->close() || die "Could not close new robots.txt file: $!";
+}
+
+__PACKAGE__->NAME;
diff --git a/extensions/SiteMapIndex/lib/Constants.pm b/extensions/SiteMapIndex/lib/Constants.pm
new file mode 100644
index 000000000..fce858121
--- /dev/null
+++ b/extensions/SiteMapIndex/lib/Constants.pm
@@ -0,0 +1,47 @@
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+#
+# 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 the Sitemap Bugzilla Extension.
+#
+# The Initial Developer of the Original Code is Everything Solved, Inc.
+# Portions created by the Initial Developer are Copyright (C) 2010 the
+# Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+# Max Kanat-Alexander
+
+package Bugzilla::Extension::SiteMapIndex::Constants;
+use strict;
+use base qw(Exporter);
+our @EXPORT = qw(
+ SITEMAP_AGE
+ SITEMAP_MAX
+ SITEMAP_DELAY
+ SITEMAP_URL
+);
+
+# This is the amount of hours a sitemap index and it's files are considered
+# valid before needing to be regenerated.
+use constant SITEMAP_AGE => 12;
+
+# This is the largest number of entries that can be in a single sitemap file,
+# per the sitemaps.org standard.
+use constant SITEMAP_MAX => 50_000;
+
+# We only show bugs that are at least 12 hours old, because if somebody
+# files a bug that's a security bug but doesn't protect it, we want to give
+# them time to fix that.
+use constant SITEMAP_DELAY => 12;
+
+use constant SITEMAP_URL => 'page.cgi?id=sitemap/sitemap.xml';
+
+1;
diff --git a/extensions/SiteMapIndex/lib/Util.pm b/extensions/SiteMapIndex/lib/Util.pm
new file mode 100644
index 000000000..3c322d8c7
--- /dev/null
+++ b/extensions/SiteMapIndex/lib/Util.pm
@@ -0,0 +1,205 @@
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+#
+# 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 the Sitemap Bugzilla Extension.
+#
+# The Initial Developer of the Original Code is Everything Solved, Inc.
+# Portions created by the Initial Developer are Copyright (C) 2010 the
+# Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+# Max Kanat-Alexander
+# Dave Lawrence
+
+package Bugzilla::Extension::SiteMapIndex::Util;
+use strict;
+use base qw(Exporter);
+our @EXPORT = qw(
+ generate_sitemap
+ bug_is_ok_to_index
+);
+
+use Bugzilla::Extension::SiteMapIndex::Constants;
+
+use Bugzilla::Util qw(correct_urlbase datetime_from url_quote);
+use Bugzilla::Constants qw(bz_locations);
+
+use Scalar::Util qw(blessed);
+use IO::Compress::Gzip qw(gzip $GzipError);
+
+sub too_young_date {
+ my $hours_ago = DateTime->now(time_zone => Bugzilla->local_timezone);
+ $hours_ago->subtract(hours => SITEMAP_DELAY);
+ return $hours_ago;
+}
+
+sub bug_is_ok_to_index {
+ my ($bug) = @_;
+ return 1 unless blessed($bug) && $bug->isa('Bugzilla::Bug');
+ my $creation_ts = datetime_from($bug->creation_ts);
+ return ($creation_ts lt too_young_date()) ? 1 : 0;
+}
+
+# We put two things in the Sitemap: a list of Browse links for products,
+# and links to bugs.
+sub generate_sitemap {
+ my ($extension_name) = @_;
+
+ # If file is less than SITEMAP_AGE hours old, then read in and send to caller.
+ # If greater, then regenerate and send the new version.
+ my $index_file = bz_locations->{'datadir'} . "/$extension_name/sitemap_index.xml";
+ if (-e $index_file) {
+ my $index_mtime = (stat($index_file))[9];
+ my $index_hours = sprintf("%d", (time() - $index_mtime) / 60 / 60); # in hours
+ if ($index_hours < SITEMAP_AGE) {
+ my $index_fh = new IO::File($index_file, 'r');
+ $index_fh || die "Could not open current sitemap index: $!";
+ my $index_xml;
+ { local $/; $index_xml = <$index_fh> }
+ $index_fh->close() || die "Could not close current sitemap index: $!";
+
+ return $index_xml;
+ }
+ }
+
+ # Set the atime and mtime of the index file to the current time
+ # in case another request is made before we finish.
+ utime(undef, undef, $index_file);
+
+ # Sitemaps must never contain private data.
+ Bugzilla->logout_request();
+ my $user = Bugzilla->user;
+ my $products = $user->get_accessible_products;
+
+ my $num_bugs = SITEMAP_MAX - scalar(@$products);
+ # We do this date math outside of the database because databases
+ # usually do better with a straight comparison value.
+ my $hours_ago = too_young_date();
+
+ # We don't use Bugzilla::Bug objects, because this could be a tremendous
+ # amount of data, and we only want a little. Also, we only display
+ # bugs that are not in any group. We show the last $num_bugs
+ # most-recently-updated bugs.
+ my $dbh = Bugzilla->dbh;
+ my $bug_sth = $dbh->prepare(
+ 'SELECT bugs.bug_id, bugs.delta_ts
+ FROM bugs
+ LEFT JOIN bug_group_map ON bugs.bug_id = bug_group_map.bug_id
+ WHERE bug_group_map.bug_id IS NULL AND creation_ts < ?
+ ' . $dbh->sql_limit($num_bugs, '?'));
+
+ my $filecount = 1;
+ my $filelist = [];
+ my $offset = 0;
+
+ while (1) {
+ my $bugs = [];
+
+ $bug_sth->execute($hours_ago, $offset);
+
+ while (my ($bug_id, $delta_ts) = $bug_sth->fetchrow_array()) {
+ push(@$bugs, { bug_id => $bug_id, delta_ts => $delta_ts });
+ }
+
+ last if !@$bugs;
+
+ # We only need the product links in the first sitemap file
+ $products = [] if $filecount > 1;
+
+ push(@$filelist, _generate_sitemap_file($extension_name, $filecount, $products, $bugs));
+
+ $filecount++;
+ $offset += $num_bugs;
+ }
+
+ # Generate index file
+ return _generate_sitemap_index($extension_name, $filelist);
+}
+
+sub _generate_sitemap_index {
+ my ($extension_name, $filelist) = @_;
+
+ my $dbh = Bugzilla->dbh;
+ my $timestamp = $dbh->selectrow_array(
+ "SELECT " . $dbh->sql_date_format('NOW()', '%Y-%m-%d'));
+
+ my $index_xml = <
+
+END
+
+ foreach my $filename (@$filelist) {
+ $index_xml .= "
+
+ " . correct_urlbase() . "data/$extension_name/$filename
+ $timestamp
+
+";
+ }
+
+ $index_xml .= <
+END
+
+ my $index_file = bz_locations->{'datadir'} . "/$extension_name/sitemap_index.xml";
+ my $index_fh = new IO::File($index_file, 'w');
+ $index_fh || die "Could not open new sitemap index: $!";
+ print $index_fh $index_xml;
+ $index_fh->close() || die "Could not close new sitemap index: $!";
+
+ return $index_xml;
+}
+
+sub _generate_sitemap_file {
+ my ($extension_name, $filecount, $products, $bugs) = @_;
+
+ my $bug_url = correct_urlbase() . 'show_bug.cgi?id=';
+ my $product_url = correct_urlbase() . 'describecomponents.cgi?product=';
+
+ my $sitemap_xml = <
+
+END
+
+ foreach my $product (@$products) {
+ $sitemap_xml .= "
+
+ " . $product_url . url_quote($product->name) . "
+ daily
+ 0.4
+
+";
+ }
+
+ foreach my $bug (@$bugs) {
+ $sitemap_xml .= "
+
+ " . $bug_url . $bug->{bug_id} . "
+ " . datetime_from($bug->{delta_ts}, 'UTC')->iso8601 . 'Z' . "
+
+";
+ }
+
+ $sitemap_xml .= <
+END
+
+ # Write the compressed sitemap data to a file in the cgi root so that they can
+ # be accessed by the search engines.
+ my $filename = "sitemap$filecount.xml.gz";
+ gzip \$sitemap_xml => bz_locations->{'datadir'} . "/$extension_name/$filename"
+ || die "gzip failed: $GzipError\n";
+
+ return $filename;
+}
+
+1;
diff --git a/extensions/SiteMapIndex/robots.txt b/extensions/SiteMapIndex/robots.txt
new file mode 100644
index 000000000..139edbf93
--- /dev/null
+++ b/extensions/SiteMapIndex/robots.txt
@@ -0,0 +1,9 @@
+User-agent: *
+Disallow: /*.cgi
+Disallow: /*show_bug.cgi*ctype=*
+Allow: /
+Allow: /*index.cgi
+Allow: /*page.cgi
+Allow: /*show_bug.cgi
+Allow: /*describecomponents.cgi
+Sitemap: SITEMAP_URL
diff --git a/extensions/SiteMapIndex/template/en/default/hook/global/header-additional_header.html.tmpl b/extensions/SiteMapIndex/template/en/default/hook/global/header-additional_header.html.tmpl
new file mode 100644
index 000000000..682f6093f
--- /dev/null
+++ b/extensions/SiteMapIndex/template/en/default/hook/global/header-additional_header.html.tmpl
@@ -0,0 +1,23 @@
+[%# 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 Initial Developer of the Original Code is Everything Solved, Inc.
+ # Portions created by Everything Solved are Copyright (C) 2010
+ # Everything Solved. All Rights Reserved.
+ #
+ # The Original Code is the Bugzilla Sitemap Extension.
+ #
+ # Contributor(s):
+ # Max Kanat-Alexander
+ #%]
+
+[% SET meta_robots = ['noarchive'] %]
+[% meta_robots.push('noindex') IF sitemap_noindex %]
+
diff --git a/extensions/SiteMapIndex/template/en/default/hook/global/messages-messages.html.tmpl b/extensions/SiteMapIndex/template/en/default/hook/global/messages-messages.html.tmpl
new file mode 100644
index 000000000..0d0e9fd74
--- /dev/null
+++ b/extensions/SiteMapIndex/template/en/default/hook/global/messages-messages.html.tmpl
@@ -0,0 +1,37 @@
+[%# 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 Initial Developer of the Original Code is Everything Solved, Inc.
+ # Portions created by Everything Solved are Copyright (C) 2010
+ # Everything Solved. All Rights Reserved.
+ #
+ # The Original Code is the Bugzilla Sitemap Extension.
+ #
+ # Contributor(s):
+ # Max Kanat-Alexander
+ #%]
+
+[% IF message_tag == "sitemap_fixing_robots" %]
+ Replacing [% current FILTER html %]. (The old version will be saved
+ as "[% backup FILTER html %]". You can delete the old version if you
+ do not need its contents.)
+
+[% ELSIF message_tag == "sitemap_requirelogin" %]
+ Not updating search engines with your sitemap, because you have the
+ "requirelogin" parameter turned on, and so search engines will not be
+ able to access your sitemap.
+
+[% ELSIF message_tag == "sitemap_no_urlbase" %]
+ You have not yet set the "urlbase" parameter. We cannot update
+ search engines and inform them about your sitemap without a
+ urlbase. Please set the "urlbase" parameter and re-run
+ checksetup.pl.
+
+[% END %]
diff --git a/extensions/Splinter/Config.pm b/extensions/Splinter/Config.pm
new file mode 100644
index 000000000..d36a28922
--- /dev/null
+++ b/extensions/Splinter/Config.pm
@@ -0,0 +1,5 @@
+package Bugzilla::Extension::Splinter;
+use strict;
+use constant NAME => 'Splinter';
+
+__PACKAGE__->NAME;
diff --git a/extensions/Splinter/Extension.pm b/extensions/Splinter/Extension.pm
new file mode 100644
index 000000000..75c0dbb0c
--- /dev/null
+++ b/extensions/Splinter/Extension.pm
@@ -0,0 +1,137 @@
+package Bugzilla::Extension::Splinter;
+
+use strict;
+
+use base qw(Bugzilla::Extension);
+
+use Bugzilla;
+use Bugzilla::Bug;
+use Bugzilla::Template;
+use Bugzilla::Attachment;
+use Bugzilla::BugMail;
+use Bugzilla::Constants;
+use Bugzilla::Error;
+use Bugzilla::Field;
+use Bugzilla::Util qw(trim detaint_natural);
+
+use Bugzilla::Extension::Splinter::Util;
+
+our $VERSION = '0.1';
+
+sub page_before_template {
+ my ($self, $args) = @_;
+
+ my ($vars, $page) = @$args{qw(vars page_id)};
+ my $input = Bugzilla->input_params;
+
+ if ($page eq 'splinter.html') {
+ # Login is required for performing a review
+ my $user = Bugzilla->login(LOGIN_REQUIRED);
+
+ # We can either provide just a bug id to see a list
+ # of prior reviews by the user, or just an attachment
+ # id to go directly to a review page for the attachment.
+ # If both are give they will be checked later to make
+ # sure they are connected.
+
+ if ($input->{'bug'}) {
+ $vars->{'bug_id'} = $input->{'bug'};
+ $vars->{'attach_id'} = $input->{'attachment'};
+ $vars->{'bug'} = Bugzilla::Bug->check($input->{'bug'});
+ }
+
+ if ($input->{'attachment'}) {
+ my $attachment = Bugzilla::Attachment->check({ id => $input->{'attachment'} });
+
+ # Check to see if the user can see the bug this attachment is connected to.
+ Bugzilla::Bug->check($attachment->bug_id);
+ if ($attachment->isprivate && $user->id != $attachment->attacher->id
+ && !$user->is_insider)
+ {
+ ThrowUserError('auth_failure', {action => 'access',
+ object => 'attachment'});
+ }
+
+ # If the user provided both a bug id and an attachment id, they must
+ # be connected to each other
+ if ($input->{'bug'} && $input->{'bug'} != $attachment->bug_id) {
+ ThrowUserError('bug_attach_id_mismatch');
+ }
+
+ # The patch is going to be displayed in a HTML page and if the utf8
+ # param is enabled, we have to encode attachment data as utf8.
+ if (Bugzilla->params->{'utf8'}) {
+ $attachment->data; # load data
+ utf8::decode($attachment->{data});
+ }
+
+ $vars->{'attach_id'} = $attachment->id;
+ $vars->{'attach_data'} = $attachment->data;
+ }
+
+ my $field_object = new Bugzilla::Field({ name => 'attachments.status' });
+ my $statuses;
+ if ($field_object) {
+ $statuses = [map { $_->name } @{ $field_object->legal_values }];
+ } else {
+ $statuses = [];
+ }
+ $vars->{'attachment_statuses'} = $statuses;
+ }
+}
+
+
+sub bug_format_comment {
+ my ($self, $args) = @_;
+
+ my $bug = $args->{'bug'};
+ my $regexes = $args->{'regexes'};
+ my $text = $args->{'text'};
+
+ # Add [review] link to the end of "Created attachment" comments
+ #
+ # We need to work around the way that the hook works, which is intended
+ # to avoid overlapping matches, since we *want* an overlapping match
+ # here (the normal handling of "Created attachment"), so we add in
+ # dummy text and then replace in the regular expression we return from
+ # the hook.
+ $$text =~ s~((?:^Created\ |\b)attachment\s*\#?\s*(\d+)(\s\[details\])?)
+ ~(push(@$regexes, { match => qr/__REVIEW__$2/,
+ replace => get_review_link("$2", "[review]") })) &&
+ (attachment_id_is_patch($2) ? "$1 __REVIEW__$2" : $1)
+ ~egmx;
+
+ # And linkify "Review of attachment", this is less of a workaround since
+ # there is no issue with overlap; note that there is an assumption that
+ # there is only one match in the text we are linkifying, since they all
+ # get the same link.
+ my $REVIEW_RE = qr/Review\s+of\s+attachment\s+(\d+)\s*:/;
+
+ if ($$text =~ $REVIEW_RE) {
+ my $review_link = get_review_link($bug, $1, "Review");
+ my $attach_link = Bugzilla::Template::get_attachment_link($1, "attachment $1");
+
+ push(@$regexes, { match => $REVIEW_RE,
+ replace => "$review_link of $attach_link:"});
+ }
+}
+
+sub config_add_panels {
+ my ($self, $args) = @_;
+
+ my $modules = $args->{panel_modules};
+ $modules->{Splinter} = "Bugzilla::Extension::Splinter::Config";
+}
+
+sub mailer_before_send {
+ my ($self, $args) = @_;
+
+ # Post-process bug mail to add review links to bug mail.
+ # It would be nice to be able to hook in earlier in the
+ # process when the email body is being formatted in the
+ # style of the bug-format_comment link for HTML but this
+ # is the only hook available as of Bugzilla-3.4.
+ add_review_links_to_email($args->{'email'});
+}
+
+__PACKAGE__->NAME;
diff --git a/extensions/Splinter/lib/Config.pm b/extensions/Splinter/lib/Config.pm
new file mode 100644
index 000000000..90f1c8073
--- /dev/null
+++ b/extensions/Splinter/lib/Config.pm
@@ -0,0 +1,46 @@
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+#
+# 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 the Bugzilla Example Plugin.
+#
+# The Initial Developer of the Original Code is Canonical Ltd.
+# Portions created by Canonical Ltd. are Copyright (C) 2008
+# Canonical Ltd. All Rights Reserved.
+#
+# Contributor(s): Max Kanat-Alexander
+# Bradley Baetz
+# Owen Taylor
+
+package Bugzilla::Extension::Splinter::Config;
+
+use strict;
+use warnings;
+
+use Bugzilla::Config::Common;
+
+our $sortkey = 30;
+
+sub get_param_list {
+ my ($class) = @_;
+
+ my @param_list = (
+ {
+ name => 'splinter_base',
+ type => 't',
+ default => 'page.cgi?id=splinter.html',
+ },
+ );
+
+ return @param_list;
+}
+
+1;
diff --git a/extensions/Splinter/lib/Util.pm b/extensions/Splinter/lib/Util.pm
new file mode 100644
index 000000000..c8c0d52d2
--- /dev/null
+++ b/extensions/Splinter/lib/Util.pm
@@ -0,0 +1,143 @@
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+#
+# 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 the Splinter Bugzilla Extension.
+#
+# The Initial Developer of the Original Code is Red Hat, Inc.
+# Portions created by Red Hat, Inc. are Copyright (C) 2009
+# Red Hat Inc. All Rights Reserved.
+#
+# Contributor(s):
+# Owen Taylor
+
+package Bugzilla::Extension::Splinter::Util;
+
+use strict;
+
+use Bugzilla;
+use Bugzilla::Util;
+
+use base qw(Exporter);
+
+@Bugzilla::Extension::Splinter::Util::EXPORT = qw(
+ attachment_is_visible
+ attachment_id_is_patch
+ get_review_url
+ get_review_link
+ add_review_links_to_email
+);
+
+# Checks if the current user can see an attachment
+# Based on code from attachment.cgi
+sub attachment_is_visible {
+ my $attachment = shift;
+
+ $attachment->isa('Bugzilla::Attachment') || return 0;
+
+ return (Bugzilla->user->can_see_bug($attachment->bug->id)
+ && (!$attachment->isprivate
+ || Bugzilla->user->id == $attachment->attacher->id
+ || Bugzilla->user->is_insider));
+}
+
+sub attachment_id_is_patch {
+ my $attach_id = shift;
+ my $attachment = new Bugzilla::Attachment($attach_id);
+
+ # The check on attachment_is_visible here is to prevent a tiny
+ # information leak where someone could check if a private
+ # attachment was a patch by creating text that would get linkified
+ # differently. Likely excess paranoia
+ return (defined $attachment
+ && attachment_is_visible($attachment)
+ && $attachment->ispatch);
+}
+
+sub get_review_url {
+ my ($bug, $attach_id, $absolute) = @_;
+ my $base = Bugzilla->params->{'splinter_base'};
+ my $bug_id = $bug->id;
+
+ if (defined $absolute && $absolute) {
+ my $urlbase = correct_urlbase();
+ $urlbase =~ s!/$!! if $base =~ "^/";
+ $base = $urlbase . $base;
+ }
+
+ if ($base =~ /\?/) {
+ return "$base&bug=$bug_id&attachment=$attach_id";
+ }
+ else {
+ return "$base?bug=$bug_id&attachment=$attach_id";
+ }
+}
+
+sub get_review_link {
+ my ($attach_id, $link_text) = @_;
+
+ my $attachment = Bugzilla::Attachment->new($attach_id);
+
+ if ($attachment && $attachment->ispatch) {
+ return "$link_text";
+ }
+}
+
+sub munge_create_attachment {
+ my ($bug, $intro_text, $attach_id, $view_link) = @_;
+
+ if (attachment_id_is_patch ($attach_id)) {
+ return ("$intro_text" .
+ " View: $view_link\015\012" .
+ " Review: " . get_review_url($bug, $attach_id, 1) . "\015\012");
+ }
+ else {
+ return ("$intro_text --> ($view_link)");
+ }
+}
+
+# This adds review links into a bug mail before we send it out.
+# Since this is happening after newlines have been converted into
+# RFC-2822 style \r\n, we need handle line ends carefully.
+# (\015 and \012 are used because Perl \n is platform-dependent)
+sub add_review_links_to_email {
+ my $email = shift;
+ my $body = $email->body;
+ my $new_body = 0;
+ my $bug;
+
+ if ($email->header('Subject') =~ /^\[Bug\s+(\d+)\]/
+ && Bugzilla->user->can_see_bug($1))
+ {
+ $bug = Bugzilla::Bug->new($1);
+ }
+
+ return unless defined $bug;
+
+ if ($body =~ /Review\s+of\s+attachment\s+\d+\s*:/) {
+ $body =~ s~(Review\s+of\s+attachment\s+(\d+)\s*:)
+ ~"$1\015\012 --> (" . get_review_url($bug, $2, 1) . ")"
+ ~egx;
+ $new_body = 1;
+ }
+
+ if ($body =~ /Created attachment \d+\015\012 --> /) {
+ $body =~ s~(Created\ attachment\ (\d+)\015\012)
+ \ -->\ \(([^\015\012]*)\)[^\015\012]*
+ ~munge_create_attachment($bug, $1, $2, $3)
+ ~egx;
+ $new_body = 1;
+ }
+
+ $email->body_set($body) if $new_body;
+}
+
+1;
diff --git a/extensions/Splinter/template/en/default/admin/params/splinter.html.tmpl b/extensions/Splinter/template/en/default/admin/params/splinter.html.tmpl
new file mode 100644
index 000000000..834c0c7d4
--- /dev/null
+++ b/extensions/Splinter/template/en/default/admin/params/splinter.html.tmpl
@@ -0,0 +1,38 @@
+[%#
+ # 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 the Bugzilla Example Plugin.
+ #
+ # The Initial Developer of the Original Code is Canonical Ltd.
+ # Portions created by Canonical Ltd. are Copyright (C) 2008
+ # Canonical Ltd. All Rights Reserved.
+ #
+ # Contributor(s): Bradley Baetz
+ # Owen Taylor
+ #%]
+[%
+ title = "Splinter Patch Review"
+ desc = "Configure Splinter"
+%]
+
+[% param_descs = {
+ splinter_base => "This is the base URL for the Splinter patch review page; " _
+ "the default value '/page.cgi?id=splinter.html' works without " _
+ "further configuration, however you may want to internally forward " _
+ "/review to that URL in your web server's configuration and then change " _
+ "this parameter. For example, withr the Apache HTTP server you can add " _
+ "the following lines to the .htaccess for Bugzilla: " _
+ "
"
+ }
+%]
diff --git a/extensions/Splinter/template/en/default/hook/attachment/edit-action.html.tmpl b/extensions/Splinter/template/en/default/hook/attachment/edit-action.html.tmpl
new file mode 100644
index 000000000..ba564d4b4
--- /dev/null
+++ b/extensions/Splinter/template/en/default/hook/attachment/edit-action.html.tmpl
@@ -0,0 +1,31 @@
+[%#
+ # 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 the Splinter Bugzilla Extension.
+ #
+ # The Initial Developer of the Original Code is Red Hat, Inc.
+ # Portions created by Red Hat, Inc. are Copyright (C) 2008
+ # Red Hat, Inc. All Rights Reserved.
+ #
+ # Contributor(s): Owen Taylor
+ # David Lawrence
+ #%]
+
+[% IF attachment.ispatch %]
+ |
+ [% IF Param("splinter_base").search('\?') %]
+
+ Splinter Review
+ [% ELSE %]
+
+ Splinter Review
+ [% END %]
+[% END %]
diff --git a/extensions/Splinter/template/en/default/hook/attachment/list-action.html.tmpl b/extensions/Splinter/template/en/default/hook/attachment/list-action.html.tmpl
new file mode 100644
index 000000000..51babf079
--- /dev/null
+++ b/extensions/Splinter/template/en/default/hook/attachment/list-action.html.tmpl
@@ -0,0 +1,31 @@
+[%#
+ # 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 the Splinter Bugzilla Extension.
+ #
+ # The Initial Developer of the Original Code is Red Hat, Inc.
+ # Portions created by Red Hat, Inc. are Copyright (C) 2008
+ # Red Hat, Inc. All Rights Reserved.
+ #
+ # Contributor(s): Owen Taylor
+ # David Lawrence
+ #%]
+
+[% IF attachment.ispatch %]
+ |
+ [% IF Param("splinter_base").search('\?') %]
+
+ Splinter Review
+ [% ELSE %]
+
+ Splinter Review
+ [% END %]
+[% END %]
diff --git a/extensions/Splinter/template/en/default/hook/global/user-error-errors.html.tmpl b/extensions/Splinter/template/en/default/hook/global/user-error-errors.html.tmpl
new file mode 100644
index 000000000..17ef5c08f
--- /dev/null
+++ b/extensions/Splinter/template/en/default/hook/global/user-error-errors.html.tmpl
@@ -0,0 +1,5 @@
+[% IF error == "bug_attach_id_mismatch" %]
+ [% title = "Bug ID and Attachment ID Mismatch" %]
+ The [% terms.bug %] id and attachment id you provided
+ are not connected to each other.
+[% END %]
diff --git a/extensions/Splinter/template/en/default/hook/request/email-after_summary.txt.tmpl b/extensions/Splinter/template/en/default/hook/request/email-after_summary.txt.tmpl
new file mode 100644
index 000000000..320d20a82
--- /dev/null
+++ b/extensions/Splinter/template/en/default/hook/request/email-after_summary.txt.tmpl
@@ -0,0 +1,6 @@
+[% IF flag && flag.status == '?' && flag.type.name == 'review' && attachment && attachment.ispatch %]
+
+Splinter Review
+[%+ urlbase FILTER none %][% Param('splinter_base') %]&bug=[% bug.bug_id FILTER uri %]&attachment=[% attachment.id FILTER uri %]
+[%- END %]
+
diff --git a/extensions/Splinter/template/en/default/hook/request/queue-after_column.html.tmpl b/extensions/Splinter/template/en/default/hook/request/queue-after_column.html.tmpl
new file mode 100644
index 000000000..5d1c7a2bb
--- /dev/null
+++ b/extensions/Splinter/template/en/default/hook/request/queue-after_column.html.tmpl
@@ -0,0 +1,8 @@
+[% IF column == 'attachment' && request.ispatch %]
+
+ [% IF Param("splinter_base").search('\?') %]
+ [review]
+ [% ELSE %]
+ [review]
+ [% END %]
+[% END %]
diff --git a/extensions/Splinter/template/en/default/pages/splinter.html.tmpl b/extensions/Splinter/template/en/default/pages/splinter.html.tmpl
new file mode 100644
index 000000000..adb8b52f9
--- /dev/null
+++ b/extensions/Splinter/template/en/default/pages/splinter.html.tmpl
@@ -0,0 +1,264 @@
+[%#
+ # 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 the Splinter Bugzilla Extension.
+ #
+ # The Initial Developer of the Original Code is Red Hat, Inc.
+ # Portions created by Red Hat, Inc. are Copyright (C) 2008
+ # Red Hat, Inc. All Rights Reserved.
+ #
+ # Contributor(s): Owen Taylor
+ # David Lawrence
+ #%]
+
+[% PROCESS global/header.html.tmpl
+ title = "Patch Review"
+ header = "Patch Review"
+ style_urls = [ "js/yui/assets/skins/sam/container.css",
+ "js/yui/assets/skins/sam/button.css",
+ "js/yui/assets/skins/sam/datatable.css",
+ "extensions/Splinter/web/splinter.css" ]
+ javascript_urls = [ "js/yui/element/element-min.js",
+ "js/yui/connection/connection-min.js",
+ "js/yui/container/container-min.js",
+ "js/yui/button/button-min.js",
+ "js/yui/json/json-min.js",
+ "js/yui/datasource/datasource-min.js",
+ "js/yui/datatable/datatable-min.js",
+ "extensions/Splinter/web/splinter.js" ]
+%]
+
+[% can_edit = 0 %]
+
+
+
+
+
+
From the Overview page, you can add a more generic overview comment that will appear at the beginning of your review.
+
To comment on a specific lines in the patch, first select the filename from the file navigation links.
+
Then double click the line you want to review and a comment box will appear below the line.
+
When the review is complete and you publish it, the overview comment and all line specific comments with their context,
+ will be combined together into a single review comment on the [% terms.bug %] report.
+
For more detailed instructions, read the Splinter
+ help page.
+
+
+
+
+[% PROCESS global/footer.html.tmpl %]
diff --git a/extensions/Splinter/template/en/default/pages/splinter/help.html.tmpl b/extensions/Splinter/template/en/default/pages/splinter/help.html.tmpl
new file mode 100644
index 000000000..bff004c8f
--- /dev/null
+++ b/extensions/Splinter/template/en/default/pages/splinter/help.html.tmpl
@@ -0,0 +1,153 @@
+[%#
+ # 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 the Splinter Bugzilla Extension.
+ #
+ # The Initial Developer of the Original Code is Red Hat, Inc.
+ # Portions created by Red Hat, Inc. are Copyright (C) 2008
+ # Red Hat, Inc. All Rights Reserved.
+ #
+ # Contributor(s): Owen Taylor
+ #%]
+
+[% PROCESS global/header.html.tmpl
+ title = "Patch Review Help"
+ header = "Patch Review Help"
+%]
+
+
Splinter Patch Review
+
+ Splinter is an add-on for [% terms.Bugzilla %] to allow conveniently
+ reviewing patches that people have attached to
+ [% terms.Bugzilla %]. More
+ information about Splinter.
+
+
The patch review view
+
+ If you get to Splinter by clicking on a link next to an
+ attachment in [% terms.Bugzilla %], you are presented with the patch
+ review view. This view has a number of different pages that can
+ be switched between with the links at the top of the screen.
+ The first page is the Overview page, the other pages correspond to
+ individual files changed by the review.
+
+
+ On the Overview page, from top to bottom are shown:
+
+
+
Introductory text to the patch. For a patch that was created
+ using 'git format-patch' this will be the Git commit
+ message.
+
Controls for creating a new review
+
Previous reviews that other people have written.
+
+
+ The pages for each file show a two-column view of the changes.
+ The left column is the previous contents of the file,
+ the right column is the new contents of the file. (If the file
+ is an entirely new file or an entirely deleted file, only one
+ column will be shown.) Red indicates lines that have been
+ removed, green lines that have been added, and blue lines that
+ were modified.
+
+
+ If people have previously made comments on individual lines of
+ the patch, they will show up both summarized on the Overview
+ page and also inline when looking at the files of the patch.
+
+
Reviewing an existing patch
+
+ There are three components to a review:
+
+
+
+ An overall comment. The text area on the first page allows
+ you to enter your overall thoughts on the [% terms.bug %].
+
+
+ Detailed comments on changes within the files. To comment on a
+ line in a patch, double click on it, and a text area will open
+ beneath that comment. When you are done, click the Save button
+ to save your comment or the Cancel button to throw your
+ comment away. You can double-click on a saved comment to start
+ editing it again and make further changes.
+
+
+ A change to the attachment status. (This is specific to
+ [% terms.Bugzilla %] instances that have attachment status, which is a
+ non-upstream patch. It's somewhat similar to attachment flags,
+ which splinter doesn't currently support displaying or
+ changing.) This allows you to mark a patch as read to commit
+ or needing additional work. This is done by changing the
+ drop-down next to the Publish button.
+
+
+
+ Once you are done writing your review, go back to Overview page
+ and click the "Publish" button to submit it as a comment on the
+ [% terms.bug %]. The comment will have a link back to the review page so
+ that people can see your comments with the full context.
+
+
Saved drafts
+
+ Whenever you start making changes, a draft is automatically
+ saved. If you come back to the patch review page for the same
+ attachment, that draft will automatically be resumed. Drafts are
+ not visible to anybody else until published.
+
+
+ Note that saving drafts requires the your browser to have support
+ for the "DOM Storage" standard. At time of writing, this is
+ available only in a few very recent browsers, like Firefox
+ 3.5. Strict privacy protections like disabling cookies may also
+ disable DOM Storage, since it provides another mechanism for
+ sites to track information about their users.
+
+
Responding to someone's review
+
+ A response is treated just like any other review and created the
+ same way. A couple of features are helpful when responding: you
+ can double-click on an inline comment to respond to it. And on
+ the overview page, when you click on a detailed comment, you are
+ taken directly to the original location of the comment.
+
+
Uploading patches for review
+
+ Splinter doesn't really care how patches are provided to
+ [% terms.Bugzilla %], as long as they are well-formatted patches. If you are
+ using Git for version control, you can either format changes as
+ patches
+ using 'git
+ format-patch and attach them manually to the [% terms.bug %], or you
+ can
+ use git-bz.
+ git-bz is highly recommended; it automates most of the steps
+ that Splinter can't handle: it files new [% terms.bugs %], attaches updated
+ attachments to existing [% terms.bugs %], and closes [% terms.bugs %] when you push the
+ corresponding git commits to your central repository.
+
+
The [% terms.bug %] review view
+
+ Splinter also has a view where it shows all patches attached to
+ the [% terms.bug %] with their status and links to review them. You are
+ taken to this page after publishing a review. You can also get
+ to this page with the [% terms.bug %] link in the upper-right corner of the
+ patch review view.
+
+
Your reviews
+
+ Splinter can also show you a list of all your draft and
+ published reviews. Access this page with the "Your reviews"
+ link at the bottom of the [% terms.bug %] review view. In-progress drafts
+ are shown in bold.
+
+
+[% PROCESS global/footer.html.tmpl %]
diff --git a/extensions/Splinter/web/splinter.css b/extensions/Splinter/web/splinter.css
new file mode 100644
index 000000000..a4b4f0b6f
--- /dev/null
+++ b/extensions/Splinter/web/splinter.css
@@ -0,0 +1,402 @@
+textarea:focus {
+ background: #f7f2d0;
+}
+
+#note {
+ background: #ffee88;
+ padding: 0.5em;
+}
+
+#error {
+ border: 1px solid black;
+ padding: 0.5em;
+ color: #bb0000;
+}
+
+#chooseReview {
+ margin-top: 1em;
+}
+
+.review-draft .review-desc, .review-draft .review-attachment {
+ font-weight: bold;
+}
+
+#bugInfo, #attachInfo {
+ margin-top: 0.5em;
+ margin-bottom: 1em;
+}
+
+#helpful-links {
+ float:right;
+}
+
+#chooseAttachment table {
+ margin-bottom: 1em;
+}
+
+.attachment-draft .attachment-id, .attachment-draft .attachment-desc {
+ font-weight: bold;
+}
+
+.attachment-obsolete .attachment-desc {
+ text-decoration: line-through ;
+}
+
+#navigation {
+ color: #888888;
+}
+
+.navigation-link {
+ text-decoration: none;
+ white-space: nowrap;
+}
+
+.navigation-link-selected {
+ color: black;
+}
+
+#haveDraftNotice {
+ float: right;
+ color: #bb0000;
+ font-weight: bold;
+}
+
+#overview {
+ margin-top: 0.5em;
+ margin-bottom: 0.5em;
+}
+
+#patchIntro {
+ border: 1px solid #888888;
+ font-size: 90%;
+ margin-bottom: 1em;
+ padding: 0.5em;
+}
+
+.reviewer-box {
+ padding: 0.5em;
+}
+
+.reviewer-0 .reviewer-box {
+ border-left: 10px solid green;
+}
+
+.reviewer-1 .reviewer-box {
+ border-left: 10px solid blue;
+}
+
+.reviewer-2 .reviewer-box {
+ border-left: 10px solid red;
+}
+
+.reviewer-3 .reviewer-box {
+ border-left: 10px solid yellow;
+}
+
+.reviewer-4 .reviewer-box {
+ border-left: 10px solid purple;
+}
+
+.reviewer-5 .reviewer-box {
+ border-left: 10px solid orange;
+}
+
+.reviewer {
+ float: left;
+}
+
+.review-date {
+ float: right;
+}
+
+.review-info-bottom {
+ clear: both;
+}
+
+.review {
+ border: 1px solid black;
+ font-size: 90%;
+ margin-top: 0.25em;
+ margin-bottom: 1em;
+}
+
+.review-intro {
+ margin-top: 0.5em;
+}
+
+.review-patch-file {
+ margin-top: 0.5em;
+ font-weight: bold;
+}
+
+.review-patch-comment {
+ border: 1px solid white;
+ padding: 1px;
+ margin-top: 0.5em;
+ margin-bottom: 0.5em;
+ cursor: pointer;
+}
+
+.review-patch-comment:hover {
+ border: 1px solid #8888ff;
+}
+
+.review-patch-comment .file-table {
+ width: 50%;
+}
+
+.review-patch-comment .file-table-changed {
+ width: 100%;
+}
+
+.review-patch-comment-separator {
+ margin: 0.5em;
+ border-bottom: 1px solid #888888;
+}
+
+div.review-patch-comment-text {
+ margin-left: 2em;
+}
+
+.review-patch-comment .reviewer-box {
+ border-left-width: 4px;
+}
+
+#restored {
+ color: #bb0000;
+ margin-bottom: 0.5em;
+}
+
+#myCommentFrame {
+ margin-top: 0.25em;
+ position: relative;
+ border: 1px solid black;
+ padding-right: 8px; /* compensate for child's padding */
+}
+
+#myComment {
+ border: 0px solid black;
+ padding: 4px;
+ margin: 0px;
+ width: 100%;
+ height: 10em;
+}
+
+#emptyCommentNotice {
+ position: absolute;
+ top: 4px;
+ left: 4px;
+ color: #888888;
+}
+
+#myPatchComments {
+ border: 1px solid black;
+ border-top-width: 0px;
+ padding: 0.5em;
+ font-size: 90%;
+}
+
+#buttonBox {
+ margin-top: 0.5em;
+ float: right;
+}
+
+.clear {
+ clear: both;
+}
+
+/* Used for IE <= 7, overridden for modern browsers */
+.pre-wrap {
+ white-space: pre;
+ word-wrap: break-word;
+}
+
+.pre-wrap {
+ white-space: pre-wrap;
+}
+
+#files {
+ position: relative;
+ margin-top: 0.5em;
+ margin-bottom: 0.5em;
+}
+
+.file-label {
+ margin-top: 1em;
+ margin-bottom: 0.5em;
+}
+
+.file-label-name {
+ font-weight: bold;
+}
+
+.hunk-header td {
+ background: #ddccbb;
+ font-family: "DejaVu Sans Mono", monospace;
+ font-size: 80%;
+}
+
+.hunk-cell {
+ padding: 2px;
+}
+
+.old-line, .new-line {
+ font-family: "DejaVu Sans Mono", monospace;
+ font-size: 80%;
+ white-space: pre-wrap; /* CSS 3 & 2.1 */
+ white-space: -moz-pre-wrap; /* Gecko */
+ white-space: -pre-wrap; /* Opera 4-6 */
+ white-space: -o-pre-wrap; /* Opera 7 */
+}
+
+.removed-line {
+ background: #ffccaa;;
+}
+
+.added-line {
+ background: #bbffbb;
+}
+
+.changed-line {
+ background: #aaccff;
+}
+
+.file-table {
+ width: 100%;
+ border-collapse: collapse;
+ table-layout: fixed;
+}
+
+.line-number {
+ font-size: 80%;
+ text-align: right;
+ padding-right: 0.25em;
+ color: #888888;
+ -moz-user-select: none;
+}
+
+.line-number-column {
+ width: 2em;
+}
+
+.file-table-wide-numbers .line-number-column {
+ width: 3em;
+}
+
+.middle-column {
+ width: 3px;
+}
+
+.file-table-changed .comment-removed {
+ width: 50%;
+ float: left;
+}
+
+.file-table-changed .comment-changed {
+ margin-left: 25%;
+ margin-right: 25%;
+ clear: both;
+}
+
+.file-table-changed .comment-added {
+ width: 50%;
+ float: right;
+}
+
+.comment-frame {
+ border: 1px solid black;
+ margin-top: 5px;
+ margin-bottom: 5px;
+ margin-left: 2em;
+}
+
+.file-table-wide-numbers .comment-frame {
+ margin-left: 3em;
+}
+
+.comment .review-info {
+ margin-top: 0.5em;
+ font-size: 80%;
+}
+
+#commentTextFrame {
+ border: 1px solid #ffeeaa;
+ margin-bottom: 5px;
+}
+
+#commentEditor.focused #commentTextFrame {
+ border: 1px solid #8888bb;
+}
+
+#commentEditorInner {
+ background: #ffeeaa;
+ padding: 0.5em;
+ margin-left: 2em;
+}
+
+.file-table-wide-numbers #commentEditorInner {
+ margin-left: 3em;
+}
+
+#commentEditor textarea {
+ width: 100%;
+ height: 10em;
+ border: 0px;
+}
+
+#commentEditor textarea:focus {
+ background: white;
+}
+
+#commentEditorLeftButtons {
+ float: left;
+}
+
+#commentEditorLeftButtons input {
+ margin-right: 0.5em;
+}
+
+#commentEditorRightButtons {
+ float: right;
+}
+
+.comment-separator-removed {
+ clear: left;
+}
+
+.comment-separator-added {
+ clear: right;
+}
+
+#saveDraftNotice {
+ border: 1px solid black;
+ padding: 0.5em;
+ background: #ffccaa;
+ position: fixed;
+ bottom: 0px;
+ right: 0px;
+}
+
+#credits {
+ font-size: 80%;
+ color: #888888;
+ padding: 10px;
+ text-align: center;
+}
+
+#quickHelpShow a, #quickHelpContent a {
+ text-decoration: none;
+}
+
+#quickHelpContent {
+ border: 1px solid #000;
+ -moz-border-radius: 10px;
+ border-radius: 10px;
+ padding-left: 0.5em;
+ margin-bottom: 10px;
+}
+
+.file-label-collapse {
+ padding-right: 5px;
+ font-family: monospace;
+}
diff --git a/extensions/Splinter/web/splinter.js b/extensions/Splinter/web/splinter.js
new file mode 100644
index 000000000..9704b6716
--- /dev/null
+++ b/extensions/Splinter/web/splinter.js
@@ -0,0 +1,2479 @@
+// Splinter - patch review add-on for Bugzilla
+// By Owen Taylor
+// Copyright 2009, Red Hat, Inc.
+// Licensed under MPL 1.1 or later, or GPL 2 or later
+// http://git.fishsoup.net/cgit/splinter
+// Converted to YUI by David Lawrence
+
+YAHOO.namespace('Splinter');
+
+var Dom = YAHOO.util.Dom;
+var Event = YAHOO.util.Event;
+var Splinter = YAHOO.Splinter;
+var Element = YAHOO.util.Element;
+
+Splinter.domCache = {
+ cache : [0],
+ expando : 'data' + new Date(),
+ data : function (elem) {
+ var cacheIndex = elem[Splinter.domCache.expando];
+ var nextCacheIndex = Splinter.domCache.cache.length;
+ if (!cacheIndex) {
+ cacheIndex = elem[Splinter.domCache.expando] = nextCacheIndex;
+ Splinter.domCache.cache[cacheIndex] = {};
+ }
+ return Splinter.domCache.cache[cacheIndex];
+ }
+};
+
+Splinter.Utils = {
+ assert : function(condition) {
+ if (!condition) {
+ throw new Error("Assertion failed");
+ }
+ },
+
+ assertNotReached : function() {
+ throw new Error("Assertion failed: should not be reached");
+ },
+
+ strip : function(string) {
+ return (/^\s*([\s\S]*?)\s*$/).exec(string)[1];
+ },
+
+ lstrip : function(string) {
+ return (/^\s*([\s\S]*)$/).exec(string)[1];
+ },
+
+ rstrip : function(string) {
+ return (/^([\s\S]*?)\s*$/).exec(string)[1];
+ },
+
+ formatDate : function(date, now) {
+ if (now == null) {
+ now = new Date();
+ }
+ var daysAgo = (now.getTime() - date.getTime()) / (24 * 60 * 60 * 1000);
+ if (daysAgo < 0 && now.getDate() != date.getDate()) {
+ return date.toLocaleDateString();
+ } else if (daysAgo < 1 && now.getDate() == date.getDate()) {
+ return date.toLocaleTimeString();
+ } else if (daysAgo < 7 && now.getDay() != date.getDay()) {
+ return ['Sun', 'Mon','Tue','Wed','Thu','Fri','Sat'][date.getDay()] + " " + date.toLocaleTimeString();
+ } else {
+ return date.toLocaleDateString();
+ }
+ },
+
+ preWrapLines : function(el, text) {
+ while ((m = Splinter.LINE_RE.exec(text)) != null) {
+ var div = document.createElement("div");
+ div.className = "pre-wrap";
+ div.appendChild(document.createTextNode(m[1].length == 0 ? " " : m[1]));
+ el.appendChild(div);
+ }
+ },
+
+ isDigits : function (str) {
+ return str.match(/^[0-9]+$/);
+ }
+};
+
+Splinter.Bug = {
+ TIMEZONES : {
+ CEST: '200',
+ CET: '100',
+ BST: '100',
+ GMT: '000',
+ UTC: '000',
+ EDT: '-400',
+ EST: '-500',
+ CDT: '-500',
+ CST: '-600',
+ MDT: '-600',
+ MST: '-700',
+ PDT: '-700',
+ PST: '-800'
+ },
+
+ parseDate : function(d) {
+ var m = /^\s*(\d+)-(\d+)-(\d+)\s+(\d+):(\d+)(?::(\d+))?\s+(?:([A-Z]{3,})|([-+]\d{3,}))\s*$/.exec(d);
+ if (!m) {
+ return null;
+ }
+
+ var year = parseInt(m[1], 10);
+ var month = parseInt(m[2] - 1, 10);
+ var day = parseInt(m[3], 10);
+ var hour = parseInt(m[4], 10);
+ var minute = parseInt(m[5], 10);
+ var second = m[6] ? parseInt(m[6], 10) : 0;
+
+ var tzoffset = 0;
+ if (m[7]) {
+ if (m[7] in Splinter.Bug.TIMEZONES) {
+ tzoffset = Splinter.Bug.TIMEZONES[m[7]];
+ }
+ } else {
+ tzoffset = parseInt(m[8], 10);
+ }
+
+ var unadjustedDate = new Date(Date.UTC(m[1], m[2] - 1, m[3], m[4], m[5]));
+
+ // 430 => 4:30. Easier to do this computation for only positive offsets
+ var sign = tzoffset < 0 ? -1 : 1;
+ tzoffset *= sign;
+ var adjustmentHours = Math.floor(tzoffset/100);
+ var adjustmentMinutes = tzoffset - adjustmentHours * 100;
+
+ return new Date(unadjustedDate.getTime() -
+ sign * adjustmentHours * 3600000 -
+ sign * adjustmentMinutes * 60000);
+ },
+
+ _formatWho : function(name, email) {
+ if (name && email) {
+ return name + " <" + email + ">";
+ } else if (name) {
+ return name;
+ } else {
+ return email;
+ }
+ }
+};
+
+Splinter.Bug.Attachment = function(bug, id) {
+ this._init(bug, id);
+};
+
+Splinter.Bug.Attachment.prototype = {
+ _init : function(bug, id) {
+ this.bug = bug;
+ this.id = id;
+ }
+};
+
+Splinter.Bug.Comment = function(bug) {
+ this._init(bug);
+};
+
+Splinter.Bug.Comment.prototype = {
+ _init : function(bug) {
+ this.bug = bug;
+ },
+
+ getWho : function() {
+ return Splinter.Bug._formatWho(this.whoName, this.whoEmail);
+ }
+};
+
+Splinter.Bug.Bug = function() {
+ this._init();
+};
+
+Splinter.Bug.Bug.prototype = {
+ _init : function() {
+ this.attachments = [];
+ this.comments = [];
+ },
+
+ getAttachment : function(attachmentId) {
+ var i;
+ for (i = 0; i < this.attachments.length; i++) {
+ if (this.attachments[i].id == attachmentId) {
+ return this.attachments[i];
+ }
+ }
+ return null;
+ },
+
+ getReporter : function() {
+ return Splinter.Bug._formatWho(this.reporterName, this.reporterEmail);
+ }
+};
+
+Splinter.Dialog = function() {
+ this._init.apply(this, arguments);
+};
+
+Splinter.Dialog.prototype = {
+ _init: function(prompt) {
+ this.buttons = [];
+ this.dialog = new YAHOO.widget.SimpleDialog('dialog', {
+ width: "300px",
+ fixedcenter: true,
+ visible: false,
+ modal: true,
+ draggable: false,
+ close: false,
+ hideaftersubmit: true,
+ constraintoviewport: true
+ });
+ this.dialog.setHeader(prompt);
+ },
+
+ addButton : function (label, callback, isdefault) {
+ this.buttons.push({ text : label,
+ handler : function () { this.hide(); callback(); },
+ isDefault : isdefault });
+ this.dialog.cfg.queueProperty("buttons", this.buttons);
+ },
+
+ show : function () {
+ this.dialog.render(document.body);
+ this.dialog.show();
+ }
+};
+
+Splinter.Patch = {
+ ADDED : 1 << 0,
+ REMOVED : 1 << 1,
+ CHANGED : 1 << 2,
+ NEW_NONEWLINE : 1 << 3,
+ OLD_NONEWLINE : 1 << 4,
+
+ FILE_START_RE : /^(?:(?:Index|index|===|RCS|diff).*\n)*\-\-\-[ \t]*(\S+).*\n\+\+\+[ \t]*(\S+).*\n(?=@@)/mg,
+ HUNK_START_RE : /^@@[ \t]+-(\d+),(\d+)[ \t]+\+(\d+),(\d+)[ \t]+@@(.*)\n/mg,
+ HUNK_RE : /((?:[ +\\-].*\n)*)/mg,
+
+ _cleanIntro : function(intro) {
+ var m;
+
+ intro = Splinter.Utils.strip(intro);
+
+ // Git: remove leading 'From 0 && Splinter.Utils.strip(rawlines[rawlines.length - 1]) == "") {
+ rawlines.pop(); // Remove trailing element from final \n
+ }
+
+ this.oldStart = oldStart;
+ this.oldCount = oldCount;
+ this.newStart = newStart;
+ this.newCount = newCount;
+ this.functionLine = Splinter.Utils.strip(functionLine);
+ this.comment = null;
+
+ var lines = [];
+ var totalOld = 0;
+ var totalNew = 0;
+
+ var currentStart = -1;
+ var currentOldCount = 0;
+ var currentNewCount = 0;
+
+ // A segment is a series of lines added/removed/changed with no intervening
+ // unchanged lines. We make the classification of Patch.ADDED/Patch.REMOVED/Patch.CHANGED
+ // in the flags for the entire segment
+ function startSegment() {
+ if (currentStart < 0) {
+ currentStart = lines.length;
+ }
+ }
+
+ function endSegment() {
+ if (currentStart >= 0) {
+ if (currentOldCount > 0 && currentNewCount > 0) {
+ var j;
+ for (j = currentStart; j < lines.length; j++) {
+ lines[j][2] &= ~(Splinter.Patch.ADDED | Splinter.Patch.REMOVED);
+ lines[j][2] |= Splinter.Patch.CHANGED;
+ }
+ }
+
+ currentStart = -1;
+ currentOldCount = 0;
+ currentNewCount = 0;
+ }
+ }
+
+ var i;
+ for (i = 0; i < rawlines.length; i++) {
+ var line = rawlines[i];
+ var op = line.substr(0, 1);
+ var strippedLine = line.substring(1);
+ var noNewLine = 0;
+ if (i + 1 < rawlines.length && rawlines[i + 1].substr(0, 1) == '\\') {
+ noNewLine = op == '-' ? Splinter.Patch.OLD_NONEWLINE : Splinter.Patch.NEW_NONEWLINE;
+ }
+
+ if (op == ' ') {
+ endSegment();
+ totalOld++;
+ totalNew++;
+ lines.push([strippedLine, strippedLine, 0]);
+ } else if (op == '-') {
+ totalOld++;
+ startSegment();
+ lines.push([strippedLine, null, Splinter.Patch.REMOVED | noNewLine]);
+ currentOldCount++;
+ } else if (op == '+') {
+ totalNew++;
+ startSegment();
+ if (currentStart + currentNewCount >= lines.length) {
+ lines.push([null, strippedLine, Splinter.Patch.ADDED | noNewLine]);
+ } else {
+ lines[currentStart + currentNewCount][1] = strippedLine;
+ lines[currentStart + currentNewCount][2] |= Splinter.Patch.ADDED | noNewLine;
+ }
+ currentNewCount++;
+ } // else if (op == '\\') {
+ // Handled with preceding line
+ // } else {
+ // Junk in the patch - hope the patch got line wrapped and just ignoring
+ // it produces something meaningful. (For a patch displayer, anyways.
+ // would be bad for applying the patch.)
+ // Utils.assertNotReached();
+ // }
+ }
+
+ // git mail-formatted patches end with --\n like a signature
+ // This is troublesome since it looks like a subtraction at the end
+ // of last hunk of the last file. Handle this specifically rather than
+ // generically stripping excess lines to be kind to hand-edited patches
+ if (totalOld > oldCount &&
+ lines[lines.length - 1][1] == null &&
+ lines[lines.length - 1][0].substr(0, 1) == '-')
+ {
+ lines.pop();
+ currentOldCount--;
+ if (currentOldCount == 0 && currentNewCount == 0) {
+ currentStart = -1;
+ }
+ }
+
+ endSegment();
+
+ this.lines = lines;
+ },
+
+ iterate : function(cb) {
+ var i;
+ var oldLine = this.oldStart;
+ var newLine = this.newStart;
+ for (i = 0; i < this.lines.length; i++) {
+ var line = this.lines[i];
+ cb(this.location + i, oldLine, line[0], newLine, line[1], line[2], line);
+ if (line[0] != null) {
+ oldLine++;
+ }
+ if (line[1] != null) {
+ newLine++;
+ }
+ }
+ }
+};
+
+Splinter.Patch.File = function(filename, status, hunks) {
+ this._init(filename, status, hunks);
+};
+
+Splinter.Patch.File.prototype = {
+ _init : function(filename, status, hunks) {
+ this.filename = filename;
+ this.status = status;
+ this.hunks = hunks;
+
+ var l = 0;
+ var i;
+ for (i = 0; i < this.hunks.length; i++) {
+ var hunk = this.hunks[i];
+ hunk.location = l;
+ l += hunk.lines.length;
+ }
+ },
+
+ // A "location" is just a linear index into the lines of the patch in this file
+ getLocation : function(oldLine, newLine) {
+ var i;
+ for (i = 0; i < this.hunks.length; i++) {
+ var hunk = this.hunks[i];
+ if (oldLine != null && hunk.oldStart > oldLine) {
+ continue;
+ }
+ if (newLine != null && hunk.newStart > newLine) {
+ continue;
+ }
+
+ if ((oldLine != null && oldLine < hunk.oldStart + hunk.oldCount) ||
+ (newLine != null && newLine < hunk.newStart + hunk.newCount))
+ {
+ var location = -1;
+ hunk.iterate(function(loc, oldl, oldText, newl, newText, flags) {
+ if ((oldLine == null || oldl == oldLine) &&
+ (newLine == null || newl == newLine))
+ {
+ location = loc;
+ }
+ });
+
+ if (location != -1) {
+ return location;
+ }
+ }
+ }
+
+ throw "Bad oldLine,newLine: " + oldLine + "," + newLine;
+ },
+
+ getHunk : function(location) {
+ var i;
+ for (i = 0; i < this.hunks.length; i++) {
+ var hunk = this.hunks[i];
+ if (location >= hunk.location && location < hunk.location + hunk.lines.length) {
+ return hunk;
+ }
+ }
+
+ throw "Bad location: " + location;
+ },
+
+ toString : function() {
+ return "Splinter.Patch.File(" + this.filename + ")";
+ }
+};
+
+Splinter.Patch.Patch = function(text) {
+ this._init(text);
+};
+
+Splinter.Patch.Patch.prototype = {
+ // cf. parsing in Review.Review.parse()
+ _init : function(text) {
+ // Canonicalize newlines to simplify the following
+ if (/\r/.test(text)) {
+ text = text.replace(/(\r\n|\r|\n)/g, "\n");
+ }
+
+ this.files = [];
+
+ var m = Splinter.Patch.FILE_START_RE.exec(text);
+ if (m != null) {
+ this.intro = Splinter.Patch._cleanIntro(text.substring(0, m.index));
+ } else {
+ throw "Not a patch";
+ }
+
+ while (m != null) {
+ // git and hg show a diff between a/foo/bar.c and b/foo/bar.c
+ // or between a/foo/bar.c and /dev/null for removals and the
+ // reverse for additions.
+ var filename;
+ var status = undefined;
+
+ if (/^a\//.test(m[1]) && /^b\//.test(m[2])) {
+ filename = m[1].substring(2);
+ status = Splinter.Patch.CHANGED;
+ } else if (/^a\//.test(m[1]) && /^\/dev\/null/.test(m[2])) {
+ filename = m[1].substring(2);
+ status = Splinter.Patch.REMOVED;
+ } else if (/^\/dev\/null/.test(m[1]) && /^b\//.test(m[2])) {
+ filename = m[2].substring(2);
+ status = Splinter.Patch.ADDED;
+ // Handle non-git and non-hg cases as well
+ } else if (!/^\/dev\/null/.test(m[1]) && /^\/dev\/null/.test(m[2])) {
+ filename = m[1];
+ status = Splinter.Patch.REMOVED;
+ } else if (/^\/dev\/null/.test(m[1]) && !/^\/dev\/null/.test(m[2])) {
+ filename = m[2];
+ status = Splinter.Patch.ADDED;
+ } else {
+ filename = m[1];
+ }
+
+ var hunks = [];
+ var pos = Splinter.Patch.FILE_START_RE.lastIndex;
+ while (true) {
+ Splinter.Patch.HUNK_START_RE.lastIndex = pos;
+ var m2 = Splinter.Patch.HUNK_START_RE.exec(text);
+ if (m2 == null || m2.index != pos) {
+ break;
+ }
+
+ var oldStart = parseInt(m2[1], 10);
+ var oldCount = parseInt(m2[2], 10);
+ var newStart = parseInt(m2[3], 10);
+ var newCount = parseInt(m2[4], 10);
+
+ pos = Splinter.Patch.HUNK_START_RE.lastIndex;
+ Splinter.Patch.HUNK_RE.lastIndex = pos;
+ var m3 = Splinter.Patch.HUNK_RE.exec(text);
+ if (m3 == null || m3.index != pos) {
+ break;
+ }
+
+ pos = Splinter.Patch.HUNK_RE.lastIndex;
+ hunks.push(new Splinter.Patch.Hunk(oldStart, oldCount, newStart, newCount, m2[5], m3[1]));
+ }
+
+ if (status === undefined) {
+ // For non-Hg/Git we use assume patch was generated non-zero context
+ // and just look at the patch to detect added/removed. Bzr actually
+ // says added/removed in the diff, but SVN/CVS don't
+ if (hunks.length == 1 && hunks[0].oldCount == 0) {
+ status = Splinter.Patch.ADDED;
+ } else if (hunks.length == 1 && hunks[0].newCount == 0) {
+ status = Splinter.Patch.REMOVED;
+ } else {
+ status = Splinter.Patch.CHANGED;
+ }
+ }
+
+ this.files.push(new Splinter.Patch.File(filename, status, hunks));
+
+ Splinter.Patch.FILE_START_RE.lastIndex = pos;
+ m = Splinter.Patch.FILE_START_RE.exec(text);
+ }
+ },
+
+ getFile : function(filename) {
+ var i;
+ for (i = 0; i < this.files.length; i++) {
+ if (this.files[i].filename == filename) {
+ return this.files[i];
+ }
+ }
+
+ return null;
+ }
+};
+
+Splinter.Review = {
+ _removeFromArray : function(a, element) {
+ var i;
+ for (i = 0; i < a.length; i++) {
+ if (a[i] === element) {
+ a.splice(i, 1);
+ return;
+ }
+ }
+ },
+
+ _noNewLine : function(flags, flag) {
+ return ((flags & flag) != 0) ? "\n\\ No newline at end of file" : "";
+ },
+
+ _lineInSegment : function(line) {
+ return (line[2] & (Splinter.Patch.ADDED | Splinter.Patch.REMOVED | Splinter.Patch.CHANGED)) != 0;
+ },
+
+ _compareSegmentLines : function(a, b) {
+ var op1 = a[0];
+ var op2 = b[0];
+ if (op1 == op2) {
+ return 0;
+ } else if (op1 == ' ') {
+ return -1;
+ } else if (op2 == ' ') {
+ return 1;
+ } else {
+ return op1 == '-' ? -1 : 1;
+ }
+ },
+
+ FILE_START_RE : /^:::[ \t]+(\S+)[ \t]*\n/mg,
+ HUNK_START_RE : /^@@[ \t]+(?:-(\d+),(\d+)[ \t]+)?(?:\+(\d+),(\d+)[ \t]+)?@@.*\n/mg,
+ HUNK_RE : /((?:(?!@@|:::).*\n?)*)/mg,
+ REVIEW_RE : /^\s*review\s+of\s+attachment\s+(\d+)\s*:\s*/i
+};
+
+Splinter.Review.Comment = function(file, location, type, comment) {
+ this._init(file, location, type, comment);
+};
+
+Splinter.Review.Comment.prototype = {
+ _init : function(file, location, type, comment) {
+ this.file = file;
+ this.type = type;
+ this.location = location;
+ this.comment = comment;
+ },
+
+ getHunk : function() {
+ return this.file.patchFile.getHunk(this.location);
+ },
+
+ getInReplyTo : function() {
+ var i;
+ var hunk = this.getHunk();
+ var line = hunk.lines[this.location - hunk.location];
+ for (i = 0; i < line.reviewComments.length; i++) {
+ var comment = line.reviewComments[0];
+ if (comment === this) {
+ return null;
+ }
+ if (comment.type == this.type) {
+ return comment;
+ }
+ }
+
+ return null;
+ },
+
+ remove : function() {
+ var hunk = this.getHunk();
+ var line = hunk.lines[this.location - hunk.location];
+ Splinter.Review._removeFromArray(this.file.comments, this);
+ Splinter.Review._removeFromArray(line.reviewComments, this);
+ }
+};
+
+Splinter.Review.File = function(review, patchFile) {
+ this._init(review, patchFile);
+};
+
+Splinter.Review.File.prototype = {
+ _init : function(review, patchFile) {
+ this.review = review;
+ this.patchFile = patchFile;
+ this.comments = [];
+ },
+
+ addComment : function(location, type, comment) {
+ var hunk = this.patchFile.getHunk(location);
+ var line = hunk.lines[location - hunk.location];
+ comment = new Splinter.Review.Comment(this, location, type, comment);
+ if (line.reviewComments == null) {
+ line.reviewComments = [];
+ }
+ line.reviewComments.push(comment);
+ var i;
+ for (i = 0; i <= this.comments.length; i++) {
+ if (i == this.comments.length ||
+ this.comments[i].location > location ||
+ (this.comments[i].location == location && this.comments[i].type > type)) {
+ this.comments.splice(i, 0, comment);
+ break;
+ } else if (this.comments[i].location == location &&
+ this.comments[i].type == type) {
+ throw "Two comments at the same location";
+ }
+ }
+
+ return comment;
+ },
+
+ getComment : function(location, type) {
+ var i;
+ for (i = 0; i < this.comments.length; i++) {
+ if (this.comments[i].location == location &&
+ this.comments[i].type == type)
+ {
+ return this.comments[i];
+ }
+ }
+
+ return null;
+ },
+
+ toString : function() {
+ var str = "::: " + this.patchFile.filename + "\n";
+ var first = true;
+
+ var i;
+ for (i = 0; i < this.comments.length; i++) {
+ if (first) {
+ first = false;
+ } else {
+ str += '\n';
+ }
+ var comment = this.comments[i];
+ var hunk = comment.getHunk();
+
+ // Find the range of lines we might want to show. That's everything in the
+ // same segment as the commented line, plus up two two lines of non-comment
+ // diff before.
+
+ var contextFirst = comment.location - hunk.location;
+ if (Splinter.Review._lineInSegment(hunk.lines[contextFirst])) {
+ while (contextFirst > 0 && Splinter.Review._lineInSegment(hunk.lines[contextFirst - 1])) {
+ contextFirst--;
+ }
+ }
+
+ var j;
+ for (j = 0; j < 5; j++) {
+ if (contextFirst > 0 && !Splinter.Review._lineInSegment(hunk.lines[contextFirst - 1])) {
+ contextFirst--;
+ }
+ }
+
+ // Now get the diff lines (' ', '-', '+' for that range of lines)
+
+ var patchOldStart = null;
+ var patchNewStart = null;
+ var patchOldLines = 0;
+ var patchNewLines = 0;
+ var unchangedLines = 0;
+ var patchLines = [];
+
+ function addOldLine(oldLine) {
+ if (patchOldLines == 0) {
+ patchOldStart = oldLine;
+ }
+ patchOldLines++;
+ }
+
+ function addNewLine(newLine) {
+ if (patchNewLines == 0) {
+ patchNewStart = newLine;
+ }
+ patchNewLines++;
+ }
+
+ hunk.iterate(function(loc, oldLine, oldText, newLine, newText, flags) {
+ if (loc >= hunk.location + contextFirst && loc <= comment.location) {
+ if ((flags & (Splinter.Patch.ADDED | Splinter.Patch.REMOVED | Splinter.Patch.CHANGED)) == 0) {
+ patchLines.push('> ' + oldText + Splinter.Review._noNewLine(flags, Splinter.Patch.OLD_NONEWLINE | Splinter.Patch.NEW_NONEWLINE));
+ addOldLine(oldLine);
+ addNewLine(newLine);
+ unchangedLines++;
+ } else {
+ if ((comment.type == Splinter.Patch.REMOVED
+ || comment.type == Splinter.Patch.CHANGED)
+ && oldText != null)
+ {
+ patchLines.push('> -' + oldText +
+ Splinter.Review._noNewLine(flags, Splinter.Patch.OLD_NONEWLINE));
+ addOldLine(oldLine);
+ }
+ if ((comment.type == Splinter.Patch.ADDED
+ || comment.type == Splinter.Patch.CHANGED)
+ && newText != null)
+ {
+ patchLines.push('> +' + newText +
+ Splinter.Review._noNewLine(flags, Splinter.Patch.NEW_NONEWLINE));
+ addNewLine(newLine);
+ }
+ }
+ }
+ });
+
+ // Sort them into global order ' ', '-', '+'
+ patchLines.sort(Splinter.Review._compareSegmentLines);
+
+ // Completely blank context isn't useful so remove it; however if we are commenting
+ // on blank lines at the start of a segment, we have to leave something or things break
+ while (patchLines.length > 1 && patchLines[0].match(/^\s*$/)) {
+ patchLines.shift();
+ patchOldStart++;
+ patchNewStart++;
+ patchOldLines--;
+ patchNewLines--;
+ unchangedLines--;
+ }
+
+ if (comment.type == Splinter.Patch.CHANGED) {
+ // For a CHANGED comment, we have to show the the start of the hunk - but to save
+ // in length we can trim unchanged context before it
+
+ if (patchOldLines + patchNewLines - unchangedLines > 5) {
+ var toRemove = Math.min(unchangedLines, patchOldLines + patchNewLines - unchangedLines - 5);
+ patchLines.splice(0, toRemove);
+ patchOldStart += toRemove;
+ patchNewStart += toRemove;
+ patchOldLines -= toRemove;
+ patchNewLines -= toRemove;
+ unchangedLines -= toRemove;
+ }
+
+ str += '@@ -' + patchOldStart + ',' + patchOldLines + ' +' + patchNewStart + ',' + patchNewLines + ' @@\n';
+
+ // We will use up to 10 lines more:
+ // 5 old lines or 4 old lines and a "... more ... " line
+ // 5 new lines or 4 new lines and a "... more ... " line
+
+ var patchRemovals = patchOldLines - unchangedLines;
+ var showPatchRemovals = patchRemovals > 5 ? 4 : patchRemovals;
+ var patchAdditions = patchNewLines - unchangedLines;
+ var showPatchAdditions = patchAdditions > 5 ? 4 : patchAdditions;
+
+ j = 0;
+ while (j < unchangedLines + showPatchRemovals) {
+ str += "> " + patchLines[j] + "\n";
+ j++;
+ }
+ if (showPatchRemovals < patchRemovals) {
+ str += "> ... " + patchRemovals - showPatchRemovals + " more ...\n";
+ j += patchRemovals - showPatchRemovals;
+ }
+ while (j < unchangedLines + patchRemovals + showPatchAdditions) {
+ str += "> " + patchLines[j] + "\n";
+ j++;
+ }
+ if (showPatchAdditions < patchAdditions) {
+ str += "> ... " + patchAdditions - showPatchAdditions + " more ...\n";
+ j += patchAdditions - showPatchAdditions;
+ }
+ } else {
+ // We limit Patch.ADDED/Patch.REMOVED comments strictly to 5 lines after the header
+ if (patchOldLines + patchNewLines - unchangedLines > 5) {
+ var toRemove = patchOldLines + patchNewLines - unchangedLines - 5;
+ patchLines.splice(0, toRemove);
+ patchOldStart += toRemove;
+ patchNewStart += toRemove;
+ patchOldLines -= toRemove;
+ patchNewLines -= toRemove;
+ }
+
+ if (comment.type == Splinter.Patch.REMOVED) {
+ str += '@@ -' + patchOldStart + ',' + patchOldLines + ' @@\n';
+ } else {
+ str += '@@ +' + patchNewStart + ',' + patchNewLines + ' @@\n';
+ }
+ str += patchLines.join("\n") + "\n";
+ }
+ str += "\n" + comment.comment + "\n";
+ }
+
+ return str;
+ }
+};
+
+Splinter.Review.Review = function(patch, who, date) {
+ this._init(patch, who, date);
+};
+
+Splinter.Review.Review.prototype = {
+ _init : function(patch, who, date) {
+ this.date = null;
+ this.patch = patch;
+ this.who = who;
+ this.date = date;
+ this.intro = null;
+ this.files = [];
+
+ var i;
+ for (i = 0; i < patch.files.length; i++) {
+ this.files.push(new Splinter.Review.File(this, patch.files[i]));
+ }
+ },
+
+ // cf. parsing in Patch.Patch._init()
+ parse : function(text) {
+ Splinter.Review.FILE_START_RE.lastIndex = 0;
+ var m = Splinter.Review.FILE_START_RE.exec(text);
+
+ var intro;
+ if (m != null) {
+ this.setIntro(text.substr(0, m.index));
+ } else{
+ this.setIntro(text);
+ return;
+ }
+
+ while (m != null) {
+ var filename = m[1];
+ var file = this.getFile(filename);
+ if (file == null) {
+ throw "Review.Review refers to filename '" + filename + "' not in reviewed Patch.";
+ }
+
+ var pos = Splinter.Review.FILE_START_RE.lastIndex;
+
+ while (true) {
+ Splinter.Review.HUNK_START_RE.lastIndex = pos;
+ var m2 = Splinter.Review.HUNK_START_RE.exec(text);
+ if (m2 == null || m2.index != pos) {
+ break;
+ }
+
+ pos = Splinter.Review.HUNK_START_RE.lastIndex;
+
+ var oldStart, oldCount, newStart, newCount;
+ if (m2[1]) {
+ oldStart = parseInt(m2[1], 10);
+ oldCount = parseInt(m2[2], 10);
+ } else {
+ oldStart = oldCount = null;
+ }
+
+ if (m2[3]) {
+ newStart = parseInt(m2[3], 10);
+ newCount = parseInt(m2[4], 10);
+ } else {
+ newStart = newCount = null;
+ }
+
+ var type;
+ if (oldStart != null && newStart != null) {
+ type = Splinter.Patch.CHANGED;
+ } else if (oldStart != null) {
+ type = Splinter.Patch.REMOVED;
+ } else if (newStart != null) {
+ type = Splinter.Patch.ADDED;
+ } else {
+ throw "Either old or new line numbers must be given";
+ }
+
+ var oldLine = oldStart;
+ var newLine = newStart;
+
+ Splinter.Review.HUNK_RE.lastIndex = pos;
+ var m3 = Splinter.Review.HUNK_RE.exec(text);
+ if (m3 == null || m3.index != pos) {
+ break;
+ }
+
+ pos = Splinter.Review.HUNK_RE.lastIndex;
+
+ var rawlines = m3[1].split("\n");
+ if (rawlines.length > 0 && rawlines[rawlines.length - 1].match('^/s+$')) {
+ rawlines.pop(); // Remove trailing element from final \n
+ }
+
+ var commentText = null;
+
+ var lastSegmentOld = 0;
+ var lastSegmentNew = 0;
+ var i;
+ for (i = 0; i < rawlines.length; i++) {
+ var line = rawlines[i];
+ var count = 1;
+ if (i < rawlines.length - 1 && rawlines[i + 1].match(/^... \d+\s+/)) {
+ var m3 = /^\.\.\.\s+(\d+)\s+/.exec(rawlines[i + 1]);
+ count += parseInt(m3[1], 10);
+ i += 1;
+ }
+ // The check for /^$/ is because if Bugzilla is line-wrapping it also
+ // strips completely whitespace lines
+ if (line.match(/^>?\s+/) || line.match(/^$/)) {
+ oldLine += count;
+ newLine += count;
+ lastSegmentOld = 0;
+ lastSegmentNew = 0;
+ } else if (line.match(/^(> )?-/)) {
+ oldLine += count;
+ lastSegmentOld += count;
+ } else if (line.match(/^(> )?\+/)) {
+ newLine += count;
+ lastSegmentNew += count;
+ } else if (line.match(/^\\/)) {
+ // '\ No newline at end of file' - ignore
+ } else {
+ // Ignore assumming it's a result of line-wrapping
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=509152
+ YAHOO.log("WARNING: Bad content in hunk: " + line);
+ }
+
+ if ((oldStart == null || oldLine == oldStart + oldCount) &&
+ (newStart == null || newLine == newStart + newCount))
+ {
+ commentText = rawlines.slice(i + 1).join("\n");
+ break;
+ }
+ }
+
+ if (commentText == null) {
+ YAHOO.log("WARNING: No comment found in hunk");
+ commentText = "";
+ }
+
+
+ var location;
+ if (type == Splinter.Patch.CHANGED) {
+ if (lastSegmentOld >= lastSegmentNew) {
+ oldLine--;
+ }
+ if (lastSegmentOld <= lastSegmentNew) {
+ newLine--;
+ }
+ location = file.patchFile.getLocation(oldLine, newLine);
+ } else if (type == Splinter.Patch.REMOVED) {
+ oldLine--;
+ location = file.patchFile.getLocation(oldLine, null);
+ } else if (type == Splinter.Patch.ADDED) {
+ newLine--;
+ location = file.patchFile.getLocation(null, newLine);
+ }
+ file.addComment(location, type, Splinter.Utils.strip(commentText));
+ }
+
+ Splinter.Review.FILE_START_RE.lastIndex = pos;
+ m = Splinter.Review.FILE_START_RE.exec(text);
+ }
+ },
+
+ setIntro : function (intro) {
+ intro = Splinter.Utils.strip(intro);
+ this.intro = intro != "" ? intro : null;
+ },
+
+ getFile : function (filename) {
+ var i;
+ for (i = 0; i < this.files.length; i++) {
+ if (this.files[i].patchFile.filename == filename) {
+ return this.files[i];
+ }
+ }
+
+ return null;
+ },
+
+ // Making toString() serialize to our seriaization format is maybe a bit sketchy
+ // But the serialization format is designed to be human readable so it works
+ // pretty well.
+ toString : function () {
+ var str = '';
+ if (this.intro != null) {
+ str += Splinter.Utils.strip(this.intro);
+ str += '\n';
+ }
+
+ var first = this.intro == null;
+ var i;
+ for (i = 0; i < this.files.length; i++) {
+ var file = this.files[i];
+ if (file.comments.length > 0) {
+ if (first) {
+ first = false;
+ } else {
+ str += '\n';
+ }
+ str += file.toString();
+ }
+ }
+
+ return str;
+ }
+};
+
+Splinter.ReviewStorage = {};
+
+Splinter.ReviewStorage.LocalReviewStorage = function() {
+ this._init();
+};
+
+Splinter.ReviewStorage.LocalReviewStorage.available = function() {
+ // The try is a workaround for
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=517778
+ // where if cookies are disabled or set to ask, then the first attempt
+ // to access the localStorage property throws a security error.
+ try {
+ return 'localStorage' in window && window.localStorage != null;
+ } catch (e) {
+ return false;
+ }
+};
+
+Splinter.ReviewStorage.LocalReviewStorage.prototype = {
+ _init : function() {
+ var reviewInfosText = localStorage.splinterReviews;
+ if (reviewInfosText == null) {
+ this._reviewInfos = [];
+ } else {
+ this._reviewInfos = YAHOO.lang.JSON.parse(reviewInfosText);
+ }
+ },
+
+ listReviews : function() {
+ return this._reviewInfos;
+ },
+
+ _reviewPropertyName : function(bug, attachment) {
+ return 'splinterReview_' + bug.id + '_' + attachment.id;
+ },
+
+ loadDraft : function(bug, attachment, patch) {
+ var propertyName = this._reviewPropertyName(bug, attachment);
+ var reviewText = localStorage[propertyName];
+ if (reviewText != null) {
+ var review = new Splinter.Review.Review(patch);
+ review.parse(reviewText);
+ return review;
+ } else {
+ return null;
+ }
+ },
+
+ _findReview : function(bug, attachment) {
+ var i;
+ for (i = 0 ; i < this._reviewInfos.length; i++) {
+ if (this._reviewInfos[i].bugId == bug.id && this._reviewInfos[i].attachmentId == attachment.id) {
+ return i;
+ }
+ }
+
+ return -1;
+ },
+
+ _updateOrCreateReviewInfo : function(bug, attachment, props) {
+ var reviewIndex = this._findReview(bug, attachment);
+ var reviewInfo;
+
+ var nowTime = Date.now();
+ if (reviewIndex >= 0) {
+ reviewInfo = this._reviewInfos[reviewIndex];
+ this._reviewInfos.splice(reviewIndex, 1);
+ } else {
+ reviewInfo = {
+ bugId: bug.id,
+ bugShortDesc: bug.shortDesc,
+ attachmentId: attachment.id,
+ attachmentDescription: attachment.description,
+ creationTime: nowTime
+ };
+ }
+
+ reviewInfo.modificationTime = nowTime;
+ for (var prop in props) {
+ reviewInfo[prop] = props[prop];
+ }
+
+ this._reviewInfos.push(reviewInfo);
+ localStorage.splinterReviews = YAHOO.lang.JSON.stringify(this._reviewInfos);
+ },
+
+ _deleteReviewInfo : function(bug, attachment) {
+ var reviewIndex = this._findReview(bug, attachment);
+ if (reviewIndex >= 0) {
+ this._reviewInfos.splice(reviewIndex, 1);
+ localStorage.splinterReviews = YAHOO.lang.JSON.stringify(this._reviewInfos);
+ }
+ },
+
+ saveDraft : function(bug, attachment, review) {
+ var propertyName = this._reviewPropertyName(bug, attachment);
+
+ this._updateOrCreateReviewInfo(bug, attachment, { isDraft: true });
+ localStorage[propertyName] = "" + review;
+ },
+
+ deleteDraft : function(bug, attachment, review) {
+ var propertyName = this._reviewPropertyName(bug, attachment);
+
+ this._deleteReviewInfo(bug, attachment);
+ delete localStorage[propertyName];
+ },
+
+ draftPublished : function(bug, attachment) {
+ var propertyName = this._reviewPropertyName(bug, attachment);
+
+ this._updateOrCreateReviewInfo(bug, attachment, { isDraft: false });
+ delete localStorage[propertyName];
+ }
+};
+
+Splinter.saveDraftNoticeTimeoutId = null;
+Splinter.navigationLinks = {};
+Splinter.reviewers = {};
+Splinter.savingDraft = false;
+Splinter.UPDATE_ATTACHMENT_SUCCESS = /\s*Changes\s+Submitted/;
+Splinter.LINE_RE = /(?!$)([^\r\n]*)(?:\r\n|\r|\n|$)/g;
+
+Splinter.displayError = function (msg) {
+ var el = new Element(document.createElement('p'));
+ el.appendChild(document.createTextNode(msg));
+ Dom.get('error').appendChild(Dom.get(el));
+ Dom.setStyle('error', 'display', 'block');
+};
+
+//Splinter.updateAttachmentStatus = function (attachment, newStatus, success, failure) {
+// var data = {
+// action: 'update',
+// id: attachment.id,
+// description: attachment.description,
+// filename: attachment.filename,
+// ispatch: attachment.isPatch ? 1 : 0,
+// isobsolete: attachment.isObsolete ? 1 : 0,
+// isprivate: attachment.isPrivate ? 1 : 0,
+// 'attachments.status': newStatus
+// };
+//
+// if (attachment.token) {
+// data.token = attachment.token;
+// }
+//
+// $.ajax({
+// data : data,
+// dataType : 'text',
+// error : function(xmlHttpRequest, textStatus, errorThrown) {
+// failure();
+// },
+// success : function(data, textStatus) {
+// if (data.search(Splinter.UPDATE_ATTACHMENT_SUCCESS) != -1) {
+// success();
+// } else {
+// failure();
+// }
+// },
+// type : 'POST',
+// url : "attachment.cgi"
+// });
+//}
+
+Splinter.publishReview = function () {
+ Splinter.saveComment();
+ Splinter.theReview.setIntro(Dom.get('myComment').value);
+
+ if (Splinter.reviewStorage) {
+ Splinter.reviewStorage.draftPublished(Splinter.theBug,
+ Splinter.theAttachment);
+ }
+
+ var publish_form = Dom.get('publish');
+ var publish_token = Dom.get('publish_token');
+ var publish_attach_id = Dom.get('publish_attach_id');
+ var publish_attach_desc = Dom.get('publish_attach_desc');
+ var publish_attach_filename = Dom.get('publish_attach_filename');
+ var publish_attach_contenttype = Dom.get('publish_attach_contenttype');
+ var publish_attach_ispatch = Dom.get('publish_attach_ispatch');
+ var publish_attach_isobsolete = Dom.get('publish_attach_isobsolete');
+ var publish_attach_isprivate = Dom.get('publish_attach_isprivate');
+ var publish_attach_status = Dom.get('publish_attach_status');
+ var publish_review = Dom.get('publish_review');
+
+ publish_token.value = Splinter.theAttachment.token;
+ publish_attach_id.value = Splinter.theAttachment.id;
+ publish_attach_desc.value = Splinter.theAttachment.description;
+ publish_attach_filename.value = Splinter.theAttachment.filename;
+ publish_attach_contenttype.value = Splinter.theAttachment.contenttypeentry;
+ publish_attach_ispatch.value = Splinter.theAttachment.isPatch;
+ publish_attach_isobsolete.value = Splinter.theAttachment.isObsolete;
+ publish_attach_isprivate.value = Splinter.theAttachment.isPrivate;
+
+ // This is a "magic string" used to identify review comments
+ if (Splinter.theReview.toString()) {
+ var comment = "Review of attachment " + Splinter.theAttachment.id + ":\n" +
+ "-----------------------------------------------------------------\n\n" +
+ Splinter.theReview.toString();
+ publish_review.value = comment;
+ }
+
+ if (Splinter.theAttachment.status
+ && Dom.get('attachmentStatus').value != Splinter.theAttachment.status)
+ {
+ publish_attach_status.value = Dom.get('attachmentStatus').value;
+ }
+
+ publish_form.submit();
+};
+
+Splinter.doDiscardReview = function () {
+ if (Splinter.theAttachment.status) {
+ Dom.get('attachmentStatus').value = Splinter.theAttachment.status;
+ }
+
+ Dom.get('myComment').value = '';
+ Dom.setStyle('emptyCommentNotice', 'display', 'block');
+
+ var i;
+ for (i = 0; i < Splinter.theReview.files.length; i++) {
+ while (Splinter.theReview.files[i].comments.length > 0) {
+ Splinter.theReview.files[i].comments[0].remove();
+ }
+ }
+
+ Splinter.updateMyPatchComments();
+ Splinter.updateHaveDraft();
+ Splinter.saveDraft();
+};
+
+Splinter.discardReview = function () {
+ var dialog = new Splinter.Dialog("Really discard your changes?");
+ dialog.addButton('No', function() {}, true);
+ dialog.addButton('Yes', Splinter.doDiscardReview, false);
+ dialog.show();
+};
+
+Splinter.haveDraft = function () {
+ if (Splinter.theAttachment.status && Dom.get('attachmentStatus').value != Splinter.theAttachment.status) {
+ return true;
+ }
+
+ if (Dom.get('myComment').value != '') {
+ return true;
+ }
+
+ var i;
+ for (i = 0; i < Splinter.theReview.files.length; i++) {
+ if (Splinter.theReview.files[i].comments.length > 0) {
+ return true;
+ }
+ }
+
+ if (Splinter.flagChanged == 1) {
+ return true;
+ }
+
+ return false;
+};
+
+Splinter.updateHaveDraft = function () {
+ clearTimeout(Splinter.updateHaveDraftTimeoutId);
+ Splinter.updateHaveDraftTimeoutId = null;
+
+ if (Splinter.haveDraft()) {
+ Dom.get('publishButton').removeAttribute('disabled');
+ Dom.get('cancelButton').removeAttribute('disabled');
+ Dom.setStyle('haveDraftNotice', 'display', 'block');
+ } else {
+ Dom.get('publishButton').setAttribute('disabled', 'true');
+ Dom.get('cancelButton').setAttribute('disabled', 'true');
+ Dom.setStyle('haveDraftNotice', 'display', 'none');
+ }
+};
+
+Splinter.queueUpdateHaveDraft = function () {
+ if (Splinter.updateHaveDraftTimeoutId == null) {
+ Splinter.updateHaveDraftTimeoutId = setTimeout(Splinter.updateHaveDraft, 0);
+ }
+};
+
+Splinter.hideSaveDraftNotice = function () {
+ clearTimeout(Splinter.saveDraftNoticeTimeoutId);
+ Splinter.saveDraftNoticeTimeoutId = null;
+ Dom.setStyle('saveDraftNotice', 'display', 'none');
+};
+
+Splinter.saveDraft = function () {
+ if (Splinter.reviewStorage == null) {
+ return;
+ }
+
+ clearTimeout(Splinter.saveDraftTimeoutId);
+ Splinter.saveDraftTimeoutId = null;
+
+ Splinter.savingDraft = true;
+ Dom.get('saveDraftNotice').innerHTML = "Saving Draft...";
+ Dom.setStyle('saveDraftNotice', 'display', 'block');
+ clearTimeout(Splinter.saveDraftNoticeTimeoutId);
+ setTimeout(Splinter.hideSaveDraftNotice, 3000);
+
+ if (Splinter.currentEditComment) {
+ Splinter.currentEditComment.comment = Splinter.Utils.strip(Dom.get("commentEditor").getElementsByTagName("textarea")[0].value);
+ // Messy, we don't want the empty comment in the saved draft, so remove it and
+ // then add it back.
+ if (!Splinter.currentEditComment.comment) {
+ Splinter.currentEditComment.remove();
+ }
+ }
+
+ Splinter.theReview.setIntro(Dom.get('myComment').value);
+
+ var draftSaved = false;
+ if (Splinter.haveDraft()) {
+ Splinter.reviewStorage.saveDraft(Splinter.theBug, Splinter.theAttachment, Splinter.theReview);
+ draftSaved = true;
+ } else {
+ Splinter.reviewStorage.deleteDraft(Splinter.theBug, Splinter.theAttachment, Splinter.theReview);
+ }
+
+ if (Splinter.currentEditComment && !Splinter.currentEditComment.comment) {
+ Splinter.currentEditComment = Splinter.currentEditComment.file.addComment(Splinter.currentEditComment.location,
+ Splinter.currentEditComment.type, "");
+ }
+
+ Splinter.savingDraft = false;
+ if (draftSaved) {
+ Dom.get('saveDraftNotice').innerHTML = "Saved Draft";
+ } else {
+ Splinter.hideSaveDraftNotice();
+ }
+};
+
+Splinter.queueSaveDraft = function () {
+ if (Splinter.saveDraftTimeoutId == null) {
+ Splinter.saveDraftTimeoutId = setTimeout(Splinter.saveDraft, 10000);
+ }
+};
+
+Splinter.flushSaveDraft = function () {
+ if (Splinter.saveDraftTimeoutId != null) {
+ Splinter.saveDraft();
+ }
+};
+
+Splinter.ensureCommentArea = function (row) {
+ var file = Splinter.domCache.data(row).patchFile;
+ var colSpan = file.status == Splinter.Patch.CHANGED ? 5 : 2;
+
+ if (!row.nextSibling || row.nextSibling.className != "comment-area") {
+ var tr = new Element(document.createElement('tr'));
+ Dom.addClass(tr, 'comment-area');
+ var td = new Element(document.createElement('td'));
+ Dom.setAttribute(td, 'colspan', colSpan);
+ td.appendTo(tr);
+ Dom.insertAfter(tr, row);
+ }
+
+ return row.nextSibling.firstChild;
+};
+
+Splinter.getTypeClass = function (type) {
+ switch (type) {
+ case Splinter.Patch.ADDED:
+ return "comment-added";
+ case Splinter.Patch.REMOVED:
+ return "comment-removed";
+ case Splinter.Patch.CHANGED:
+ return "comment-changed";
+ }
+
+ return null;
+};
+
+Splinter.getSeparatorClass = function (type) {
+ switch (type) {
+ case Splinter.Patch.ADDED:
+ return "comment-separator-added";
+ case Splinter.Patch.REMOVED:
+ return "comment-separator-removed";
+ }
+
+ return null;
+};
+
+Splinter.getReviewerClass = function (review) {
+ var reviewerIndex;
+ if (review == Splinter.theReview) {
+ reviewerIndex = 0;
+ } else {
+ reviewerIndex = (Splinter.reviewers[review.who] - 1) % 5 + 1;
+ }
+
+ return "reviewer-" + reviewerIndex;
+};
+
+Splinter.addCommentDisplay = function (commentArea, comment) {
+ var review = comment.file.review;
+
+ var separatorClass = Splinter.getSeparatorClass(comment.type);
+ if (separatorClass) {
+ var div = new Element(document.createElement('div'));
+ Dom.addClass(div, separatorClass);
+ Dom.addClass(div, Splinter.getReviewerClass(review));
+ div.appendTo(commentArea);
+ }
+
+ var commentDiv = new Element(document.createElement('div'));
+ Dom.addClass(commentDiv, 'comment');
+ Dom.addClass(commentDiv, Splinter.getTypeClass(comment.type));
+ Dom.addClass(commentDiv, Splinter.getReviewerClass(review));
+
+ Event.addListener(Dom.get(commentDiv), 'dblclick', function () {
+ Splinter.saveComment();
+ Splinter.insertCommentEditor(commentArea, comment.file.patchFile,
+ comment.location, comment.type);
+ });
+
+ var commentFrame = new Element(document.createElement('div'));
+ Dom.addClass(commentFrame, 'comment-frame');
+ commentFrame.appendTo(commentDiv);
+
+ var reviewerBox = new Element(document.createElement('div'));
+ Dom.addClass(reviewerBox, 'reviewer-box');
+ reviewerBox.appendTo(commentFrame);
+
+ var commentText = new Element(document.createElement('div'));
+ Dom.addClass(commentText, 'comment-text');
+ Splinter.Utils.preWrapLines(commentText, comment.comment);
+ commentText.appendTo(reviewerBox);
+
+ commentDiv.appendTo(commentArea);
+
+ if (review != Splinter.theReview) {
+ var reviewInfo = new Element(document.createElement('div'));
+ Dom.addClass(reviewInfo, 'review-info');
+
+ var reviewer = new Element(document.createElement('div'));
+ Dom.addClass(reviewer, 'reviewer');
+ reviewer.appendChild(document.createTextNode(review.who));
+ reviewer.appendTo(reviewInfo);
+
+ var reviewDate = new Element(document.createElement('div'));
+ Dom.addClass(reviewDate, 'review-date');
+ reviewDate.appendChild(document.createTextNode(Splinter.Utils.formatDate(review.date)));
+ reviewDate.appendTo(reviewInfo);
+
+ var reviewInfoBottom = new Element(document.createElement('div'));
+ Dom.addClass(reviewInfoBottom, 'review-info-bottom');
+ reviewInfoBottom.appendTo(reviewInfo);
+
+ reviewInfo.appendTo(reviewerBox);
+ }
+
+ comment.div = commentDiv;
+};
+
+Splinter.saveComment = function () {
+ var comment = Splinter.currentEditComment;
+ if (!comment) {
+ return;
+ }
+
+ var commentEditor = Dom.get('commentEditor');
+ var commentArea = commentEditor.parentNode;
+ var reviewFile = comment.file;
+
+ var hunk = comment.getHunk();
+ var line = hunk.lines[comment.location - hunk.location];
+
+ var value = Splinter.Utils.strip(commentEditor.getElementsByTagName('textarea')[0].value);
+ if (value != "") {
+ comment.comment = value;
+ Splinter.addCommentDisplay(commentArea, comment);
+ } else {
+ comment.remove();
+ }
+
+ if (line.reviewComments.length > 0) {
+ commentEditor.parentNode.removeChild(commentEditor);
+ var commentEditorSeparator = Dom.get('commentEditorSeparator');
+ if (commentEditorSeparator) {
+ commentEditorSeparator.parentNode.removeChild(commentEditorSeparator);
+ }
+ } else {
+ var parentToRemove = commentArea.parentNode;
+ commentArea.parentNode.parentNode.removeChild(parentToRemove);
+ }
+
+ Splinter.currentEditComment = null;
+ Splinter.saveDraft();
+ Splinter.queueUpdateHaveDraft();
+};
+
+Splinter.cancelComment = function (previousText) {
+ Dom.get("commentEditor").getElementsByTagName("textarea")[0].value = previousText;
+ Splinter.saveComment();
+};
+
+Splinter.deleteComment = function () {
+ Dom.get('commentEditor').getElementsByTagName('textarea')[0].value = "";
+ Splinter.saveComment();
+};
+
+Splinter.insertCommentEditor = function (commentArea, file, location, type) {
+ Splinter.saveComment();
+
+ var reviewFile = Splinter.theReview.getFile(file.filename);
+ var comment = reviewFile.getComment(location, type);
+ if (!comment) {
+ comment = reviewFile.addComment(location, type, "");
+ Splinter.queueUpdateHaveDraft();
+ }
+
+ var previousText = comment.comment;
+
+ var typeClass = Splinter.getTypeClass(type);
+ var separatorClass = Splinter.getSeparatorClass(type);
+
+ var nodes = Dom.getElementsByClassName('reviewer-0', 'div', commentArea);
+ var i;
+ for (i = 0; i < nodes.length; i++) {
+ if (separatorClass && Dom.hasClass(nodes[i], separatorClass)) {
+ nodes[i].parentNode.removeChild(nodes[i]);
+ }
+ if (Dom.hasClass(nodes[i], typeClass)) {
+ nodes[i].parentNode.removeChild(nodes[i]);
+ }
+ }
+
+ if (separatorClass) {
+ var commentEditorSeparator = new Element(document.createElement('div'));
+ commentEditorSeparator.set('id', 'commentEditorSeparator');
+ Dom.addClass(commentEditorSeparator, separatorClass);
+ commentEditorSeparator.appendTo(commentArea);
+ }
+
+ var commentEditor = new Element(document.createElement('div'));
+ Dom.setAttribute(commentEditor, 'id', 'commentEditor');
+ Dom.addClass(commentEditor, typeClass);
+ commentEditor.appendTo(commentArea);
+
+ var commentEditorInner = new Element(document.createElement('div'));
+ Dom.setAttribute(commentEditorInner, 'id', 'commentEditorInner');
+ commentEditorInner.appendTo(commentEditor);
+
+ var commentTextFrame = new Element(document.createElement('div'));
+ Dom.setAttribute(commentTextFrame, 'id', 'commentTextFrame');
+ commentTextFrame.appendTo(commentEditorInner);
+
+ var commentTextArea = new Element(document.createElement('textarea'));
+ Dom.setAttribute(commentTextArea, 'id', 'commentTextArea');
+ commentTextArea.appendChild(document.createTextNode(previousText));
+ commentTextArea.appendTo(commentTextFrame);
+ Event.addListener('commentTextArea', 'keydown', function (e) {
+ if (e.which == 13 && e.ctrlKey) {
+ Splinter.saveComment();
+ } else {
+ Splinter.queueSaveDraft();
+ }
+ });
+ Event.addListener('commentTextArea', 'focusin', function () { Dom.addClass(commentEditor, 'focused'); });
+ Event.addListener('commentTextArea', 'focusout', function () { Dom.removeClass(commentEditor, 'focused'); });
+ Dom.get(commentTextArea).focus();
+
+ var commentEditorLeftButtons = new Element(document.createElement('div'));
+ commentEditorLeftButtons.set('id', 'commentEditorLeftButtons');
+ commentEditorLeftButtons.appendTo(commentEditorInner);
+
+ var commentCancel = new Element(document.createElement('input'));
+ commentCancel.set('id','commentCancel');
+ commentCancel.set('type', 'button');
+ commentCancel.set('value', 'Cancel');
+ commentCancel.appendTo(commentEditorLeftButtons);
+ Event.addListener('commentCancel', 'click', function () { Splinter.cancelComment(previousText); });
+
+ if (previousText) {
+ var commentDelete = new Element(document.createElement('input'));
+ commentDelete.set('id','commentDelete');
+ commentDelete.set('type', 'button');
+ commentDelete.set('value', 'Delete');
+ commentDelete.appendTo(commentEditorLeftButtons);
+ Event.addListener('commentDelete', 'click', Splinter.deleteComment);
+ }
+
+ var commentEditorRightButtons = new Element(document.createElement('div'));
+ commentEditorRightButtons.set('id', 'commentEditorRightButtons');
+ commentEditorRightButtons.appendTo(commentEditorInner);
+
+ var commentSave = new Element(document.createElement('input'));
+ commentSave.set('id','commentSave');
+ commentSave.set('type', 'button');
+ commentSave.set('value', 'Save');
+ commentSave.appendTo(commentEditorRightButtons);
+ Event.addListener('commentSave', 'click', Splinter.saveComment);
+
+ var clear = new Element(document.createElement('div'));
+ Dom.addClass(clear, 'clear');
+ clear.appendTo(commentEditorInner);
+
+ Splinter.currentEditComment = comment;
+};
+
+Splinter.insertCommentForRow = function (clickRow, clickType) {
+ var file = Splinter.domCache.data(clickRow).patchFile;
+ var clickLocation = Splinter.domCache.data(clickRow).patchLocation;
+
+ var row = clickRow;
+ var location = clickLocation;
+ var type = clickType;
+
+ Splinter.saveComment();
+ var commentArea = Splinter.ensureCommentArea(row);
+ Splinter.insertCommentEditor(commentArea, file, location, type);
+};
+
+Splinter.EL = function (element, cls, text, title) {
+ var e = document.createElement(element);
+ if (text != null) {
+ e.appendChild(document.createTextNode(text));
+ }
+ if (cls) {
+ e.className = cls;
+ }
+ if (title) {
+ Dom.setAttribute(e, 'title', title);
+ }
+
+ return e;
+};
+
+Splinter.getElementPosition = function (element) {
+ var left = element.offsetLeft;
+ var top = element.offsetTop;
+ var parent = element.offsetParent;
+ while (parent && parent != document.body) {
+ left += parent.offsetLeft;
+ top += parent.offsetTop;
+ parent = parent.offsetParent;
+ }
+
+ return [left, top];
+};
+
+Splinter.scrollToElement = function (element) {
+ var windowHeight;
+ if ('innerHeight' in window) { // Not IE
+ windowHeight = window.innerHeight;
+ } else { // IE
+ windowHeight = document.documentElement.clientHeight;
+ }
+ var pos = Splinter.getElementPosition(element);
+ var yCenter = pos[1] + element.offsetHeight / 2;
+ window.scrollTo(0, yCenter - windowHeight / 2);
+};
+
+Splinter.onRowDblClick = function (e) {
+ var file = Splinter.domCache.data(this).patchFile;
+ var type;
+
+ if (file.status == Splinter.Patch.CHANGED) {
+ var pos = Splinter.getElementPosition(this);
+ var delta = e.pageX - (pos[0] + this.offsetWidth/2);
+ if (delta < - 20) {
+ type = Splinter.Patch.REMOVED;
+ } else if (delta < 20) {
+ type = Splinter.Patch.CHANGED;
+ } else {
+ type = Splinter.Patch.ADDED;
+ }
+ } else {
+ type = file.status;
+ }
+
+ Splinter.insertCommentForRow(this, type);
+};
+
+Splinter.appendPatchTable = function (type, maxLine, parentDiv) {
+ var fileTableContainer = new Element(document.createElement('div'));
+ Dom.addClass(fileTableContainer, 'file-table-container');
+ fileTableContainer.appendTo(parentDiv);
+
+ var fileTable = new Element(document.createElement('table'));
+ Dom.addClass(fileTable, 'file-table');
+ fileTable.appendTo(fileTableContainer);
+
+ var colQ = new Element(document.createElement('colgroup'));
+ colQ.appendTo(fileTable);
+
+ var col1, col2;
+ if (type != Splinter.Patch.ADDED) {
+ col1 = new Element(document.createElement('col'));
+ Dom.addClass(col1, 'line-number-column');
+ Dom.setAttribute(col1, 'span', '1');
+ col1.appendTo(colQ);
+ col2 = new Element(document.createElement('col'));
+ Dom.addClass(col2, 'old-column');
+ Dom.setAttribute(col2, 'span', '1');
+ col2.appendTo(colQ);
+ }
+ if (type == Splinter.Patch.CHANGED) {
+ col1 = new Element(document.createElement('col'));
+ Dom.addClass(col1, 'middle-column');
+ Dom.setAttribute(col1, 'span', '1');
+ col1.appendTo(colQ);
+ }
+ if (type != Splinter.Patch.REMOVED) {
+ col1 = new Element(document.createElement('col'));
+ Dom.addClass(col1, 'line-number-column');
+ Dom.setAttribute(col1, 'span', '1');
+ col1.appendTo(colQ);
+ col2 = new Element(document.createElement('col'));
+ Dom.addClass(col2, 'new-column');
+ Dom.setAttribute(col2, 'span', '1');
+ col2.appendTo(colQ);
+ }
+
+ if (type == Splinter.Patch.CHANGED) {
+ Dom.addClass(fileTable, 'file-table-changed');
+ }
+
+ if (maxLine >= 1000) {
+ Dom.addClass(fileTable, "file-table-wide-numbers");
+ }
+
+ var tbody = new Element(document.createElement('tbody'));
+ tbody.appendTo(fileTable);
+
+ return tbody;
+};
+
+Splinter.appendPatchHunk = function (file, hunk, tableType, includeComments, clickable, tbody, filter) {
+ hunk.iterate(function(loc, oldLine, oldText, newLine, newText, flags, line) {
+ if (filter && !filter(loc)) {
+ return;
+ }
+
+ var tr = document.createElement("tr");
+
+ var oldStyle = "";
+ var newStyle = "";
+ if ((flags & Splinter.Patch.CHANGED) != 0) {
+ oldStyle = newStyle = "changed-line";
+ } else if ((flags & Splinter.Patch.REMOVED) != 0) {
+ oldStyle = "removed-line";
+ } else if ((flags & Splinter.Patch.ADDED) != 0) {
+ newStyle = "added-line";
+ }
+
+ var title = "Double click the line to add a review comment";
+
+ if (tableType != Splinter.Patch.ADDED) {
+ if (oldText != null) {
+ tr.appendChild(Splinter.EL("td", "line-number", oldLine.toString(), title));
+ tr.appendChild(Splinter.EL("td", "old-line " + oldStyle, oldText != "" ? oldText : "\u00a0", title));
+ oldLine++;
+ } else {
+ tr.appendChild(Splinter.EL("td", "line-number"));
+ tr.appendChild(Splinter.EL("td", "old-line"));
+ }
+ }
+
+ if (tableType == Splinter.Patch.CHANGED) {
+ tr.appendChild(Splinter.EL("td", "line-middle"));
+ }
+
+ if (tableType != Splinter.Patch.REMOVED) {
+ if (newText != null) {
+ tr.appendChild(Splinter.EL("td", "line-number", newLine.toString(), title));
+ tr.appendChild(Splinter.EL("td", "new-line " + newStyle, newText != "" ? newText : "\u00a0", title));
+ newLine++;
+ } else if (tableType == Splinter.Patch.CHANGED) {
+ tr.appendChild(Splinter.EL("td", "line-number"));
+ tr.appendChild(Splinter.EL("td", "new-line"));
+ }
+ }
+
+ if (clickable) {
+ Splinter.domCache.data(tr).patchFile = file;
+ Splinter.domCache.data(tr).patchLocation = loc;
+ Event.addListener(tr, 'dblclick', Splinter.onRowDblClick);
+ }
+
+ tbody.appendChild(tr);
+
+ if (includeComments && line.reviewComments != null) {
+ var k;
+ for (k = 0; k < line.reviewComments.length; k++) {
+ var commentArea = Splinter.ensureCommentArea(tr);
+ Splinter.addCommentDisplay(commentArea, line.reviewComments[k]);
+ }
+ }
+ });
+};
+
+Splinter.addPatchFile = function (file) {
+ var fileDiv = new Element(document.createElement('div'));
+ Dom.addClass(fileDiv, 'file');
+ fileDiv.appendTo(Dom.get('files'));
+ file.div = fileDiv;
+
+ var statusString;
+ switch (file.status) {
+ case Splinter.Patch.ADDED:
+ statusString = " (new file)";
+ break;
+ case Splinter.Patch.REMOVED:
+ statusString = " (removed)";
+ break;
+ case Splinter.Patch.CHANGED:
+ statusString = "";
+ break;
+ }
+
+ var fileLabel = new Element(document.createElement('div'));
+ Dom.addClass(fileLabel, 'file-label');
+ fileLabel.appendTo(fileDiv);
+
+ var fileCollapseLink = new Element(document.createElement('a'));
+ Dom.addClass(fileCollapseLink, 'file-label-collapse');
+ fileCollapseLink.appendChild(document.createTextNode('[-]'));
+ Dom.setAttribute(fileCollapseLink, 'href', 'javascript:void(0);')
+ Dom.setAttribute(fileCollapseLink, 'onclick', "Splinter.toggleCollapsed('" +
+ encodeURIComponent(file.filename) + "');");
+ Dom.setAttribute(fileCollapseLink, 'title', 'Click to expand or collapse this file table');
+ fileCollapseLink.appendTo(fileLabel);
+
+ var fileLabelName = new Element(document.createElement('span'));
+ Dom.addClass(fileLabelName, 'file-label-name');
+ fileLabelName.appendChild(document.createTextNode(file.filename));
+ fileLabelName.appendTo(fileLabel);
+
+ var fileLabelStatus = new Element(document.createElement('span'));
+ Dom.addClass(fileLabelStatus, 'file-label-status');
+ fileLabelStatus.appendChild(document.createTextNode(statusString));
+ fileLabelStatus.appendTo(fileLabel);
+
+ var lastHunk = file.hunks[file.hunks.length - 1];
+ var lastLine = Math.max(lastHunk.oldStart + lastHunk.oldCount - 1,
+ lastHunk.newStart + lastHunk.newCount - 1);
+
+ var tbody = Splinter.appendPatchTable(file.status, lastLine, fileDiv);
+
+ var i;
+ for (i = 0; i < file.hunks.length; i++) {
+ var hunk = file.hunks[i];
+ if (hunk.oldStart > 1) {
+ var hunkHeader = Splinter.EL("tr", "hunk-header");
+ tbody.appendChild(hunkHeader);
+ hunkHeader.appendChild(Splinter.EL("td")); // line number column
+ var hunkCell = Splinter.EL("td", "hunk-cell", hunk.functionLine ? hunk.functionLine : "\u00a0");
+ hunkCell.colSpan = file.status == Splinter.Patch.CHANGED ? 4 : 1;
+ hunkHeader.appendChild(hunkCell);
+ }
+
+ Splinter.appendPatchHunk(file, hunk, file.status, true, true, tbody);
+ }
+};
+
+Splinter.appendReviewComment = function (comment, parentDiv) {
+ var commentDiv = Splinter.EL("div", "review-patch-comment");
+ Event.addListener(commentDiv, 'click', function() {
+ Splinter.showPatchFile(comment.file.patchFile);
+ if (comment.file.review == Splinter.theReview) {
+ // Immediately start editing the comment again
+ var commentDivParent = Dom.getAncestorByClassName(comment.div, 'comment-area');
+ var commentArea = commentDivParent.getElementsByTagName('td')[0];
+ Splinter.insertCommentEditor(commentArea, comment.file.patchFile, comment.location, comment.type);
+ Splinter.scrollToElement(Dom.get('commentEditor'));
+ } else {
+ // Just scroll to the comment, don't start a reply yet
+ Splinter.scrollToElement(Dom.get(comment.div));
+ }
+ });
+
+ var inReplyTo = comment.getInReplyTo();
+ if (inReplyTo) {
+ var div = new Element(document.createElement('div'));
+ Dom.addClass(div, Splinter.getReviewerClass(inReplyTo.file.review));
+ div.appendTo(commentDiv);
+
+ var reviewerBox = new Element(document.createElement('div'));
+ Dom.addClass(reviewerBox, 'reviewer-box');
+ Splinter.Utils.preWrapLines(reviewerBox, inReplyTo.comment);
+ reviewerBox.appendTo(div);
+
+ var reviewPatchCommentText = new Element(document.createElement('div'));
+ Dom.addClass(reviewPatchCommentText, 'review-patch-comment-text');
+ Splinter.Utils.preWrapLines(reviewPatchCommentText, comment.comment);
+ reviewPatchCommentText.appendTo(commentDiv);
+
+ } else {
+ var hunk = comment.getHunk();
+
+ var lastLine = Math.max(hunk.oldStart + hunk.oldCount- 1,
+ hunk.newStart + hunk.newCount- 1);
+ var tbody = Splinter.appendPatchTable(comment.type, lastLine, commentDiv);
+
+ Splinter.appendPatchHunk(comment.file.patchFile, hunk, comment.type, false, false, tbody,
+ function(loc) {
+ return (loc <= comment.location && comment.location - loc < 5);
+ });
+
+ var tr = new Element(document.createElement('tr'));
+ var td = new Element(document.createElement('td'));
+ td.appendTo(tr);
+ td = new Element(document.createElement('td'));
+ Dom.addClass(td, 'review-patch-comment-text');
+ Splinter.Utils.preWrapLines(td, comment.comment);
+ td.appendTo(tr);
+ tr.appendTo(tbody);
+ }
+
+ parentDiv.appendChild(commentDiv);
+};
+
+Splinter.appendReviewComments = function (review, parentDiv) {
+ var i;
+ for (i = 0; i < review.files.length; i++) {
+ var file = review.files[i];
+
+ if (file.comments.length == 0) {
+ continue;
+ }
+
+ parentDiv.appendChild(Splinter.EL("div", "review-patch-file", file.patchFile.filename));
+ var firstComment = true;
+ var j;
+ for (j = 0; j < file.comments.length; j++) {
+ if (firstComment) {
+ firstComment = false;
+ } else {
+ parentDiv.appendChild(Splinter.EL("div", "review-patch-comment-separator"));
+ }
+
+ Splinter.appendReviewComment(file.comments[j], parentDiv);
+ }
+ }
+};
+
+Splinter.updateMyPatchComments = function () {
+ var myPatchComments = Dom.get("myPatchComments");
+ myPatchComments.innerHTML = '';
+ Splinter.appendReviewComments(Splinter.theReview, myPatchComments);
+ if (Dom.getChildren(myPatchComments).length > 0) {
+ Dom.setStyle(myPatchComments, 'display', 'block');
+ } else {
+ Dom.setStyle(myPatchComments, 'display', 'none');
+ }
+};
+
+Splinter.selectNavigationLink = function (identifier) {
+ var navigationLinks = Dom.getElementsByClassName('navigation-link');
+ var i;
+ for (i = 0; i < navigationLinks.length; i++) {
+ Dom.removeClass(navigationLinks[i], 'navigation-link-selected');
+ }
+ Dom.addClass(Splinter.navigationLinks[identifier], 'navigation-link-selected');
+};
+
+Splinter.addNavigationLink = function (identifier, title, callback, selected) {
+ var navigationDiv = Dom.get('navigation');
+ if (Dom.getChildren(navigationDiv).length > 0) {
+ navigationDiv.appendChild(document.createTextNode(' | '));
+ }
+
+ var navigationLink = new Element(document.createElement('a'));
+ Dom.addClass(navigationLink, 'navigation-link');
+ Dom.setAttribute(navigationLink, 'href', 'javascript:void(0);');
+ Dom.setAttribute(navigationLink, 'id', 'switch-' + encodeURIComponent(identifier));
+ Dom.setAttribute(navigationLink, 'title', identifier);
+ navigationLink.appendChild(document.createTextNode(title));
+ navigationLink.appendTo(navigationDiv);
+
+ // FIXME: Find out why I need to use an id here instead of just passing
+ // navigationLink to Event.addListener()
+ Event.addListener('switch-' + encodeURIComponent(identifier), 'click', function () {
+ if (!Dom.hasClass(this, 'navigation-link-selected')) {
+ callback();
+ }
+ });
+
+ if (selected) {
+ Dom.addClass(navigationLink, 'navigation-link-selected');
+ }
+
+ Splinter.navigationLinks[identifier] = navigationLink;
+};
+
+Splinter.showOverview = function () {
+ Splinter.selectNavigationLink('__OVERVIEW__');
+ Dom.setStyle('overview', 'display', 'block');
+ Dom.getElementsByClassName('file', 'div', '', function (node) {
+ Dom.setStyle(node, 'display', 'none');
+ });
+ Splinter.updateMyPatchComments();
+};
+
+Splinter.showAllFiles = function () {
+ Splinter.selectNavigationLink('__ALL__');
+ Dom.setStyle('overview', 'display', 'none');
+ Dom.setStyle('file-collapse-all', 'display', 'block');
+
+ var i;
+ for (i = 0; i < Splinter.thePatch.files.length; i++) {
+ var file = Splinter.thePatch.files[i];
+ if (!file.div) {
+ Splinter.addPatchFile(file);
+ } else {
+ Dom.setStyle(file.div, 'display', 'block');
+ }
+ }
+}
+
+Splinter.toggleCollapsed = function (filename, display) {
+ filename = decodeURIComponent(filename);
+ var i;
+ for (i = 0; i < Splinter.thePatch.files.length; i++) {
+ var file = Splinter.thePatch.files[i];
+ if ((filename && file.filename == filename) || !filename) {
+ var fileTableContainer = file.div.getElementsByClassName('file-table-container')[0];
+ var fileCollapseLink = file.div.getElementsByClassName('file-label-collapse')[0];
+ if (!display) {
+ display = Dom.getStyle(fileTableContainer, 'display') == 'block' ? 'none' : 'block';
+ }
+ Dom.setStyle(fileTableContainer, 'display', display);
+ fileCollapseLink.innerHTML = display == 'block' ? '[-]' : '[+]';
+ }
+ }
+}
+
+Splinter.showPatchFile = function (file) {
+ Splinter.selectNavigationLink(file.filename);
+ Dom.setStyle('overview', 'display', 'none');
+ Dom.setStyle('file-collapse-all', 'display', 'none');
+
+ Dom.getElementsByClassName('file', 'div', '', function (node) {
+ Dom.setStyle(node, 'display', 'none');
+ });
+
+ if (file.div) {
+ Dom.setStyle(file.div, 'display', 'block');
+ } else {
+ Splinter.addPatchFile(file);
+ }
+};
+
+Splinter.addFileNavigationLink = function (file) {
+ var basename = file.filename.replace(/.*\//, "");
+ Splinter.addNavigationLink(file.filename, basename, function() {
+ // Splinter.addNavigationLink(file.filename, file.filename, function() {
+ Splinter.showPatchFile(file);
+ });
+};
+
+Splinter.start = function () {
+ Dom.setStyle('attachmentInfo', 'display', 'block');
+ Dom.setStyle('navigationContainer', 'display', 'block');
+ Dom.setStyle('overview', 'display', 'block');
+ Dom.setStyle('files', 'display', 'block');
+
+ //for (var i = 0; i < Splinter.configAttachmentStatuses.length; i++) {
+ // $("= 0; i--) {
+ var reviewInfo = storedReviews[i];
+ var modificationDate = Splinter.Utils.formatDate(new Date(reviewInfo.modificationTime));
+ var extra = reviewInfo.isDraft ? "(draft)" : "";
+
+ reviewData.push([
+ reviewInfo.bugId,
+ reviewInfo.bugId + ":" + reviewInfo.attachmentId + ":" + reviewInfo.attachmentDescription,
+ modificationDate,
+ extra
+ ]);
+ }
+
+ var attachLink = function (elLiner, oRecord, oColumn, oData) {
+ var splitResult = oData.split(':', 3);
+ elLiner.innerHTML = "" + splitResult[1] + " - " + splitResult[2] + "";
+ };
+
+ var bugLink = function (elLiner, oRecord, oColumn, oData) {
+ elLiner.innerHTML = "" + oData + "";
+ };
+
+ dsConfig = {
+ responseType: YAHOO.util.DataSource.TYPE_JSARRAY,
+ responseSchema: { fields:["bug_id","attachment", "date", "extra"] }
+ };
+
+ var columnDefs = [
+ { key: "bug_id", label: "Bug", formatter: bugLink },
+ { key: "attachment", label: "Attachment", formatter: attachLink },
+ { key: "date", label: "Date" },
+ { key: "extra", label: "Extra" }
+ ];
+
+ var dataSource = new YAHOO.util.LocalDataSource(reviewData, dsConfig);
+ var dataTable = new YAHOO.widget.DataTable("chooseReviewTable", columnDefs, dataSource);
+
+ Dom.setStyle('chooseReview', 'display', 'block');
+};
+
+Splinter.showChooseAttachment = function () {
+ var drafts = {};
+ var published = {};
+ if (Splinter.reviewStorage) {
+ var storedReviews = Splinter.reviewStorage.listReviews();
+ var j;
+ for (j = 0; j < storedReviews.length; j++) {
+ var reviewInfo = storedReviews[j];
+ if (reviewInfo.bugId == Splinter.theBug.id) {
+ if (reviewInfo.isDraft) {
+ drafts[reviewInfo.attachmentId] = 1;
+ } else {
+ published[reviewInfo.attachmentId] = 1;
+ }
+ }
+ }
+ }
+
+ var attachData = [];
+
+ var i;
+ for (i = 0; i < Splinter.theBug.attachments.length; i++) {
+ var attachment = Splinter.theBug.attachments[i];
+
+ if (!attachment.isPatch || attachment.isObsolete) {
+ continue;
+ }
+
+ var href = Splinter.newPageUrl(Splinter.theBug.id, attachment.id);
+
+ var date = Splinter.Utils.formatDate(attachment.date);
+ var status = (attachment.status && attachment.status != 'none') ? attachment.status : '';
+
+ var extra = '';
+ if (attachment.id in drafts) {
+ extra = '(draft)';
+ } else if (attachment.id in published) {
+ extra = '(published)';
+ }
+
+ attachData.push([ attachment.id, attachment.description, attachment.date, extra ]);
+ }
+
+ var attachLink = function (elLiner, oRecord, oColumn, oData) {
+ elLiner.innerHTML = "" + oData + "";
+ };
+
+ dsConfig = {
+ responseType: YAHOO.util.DataSource.TYPE_JSARRAY,
+ responseSchema: { fields:["id","description","date", "extra"] }
+ };
+
+ var columnDefs = [
+ { key: "id", label: "ID", formatter: attachLink },
+ { key: "description", label: "Description" },
+ { key: "date", label: "Date" },
+ { key: "extra", label: "Extra" }
+ ];
+
+ var dataSource = new YAHOO.util.LocalDataSource(attachData, dsConfig);
+ var dataTable = new YAHOO.widget.DataTable("chooseAttachmentTable", columnDefs, dataSource);
+
+ Dom.setStyle('chooseAttachment', 'display', 'block');
+};
+
+Splinter.quickHelpToggle = function () {
+ var quickHelpShow = Dom.get('quickHelpShow');
+ var quickHelpContent = Dom.get('quickHelpContent');
+ var quickHelpToggle = Dom.get('quickHelpToggle');
+
+ if (quickHelpContent.style.display == 'none') {
+ quickHelpContent.style.display = 'block';
+ quickHelpShow.style.display = 'none';
+ } else {
+ quickHelpContent.style.display = 'none';
+ quickHelpShow.style.display = 'block';
+ }
+};
+
+Splinter.init = function () {
+ Splinter.showNote();
+
+ if (Splinter.ReviewStorage.LocalReviewStorage.available()) {
+ Splinter.reviewStorage = new Splinter.ReviewStorage.LocalReviewStorage();
+ }
+
+ if (Splinter.theBug == null) {
+ Splinter.showEnterBug();
+ return;
+ }
+
+ Dom.get("bugId").innerHTML = Splinter.theBug.id;
+ Dom.get("bugLink").setAttribute('href', Splinter.configBugzillaUrl + "show_bug.cgi?id=" + Splinter.theBug.id);
+ Dom.get("bugShortDesc").innerHTML = YAHOO.lang.escapeHTML(Splinter.theBug.shortDesc);
+ Dom.get("bugReporter").appendChild(document.createTextNode(Splinter.theBug.getReporter()));
+ Dom.get("bugCreationDate").innerHTML = Splinter.Utils.formatDate(Splinter.theBug.creationDate);
+ Dom.setStyle('bugInfo', 'display', 'block');
+
+ if (Splinter.attachmentId) {
+ Splinter.theAttachment = Splinter.theBug.getAttachment(Splinter.attachmentId);
+
+ if (Splinter.theAttachment == null) {
+ Splinter.displayError("Attachment " + Splinter.attachmentId + " is not an attachment to bug " + Splinter.theBug.id);
+ }
+ else if (!Splinter.theAttachment.isPatch) {
+ Splinter.displayError("Attachment " + Splinter.attachmentId + " is not a patch");
+ Splinter.theAttachment = null;
+ }
+ }
+
+ if (Splinter.theAttachment == null) {
+ Splinter.showChooseAttachment();
+
+ } else {
+ Dom.get("attachId").innerHTML = Splinter.theAttachment.id;
+ Dom.get("attachLink").setAttribute('href', Splinter.configBugzillaUrl + "attachment.cgi?id=" + Splinter.theAttachment.id);
+ Dom.get("attachDesc").innerHTML = YAHOO.lang.escapeHTML(Splinter.theAttachment.description);
+ Dom.get("attachCreator").appendChild(document.createTextNode(Splinter.Bug._formatWho(Splinter.theAttachment.whoName,
+ Splinter.theAttachment.whoEmail)));
+ Dom.get("attachDate").innerHTML = Splinter.Utils.formatDate(Splinter.theAttachment.date);
+ Dom.setStyle('attachInfo', 'display', 'block');
+
+ Dom.setStyle('quickHelpShow', 'display', 'block');
+
+ document.title = "Patch Review of Attachment " + Splinter.theAttachment.id +
+ " for Bug " + Splinter.theBug.id;
+
+ Splinter.thePatch = new Splinter.Patch.Patch(Splinter.theAttachment.data);
+ if (Splinter.thePatch != null) {
+ Splinter.start();
+ }
+ }
+};
+
+YAHOO.util.Event.addListener(window, 'load', Splinter.init);
diff --git a/extensions/TagNewUsers/Config.pm b/extensions/TagNewUsers/Config.pm
new file mode 100644
index 000000000..cfa635c32
--- /dev/null
+++ b/extensions/TagNewUsers/Config.pm
@@ -0,0 +1,33 @@
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+#
+# 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 the TagNewUsers Extension.
+#
+# The Initial Developer of the Original Code is the Mozilla Foundation
+# Portions created by the Initial Developers are Copyright (C) 2011 the
+# Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+# Byron Jones
+
+package Bugzilla::Extension::TagNewUsers;
+use strict;
+
+use constant NAME => 'TagNewUsers';
+
+use constant REQUIRED_MODULES => [
+];
+
+use constant OPTIONAL_MODULES => [
+];
+
+__PACKAGE__->NAME;
diff --git a/extensions/TagNewUsers/Extension.pm b/extensions/TagNewUsers/Extension.pm
new file mode 100644
index 000000000..382a3c3d1
--- /dev/null
+++ b/extensions/TagNewUsers/Extension.pm
@@ -0,0 +1,226 @@
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+#
+# 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 the TagNewUsers Extension.
+#
+# The Initial Developer of the Original Code is the Mozilla Foundation
+# Portions created by the Initial Developers are Copyright (C) 2011 the
+# Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+# Byron Jones
+
+package Bugzilla::Extension::TagNewUsers;
+use strict;
+use base qw(Bugzilla::Extension);
+use Bugzilla::Field;
+use Bugzilla::User;
+use Bugzilla::Install::Util qw(indicate_progress);
+use Date::Parse;
+
+# users younger than PROFILE_AGE days will be tagged as new
+use constant PROFILE_AGE => 60;
+
+# users with fewer comments than COMMENT_COUNT will be tagged as new
+use constant COMMENT_COUNT => 25;
+
+our $VERSION = '1';
+
+#
+# install
+#
+
+sub install_update_db {
+ my ($self) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ if (!$dbh->bz_column_info('profiles', 'comment_count')) {
+ $dbh->bz_add_column('profiles', 'comment_count',
+ {TYPE => 'INT3', NOTNULL => 1, DEFAULT => 0});
+ my $sth = $dbh->prepare('UPDATE profiles SET comment_count=? WHERE userid=?');
+ my $ra = $dbh->selectall_arrayref('SELECT who,COUNT(*) FROM longdescs GROUP BY who');
+ my $count = 1;
+ my $total = scalar @$ra;
+ foreach my $ra_row (@$ra) {
+ indicate_progress({ current => $count++, total => $total, every => 25 });
+ my ($user_id, $count) = @$ra_row;
+ $sth->execute($count, $user_id);
+ }
+ }
+
+ if (!$dbh->bz_column_info('profiles', 'creation_ts')) {
+ $dbh->bz_add_column('profiles', 'creation_ts',
+ {TYPE => 'DATETIME'});
+ my $creation_date_fieldid = get_field_id('creation_ts');
+ my $sth = $dbh->prepare('UPDATE profiles SET creation_ts=? WHERE userid=?');
+ my $ra = $dbh->selectall_arrayref("
+ SELECT p.userid, a.profiles_when
+ FROM profiles p
+ LEFT JOIN profiles_activity a ON a.userid=p.userid
+ AND a.fieldid=$creation_date_fieldid
+ ");
+ my ($now) = Bugzilla->dbh->selectrow_array("SELECT NOW()");
+ my $count = 1;
+ my $total = scalar @$ra;
+ foreach my $ra_row (@$ra) {
+ indicate_progress({ current => $count++, total => $total, every => 25 });
+ my ($user_id, $when) = @$ra_row;
+ if (!$when) {
+ ($when) = $dbh->selectrow_array(
+ "SELECT bug_when FROM bugs_activity WHERE who=? ORDER BY bug_when " .
+ $dbh->sql_limit(1),
+ undef, $user_id
+ );
+ }
+ if (!$when) {
+ ($when) = $dbh->selectrow_array(
+ "SELECT bug_when FROM longdescs WHERE who=? ORDER BY bug_when " .
+ $dbh->sql_limit(1),
+ undef, $user_id
+ );
+ }
+ if (!$when) {
+ ($when) = $dbh->selectrow_array(
+ "SELECT creation_ts FROM bugs WHERE reporter=? ORDER BY creation_ts " .
+ $dbh->sql_limit(1),
+ undef, $user_id
+ );
+ }
+ if (!$when) {
+ $when = $now;
+ }
+
+ $sth->execute($when, $user_id);
+ }
+ }
+
+ if (!$dbh->bz_column_info('profiles', 'first_patch_bug_id')) {
+ $dbh->bz_add_column('profiles', 'first_patch_bug_id', {TYPE => 'INT3'});
+ my $sth_update = $dbh->prepare('UPDATE profiles SET first_patch_bug_id=? WHERE userid=?');
+ my $sth_select = $dbh->prepare(
+ 'SELECT bug_id FROM attachments WHERE submitter_id=? AND ispatch=1 ORDER BY creation_ts ' . $dbh->sql_limit(1)
+ );
+ my $ra = $dbh->selectcol_arrayref('SELECT DISTINCT submitter_id FROM attachments WHERE ispatch=1');
+ my $count = 1;
+ my $total = scalar @$ra;
+ foreach my $user_id (@$ra) {
+ indicate_progress({ current => $count++, total => $total, every => 25 });
+ $sth_select->execute($user_id);
+ my ($bug_id) = $sth_select->fetchrow_array;
+ $sth_update->execute($bug_id, $user_id);
+ }
+ }
+}
+
+#
+# objects
+#
+
+BEGIN {
+ *Bugzilla::User::update_comment_count = \&_update_comment_count;
+ *Bugzilla::User::first_patch_bug_id = \&_first_patch_bug_id;
+}
+
+sub object_columns {
+ my ($self, $args) = @_;
+ my ($class, $columns) = @$args{qw(class columns)};
+ if ($class->isa('Bugzilla::User')) {
+ push(@$columns, qw(comment_count creation_ts first_patch_bug_id));
+ }
+}
+
+sub object_before_create {
+ my ($self, $args) = @_;
+ my ($class, $params) = @$args{qw(class params)};
+ if ($class->isa('Bugzilla::User')) {
+ my ($timestamp) = Bugzilla->dbh->selectrow_array("SELECT NOW()");
+ $params->{comment_count} = 0;
+ $params->{creation_ts} = $timestamp;
+ } elsif ($class->isa('Bugzilla::Attachment')) {
+ if ($params->{ispatch} && !Bugzilla->user->first_patch_bug_id) {
+ Bugzilla->user->first_patch_bug_id($params->{bug}->id);
+ }
+ }
+}
+
+sub bug_end_of_create {
+ Bugzilla->user->update_comment_count();
+}
+
+sub bug_end_of_update {
+ Bugzilla->user->update_comment_count();
+}
+
+sub _update_comment_count {
+ my $self = shift;
+ my $dbh = Bugzilla->dbh;
+
+ my $id = $self->id;
+ my ($count) = $dbh->selectrow_array(
+ "SELECT COUNT(*) FROM longdescs WHERE who=?",
+ undef, $id
+ );
+ return if $self->{comment_count} == $count;
+ $dbh->do(
+ 'UPDATE profiles SET comment_count=? WHERE userid=?',
+ undef, $count, $id
+ );
+ $self->{comment_count} = $count;
+}
+
+sub _first_patch_bug_id {
+ my ($self, $bug_id) = @_;
+ return $self->{first_patch_bug_id} unless defined $bug_id;
+
+ Bugzilla->dbh->do(
+ 'UPDATE profiles SET first_patch_bug_id=? WHERE userid=?',
+ undef, $bug_id, $self->id
+ );
+ $self->{first_patch_bug_id} = $bug_id;
+}
+
+#
+#
+#
+
+sub template_before_process {
+ my ($self, $args) = @_;
+ my ($vars, $file) = @$args{qw(vars file)};
+ if ($file eq 'bug/comments.html.tmpl') {
+
+ # only users in canconfirm will see the new-to-bugzilla tag
+ return unless Bugzilla->user->in_group('canconfirm');
+
+ # calculate if each user that has commented on the bug is new
+ foreach my $comment (@{$vars->{bug}{comments}}) {
+ my $user = $comment->author;
+ $user->{is_new} = $self->_user_is_new($user);
+ }
+ }
+}
+
+sub _user_is_new {
+ my ($self, $user) = (shift, shift);
+
+ # if the user can confirm bugs, they are no longer new
+ return 0 if $user->in_group('canconfirm');
+
+ # store the age in days, for the 'new to bugzilla' tooltip
+ my $age = sprintf("%.0f", (time() - str2time($user->{creation_ts})) / 86400);
+ $user->{creation_age} = $age;
+
+ return
+ ($user->{comment_count} <= COMMENT_COUNT)
+ || ($user->{creation_age} <= PROFILE_AGE);
+}
+
+__PACKAGE__->NAME;
diff --git a/extensions/TagNewUsers/template/en/default/hook/bug/comments-comment_banner.html.tmpl b/extensions/TagNewUsers/template/en/default/hook/bug/comments-comment_banner.html.tmpl
new file mode 100644
index 000000000..6201c587a
--- /dev/null
+++ b/extensions/TagNewUsers/template/en/default/hook/bug/comments-comment_banner.html.tmpl
@@ -0,0 +1,25 @@
+[%#
+ # 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 the TagNewUsers Extension
+ #
+ # The Initial Developer of the Original Code is the Mozilla Foundation
+ # Portions created by the Initial Developers are Copyright (C) 2011 the
+ # Initial Developer. All Rights Reserved.
+ #
+ # Contributor(s):
+ # Byron Jones
+ #%]
+
+
+
diff --git a/extensions/TagNewUsers/template/en/default/hook/bug/comments-user.html.tmpl b/extensions/TagNewUsers/template/en/default/hook/bug/comments-user.html.tmpl
new file mode 100644
index 000000000..316d381bb
--- /dev/null
+++ b/extensions/TagNewUsers/template/en/default/hook/bug/comments-user.html.tmpl
@@ -0,0 +1,37 @@
+[%#
+ # 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 the TagNewUsers Extension
+ #
+ # The Initial Developer of the Original Code is the Mozilla Foundation
+ # Portions created by the Initial Developers are Copyright (C) 2011 the
+ # Initial Developer. All Rights Reserved.
+ #
+ # Contributor(s):
+ # Byron Jones
+ #%]
+
+[% IF comment.author.is_new %]
+ 365 %]more than a year ago[%
+ELSE %][% comment.author.creation_age FILTER html %] day[% "s" IF comment.author.creation_age != 1 %] ago[% END %]."
+ >
+(New to [% terms.Bugzilla %])
+
+[% END %]
+[% IF comment.is_about_attachment && comment.author.first_patch_bug_id == bug.id %]
+(First Patch)
+[% END %]
diff --git a/extensions/TagNewUsers/web/style.css b/extensions/TagNewUsers/web/style.css
new file mode 100644
index 000000000..2e863eb13
--- /dev/null
+++ b/extensions/TagNewUsers/web/style.css
@@ -0,0 +1,16 @@
+/* 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.
+ *
+ */
+
+.new_user {
+ color: #448844;
+}
+
diff --git a/extensions/TypeSniffer/Config.pm b/extensions/TypeSniffer/Config.pm
new file mode 100644
index 000000000..6ad03b362
--- /dev/null
+++ b/extensions/TypeSniffer/Config.pm
@@ -0,0 +1,40 @@
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+#
+# 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 the TypeSniffer Bugzilla Extension.
+#
+# The Initial Developer of the Original Code is The Mozilla Foundation.
+# Portions created by the Initial Developer are Copyright (C) 2010 the
+# Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+# Gervase Markham
+
+package Bugzilla::Extension::TypeSniffer;
+use strict;
+
+use constant NAME => 'TypeSniffer';
+
+use constant REQUIRED_MODULES => [
+ {
+ package => 'File-MimeInfo',
+ module => 'File::MimeInfo::Magic',
+ version => '0'
+ },
+ {
+ package => 'IO-stringy',
+ module => 'IO::Scalar',
+ version => '0'
+ },
+];
+
+__PACKAGE__->NAME;
\ No newline at end of file
diff --git a/extensions/TypeSniffer/Extension.pm b/extensions/TypeSniffer/Extension.pm
new file mode 100644
index 000000000..ead4f40cf
--- /dev/null
+++ b/extensions/TypeSniffer/Extension.pm
@@ -0,0 +1,74 @@
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+#
+# 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 the TypeSniffer Bugzilla Extension.
+#
+# The Initial Developer of the Original Code is The Mozilla Foundation.
+# Portions created by the Initial Developer are Copyright (C) 2010 the
+# Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+# Gervase Markham
+
+package Bugzilla::Extension::TypeSniffer;
+use strict;
+use base qw(Bugzilla::Extension);
+
+use File::MimeInfo::Magic;
+use IO::Scalar;
+
+our $VERSION = '0.02';
+################################################################################
+# This extension uses magic to guess MIME types for data where the browser has
+# told us it's application/octet-stream (probably because there's no file
+# extension, or it's a text type with a non-.txt file extension).
+################################################################################
+sub attachment_process_data {
+ my ($self, $args) = @_;
+ my $attributes = $args->{'attributes'};
+ my $params = Bugzilla->input_params;
+
+ # If we have autodetected application/octet-stream from the Content-Type
+ # header, let's have a better go using a sniffer.
+ if ($params->{'contenttypemethod'} eq 'autodetect' &&
+ $attributes->{'mimetype'} eq 'application/octet-stream')
+ {
+ # data attribute can be either scalar data or filehandle
+ # bugzilla.org/docs/3.6/en/html/api/Bugzilla/Attachment.html#create
+ my $fh = $attributes->{'data'};
+ if (!ref($fh)) {
+ my $data = $attributes->{'data'};
+ $fh = new IO::Scalar \$data;
+ }
+ else {
+ # CGI.pm sends us an Fh that isn't actually an IO::Handle, but
+ # has a method for getting an actual handle out of it.
+ if (!$fh->isa('IO::Handle')) {
+ $fh = $fh->handle;
+ # ->handle returns an literal IO::Handle, even though the
+ # underlying object is a file. So we rebless it to be a proper
+ # IO::File object so that we can call ->seek on it and so on.
+ # Just in case CGI.pm fixes this some day, we check ->isa first.
+ if (!$fh->isa('IO::File')) {
+ bless $fh, 'IO::File';
+ }
+ }
+ }
+
+ my $mimetype = mimetype($fh);
+ if ($mimetype) {
+ $attributes->{'mimetype'} = $mimetype;
+ }
+ }
+}
+
+__PACKAGE__->NAME;
diff --git a/t/001compile.t b/t/001compile.t
index 9e63da0b4..9c5b50e2d 100644
--- a/t/001compile.t
+++ b/t/001compile.t
@@ -45,6 +45,11 @@ sub compile_file {
# Bugzilla::Install::CPAN.)
local @INC = @INC;
+ if ($file =~ /extensions/) {
+ skip "$file: extensions not tested", 1;
+ return;
+ }
+
if ($file =~ s/\.pm$//) {
$file =~ s{/}{::}g;
use_ok($file);
diff --git a/t/008filter.t b/t/008filter.t
index e73d23835..f0e7a6429 100644
--- a/t/008filter.t
+++ b/t/008filter.t
@@ -224,7 +224,7 @@ sub directive_ok {
return 1 if $directive =~ /FILTER\ (html|csv|js|base64|css_class_quote|ics|
quoteUrls|time|uri|xml|lower|html_light|
obsolete|inactive|closed|unitconvert|
- txt|html_linebreak|none)\b/x;
+ txt|html_linebreak|none|json)\b/x;
return 0;
}
diff --git a/t/012throwables.t b/t/012throwables.t
index 3738ad524..590fb8aa5 100644
--- a/t/012throwables.t
+++ b/t/012throwables.t
@@ -62,7 +62,7 @@ foreach my $include_path (@include_paths) {
$file =~ s/\s.*$//; # nuke everything after the first space
$file =~ s|\\|/|g if ON_WINDOWS; # convert \ to / in path if on windows
$test_templates{$file} = ()
- if $file =~ m#global/(code|user)-error\.html\.tmpl#;
+ if $file =~ m#global/(code|user)-error(?:-errors)?\.html\.tmpl#;
}
}
@@ -75,7 +75,7 @@ plan tests => $tests;
# Collect all errors defined in templates
foreach my $file (keys %test_templates) {
- $file =~ m|template/([^/]+).*/global/([^/]+)-error\.html\.tmpl|;
+ $file =~ m|template/([^/]+).*/global/([^/]+)-error(?:-errors)?\.html\.tmpl|;
my $lang = $1;
my $errtype = $2;
diff --git a/t/Support/Files.pm b/t/Support/Files.pm
index 6c6e0ee57..31a0058db 100644
--- a/t/Support/Files.pm
+++ b/t/Support/Files.pm
@@ -23,12 +23,15 @@
package Support::Files;
+use Bugzilla;
+
use File::Find;
@additional_files = ();
@files = glob('*');
-find(sub { push(@files, $File::Find::name) if $_ =~ /\.pm$/;}, 'Bugzilla');
+my @extension_paths = map { $_->package_dir } @{ Bugzilla->extensions };
+find(sub { push(@files, $File::Find::name) if $_ =~ /\.pm$/;}, 'Bugzilla', @extension_paths);
push(@files, 'extensions/create.pl');
sub isTestingFile {
--
cgit v1.2.3-24-g4f1b