summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.bzrignore1
-rw-r--r--.htaccess24
-rw-r--r--Bugzilla.pm34
-rw-r--r--Bugzilla/Arecibo.pm335
-rw-r--r--Bugzilla/Attachment.pm50
-rw-r--r--Bugzilla/Attachment/PatchReader.pm40
-rw-r--r--Bugzilla/Auth.pm9
-rw-r--r--Bugzilla/Bug.pm91
-rw-r--r--Bugzilla/BugMail.pm133
-rw-r--r--Bugzilla/BugUrl.pm1
-rw-r--r--Bugzilla/BugUrl/GitHub.pm36
-rw-r--r--Bugzilla/CGI.pm13
-rw-r--r--Bugzilla/Component.pm6
-rw-r--r--Bugzilla/Config/Advanced.pm12
-rw-r--r--Bugzilla/Config/Auth.pm6
-rw-r--r--Bugzilla/Constants.pm32
-rw-r--r--Bugzilla/DB.pm2
-rw-r--r--Bugzilla/DB/Schema.pm14
-rw-r--r--Bugzilla/Error.pm93
-rw-r--r--Bugzilla/Field.pm174
-rw-r--r--Bugzilla/Flag.pm71
-rw-r--r--Bugzilla/FlagType.pm7
-rw-r--r--Bugzilla/Group.pm5
-rw-r--r--Bugzilla/Hook.pm16
-rw-r--r--Bugzilla/Install.pm4
-rw-r--r--Bugzilla/Install/DB.pm77
-rw-r--r--Bugzilla/Install/Filesystem.pm14
-rw-r--r--Bugzilla/Mailer.pm57
-rw-r--r--Bugzilla/Object.pm12
-rw-r--r--Bugzilla/PatchReader.pm117
-rw-r--r--Bugzilla/PatchReader/AddCVSContext.pm226
-rw-r--r--Bugzilla/PatchReader/Base.pm23
-rw-r--r--Bugzilla/PatchReader/CVSClient.pm48
-rw-r--r--Bugzilla/PatchReader/DiffPrinter/raw.pm61
-rw-r--r--Bugzilla/PatchReader/DiffPrinter/template.pm119
-rw-r--r--Bugzilla/PatchReader/FilterPatch.pm43
-rw-r--r--Bugzilla/PatchReader/FixPatchRoot.pm130
-rw-r--r--Bugzilla/PatchReader/NarrowPatch.pm44
-rw-r--r--Bugzilla/PatchReader/PatchInfoGrabber.pm45
-rw-r--r--Bugzilla/PatchReader/Raw.pm268
-rw-r--r--Bugzilla/Product.pm9
-rw-r--r--Bugzilla/Search.pm3
-rw-r--r--Bugzilla/Search/Quicksearch.pm10
-rw-r--r--Bugzilla/Search/Recent.pm13
-rw-r--r--Bugzilla/Send/Sendmail.pm95
-rw-r--r--Bugzilla/Template.pm44
-rw-r--r--Bugzilla/Token.pm2
-rw-r--r--Bugzilla/User.pm22
-rw-r--r--Bugzilla/UserAgent.pm249
-rw-r--r--Bugzilla/Util.pm21
-rw-r--r--Bugzilla/WebService.pm5
-rw-r--r--Bugzilla/WebService/Bug.pm254
-rw-r--r--Bugzilla/WebService/Product.pm13
-rw-r--r--Bugzilla/WebService/Server/JSONRPC.pm5
-rw-r--r--Bugzilla/WebService/Server/XMLRPC.pm7
-rw-r--r--Bugzilla/WebService/User.pm117
-rw-r--r--Bugzilla/WebService/Util.pm24
-rwxr-xr-xattachment.cgi12
-rwxr-xr-xbuglist.cgi3
-rw-r--r--bzr-update.sh9
-rwxr-xr-xchart.cgi3
-rwxr-xr-xconfig.cgi2
-rwxr-xr-xcontrib/addcustomfield.pl62
-rwxr-xr-xcontrib/fix_comment_text.pl75
-rwxr-xr-xcontrib/moco-ldap-check.pl542
-rwxr-xr-xcontrib/recode.pl2
-rw-r--r--contrib/reorg-tools/README9
-rwxr-xr-xcontrib/reorg-tools/bmo-plan.txt82
-rw-r--r--contrib/reorg-tools/fix_all_open_status_queries.pl140
-rwxr-xr-xcontrib/reorg-tools/fixgroupqueries.pl119
-rwxr-xr-xcontrib/reorg-tools/fixqueries.pl132
-rwxr-xr-xcontrib/reorg-tools/migrate_crash_signatures.pl126
-rw-r--r--contrib/reorg-tools/migrate_orange_bugs.pl151
-rwxr-xr-xcontrib/reorg-tools/move_flag_types.pl168
-rwxr-xr-xcontrib/reorg-tools/movebugs.pl151
-rwxr-xr-xcontrib/reorg-tools/movecomponent.pl193
-rw-r--r--contrib/reorg-tools/reset_default_user.pl143
-rwxr-xr-xcontrib/reorg-tools/syncflags.pl86
-rwxr-xr-xcontrib/reorg-tools/syncmsandversions.pl107
-rwxr-xr-xcontrib/sanitizeme.pl176
-rwxr-xr-xcontrib/verify-user.pl129
-rwxr-xr-xdescribecomponents.cgi7
-rwxr-xr-xdescribekeywords.cgi12
-rwxr-xr-xeditusers.cgi16
-rwxr-xr-xenter_bug.cgi252
-rw-r--r--extensions/BMO/Config.pm38
-rw-r--r--extensions/BMO/Extension.pm1014
-rw-r--r--extensions/BMO/lib/Constants.pm33
-rw-r--r--extensions/BMO/lib/Data.pm410
-rw-r--r--extensions/BMO/lib/FakeBug.pm42
-rw-r--r--extensions/BMO/lib/Reports.pm1078
-rw-r--r--extensions/BMO/lib/WebService.pm274
-rw-r--r--extensions/BMO/template/en/default/account/create.html.tmpl184
-rw-r--r--extensions/BMO/template/en/default/bug/create/comment-brownbag.txt.tmpl34
-rw-r--r--extensions/BMO/template/en/default/bug/create/comment-employee-incident.txt.tmpl57
-rw-r--r--extensions/BMO/template/en/default/bug/create/comment-finance.txt.tmpl35
-rw-r--r--extensions/BMO/template/en/default/bug/create/comment-legal.txt.tmpl39
-rw-r--r--extensions/BMO/template/en/default/bug/create/comment-mktgevent.txt.tmpl48
-rw-r--r--extensions/BMO/template/en/default/bug/create/comment-mozlist.txt.tmpl44
-rw-r--r--extensions/BMO/template/en/default/bug/create/comment-privacy-data.txt.tmpl30
-rw-r--r--extensions/BMO/template/en/default/bug/create/comment-recoverykey.txt.tmpl28
-rw-r--r--extensions/BMO/template/en/default/bug/create/comment-swag.txt.tmpl48
-rw-r--r--extensions/BMO/template/en/default/bug/create/create-brownbag.html.tmpl331
-rw-r--r--extensions/BMO/template/en/default/bug/create/create-employee-incident.html.tmpl288
-rw-r--r--extensions/BMO/template/en/default/bug/create/create-finance.html.tmpl257
-rw-r--r--extensions/BMO/template/en/default/bug/create/create-itrequest.html.tmpl230
-rw-r--r--extensions/BMO/template/en/default/bug/create/create-legal.html.tmpl226
-rw-r--r--extensions/BMO/template/en/default/bug/create/create-mktgevent.html.tmpl251
-rw-r--r--extensions/BMO/template/en/default/bug/create/create-mozlist.html.tmpl321
-rw-r--r--extensions/BMO/template/en/default/bug/create/create-mozpr.html.tmpl654
-rw-r--r--extensions/BMO/template/en/default/bug/create/create-poweredby.html.tmpl87
-rw-r--r--extensions/BMO/template/en/default/bug/create/create-presentation.html.tmpl219
-rw-r--r--extensions/BMO/template/en/default/bug/create/create-privacy-data.html.tmpl219
-rw-r--r--extensions/BMO/template/en/default/bug/create/create-recoverykey.html.tmpl70
-rw-r--r--extensions/BMO/template/en/default/bug/create/create-swag.html.tmpl222
-rw-r--r--extensions/BMO/template/en/default/bug/create/create-trademark.html.tmpl87
-rw-r--r--extensions/BMO/template/en/default/bug/create/create-winqual.html.tmpl800
-rw-r--r--extensions/BMO/template/en/default/bug/create/created-mozreps.html.tmpl38
-rw-r--r--extensions/BMO/template/en/default/bug/create/user-message.html.tmpl37
-rw-r--r--extensions/BMO/template/en/default/email/bugmail-header.txt.tmpl39
-rw-r--r--extensions/BMO/template/en/default/email/bugmail.html.tmpl218
-rw-r--r--extensions/BMO/template/en/default/email/bugmail.txt.tmpl90
-rw-r--r--extensions/BMO/template/en/default/global/choose-product.html.tmpl210
-rw-r--r--extensions/BMO/template/en/default/global/prod-comp-search.html.tmpl43
-rw-r--r--extensions/BMO/template/en/default/hook/attachment/createformcontents-mimetypes.html.tmpl2
-rw-r--r--extensions/BMO/template/en/default/hook/attachment/createformcontents-patch_notes.html.tmpl1
-rw-r--r--extensions/BMO/template/en/default/hook/bug/comments-a_comment-end.html.tmpl19
-rw-r--r--extensions/BMO/template/en/default/hook/bug/comments-aftercomments.html.tmpl42
-rw-r--r--extensions/BMO/template/en/default/hook/bug/comments-comment_banner.html.tmpl13
-rw-r--r--extensions/BMO/template/en/default/hook/bug/comments-end.html.tmpl20
-rw-r--r--extensions/BMO/template/en/default/hook/bug/create/create-form.html.tmpl32
-rw-r--r--extensions/BMO/template/en/default/hook/bug/create/create-guided-form.html.tmpl22
-rw-r--r--extensions/BMO/template/en/default/hook/bug/edit-after_custom_fields.html.tmpl114
-rw-r--r--extensions/BMO/template/en/default/hook/bug/field-help-end.none.tmpl114
-rw-r--r--extensions/BMO/template/en/default/hook/bug/process/header-title.html.tmpl9
-rw-r--r--extensions/BMO/template/en/default/hook/bug/show-header-end.html.tmpl14
-rw-r--r--extensions/BMO/template/en/default/hook/global/field-descs-end.none.tmpl11
-rw-r--r--extensions/BMO/template/en/default/hook/global/footer-outro.html.tmpl1
-rw-r--r--extensions/BMO/template/en/default/hook/global/header-additional_header.html.tmpl73
-rw-r--r--extensions/BMO/template/en/default/hook/global/header-start.html.tmpl40
-rw-r--r--extensions/BMO/template/en/default/hook/global/messages-messages.html.tmpl5
-rw-r--r--extensions/BMO/template/en/default/hook/global/setting-descs-settings.none.tmpl5
-rw-r--r--extensions/BMO/template/en/default/hook/global/user-error-auth_failure_object.html.tmpl5
-rw-r--r--extensions/BMO/template/en/default/hook/global/user-error-error_message.html.tmpl15
-rw-r--r--extensions/BMO/template/en/default/hook/global/user-error-errors.html.tmpl36
-rw-r--r--extensions/BMO/template/en/default/hook/global/user-error.html.tmpl/auth_failure/permissions.html.tmpl29
-rw-r--r--extensions/BMO/template/en/default/hook/global/variables-end.none.tmpl3
-rw-r--r--extensions/BMO/template/en/default/hook/index-additional_links.html.tmpl14
-rw-r--r--extensions/BMO/template/en/default/hook/index-intro.html.tmpl2
-rw-r--r--extensions/BMO/template/en/default/hook/pages/fields-open-status.html.tmpl11
-rw-r--r--extensions/BMO/template/en/default/hook/pages/fields-resolution.html.tmpl13
-rw-r--r--extensions/BMO/template/en/default/hook/reports/menu-end.html.tmpl49
-rw-r--r--extensions/BMO/template/en/default/list/list.microsummary.tmpl29
-rw-r--r--extensions/BMO/template/en/default/list/server-push.html.tmpl52
-rw-r--r--extensions/BMO/template/en/default/pages/bug-writing.html.tmpl25
-rw-r--r--extensions/BMO/template/en/default/pages/email_queue.html.tmpl66
-rw-r--r--extensions/BMO/template/en/default/pages/etiquette.html.tmpl146
-rw-r--r--extensions/BMO/template/en/default/pages/get_help.html.tmpl42
-rw-r--r--extensions/BMO/template/en/default/pages/get_permissions.html.tmpl44
-rw-r--r--extensions/BMO/template/en/default/pages/group_admins.html.tmpl54
-rw-r--r--extensions/BMO/template/en/default/pages/group_membership.html.tmpl75
-rw-r--r--extensions/BMO/template/en/default/pages/group_membership.txt.tmpl16
-rw-r--r--extensions/BMO/template/en/default/pages/release_tracking_report.html.tmpl103
-rw-r--r--extensions/BMO/template/en/default/pages/researchers.html.tmpl40
-rw-r--r--extensions/BMO/template/en/default/pages/triage_reports.html.tmpl199
-rw-r--r--extensions/BMO/template/en/default/pages/upgrade-3.6.html.tmpl304
-rw-r--r--extensions/BMO/template/en/default/pages/user_activity.html.tmpl226
-rw-r--r--extensions/BMO/template/en/default/search/search-plugin.xml.tmpl24
-rw-r--r--extensions/BMO/web/images/background.pngbin0 -> 1695 bytes
-rw-r--r--extensions/BMO/web/images/bugzilla.pngbin0 -> 1242 bytes
-rw-r--r--extensions/BMO/web/images/favicon.icobin0 -> 1150 bytes
-rw-r--r--extensions/BMO/web/images/groups/bugzilla-approvers.pngbin0 -> 829 bytes
-rw-r--r--extensions/BMO/web/images/groups/calendar-drivers.pngbin0 -> 744 bytes
-rw-r--r--extensions/BMO/web/images/guided.pngbin0 -> 1045 bytes
-rw-r--r--extensions/BMO/web/images/mozchomp.gifbin0 -> 89485 bytes
-rw-r--r--extensions/BMO/web/images/presshat.pngbin0 -> 23450 bytes
-rw-r--r--extensions/BMO/web/images/stop-sign.gifbin0 -> 3227 bytes
-rw-r--r--extensions/BMO/web/images/throbber.gifbin0 -> 723 bytes
-rw-r--r--extensions/BMO/web/js/edit_bug.js91
-rw-r--r--extensions/BMO/web/js/edituser_menu.js33
-rw-r--r--extensions/BMO/web/js/form_validate.js21
-rw-r--r--extensions/BMO/web/js/prod_comp_search.js85
-rw-r--r--extensions/BMO/web/js/release_tracking_report.js203
-rw-r--r--extensions/BMO/web/js/sorttable.js709
-rw-r--r--extensions/BMO/web/js/swag.js60
-rw-r--r--extensions/BMO/web/js/triage_reports.js83
-rw-r--r--extensions/BMO/web/js/webtrends.js213
-rw-r--r--extensions/BMO/web/producticons/camino.pngbin0 -> 6060 bytes
-rw-r--r--extensions/BMO/web/producticons/dino.pngbin0 -> 3375 bytes
-rw-r--r--extensions/BMO/web/producticons/fennec.pngbin0 -> 9023 bytes
-rw-r--r--extensions/BMO/web/producticons/firefox.pngbin0 -> 9395 bytes
-rw-r--r--extensions/BMO/web/producticons/idea.pngbin0 -> 6189 bytes
-rw-r--r--extensions/BMO/web/producticons/input.pngbin0 -> 8333 bytes
-rw-r--r--extensions/BMO/web/producticons/labs.pngbin0 -> 4085 bytes
-rw-r--r--extensions/BMO/web/producticons/mozilla.pngbin0 -> 10808 bytes
-rw-r--r--extensions/BMO/web/producticons/other.pngbin0 -> 6654 bytes
-rw-r--r--extensions/BMO/web/producticons/seamonkey.pngbin0 -> 5255 bytes
-rw-r--r--extensions/BMO/web/producticons/sunbird.pngbin0 -> 10462 bytes
-rw-r--r--extensions/BMO/web/producticons/thunderbird.pngbin0 -> 9939 bytes
-rw-r--r--extensions/BMO/web/styles/choose_product.css16
-rw-r--r--extensions/BMO/web/styles/create_account.css62
-rw-r--r--extensions/BMO/web/styles/edit_bug.css38
-rw-r--r--extensions/BMO/web/styles/prod_comp_search.css22
-rw-r--r--extensions/BMO/web/styles/reports.css58
-rw-r--r--extensions/BMO/web/styles/triage_reports.css23
-rw-r--r--extensions/BrowserID/Config.pm43
-rw-r--r--extensions/BrowserID/Extension.pm49
-rw-r--r--extensions/BrowserID/TODO19
-rw-r--r--extensions/BrowserID/lib/Login.pm126
-rw-r--r--extensions/BrowserID/template/en/default/hook/account/auth/login-additional_methods.html.tmpl57
-rw-r--r--extensions/BrowserID/template/en/default/hook/account/auth/login-small-additional_methods.html.tmpl46
-rw-r--r--extensions/BrowserID/template/en/default/hook/account/create-additional_methods.html.tmpl31
-rw-r--r--extensions/BrowserID/template/en/default/hook/global/user-error-errors.html.tmpl10
-rw-r--r--extensions/BrowserID/web/sign_in_orange.pngbin0 -> 2100 bytes
-rw-r--r--extensions/BzAPI/Config.pm63
-rw-r--r--extensions/BzAPI/Extension.pm71
-rw-r--r--extensions/BzAPI/template/en/default/config.json.tmpl315
-rw-r--r--extensions/ComponentWatching/Config.pm12
-rw-r--r--extensions/ComponentWatching/Extension.pm499
-rw-r--r--extensions/ComponentWatching/template/en/default/account/prefs/component_watch.html.tmpl232
-rw-r--r--extensions/ComponentWatching/template/en/default/hook/account/prefs/email-relationships.html.tmpl10
-rw-r--r--extensions/ComponentWatching/template/en/default/hook/account/prefs/prefs-tabs.html.tmpl14
-rw-r--r--extensions/ComponentWatching/template/en/default/hook/admin/components/edit-common-rows.html.tmpl20
-rw-r--r--extensions/ComponentWatching/template/en/default/hook/admin/components/list-before_table.html.tmpl17
-rw-r--r--extensions/ComponentWatching/template/en/default/hook/global/messages-component_updated_fields.html.tmpl15
-rw-r--r--extensions/ComponentWatching/template/en/default/hook/global/reason-descs-end.none.tmpl10
-rw-r--r--extensions/ComponentWatching/template/en/default/hook/global/user-error-errors.html.tmpl17
-rw-r--r--extensions/ContributorEngagement/Config.pm19
-rw-r--r--extensions/ContributorEngagement/Extension.pm134
-rw-r--r--extensions/ContributorEngagement/lib/Constants.pm36
-rw-r--r--extensions/ContributorEngagement/template/en/default/contributor/email.txt.tmpl46
-rw-r--r--extensions/Example/Extension.pm110
-rw-r--r--extensions/FlagDefaultRequestee/Config.pm17
-rw-r--r--extensions/FlagDefaultRequestee/Extension.pm144
-rw-r--r--extensions/FlagDefaultRequestee/lib/Constants.pm25
-rw-r--r--extensions/FlagDefaultRequestee/template/en/default/flag/default_requestees.html.tmpl105
-rw-r--r--extensions/FlagDefaultRequestee/template/en/default/hook/admin/flag-type/edit-rows.html.tmpl21
-rw-r--r--extensions/FlagDefaultRequestee/template/en/default/hook/attachment/create-end.html.tmpl9
-rw-r--r--extensions/FlagDefaultRequestee/template/en/default/hook/attachment/edit-end.html.tmpl9
-rw-r--r--extensions/FlagDefaultRequestee/template/en/default/hook/bug/create/create-form.html.tmpl9
-rw-r--r--extensions/FlagDefaultRequestee/template/en/default/hook/bug/edit-after_custom_fields.html.tmpl9
-rw-r--r--extensions/FlagTypeComment/Config.pm29
-rw-r--r--extensions/FlagTypeComment/Extension.pm199
-rw-r--r--extensions/FlagTypeComment/lib/Constants.pm50
-rw-r--r--extensions/FlagTypeComment/template/en/default/flag/type_comment.html.tmpl54
-rw-r--r--extensions/FlagTypeComment/template/en/default/hook/admin/flag-type/edit-rows.html.tmpl45
-rw-r--r--extensions/FlagTypeComment/template/en/default/hook/attachment/create-end.html.tmpl23
-rw-r--r--extensions/FlagTypeComment/template/en/default/hook/attachment/edit-end.html.tmpl23
-rw-r--r--extensions/FlagTypeComment/template/en/default/hook/bug/edit-after_custom_fields.html.tmpl23
-rw-r--r--extensions/GuidedBugEntry/Config.pm19
-rw-r--r--extensions/GuidedBugEntry/Extension.pm116
-rw-r--r--extensions/GuidedBugEntry/template/en/default/bug/create/comment-guided.txt.tmpl25
-rw-r--r--extensions/GuidedBugEntry/template/en/default/guided/guided.html.tmpl545
-rw-r--r--extensions/GuidedBugEntry/template/en/default/guided/products.html.tmpl44
-rw-r--r--extensions/GuidedBugEntry/template/en/default/pages/guided_products.js.tmpl18
-rw-r--r--extensions/GuidedBugEntry/web/images/advanced.pngbin0 -> 720 bytes
-rw-r--r--extensions/GuidedBugEntry/web/images/help.pngbin0 -> 786 bytes
-rw-r--r--extensions/GuidedBugEntry/web/images/idea.pngbin0 -> 2799 bytes
-rw-r--r--extensions/GuidedBugEntry/web/images/input.pngbin0 -> 5545 bytes
-rw-r--r--extensions/GuidedBugEntry/web/images/message.pngbin0 -> 1497 bytes
-rw-r--r--extensions/GuidedBugEntry/web/images/products/camino.pngbin0 -> 6060 bytes
-rw-r--r--extensions/GuidedBugEntry/web/images/products/core.pngbin0 -> 7497 bytes
-rw-r--r--extensions/GuidedBugEntry/web/images/products/dino.pngbin0 -> 3375 bytes
-rw-r--r--extensions/GuidedBugEntry/web/images/products/fennec.pngbin0 -> 9023 bytes
-rw-r--r--extensions/GuidedBugEntry/web/images/products/firefox.pngbin0 -> 9395 bytes
-rw-r--r--extensions/GuidedBugEntry/web/images/products/labs.pngbin0 -> 4085 bytes
-rw-r--r--extensions/GuidedBugEntry/web/images/products/mozilla.pngbin0 -> 10808 bytes
-rw-r--r--extensions/GuidedBugEntry/web/images/products/other.pngbin0 -> 6654 bytes
-rw-r--r--extensions/GuidedBugEntry/web/images/products/seamonkey.pngbin0 -> 5255 bytes
-rw-r--r--extensions/GuidedBugEntry/web/images/products/sunbird.pngbin0 -> 10462 bytes
-rw-r--r--extensions/GuidedBugEntry/web/images/products/thunderbird.pngbin0 -> 9939 bytes
-rw-r--r--extensions/GuidedBugEntry/web/images/sumo.pngbin0 -> 3517 bytes
-rw-r--r--extensions/GuidedBugEntry/web/images/support.pngbin0 -> 2409 bytes
-rw-r--r--extensions/GuidedBugEntry/web/images/throbber.gifbin0 -> 723 bytes
-rw-r--r--extensions/GuidedBugEntry/web/images/warning.pngbin0 -> 1428 bytes
-rw-r--r--extensions/GuidedBugEntry/web/js/guided.js909
-rw-r--r--extensions/GuidedBugEntry/web/js/products.js128
-rw-r--r--extensions/GuidedBugEntry/web/style/guided.css229
-rw-r--r--extensions/GuidedBugEntry/web/yui-history-iframe.txt (renamed from extensions/BmpConvert/disabled)0
-rw-r--r--extensions/InlineHistory/Config.pm13
-rw-r--r--extensions/InlineHistory/Extension.pm206
-rw-r--r--extensions/InlineHistory/README10
-rw-r--r--extensions/InlineHistory/template/en/default/hook/bug/comments-aftercomments.html.tmpl152
-rw-r--r--extensions/InlineHistory/template/en/default/hook/bug/comments-comment_banner.html.tmpl13
-rw-r--r--extensions/InlineHistory/template/en/default/hook/bug/show-header-end.html.tmpl12
-rw-r--r--extensions/InlineHistory/template/en/default/hook/global/setting-descs-settings.none.tmpl11
-rw-r--r--extensions/InlineHistory/web/inline-history.js385
-rw-r--r--extensions/InlineHistory/web/style.css35
-rw-r--r--extensions/InlineImages/Config.pm33
-rw-r--r--extensions/InlineImages/Extension.pm63
-rw-r--r--extensions/InlineImages/disabled (renamed from extensions/Voting/disabled)0
-rw-r--r--extensions/InlineImages/template/en/default/hook/bug/comments-aftercomments.html.tmpl111
-rw-r--r--extensions/LastResolved/Config.pm20
-rw-r--r--extensions/LastResolved/Extension.pm112
-rw-r--r--extensions/LastResolved/template/en/default/hook/global/field-descs-end.none.tmpl11
-rw-r--r--extensions/LimitedEmail/Config.pm41
-rw-r--r--extensions/LimitedEmail/Extension.pm60
-rw-r--r--extensions/LimitedEmail/disabled0
-rw-r--r--extensions/MozProjectReview/Config.pm19
-rw-r--r--extensions/MozProjectReview/Extension.pm253
-rw-r--r--extensions/MozProjectReview/template/en/default/bug/create/comment-moz-project-review-data-safety.txt.tmpl40
-rw-r--r--extensions/MozProjectReview/template/en/default/bug/create/comment-moz-project-review-finance.txt.tmpl26
-rw-r--r--extensions/MozProjectReview/template/en/default/bug/create/comment-moz-project-review-legal.txt.tmpl22
-rw-r--r--extensions/MozProjectReview/template/en/default/bug/create/comment-moz-project-review-privacy-policy.txt.tmpl18
-rw-r--r--extensions/MozProjectReview/template/en/default/bug/create/comment-moz-project-review-privacy-tech.txt.tmpl12
-rw-r--r--extensions/MozProjectReview/template/en/default/bug/create/comment-moz-project-review-privacy-vendor.txt.tmpl16
-rw-r--r--extensions/MozProjectReview/template/en/default/bug/create/comment-moz-project-review-sec-review.txt.tmpl20
-rw-r--r--extensions/MozProjectReview/template/en/default/bug/create/comment-moz-project-review.txt.tmpl34
-rw-r--r--extensions/MozProjectReview/template/en/default/bug/create/create-moz-project-review.html.tmpl702
-rw-r--r--extensions/MozProjectReview/template/en/default/hook/global/messages-messages.html.tmpl13
-rw-r--r--extensions/MozProjectReview/web/js/moz_project_review.js149
-rw-r--r--extensions/MozProjectReview/web/style/moz_project_review.css41
-rw-r--r--extensions/MyDashboard/Config.pm14
-rw-r--r--extensions/MyDashboard/Extension.pm363
-rw-r--r--extensions/MyDashboard/lib/TimeAgo.pm182
-rw-r--r--extensions/MyDashboard/lib/Util.pm48
-rw-r--r--extensions/MyDashboard/lib/WebService.pm98
-rw-r--r--extensions/MyDashboard/template/en/default/hook/account/prefs/saved-searches-saved-header.html.tmpl11
-rw-r--r--extensions/MyDashboard/template/en/default/hook/account/prefs/saved-searches-saved-row.html.tmpl15
-rw-r--r--extensions/MyDashboard/template/en/default/hook/global/common-links-action-links.html.tmpl12
-rw-r--r--extensions/MyDashboard/template/en/default/mydashboard/prod-comp-search.html.tmpl43
-rw-r--r--extensions/MyDashboard/template/en/default/pages/mydashboard.html.tmpl222
-rw-r--r--extensions/MyDashboard/web/js/mydashboard.js159
-rw-r--r--extensions/MyDashboard/web/js/prod_comp_search.js85
-rw-r--r--extensions/MyDashboard/web/styles/mydashboard.css59
-rw-r--r--extensions/MyDashboard/web/styles/prod_comp_search.css22
-rw-r--r--extensions/Needinfo/Config.pm18
-rw-r--r--extensions/Needinfo/Extension.pm174
-rw-r--r--extensions/Needinfo/template/en/default/bug/needinfo.html.tmpl99
-rw-r--r--extensions/Needinfo/template/en/default/hook/attachment/create-form_before_submit.html.tmpl17
-rw-r--r--extensions/Needinfo/template/en/default/hook/attachment/edit-after_comment_textarea.html.tmpl12
-rw-r--r--extensions/Needinfo/template/en/default/hook/bug/edit-after_comment_commit_button.html.tmpl11
-rw-r--r--extensions/OrangeFactor/Config.pm13
-rw-r--r--extensions/OrangeFactor/Extension.pm44
-rw-r--r--extensions/OrangeFactor/template/en/default/hook/bug/edit-after_custom_fields.html.tmpl26
-rw-r--r--extensions/OrangeFactor/template/en/default/hook/bug/show-header-end.html.tmpl17
-rw-r--r--extensions/OrangeFactor/template/en/default/hook/global/setting-descs-settings.none.tmpl11
-rw-r--r--extensions/OrangeFactor/web/js/AUTHORS.processing.js35
-rw-r--r--extensions/OrangeFactor/web/js/LICENSE.processing.js22
-rw-r--r--extensions/OrangeFactor/web/js/LICENSE.sparklines.js20
-rw-r--r--extensions/OrangeFactor/web/js/orange_factor.js91
-rw-r--r--extensions/OrangeFactor/web/js/sparklines.min.js133
-rw-r--r--extensions/OrangeFactor/web/style/orangefactor.css13
-rw-r--r--extensions/ProductDashboard/Config.pm14
-rw-r--r--extensions/ProductDashboard/Extension.pm186
-rw-r--r--extensions/ProductDashboard/lib/Queries.pm467
-rw-r--r--extensions/ProductDashboard/lib/Util.pm116
-rw-r--r--extensions/ProductDashboard/template/en/default/hook/global/common-links-action-links.html.tmpl9
-rw-r--r--extensions/ProductDashboard/template/en/default/hook/global/user-error-errors.html.tmpl12
-rw-r--r--extensions/ProductDashboard/template/en/default/pages/productdashboard.html.tmpl217
-rw-r--r--extensions/ProductDashboard/template/en/default/pages/productdashboard/components.html.tmpl266
-rw-r--r--extensions/ProductDashboard/template/en/default/pages/productdashboard/duplicates.html.tmpl75
-rw-r--r--extensions/ProductDashboard/template/en/default/pages/productdashboard/popularity.html.tmpl75
-rw-r--r--extensions/ProductDashboard/template/en/default/pages/productdashboard/recents.html.tmpl135
-rw-r--r--extensions/ProductDashboard/template/en/default/pages/productdashboard/roadmap.html.tmpl57
-rw-r--r--extensions/ProductDashboard/template/en/default/pages/productdashboard/summary.html.tmpl217
-rw-r--r--extensions/ProductDashboard/web/images/spacer.gifbin0 -> 43 bytes
-rw-r--r--extensions/ProductDashboard/web/js/productdashboard.js98
-rw-r--r--extensions/ProductDashboard/web/styles/productdashboard.css25
-rw-r--r--extensions/Profanivore/Config.pm40
-rw-r--r--extensions/Profanivore/Extension.pm169
-rw-r--r--extensions/Profanivore/README14
-rw-r--r--extensions/Push/Config.pm61
-rw-r--r--extensions/Push/Extension.pm637
-rwxr-xr-xextensions/Push/bin/bugzilla-pushd.pl54
-rw-r--r--extensions/Push/lib/Admin.pm121
-rw-r--r--extensions/Push/lib/BacklogMessage.pm145
-rw-r--r--extensions/Push/lib/BacklogQueue.pm127
-rw-r--r--extensions/Push/lib/Backoff.pm105
-rw-r--r--extensions/Push/lib/Config.pm215
-rw-r--r--extensions/Push/lib/Connector.disabled/ServiceNow.pm434
-rw-r--r--extensions/Push/lib/Connector/AMQP.pm230
-rw-r--r--extensions/Push/lib/Connector/Base.pm106
-rw-r--r--extensions/Push/lib/Connector/File.pm68
-rw-r--r--extensions/Push/lib/Connector/TCL.pm241
-rw-r--r--extensions/Push/lib/Connectors.pm115
-rw-r--r--extensions/Push/lib/Constants.pm41
-rw-r--r--extensions/Push/lib/Daemon.pm96
-rw-r--r--extensions/Push/lib/Log.pm45
-rw-r--r--extensions/Push/lib/LogEntry.pm66
-rw-r--r--extensions/Push/lib/Logger.pm70
-rw-r--r--extensions/Push/lib/Message.pm99
-rw-r--r--extensions/Push/lib/Option.pm66
-rw-r--r--extensions/Push/lib/Push.pm249
-rw-r--r--extensions/Push/lib/Queue.pm72
-rw-r--r--extensions/Push/lib/Serialise.pm318
-rw-r--r--extensions/Push/lib/Util.pm162
-rw-r--r--extensions/Push/template/en/default/hook/admin/admin-end_links_right.html.tmpl18
-rw-r--r--extensions/Push/template/en/default/hook/global/code-error-errors.html.tmpl25
-rw-r--r--extensions/Push/template/en/default/hook/global/messages-messages.html.tmpl16
-rw-r--r--extensions/Push/template/en/default/hook/global/user-error-errors.html.tmpl11
-rw-r--r--extensions/Push/template/en/default/pages/push_config.html.tmpl134
-rw-r--r--extensions/Push/template/en/default/pages/push_log.html.tmpl45
-rw-r--r--extensions/Push/template/en/default/pages/push_queues.html.tmpl102
-rw-r--r--extensions/Push/template/en/default/pages/push_queues_view.html.tmpl80
-rw-r--r--extensions/Push/template/en/default/setup/strings.txt.pl11
-rw-r--r--extensions/Push/web/admin.css71
-rw-r--r--extensions/Push/web/admin.js37
-rw-r--r--extensions/REMO/Config.pm34
-rw-r--r--extensions/REMO/Extension.pm233
-rw-r--r--extensions/REMO/template/en/default/bug/create/comment-mozreps.txt.tmpl95
-rw-r--r--extensions/REMO/template/en/default/bug/create/comment-remo-budget.txt.tmpl55
-rw-r--r--extensions/REMO/template/en/default/bug/create/comment-remo-swag.txt.tmpl71
-rw-r--r--extensions/REMO/template/en/default/bug/create/create-mozreps.html.tmpl241
-rw-r--r--extensions/REMO/template/en/default/bug/create/create-remo-budget.html.tmpl248
-rw-r--r--extensions/REMO/template/en/default/bug/create/create-remo-swag.html.tmpl306
-rw-r--r--extensions/REMO/template/en/default/bug/create/create-remo-swag.xml.tmpl116
-rw-r--r--extensions/REMO/template/en/default/bug/create/created-mozreps.html.tmpl38
-rw-r--r--extensions/REMO/template/en/default/bug/create/created-remo-budget.html.tmpl27
-rw-r--r--extensions/REMO/template/en/default/hook/global/user-error-errors.html.tmpl40
-rw-r--r--extensions/REMO/template/en/default/pages/comment-remo-form-payment.txt.tmpl37
-rw-r--r--extensions/REMO/template/en/default/pages/remo-form-payment.html.tmpl243
-rw-r--r--extensions/REMO/web/js/form_validate.js21
-rw-r--r--extensions/REMO/web/js/swag.js60
-rw-r--r--extensions/REMO/web/styles/moz_reps.css44
-rw-r--r--extensions/RequestWhiner/Config.pm33
-rw-r--r--extensions/RequestWhiner/Extension.pm43
-rwxr-xr-xextensions/RequestWhiner/bin/whineatrequests.pl155
-rw-r--r--extensions/RequestWhiner/lib/Constants.pm31
-rw-r--r--extensions/RequestWhiner/template/en/default/requestwhiner/header.txt.tmpl6
-rw-r--r--extensions/RequestWhiner/template/en/default/requestwhiner/mail.html.tmpl62
-rw-r--r--extensions/RequestWhiner/template/en/default/requestwhiner/mail.txt.tmpl41
-rw-r--r--extensions/SecureMail/Config.pm47
-rw-r--r--extensions/SecureMail/Extension.pm604
-rw-r--r--extensions/SecureMail/README8
-rw-r--r--extensions/SecureMail/template/en/default/account/email/encryption-required.txt.tmpl15
-rw-r--r--extensions/SecureMail/template/en/default/account/email/securemail-test.txt.tmpl23
-rw-r--r--extensions/SecureMail/template/en/default/account/prefs/securemail.html.tmpl40
-rw-r--r--extensions/SecureMail/template/en/default/hook/account/prefs/prefs-tabs.html.tmpl28
-rw-r--r--extensions/SecureMail/template/en/default/hook/admin/groups/create-field.html.tmpl25
-rw-r--r--extensions/SecureMail/template/en/default/hook/admin/groups/edit-field.html.tmpl27
-rw-r--r--extensions/SecureMail/template/en/default/hook/global/user-error-errors.html.tmpl27
-rw-r--r--extensions/SecureMail/template/en/default/pages/securemail/help.html.tmpl99
-rw-r--r--extensions/ShadowBugs/Config.pm15
-rw-r--r--extensions/ShadowBugs/Extension.pm99
-rw-r--r--extensions/ShadowBugs/template/en/default/hook/bug/comments-aftercomments.html.tmpl70
-rw-r--r--extensions/ShadowBugs/template/en/default/hook/bug/edit-after_comment_textarea.html.tmpl13
-rw-r--r--extensions/ShadowBugs/template/en/default/hook/bug/edit-after_custom_fields.html.tmpl27
-rw-r--r--extensions/ShadowBugs/template/en/default/hook/bug/edit-custom_field.html.tmpl9
-rw-r--r--extensions/ShadowBugs/template/en/default/hook/bug/show-header-end.html.tmpl12
-rw-r--r--extensions/ShadowBugs/template/en/default/hook/global/user-error-errors.html.tmpl14
-rw-r--r--extensions/ShadowBugs/web/shadow-bugs.js51
-rw-r--r--extensions/ShadowBugs/web/style.css10
-rw-r--r--extensions/SiteMapIndex/Config.pm36
-rw-r--r--extensions/SiteMapIndex/Extension.pm157
-rw-r--r--extensions/SiteMapIndex/lib/Constants.pm47
-rw-r--r--extensions/SiteMapIndex/lib/Util.pm205
-rw-r--r--extensions/SiteMapIndex/robots.txt9
-rw-r--r--extensions/SiteMapIndex/template/en/default/hook/global/header-additional_header.html.tmpl23
-rw-r--r--extensions/SiteMapIndex/template/en/default/hook/global/messages-messages.html.tmpl37
-rw-r--r--extensions/Splinter/Config.pm5
-rw-r--r--extensions/Splinter/Extension.pm136
-rw-r--r--extensions/Splinter/lib/Config.pm46
-rw-r--r--extensions/Splinter/lib/Util.pm161
-rw-r--r--extensions/Splinter/template/en/default/admin/params/splinter.html.tmpl38
-rw-r--r--extensions/Splinter/template/en/default/hook/attachment/edit-action.html.tmpl31
-rw-r--r--extensions/Splinter/template/en/default/hook/attachment/list-action.html.tmpl31
-rw-r--r--extensions/Splinter/template/en/default/hook/global/user-error-errors.html.tmpl5
-rw-r--r--extensions/Splinter/template/en/default/hook/request/email-after_summary.txt.tmpl6
-rw-r--r--extensions/Splinter/template/en/default/hook/request/queue-after_column.html.tmpl8
-rw-r--r--extensions/Splinter/template/en/default/pages/splinter.html.tmpl270
-rw-r--r--extensions/Splinter/template/en/default/pages/splinter/help.html.tmpl153
-rw-r--r--extensions/Splinter/web/splinter.css419
-rw-r--r--extensions/Splinter/web/splinter.js2572
-rw-r--r--extensions/TagNewUsers/Config.pm33
-rw-r--r--extensions/TagNewUsers/Extension.pm264
-rw-r--r--extensions/TagNewUsers/template/en/default/hook/bug/comments-comment_banner.html.tmpl25
-rw-r--r--extensions/TagNewUsers/template/en/default/hook/bug/comments-user.html.tmpl40
-rw-r--r--extensions/TagNewUsers/web/style.css16
-rw-r--r--extensions/TellUsMore/Config.pm14
-rw-r--r--extensions/TellUsMore/Extension.pm140
-rw-r--r--extensions/TellUsMore/disabled0
-rw-r--r--extensions/TellUsMore/lib/Constants.pm89
-rw-r--r--extensions/TellUsMore/lib/Process.pm263
-rw-r--r--extensions/TellUsMore/lib/VersionMirror.pm207
-rw-r--r--extensions/TellUsMore/lib/WebService.pm259
-rw-r--r--extensions/TellUsMore/template/en/default/email/existing-account.header.tmpl10
-rw-r--r--extensions/TellUsMore/template/en/default/email/existing-account.html.tmpl36
-rw-r--r--extensions/TellUsMore/template/en/default/email/existing-account.txt.tmpl24
-rw-r--r--extensions/TellUsMore/template/en/default/email/new-account.header.tmpl10
-rw-r--r--extensions/TellUsMore/template/en/default/email/new-account.html.tmpl36
-rw-r--r--extensions/TellUsMore/template/en/default/email/new-account.txt.tmpl24
-rw-r--r--extensions/TellUsMore/template/en/default/hook/global/user-error-errors.html.tmpl39
-rw-r--r--extensions/TryAutoLand/Config.pm19
-rw-r--r--extensions/TryAutoLand/Extension.pm323
-rw-r--r--extensions/TryAutoLand/bin/TryAutoLand.getBugs.pl60
-rw-r--r--extensions/TryAutoLand/bin/TryAutoLand.updateStatus.pl65
-rw-r--r--extensions/TryAutoLand/bin/TryAutoLand.updateStatus_json.pl65
-rw-r--r--extensions/TryAutoLand/lib/Constants.pm31
-rw-r--r--extensions/TryAutoLand/lib/WebService.pm189
-rw-r--r--extensions/TryAutoLand/template/en/default/hook/bug/edit-after_custom_fields.html.tmpl101
-rw-r--r--extensions/TryAutoLand/template/en/default/hook/bug/field-help-end.none.tmpl15
-rw-r--r--extensions/TryAutoLand/template/en/default/hook/bug/show-header-end.html.tmpl11
-rw-r--r--extensions/TryAutoLand/template/en/default/hook/global/user-error-auth_failure_object.html.tmpl11
-rw-r--r--extensions/TryAutoLand/template/en/default/hook/global/user-error-errors.html.tmpl33
-rw-r--r--extensions/TryAutoLand/web/style.css23
-rw-r--r--extensions/TypeSniffer/Config.pm40
-rw-r--r--extensions/TypeSniffer/Extension.pm75
-rw-r--r--extensions/Voting/Extension.pm13
-rw-r--r--extensions/Voting/template/en/default/hook/bug/edit-after_importance.html.tmpl3
-rw-r--r--js/TUI.js10
-rw-r--r--js/comments.js27
-rw-r--r--js/create_bug.js116
-rw-r--r--js/field.js79
-rw-r--r--js/instant-search.js201
-rw-r--r--js/util.js49
-rw-r--r--js/yui/swfstore/swfstore.swfbin4841 -> 0 bytes
-rw-r--r--mod_perl.pl6
-rwxr-xr-xpost_bug.cgi6
-rwxr-xr-xprocess_bug.cgi44
-rwxr-xr-xquips.cgi1
-rwxr-xr-xrequest.cgi16
-rw-r--r--robots.txt12
-rwxr-xr-xshowdependencygraph.cgi3
-rw-r--r--skins/contrib/Dusk-Helvetica/buglist.css24
-rw-r--r--skins/contrib/Dusk-Helvetica/global.css263
-rw-r--r--skins/contrib/Dusk-Helvetica/index.css9
-rw-r--r--skins/contrib/Dusk-Segoe/buglist.css24
-rw-r--r--skins/contrib/Dusk-Segoe/global.css263
-rw-r--r--skins/contrib/Dusk-Segoe/index.css9
-rw-r--r--skins/contrib/Dusk-Segoe/show_bug.css3
-rw-r--r--skins/contrib/Dusk/global.css6
-rw-r--r--skins/custom/IE-fixes.css4
-rw-r--r--skins/custom/bug_groups.css25
-rw-r--r--skins/custom/buglist.css36
-rw-r--r--skins/custom/create_bug.css19
-rw-r--r--skins/custom/global.css76
-rw-r--r--skins/custom/index.css31
-rw-r--r--skins/custom/search_form.css6
-rw-r--r--skins/custom/show_bug.css82
-rw-r--r--skins/standard/buglist.css2
-rw-r--r--skins/standard/enter_bug.css2
-rw-r--r--skins/standard/global.css10
-rw-r--r--skins/standard/guided.css4
-rw-r--r--skins/standard/reports.css5
-rw-r--r--t/001compile.t5
-rw-r--r--t/004template.t13
-rw-r--r--t/008filter.t5
-rw-r--r--t/012throwables.t4
-rw-r--r--t/Support/Files.pm5
-rw-r--r--template/en/default/account/auth/login-small.html.tmpl8
-rw-r--r--template/en/default/account/auth/login.html.tmpl8
-rw-r--r--template/en/default/account/create.html.tmpl2
-rw-r--r--template/en/default/account/prefs/email.html.tmpl4
-rw-r--r--template/en/default/account/prefs/permissions.html.tmpl4
-rw-r--r--template/en/default/account/prefs/saved-searches.html.tmpl2
-rw-r--r--template/en/default/account/profile-activity.html.tmpl2
-rw-r--r--template/en/default/admin/params/advanced.html.tmpl8
-rw-r--r--template/en/default/admin/params/auth.html.tmpl6
-rw-r--r--template/en/default/admin/users/edit.html.tmpl12
-rw-r--r--template/en/default/admin/users/list.html.tmpl11
-rw-r--r--template/en/default/attachment/createformcontents.html.tmpl2
-rw-r--r--template/en/default/attachment/delete_reason.txt.tmpl11
-rw-r--r--template/en/default/attachment/diff-footer.html.tmpl6
-rw-r--r--template/en/default/attachment/edit.html.tmpl13
-rw-r--r--template/en/default/attachment/list.html.tmpl10
-rw-r--r--template/en/default/bug/comments.html.tmpl100
-rw-r--r--template/en/default/bug/create/comment-guided.txt.tmpl2
-rw-r--r--template/en/default/bug/create/create-guided.html.tmpl48
-rw-r--r--template/en/default/bug/create/create.html.tmpl466
-rw-r--r--template/en/default/bug/edit.html.tmpl392
-rw-r--r--template/en/default/bug/field.html.tmpl27
-rw-r--r--template/en/default/bug/navigate.html.tmpl14
-rw-r--r--template/en/default/bug/process/bugmail.html.tmpl31
-rw-r--r--template/en/default/bug/process/updates-disabled.html.tmpl73
-rw-r--r--template/en/default/bug/show-header.html.tmpl23
-rw-r--r--template/en/default/bug/show-multiple.html.tmpl2
-rw-r--r--template/en/default/bug/show.xml.tmpl5
-rw-r--r--template/en/default/config.rdf.tmpl4
-rw-r--r--template/en/default/email/bugmail-header.txt.tmpl6
-rw-r--r--template/en/default/email/bugmail.html.tmpl17
-rw-r--r--template/en/default/email/bugmail.txt.tmpl9
-rw-r--r--template/en/default/email/lockout.txt.tmpl4
-rw-r--r--template/en/default/filterexceptions.pl2
-rw-r--r--template/en/default/flag/list.html.tmpl193
-rw-r--r--template/en/default/global/code-error.html.tmpl42
-rw-r--r--template/en/default/global/common-links.html.tmpl2
-rw-r--r--template/en/default/global/header.html.tmpl5
-rw-r--r--template/en/default/global/setting-descs.none.tmpl2
-rw-r--r--template/en/default/global/user-error.html.tmpl39
-rw-r--r--template/en/default/global/user.html.tmpl4
-rw-r--r--template/en/default/index.html.tmpl38
-rw-r--r--template/en/default/list/edit-multiple.html.tmpl4
-rw-r--r--template/en/default/list/list.html.tmpl16
-rw-r--r--template/en/default/list/table.html.tmpl30
-rw-r--r--template/en/default/pages/bugzilla.dtd.tmpl179
-rw-r--r--template/en/default/pages/fields.html.tmpl61
-rw-r--r--template/en/default/pages/quicksearch.html.tmpl8
-rw-r--r--template/en/default/reports/components.html.tmpl11
-rw-r--r--template/en/default/request/email.txt.tmpl6
-rw-r--r--template/en/default/request/queue.html.tmpl7
-rw-r--r--template/en/default/search/field.html.tmpl2
-rw-r--r--template/en/default/search/form.html.tmpl1
-rw-r--r--template/en/default/search/search-google.html.tmpl57
-rw-r--r--template/en/default/search/search-instant.html.tmpl85
-rw-r--r--template/en/default/search/search-specific.html.tmpl11
-rw-r--r--template/en/default/search/tabs.html.tmpl8
597 files changed, 47735 insertions, 1278 deletions
diff --git a/.bzrignore b/.bzrignore
index 7ab83e7ad..e009f01c8 100644
--- a/.bzrignore
+++ b/.bzrignore
@@ -6,7 +6,6 @@
/docs/en/txt
/docs/en/html
/docs/en/pdf
-/skins/custom
/graphs
/data
/localconfig
diff --git a/.htaccess b/.htaccess
index c16ee19af..e7c65bdc8 100644
--- a/.htaccess
+++ b/.htaccess
@@ -1,5 +1,5 @@
# Don't allow people to retrieve non-cgi executable files or our private data
-<FilesMatch (\.pm|\.pl|\.tmpl|localconfig.*)$>
+<FilesMatch (\.pm|\.pl|\.tmpl|\.swf|localconfig.*)$>
deny from all
</FilesMatch>
<IfModule mod_expires.c>
@@ -23,3 +23,25 @@
</IfModule>
</IfModule>
</IfModule>
+
+AddType image/x-icon .ico
+
+Redirect permanent /queryhelp.cgi https://bugzilla.mozilla.org/query.cgi?format=advanced&help=1
+Redirect permanent /bug_status.html https://bugzilla.mozilla.org/page.cgi?id=fields.html
+Redirect permanent /bugwritinghelp.html https://bugzilla.mozilla.org/page.cgi?id=bug-writing.html
+Redirect permanent /etiquette.html https://bugzilla.mozilla.org/page.cgi?id=etiquette.html
+Redirect permanent /duplicates.html https://bugzilla.mozilla.org/duplicates.cgi
+
+RewriteEngine On
+RewriteRule ^favicon\.ico$ extensions/BMO/web/images/favicon.ico
+RewriteRule ^form[\.:](itrequest|mozlist|mktgevent|poweredby|presentation|swag|trademark|recoverykey)$ enter_bug.cgi?product=mozilla.org&format=$1
+RewriteRule ^form[\.:]legal$ enter_bug.cgi?product=Legal&format=legal
+RewriteRule ^form[\.:]mozpr$ enter_bug.cgi?product=Mozilla+PR&format=mozpr
+RewriteRule ^form[\.:]reps[\.:]mentorship$ enter_bug.cgi?product=Mozilla+Reps&format=mozreps
+RewriteRule ^form[\.:]reps[\.:]budget$ enter_bug.cgi?product=Mozilla+Reps&format=remo-budget
+RewriteRule ^form[\.:]reps[\.:]swag$ enter_bug.cgi?product=Mozilla+Reps&format=remo-swag
+RewriteRule ^form[\.:]reps[\.:]payment$ page.cgi?id=remo-form-payment.html
+RewriteRule ^form[\.:]employee[\.\-:]incident$ enter_bug.cgi?product=mozilla.org&format=employee-incident
+RewriteRule ^form[\.:]brownbag$ enter_bug.cgi?product=Air\ Mozilla&format=brownbag
+RewriteRule ^form[\.:]finance$ enter_bug.cgi?product=Finance&format=finance
+RewriteRule ^form[\.:]privacy[\.\-:]data$ enter_bug.cgi?product=Privacy&format=privacy-data
diff --git a/Bugzilla.pm b/Bugzilla.pm
index 5b39e4c81..9d69cd65c 100644
--- a/Bugzilla.pm
+++ b/Bugzilla.pm
@@ -26,6 +26,8 @@ package Bugzilla;
use strict;
+# FIXME : Line to be removed
+
# We want any compile errors to get to the browser, if possible.
BEGIN {
# This makes sure we're in a CGI.
@@ -42,6 +44,7 @@ use Bugzilla::Auth::Persist::Cookie;
use Bugzilla::CGI;
use Bugzilla::Extension;
use Bugzilla::DB;
+use Bugzilla::Hook;
use Bugzilla::Install::Localconfig qw(read_localconfig);
use Bugzilla::Install::Requirements qw(OPTIONAL_MODULES);
use Bugzilla::Install::Util qw(init_console);
@@ -286,9 +289,7 @@ sub input_params {
}
sub localconfig {
- my $class = shift;
- $class->request_cache->{localconfig} ||= read_localconfig();
- return $class->request_cache->{localconfig};
+ return $_[0]->process_cache->{localconfig} ||= read_localconfig();
}
sub params {
@@ -597,12 +598,20 @@ sub fields {
}
sub active_custom_fields {
- my $class = shift;
- if (!exists $class->request_cache->{active_custom_fields}) {
- $class->request_cache->{active_custom_fields} =
- Bugzilla::Field->match({ custom => 1, obsolete => 0 });
+ my ($class, $params) = @_;
+ my $cache_id = 'active_custom_fields';
+ if ($params) {
+ $cache_id .= ($params->{product} ? '_p' . $params->{product}->id : '') .
+ ($params->{component} ? '_c' . $params->{component}->id : '') .
+ ($params->{type} ? '_t' . $params->{type} : '');
}
- return @{$class->request_cache->{active_custom_fields}};
+ if (!exists $class->request_cache->{$cache_id}) {
+ my $fields = Bugzilla::Field->match({ custom => 1, obsolete => 0});
+ Bugzilla::Hook::process('active_custom_fields',
+ { fields => \$fields, params => $params });
+ $class->request_cache->{$cache_id} = $fields;
+ }
+ return @{$class->request_cache->{$cache_id}};
}
sub has_flags {
@@ -642,6 +651,15 @@ sub request_cache {
return $_request_cache;
}
+# This is a per-process cache. Under mod_cgi it's identical to the
+# request_cache. When using mod_perl, items in this cache live until the
+# worker process is terminated.
+our $_process_cache = {};
+
+sub process_cache {
+ return $_process_cache;
+}
+
# Private methods
# Per-process cleanup. Note that this is a plain subroutine, not a method,
diff --git a/Bugzilla/Arecibo.pm b/Bugzilla/Arecibo.pm
new file mode 100644
index 000000000..760c60c59
--- /dev/null
+++ b/Bugzilla/Arecibo.pm
@@ -0,0 +1,335 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Arecibo;
+
+use strict;
+use warnings;
+
+use base qw(Exporter);
+our @EXPORT = qw(
+ arecibo_handle_error
+ arecibo_generate_id
+ arecibo_should_notify
+);
+
+use Apache2::Log;
+use Apache2::SubProcess;
+use Carp;
+use Email::Date::Format qw(email_gmdate);
+use LWP::UserAgent;
+use POSIX qw(setsid nice);
+use Sys::Hostname;
+
+use Bugzilla::Constants;
+use Bugzilla::Util;
+use Bugzilla::WebService::Constants;
+
+use constant CONFIG => {
+ # 'types' maps from the error message to types and priorities
+ types => [
+ {
+ type => 'the_schwartz',
+ boost => -10,
+ match => [
+ qr/TheSchwartz\.pm/,
+ ],
+ },
+ {
+ type => 'database_error',
+ boost => -10,
+ match => [
+ qr/DBD::mysql/,
+ qr/Can't connect to the database/,
+ ],
+ },
+ {
+ type => 'patch_reader',
+ boost => +5,
+ match => [
+ qr#/PatchReader/#,
+ ],
+ },
+ {
+ type => 'uninitialized_warning',
+ boost => 0,
+ match => [
+ qr/Use of uninitialized value/,
+ ],
+ },
+ ],
+
+ # 'codes' lists the code-errors which are sent to arecibo
+ codes => [qw(
+ bug_error
+ chart_datafile_corrupt
+ chart_dir_nonexistent
+ chart_file_open_fail
+ illegal_content_type_method
+ jobqueue_insert_failed
+ ldap_bind_failed
+ mail_send_error
+ template_error
+ token_generation_error
+ )],
+
+ # any error messages matching these regex's will not be sent to arecibo
+ ignore => [
+ qr/Software caused connection abort/,
+ qr/Could not check out .*\/cvsroot/,
+ ],
+};
+
+sub arecibo_generate_id {
+ return sprintf("%s.%s", (time), $$);
+}
+
+sub arecibo_should_notify {
+ my $code_error = shift;
+ return grep { $_ eq $code_error } @{CONFIG->{codes}};
+}
+
+sub arecibo_handle_error {
+ my $class = shift;
+ my @message = split(/\n/, shift);
+ my $id = shift || arecibo_generate_id();
+
+ my $is_error = $class eq 'error';
+ if ($class ne 'error' && $class ne 'warning') {
+ # it's a code-error
+ return 0 unless arecibo_should_notify($class);
+ $is_error = 1;
+ }
+
+ # build traceback
+ my $traceback;
+ {
+ # for now don't show function arguments, in case they contain
+ # confidential data. waiting on bug 700683
+ #local $Carp::MaxArgLen = 256;
+ #local $Carp::MaxArgNums = 0;
+ local $Carp::MaxArgNums = -1;
+ local $Carp::CarpInternal{'CGI::Carp'} = 1;
+ local $Carp::CarpInternal{'Bugzilla::Error'} = 1;
+ local $Carp::CarpInternal{'Bugzilla::Arecibo'} = 1;
+ $traceback = Carp::longmess();
+ }
+
+ # strip timestamp
+ foreach my $line (@message) {
+ $line =~ s/^\[[^\]]+\] //;
+ }
+ my $message = join(" ", map { trim($_) } grep { $_ ne '' } @message);
+
+ # don't send to arecibo unless configured
+ my $arecibo_server = Bugzilla->params->{arecibo_server} || '';
+ my $send_to_arecibo = $arecibo_server ne '';
+
+ # web service filtering
+ if ($send_to_arecibo
+ && (Bugzilla->error_mode == ERROR_MODE_DIE_SOAP_FAULT || Bugzilla->error_mode == ERROR_MODE_JSON_RPC))
+ {
+ my ($code) = $message =~ /^(-?\d+): /;
+ if ($code
+ && !($code == ERROR_UNKNOWN_FATAL || $code == ERROR_UNKNOWN_TRANSIENT))
+ {
+ $send_to_arecibo = 0;
+ }
+ }
+
+ # message content filtering
+ if ($send_to_arecibo) {
+ foreach my $re (@{CONFIG->{ignore}}) {
+ if ($message =~ $re) {
+ $send_to_arecibo = 0;
+ last;
+ }
+ }
+ }
+
+ # log to apache's error_log
+ if ($send_to_arecibo) {
+ $message .= " [#$id]";
+ } else {
+ $traceback =~ s/\n/ /g;
+ $message .= " $traceback";
+ }
+ _write_to_error_log($message, $is_error);
+
+ return 0 unless $send_to_arecibo;
+
+ # set the error type and priority from the message content
+ $message = join("\n", grep { $_ ne '' } @message);
+ my $type = '';
+ my $priority = $class eq 'error' ? 3 : 10;
+ foreach my $rh_type (@{CONFIG->{types}}) {
+ foreach my $re (@{$rh_type->{match}}) {
+ if ($message =~ $re) {
+ $type = $rh_type->{type};
+ $priority += $rh_type->{boost};
+ last;
+ }
+ }
+ last if $type ne '';
+ }
+ $type ||= $class;
+ $priority = 1 if $priority < 1;
+ $priority = 10 if $priority > 10;
+
+ my $username = '';
+ eval { $username = Bugzilla->user->login };
+
+ my $request = '';
+ foreach my $name (sort { lc($a) cmp lc($b) } keys %ENV) {
+ $request .= "$name=$ENV{$name}\n";
+ }
+ chomp($request);
+
+ my $data = [
+ ip => remote_ip(),
+ msg => $message,
+ priority => $priority,
+ server => hostname(),
+ request => $request,
+ status => '500',
+ timestamp => email_gmdate(),
+ traceback => $traceback,
+ type => $type,
+ uid => $id,
+ url => Bugzilla->cgi->self_url,
+ user_agent => $ENV{HTTP_USER_AGENT},
+ username => $username,
+ ];
+
+ # fork then post
+ local $SIG{CHLD} = 'IGNORE';
+ my $pid = fork();
+ if (defined($pid) && $pid == 0) {
+ # detach
+ chdir('/');
+ open(STDIN, '</dev/null');
+ open(STDOUT, '>/dev/null');
+ open(STDERR, '>/dev/null');
+ setsid();
+ nice(19);
+
+ # post to arecibo (ignore any errors)
+ my $agent = LWP::UserAgent->new(
+ agent => 'bugzilla.mozilla.org',
+ timeout => 10, # seconds
+ );
+ $agent->post($arecibo_server, $data);
+
+ CORE::exit(0);
+ }
+ return 1;
+}
+
+sub _write_to_error_log {
+ my ($message, $is_error) = @_;
+ if ($ENV{MOD_PERL}) {
+ if ($is_error) {
+ Apache2::ServerRec::log_error($message);
+ } else {
+ Apache2::ServerRec::warn($message);
+ }
+ } else {
+ print STDERR "$message\n";
+ }
+}
+
+# lifted from Bugzilla::Error
+sub _in_eval {
+ my $in_eval = 0;
+ for (my $stack = 1; my $sub = (caller($stack))[3]; $stack++) {
+ last if $sub =~ /^ModPerl/;
+ last if $sub =~ /^Bugzilla::Template/;
+ $in_eval = 1 if $sub =~ /^\(eval\)/;
+ }
+ return $in_eval;
+}
+
+sub _arecibo_die_handler {
+ my $message = shift;
+ $message =~ s/^undef error - //;
+
+ # avoid recursion, and check for CGI::Carp::die failures
+ my $in_cgi_carp_die = 0;
+ for (my $stack = 1; my $sub = (caller($stack))[3]; $stack++) {
+ return if $sub =~ /:_arecibo_die_handler$/;
+ $in_cgi_carp_die = 1 if $sub =~ /CGI::Carp::die$/;
+ }
+
+ return if _in_eval();
+
+ # mod_perl overrides exit to call die with this string
+ exit if $message =~ /\bModPerl::Util::exit\b/;
+
+ my $nested_error = '';
+ my $is_compilation_failure = $message =~ /\bcompilation (aborted|failed)\b/i;
+
+ # if we are called via CGI::Carp::die chances are something is seriously
+ # wrong, so skip trying to use ThrowTemplateError
+ if (!$in_cgi_carp_die && !$is_compilation_failure) {
+ eval { Bugzilla::Error::ThrowTemplateError($message) };
+ $nested_error = $@ if $@;
+ }
+
+ if ($is_compilation_failure ||
+ $in_cgi_carp_die ||
+ ($nested_error && $nested_error !~ /\bModPerl::Util::exit\b/)
+ ) {
+ my $uid = arecibo_generate_id();
+ my $notified = arecibo_handle_error('error', $message, $uid);
+
+ # if we aren't dying from a web page, let perl deal with it. this
+ # does the right thing with respect to returning web service errors
+ if (Bugzilla->error_mode != ERROR_MODE_WEBPAGE) {
+ CORE::die($message);
+ }
+
+ # right now it's hard to determine if we've already returned a
+ # content-type header, it's better to return two than none
+ print "Content-type: text/html\n\n";
+
+ my $maintainer = html_quote(Bugzilla->params->{'maintainer'});
+ $message =~ s/ at \S+ line \d+\.\s*$//;
+ $message = html_quote($message);
+ $uid = html_quote($uid);
+ $nested_error = html_quote($nested_error);
+ print qq(
+ <h1>Bugzilla has suffered an internal error</h1>
+ <pre>$message</pre>
+ <hr>
+ <pre>$nested_error</pre>
+ );
+ if ($notified) {
+ print qq(
+ The <a href="mailto:$maintainer">Bugzilla maintainers</a> have
+ been notified of this error [#$uid].
+ );
+ };
+ }
+ exit;
+}
+
+sub install_arecibo_handler {
+ require CGI::Carp;
+ CGI::Carp::set_die_handler(\&_arecibo_die_handler);
+ $main::SIG{__WARN__} = sub {
+ return if _in_eval();
+ arecibo_handle_error('warning', shift);
+ };
+}
+
+BEGIN {
+ if ($ENV{SCRIPT_NAME} || $ENV{MOD_PERL}) {
+ install_arecibo_handler();
+ }
+}
+
+1;
diff --git a/Bugzilla/Attachment.pm b/Bugzilla/Attachment.pm
index 69939a657..aa7eee2a7 100644
--- a/Bugzilla/Attachment.pm
+++ b/Bugzilla/Attachment.pm
@@ -415,6 +415,53 @@ sub datasize {
return $self->{datasize};
}
+=over
+
+=item C<linecount>
+
+the number of lines of the attachment content
+
+=back
+
+=cut
+
+# linecount allows for getting the number of lines of an attachment
+# from the database directly if the data has not yet been loaded for
+# performance reasons.
+
+sub linecount {
+ my ($self) = @_;
+
+ return $self->{linecount} if exists $self->{linecount};
+
+ # Limit this to just text/* attachments as this could
+ # cause strange results for binary attachments.
+ return if $self->contenttype !~ /^text\//;
+
+ # If the data has already been loaded, we can just determine
+ # line count from the data directly.
+ if ($self->{data}) {
+ $self->{linecount} = $self->{data} =~ tr/\n/\n/;
+ }
+ else {
+ $self->{linecount} =
+ int(Bugzilla->dbh->selectrow_array("
+ SELECT LENGTH(attach_data.thedata)-LENGTH(REPLACE(attach_data.thedata,'\n',''))/LENGTH('\n')
+ FROM attach_data WHERE id = ?", undef, $self->id));
+
+ }
+
+ # If we still do not have a linecount either the attachment
+ # is stored in a local file or has been deleted. If the former,
+ # we call self->data to force a load from the filesystem and
+ # then do a split on newlines and count again.
+ unless ($self->{linecount}) {
+ $self->{linecount} = $self->data =~ tr/\n/\n/;
+ }
+
+ return $self->{linecount};
+}
+
sub _get_local_filename {
my $self = shift;
my $hash = ($self->id % 100) + 100;
@@ -458,7 +505,8 @@ sub flag_types {
my $vars = { target_type => 'attachment',
product_id => $self->bug->product_id,
component_id => $self->bug->component_id,
- attach_id => $self->id };
+ attach_id => $self->id,
+ active_or_has_flags => $self->bug_id };
$self->{flag_types} = Bugzilla::Flag->_flag_types($vars);
return $self->{flag_types};
diff --git a/Bugzilla/Attachment/PatchReader.pm b/Bugzilla/Attachment/PatchReader.pm
index cfc7610f4..a9df6e34e 100644
--- a/Bugzilla/Attachment/PatchReader.pm
+++ b/Bugzilla/Attachment/PatchReader.pm
@@ -33,8 +33,8 @@ sub process_diff {
my ($reader, $last_reader) = setup_patch_readers(undef, $context);
if ($format eq 'raw') {
- require PatchReader::DiffPrinter::raw;
- $last_reader->sends_data_to(new PatchReader::DiffPrinter::raw());
+ require Bugzilla::PatchReader::DiffPrinter::raw;
+ $last_reader->sends_data_to(new Bugzilla::PatchReader::DiffPrinter::raw());
# Actually print out the patch.
print $cgi->header(-type => 'text/plain',
-expires => '+3M');
@@ -114,8 +114,8 @@ sub process_interdiff {
my ($reader, $last_reader) = setup_patch_readers("", $context);
if ($format eq 'raw') {
- require PatchReader::DiffPrinter::raw;
- $last_reader->sends_data_to(new PatchReader::DiffPrinter::raw());
+ require Bugzilla::PatchReader::DiffPrinter::raw;
+ $last_reader->sends_data_to(new Bugzilla::PatchReader::DiffPrinter::raw());
# Actually print out the patch.
print $cgi->header(-type => 'text/plain',
-expires => '+3M');
@@ -152,29 +152,29 @@ sub get_unified_diff {
my ($attachment, $format) = @_;
# Bring in the modules we need.
- require PatchReader::Raw;
- require PatchReader::FixPatchRoot;
- require PatchReader::DiffPrinter::raw;
- require PatchReader::PatchInfoGrabber;
+ require Bugzilla::PatchReader::Raw;
+ require Bugzilla::PatchReader::FixPatchRoot;
+ require Bugzilla::PatchReader::DiffPrinter::raw;
+ require Bugzilla::PatchReader::PatchInfoGrabber;
require File::Temp;
$attachment->ispatch
|| ThrowCodeError('must_be_patch', { 'attach_id' => $attachment->id });
# Reads in the patch, converting to unified diff in a temp file.
- my $reader = new PatchReader::Raw;
+ my $reader = new Bugzilla::PatchReader::Raw;
my $last_reader = $reader;
# Fixes patch root (makes canonical if possible).
if (Bugzilla->params->{'cvsroot'}) {
my $fix_patch_root =
- new PatchReader::FixPatchRoot(Bugzilla->params->{'cvsroot'});
+ new Bugzilla::PatchReader::FixPatchRoot(Bugzilla->params->{'cvsroot'});
$last_reader->sends_data_to($fix_patch_root);
$last_reader = $fix_patch_root;
}
# Grabs the patch file info.
- my $patch_info_grabber = new PatchReader::PatchInfoGrabber();
+ my $patch_info_grabber = new Bugzilla::PatchReader::PatchInfoGrabber();
$last_reader->sends_data_to($patch_info_grabber);
$last_reader = $patch_info_grabber;
@@ -184,7 +184,7 @@ sub get_unified_diff {
# The HTML page will be displayed with the UTF-8 encoding.
binmode $fh, ':utf8';
}
- my $raw_printer = new PatchReader::DiffPrinter::raw($fh);
+ my $raw_printer = new Bugzilla::PatchReader::DiffPrinter::raw($fh);
$last_reader->sends_data_to($raw_printer);
$last_reader = $raw_printer;
@@ -228,13 +228,13 @@ sub setup_patch_readers {
# Define the patch readers.
# The reader that reads the patch in (whatever its format).
- require PatchReader::Raw;
- my $reader = new PatchReader::Raw;
+ require Bugzilla::PatchReader::Raw;
+ my $reader = new Bugzilla::PatchReader::Raw;
my $last_reader = $reader;
# Fix the patch root if we have a cvs root.
if (Bugzilla->params->{'cvsroot'}) {
- require PatchReader::FixPatchRoot;
- $last_reader->sends_data_to(new PatchReader::FixPatchRoot(Bugzilla->params->{'cvsroot'}));
+ require Bugzilla::PatchReader::FixPatchRoot;
+ $last_reader->sends_data_to(new Bugzilla::PatchReader::FixPatchRoot(Bugzilla->params->{'cvsroot'}));
$last_reader->sends_data_to->diff_root($diff_root) if defined($diff_root);
$last_reader = $last_reader->sends_data_to;
}
@@ -243,12 +243,12 @@ sub setup_patch_readers {
if ($context ne 'patch' && Bugzilla->localconfig->{cvsbin}
&& Bugzilla->params->{'cvsroot_get'})
{
- require PatchReader::AddCVSContext;
+ require Bugzilla::PatchReader::AddCVSContext;
# We need to set $cvsbin as global, because PatchReader::CVSClient
# needs it in order to find 'cvs'.
$main::cvsbin = Bugzilla->localconfig->{cvsbin};
$last_reader->sends_data_to(
- new PatchReader::AddCVSContext($context, Bugzilla->params->{'cvsroot_get'}));
+ new Bugzilla::PatchReader::AddCVSContext($context, Bugzilla->params->{'cvsroot_get'}));
$last_reader = $last_reader->sends_data_to;
}
@@ -260,7 +260,7 @@ sub setup_template_patch_reader {
my $cgi = Bugzilla->cgi;
my $template = Bugzilla->template;
- require PatchReader::DiffPrinter::template;
+ require Bugzilla::PatchReader::DiffPrinter::template;
# Define the vars for templates.
if (defined $cgi->param('headers')) {
@@ -279,7 +279,7 @@ sub setup_template_patch_reader {
print $cgi->header(-type => 'text/html',
-expires => '+3M');
- $last_reader->sends_data_to(new PatchReader::DiffPrinter::template($template,
+ $last_reader->sends_data_to(new Bugzilla::PatchReader::DiffPrinter::template($template,
"attachment/diff-header.$format.tmpl",
"attachment/diff-file.$format.tmpl",
"attachment/diff-footer.$format.tmpl",
diff --git a/Bugzilla/Auth.pm b/Bugzilla/Auth.pm
index 45034e166..ab741965a 100644
--- a/Bugzilla/Auth.pm
+++ b/Bugzilla/Auth.pm
@@ -38,6 +38,7 @@ use Bugzilla::User::Setting ();
use Bugzilla::Auth::Login::Stack;
use Bugzilla::Auth::Verify::Stack;
use Bugzilla::Auth::Persist::Cookie;
+use Socket;
sub new {
my ($class, $params) = @_;
@@ -215,10 +216,18 @@ sub _handle_login_result {
my $default_settings = Bugzilla::User::Setting::get_defaults();
my $template = Bugzilla->template_inner(
$default_settings->{lang}->{default_value});
+ my $address = $attempts->[0]->{ip_addr};
+ # Note: inet_aton will only resolve IPv4 addresses.
+ # For IPv6 we'll need to use inet_pton which requires Perl 5.12.
+ my $n = inet_aton($address);
+ if ($n) {
+ $address = gethostbyaddr($n, AF_INET) . " ($address)"
+ }
my $vars = {
locked_user => $user,
attempts => $attempts,
unlock_at => $unlock_at,
+ address => $address,
};
my $message;
$template->process('email/lockout.txt.tmpl', $vars, \$message)
diff --git a/Bugzilla/Bug.pm b/Bugzilla/Bug.pm
index 6a21b4e89..f8566be4a 100644
--- a/Bugzilla/Bug.pm
+++ b/Bugzilla/Bug.pm
@@ -1629,6 +1629,14 @@ sub _check_groups {
: $params->{product};
my %add_groups;
+ # BMO: Allow extension to add groups before the
+ # real checks are done.
+ Bugzilla::Hook::process('bug_check_groups', {
+ product => $product,
+ group_names => $group_names,
+ add_groups => \%add_groups
+ });
+
# In email or WebServices, when the "groups" item actually
# isn't specified, then just add the default groups.
if (!defined $group_names) {
@@ -1647,7 +1655,12 @@ sub _check_groups {
foreach my $name (@$group_names) {
my $group = Bugzilla::Group->check_no_disclose({ %args, name => $name });
- if (!$product->group_is_settable($group)) {
+ # BMO: Do not check group_is_settable if the group is
+ # already added, such as from the extension hook. group_is_settable
+ # will reject any group the user is not currently in.
+ if (!$add_groups{$group->id}
+ && !$product->group_is_settable($group))
+ {
ThrowUserError('group_restriction_not_allowed', { %args, name => $name });
}
$add_groups{$group->id} = $group;
@@ -1656,7 +1669,7 @@ sub _check_groups {
# Now enforce mandatory groups.
$add_groups{$_->id} = $_ foreach @{ $product->groups_mandatory };
-
+
my @add_groups = values %add_groups;
return \@add_groups;
}
@@ -3263,6 +3276,26 @@ sub depends_on_obj {
return $self->{depends_on_obj};
}
+sub duplicates {
+ my $self = shift;
+ return $self->{duplicates} if exists $self->{duplicates};
+ return [] if $self->{error};
+ $self->{duplicates} = Bugzilla::Bug->new_from_list($self->duplicate_ids);
+ return $self->{duplicates};
+}
+
+sub duplicate_ids {
+ my $self = shift;
+ return $self->{duplicate_ids} if exists $self->{duplicate_ids};
+ return [] if $self->{error};
+
+ my $dbh = Bugzilla->dbh;
+ $self->{duplicate_ids} =
+ $dbh->selectcol_arrayref('SELECT dupe FROM duplicates WHERE dupe_of = ?',
+ undef, $self->id);
+ return $self->{duplicate_ids};
+}
+
sub flag_types {
my ($self) = @_;
return $self->{'flag_types'} if exists $self->{'flag_types'};
@@ -3271,7 +3304,8 @@ sub flag_types {
my $vars = { target_type => 'bug',
product_id => $self->{product_id},
component_id => $self->{component_id},
- bug_id => $self->bug_id };
+ bug_id => $self->bug_id,
+ active_or_has_flags => $self->bug_id };
$self->{'flag_types'} = Bugzilla::Flag->_flag_types($vars);
return $self->{'flag_types'};
@@ -3786,14 +3820,26 @@ sub GetBugActivity {
$changes = [];
}
+ # If this is the same field as the previoius item, then concatenate
+ # the data into the same change.
+ if ($operation->{'who'} && $who eq $operation->{'who'}
+ && $when eq $operation->{'when'}
+ && $fieldname eq $operation->{'fieldname'}
+ && ($attachid || 0) == ($operation->{'attachid'} || 0))
+ {
+ my $old_change = pop @$changes;
+ $removed = _join_activity_entries($fieldname, $old_change->{'removed'}, $removed);
+ $added = _join_activity_entries($fieldname, $old_change->{'added'}, $added);
+ }
+
$operation->{'who'} = $who;
$operation->{'when'} = $when;
+ $operation->{'fieldname'} = $change{'fieldname'} = $fieldname;
+ $operation->{'attachid'} = $change{'attachid'} = $attachid;
- $change{'fieldname'} = $fieldname;
- $change{'attachid'} = $attachid;
$change{'removed'} = $removed;
$change{'added'} = $added;
-
+
if ($comment_id) {
$change{'comment'} = Bugzilla::Comment->new($comment_id);
}
@@ -3810,6 +3856,37 @@ sub GetBugActivity {
return(\@operations, $incomplete_data);
}
+sub _join_activity_entries {
+ my ($field, $current_change, $new_change) = @_;
+ # We need to insert characters as these were removed by old
+ # LogActivityEntry code.
+
+ if ($current_change eq '') {
+ return $new_change;
+ }
+
+ # Buglists and see_also need the comma restored
+ if ($field eq 'dependson' || $field eq 'blocked' || $field eq 'see_also') {
+ if (substr($new_change, 0, 1) eq ',') {
+ return $current_change . $new_change;
+ } else {
+ return $current_change . ', ' . $new_change;
+ }
+ }
+
+ # Assume bug_file_loc contain a single url, don't insert a delimiter
+ if ($field eq 'bug_file_loc') {
+ return $current_change . $new_change;
+ }
+
+ # All other fields get a space
+ if (substr($new_change, 0, 1) eq ' ') {
+ return $current_change . $new_change;
+ } else {
+ return $current_change . ' ' . $new_change;
+ }
+}
+
# Update the bugs_activity table to reflect changes made in bugs.
sub LogActivityEntry {
my ($i, $col, $removed, $added, $whoid, $timestamp, $comment_id) = @_;
@@ -3824,7 +3901,6 @@ sub LogActivityEntry {
my $commaposition = find_wrap_point($removed, MAX_LINE_LENGTH);
$removestr = substr($removed, 0, $commaposition);
$removed = substr($removed, $commaposition);
- $removed =~ s/^[,\s]+//; # remove any comma or space
} else {
$removed = ""; # no more entries
}
@@ -3832,7 +3908,6 @@ sub LogActivityEntry {
my $commaposition = find_wrap_point($added, MAX_LINE_LENGTH);
$addstr = substr($added, 0, $commaposition);
$added = substr($added, $commaposition);
- $added =~ s/^[,\s]+//; # remove any comma or space
} else {
$added = ""; # no more entries
}
diff --git a/Bugzilla/BugMail.pm b/Bugzilla/BugMail.pm
index 55eeeab25..696db5ceb 100644
--- a/Bugzilla/BugMail.pm
+++ b/Bugzilla/BugMail.pm
@@ -47,7 +47,8 @@ use Bugzilla::Hook;
use Date::Parse;
use Date::Format;
use Scalar::Util qw(blessed);
-use List::MoreUtils qw(uniq);
+use List::MoreUtils qw(uniq firstidx);
+use Sys::Hostname;
use constant BIT_DIRECT => 1;
use constant BIT_WATCHING => 2;
@@ -107,6 +108,7 @@ sub Send {
my %user_cache = map { $_->id => $_ } (@assignees, @qa_contacts, @ccs);
my @diffs;
+ my @referenced_bugs;
if (!$start) {
@diffs = _get_new_bugmail_fields($bug);
}
@@ -122,15 +124,31 @@ sub Send {
new => $params->{changes}->{resolution}->[1],
login_name => $changer->login,
blocker => $params->{blocker} });
+ push(@referenced_bugs, $params->{blocker}->id);
}
else {
- push(@diffs, _get_diffs($bug, $end, \%user_cache));
+ my ($diffs, $referenced) = _get_diffs($bug, $end, \%user_cache);
+ push(@diffs, @$diffs);
+ push(@referenced_bugs, @$referenced);
}
my $comments = $bug->comments({ after => $start, to => $end });
# Skip empty comments.
@$comments = grep { $_->type || $_->body =~ /\S/ } @$comments;
+ # Add duplicate bug to referenced bug list
+ foreach my $comment (@$comments) {
+ if ($comment->type == CMT_DUPE_OF || $comment->type == CMT_HAS_DUPE) {
+ push(@referenced_bugs, $comment->extra_data);
+ }
+ }
+
+ # Add dependencies to referenced bug list on new bugs
+ if (!$start) {
+ push @referenced_bugs, @{ $bug->dependson };
+ push @referenced_bugs, @{ $bug->blocked };
+ }
+
###########################################################################
# Start of email filtering code
###########################################################################
@@ -193,21 +211,23 @@ sub Send {
{ bug => $bug, recipients => \%recipients,
users => \%user_cache, diffs => \@diffs });
- # Find all those user-watching anyone on the current list, who is not
- # on it already themselves.
- my $involved = join(",", keys %recipients);
+ if (scalar keys %recipients) {
+ # Find all those user-watching anyone on the current list, who is not
+ # on it already themselves.
+ my $involved = join(",", keys %recipients);
- my $userwatchers =
- $dbh->selectall_arrayref("SELECT watcher, watched FROM watch
- WHERE watched IN ($involved)");
+ my $userwatchers =
+ $dbh->selectall_arrayref("SELECT watcher, watched FROM watch
+ WHERE watched IN ($involved)");
- # Mark these people as having the role of the person they are watching
- foreach my $watch (@$userwatchers) {
- while (my ($role, $bits) = each %{$recipients{$watch->[1]}}) {
- $recipients{$watch->[0]}->{$role} |= BIT_WATCHING
- if $bits & BIT_DIRECT;
+ # Mark these people as having the role of the person they are watching
+ foreach my $watch (@$userwatchers) {
+ while (my ($role, $bits) = each %{$recipients{$watch->[1]}}) {
+ $recipients{$watch->[0]}->{$role} |= BIT_WATCHING
+ if $bits & BIT_DIRECT;
+ }
+ push(@{$watching{$watch->[0]}}, $watch->[1]);
}
- push(@{$watching{$watch->[0]}}, $watch->[1]);
}
# Global watcher
@@ -229,6 +249,9 @@ sub Send {
my $date = $params->{dep_only} ? $end : $bug->delta_ts;
$date = format_time($date, '%a, %d %b %Y %T %z', 'UTC');
+ # Remove duplicate references, and convert to bug objects
+ @referenced_bugs = @{ Bugzilla::Bug->new_from_list([uniq @referenced_bugs]) };
+
foreach my $user_id (keys %recipients) {
my %rels_which_want;
my $sent_mail = 0;
@@ -267,8 +290,33 @@ sub Send {
}
# Make sure the user isn't in the nomail list, and the dep check passed.
- if ($user->email_enabled && $dep_ok) {
+ # BMO: never send emails to bugs or .tld addresses. this check needs to
+ # happen after the bugmail_recipients hook.
+ if ($user->email_enabled && $dep_ok &&
+ ($user->login !~ /bugs$/) &&
+ ($user->login !~ /\.tld$/))
+ {
# OK, OK, if we must. Email the user.
+
+ # Don't show summaries for bugs the user can't access, and
+ # provide a hook for extensions such as SecureMail to filter
+ # this list.
+ #
+ # We build an array with the short_desc as a separate item to
+ # allow extensions to modify the summary without touching the
+ # bug object.
+ my $referenced_bugs = [];
+ foreach my $ref (@{ $user->visible_bugs(\@referenced_bugs) }) {
+ push @$referenced_bugs, {
+ bug => $ref,
+ id => $ref->id,
+ short_desc => $ref->short_desc,
+ };
+ }
+ Bugzilla::Hook::process('bugmail_referenced_bugs',
+ { updated_bug => $bug,
+ referenced_bugs => $referenced_bugs });
+
$sent_mail = sendMail(
{ to => $user,
bug => $bug,
@@ -279,6 +327,7 @@ sub Send {
$watching{$user_id} : undef,
diffs => \@diffs,
rels_which_want => \%rels_which_want,
+ referenced_bugs => $referenced_bugs,
});
}
}
@@ -314,6 +363,7 @@ sub sendMail {
my $watchingRef = $params->{watchers};
my @diffs = @{ $params->{diffs} };
my $relRef = $params->{rels_which_want};
+ my $referenced_bugs = $params->{referenced_bugs};
# Only display changes the user is allowed see.
my @display_diffs;
@@ -352,6 +402,17 @@ sub sendMail {
push(@watchingrel, 'None') unless @watchingrel;
push @watchingrel, map { user_id_to_login($_) } @$watchingRef;
+ # BMO: Use field descriptions instead of field names in header
+ my @changedfields = uniq map { $_->{field_desc} } @display_diffs;
+ my @changedfieldnames = uniq map { $_->{field_name} } @display_diffs;
+
+ # Add attachments.created to changedfields if one or more
+ # comments contain information about a new attachment
+ if (grep($_->type == CMT_ATTACHMENT_CREATED, @send_comments)) {
+ push(@changedfields, 'Attachment Created');
+ push(@changedfieldnames, 'attachment.created');
+ }
+
my $vars = {
date => $date,
to_user => $user,
@@ -362,9 +423,11 @@ sub sendMail {
reasonswatchheader => join(" ", @watchingrel),
changer => $changer,
diffs => \@display_diffs,
- changedfields => [uniq map { $_->{field_name} } @display_diffs],
+ changedfields => \@changedfields,
+ changedfieldnames => \@changedfieldnames,
new_comments => \@send_comments,
threadingmarker => build_thread_marker($bug->id, $user->id, !$bug->lastdiffed),
+ referenced_bugs => $referenced_bugs,
};
my $msg = _generate_bugmail($user, $vars);
MessageToMTA($msg);
@@ -395,7 +458,7 @@ sub _generate_bugmail {
|| ThrowTemplateError($template->error());
push @parts, Email::MIME->create(
attributes => {
- content_type => "text/html",
+ content_type => "text/html",
},
body => $msg_html,
);
@@ -403,6 +466,10 @@ sub _generate_bugmail {
# TT trims the trailing newline, and threadingmarker may be ignored.
my $email = new Email::MIME("$msg_header\n");
+
+ # For tracking/diagnostic purposes, add our hostname
+ $email->header_set('X-Generated-By' => hostname());
+
if (scalar(@parts) == 1) {
$email->content_type_set($parts[0]->content_type);
} else {
@@ -426,6 +493,7 @@ sub _get_diffs {
my $diffs = $dbh->selectall_arrayref(
"SELECT fielddefs.name AS field_name,
+ fielddefs.description AS field_desc,
bugs_activity.bug_when, bugs_activity.removed AS old,
bugs_activity.added AS new, bugs_activity.attach_id,
bugs_activity.comment_id, bugs_activity.who
@@ -434,11 +502,12 @@ sub _get_diffs {
ON fielddefs.id = bugs_activity.fieldid
WHERE bugs_activity.bug_id = ?
$when_restriction
- ORDER BY bugs_activity.bug_when", {Slice=>{}}, @args);
+ ORDER BY bugs_activity.bug_when, fielddefs.description", {Slice=>{}}, @args);
+ my $referenced_bugs = [];
foreach my $diff (@$diffs) {
- $user_cache->{$diff->{who}} ||= new Bugzilla::User($diff->{who});
- $diff->{who} = $user_cache->{$diff->{who}};
+ $user_cache->{$diff->{who}} ||= new Bugzilla::User($diff->{who});
+ $diff->{who} = $user_cache->{$diff->{who}};
if ($diff->{attach_id}) {
$diff->{isprivate} = $dbh->selectrow_array(
'SELECT isprivate FROM attachments WHERE attach_id = ?',
@@ -449,9 +518,13 @@ sub _get_diffs {
$diff->{num} = $comment->count;
$diff->{isprivate} = $diff->{new};
}
+ elsif ($diff->{field_name} eq 'dependson' || $diff->{field_name} eq 'blocked') {
+ push @$referenced_bugs, grep { /^\d+$/ } split(/[\s,]+/, $diff->{old});
+ push @$referenced_bugs, grep { /^\d+$/ } split(/[\s,]+/, $diff->{new});
+ }
}
- return @$diffs;
+ return ($diffs, $referenced_bugs);
}
sub _get_new_bugmail_fields {
@@ -459,6 +532,20 @@ sub _get_new_bugmail_fields {
my @fields = @{ Bugzilla->fields({obsolete => 0, in_new_bugmail => 1}) };
my @diffs;
+ # Show fields in the same order as the DEFAULT_FIELDS list, which mirrors
+ # 4.0's behavour and provides sane grouping of similar fields.
+ # Any additional fields are sorted by descrsiption
+ my @prepend;
+ foreach my $name (map { $_->{name} } Bugzilla::Field::DEFAULT_FIELDS) {
+ my $idx = firstidx { $_->name eq $name } @fields;
+ if ($idx != -1) {
+ push(@prepend, $fields[$idx]);
+ splice(@fields, $idx, 1);
+ }
+ }
+ @fields = sort { $a->description cmp $b->description } @fields;
+ @fields = (@prepend, @fields);
+
foreach my $field (@fields) {
my $name = $field->name;
my $value = $bug->$name;
@@ -484,7 +571,9 @@ sub _get_new_bugmail_fields {
# If there isn't anything to show, don't include this header.
next unless $value;
- push(@diffs, {field_name => $name, new => $value});
+ push(@diffs, {field_name => $name,
+ field_desc => $field->description,
+ new => $value});
}
return @diffs;
diff --git a/Bugzilla/BugUrl.pm b/Bugzilla/BugUrl.pm
index 837c0d4fe..784600984 100644
--- a/Bugzilla/BugUrl.pm
+++ b/Bugzilla/BugUrl.pm
@@ -69,6 +69,7 @@ use constant SUB_CLASSES => qw(
Bugzilla::BugUrl::Trac
Bugzilla::BugUrl::MantisBT
Bugzilla::BugUrl::SourceForge
+ Bugzilla::BugUrl::GitHub
);
###############################
diff --git a/Bugzilla/BugUrl/GitHub.pm b/Bugzilla/BugUrl/GitHub.pm
new file mode 100644
index 000000000..63be65bed
--- /dev/null
+++ b/Bugzilla/BugUrl/GitHub.pm
@@ -0,0 +1,36 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::BugUrl::GitHub;
+use strict;
+use base qw(Bugzilla::BugUrl);
+
+###############################
+#### Methods ####
+###############################
+
+sub should_handle {
+ my ($class, $uri) = @_;
+
+ # GitHub issue URLs have only one form:
+ # https://github.com/USER_OR_TEAM_OR_ORGANIZATION_NAME/REPOSITORY_NAME/issues/111
+ return ($uri->authority =~ /^github.com$/i
+ and $uri->path =~ m|^/[^/]+/[^/]+/issues/\d+$|) ? 1 : 0;
+}
+
+sub _check_value {
+ my ($class, $uri) = @_;
+
+ $uri = $class->SUPER::_check_value($uri);
+
+ # GitHub HTTP URLs redirect to HTTPS, so just use the HTTPS scheme.
+ $uri->scheme('https');
+
+ return $uri;
+}
+
+1;
diff --git a/Bugzilla/CGI.pm b/Bugzilla/CGI.pm
index 4dd223a31..2feb0b098 100644
--- a/Bugzilla/CGI.pm
+++ b/Bugzilla/CGI.pm
@@ -73,11 +73,22 @@ sub new {
# Make sure our outgoing cookie list is empty on each invocation
$self->{Bugzilla_cookie_list} = [];
+ # Path-Info is of no use for Bugzilla and interacts badly with IIS.
+ # Moreover, it causes unexpected behaviors, such as totally breaking
+ # the rendering of pages.
+ my $script = basename($0);
+ if ($self->path_info) {
+ my @whitelist;
+ Bugzilla::Hook::process('path_info_whitelist', { whitelist => \@whitelist });
+ if (!grep($_ eq $script, @whitelist)) {
+ print $self->redirect($self->url(-path => 0, -query => 1));
+ }
+ }
+
# Send appropriate charset
$self->charset(Bugzilla->params->{'utf8'} ? 'UTF-8' : '');
# Redirect to urlbase/sslbase if we are not viewing an attachment.
- my $script = basename($0);
if ($self->url_is_attachment_base and $script ne 'attachment.cgi') {
$self->redirect_to_urlbase();
}
diff --git a/Bugzilla/Component.pm b/Bugzilla/Component.pm
index dc3cc1b9e..ad5166a0f 100644
--- a/Bugzilla/Component.pm
+++ b/Bugzilla/Component.pm
@@ -371,11 +371,13 @@ sub default_qa_contact {
}
sub flag_types {
- my $self = shift;
+ my ($self, $params) = @_;
+ $params ||= {};
if (!defined $self->{'flag_types'}) {
my $flagtypes = Bugzilla::FlagType::match({ product_id => $self->product_id,
- component_id => $self->id });
+ component_id => $self->id,
+ %$params });
$self->{'flag_types'} = {};
$self->{'flag_types'}->{'bug'} =
diff --git a/Bugzilla/Config/Advanced.pm b/Bugzilla/Config/Advanced.pm
index 941cefc4f..a5ae3048a 100644
--- a/Bugzilla/Config/Advanced.pm
+++ b/Bugzilla/Config/Advanced.pm
@@ -63,6 +63,18 @@ use constant get_param_list => (
default => 'off',
checker => \&check_multi
},
+
+ {
+ name => 'disable_bug_updates',
+ type => 'b',
+ default => 0
+ },
+
+ {
+ name => 'arecibo_server',
+ type => 't',
+ default => '',
+ },
);
1;
diff --git a/Bugzilla/Config/Auth.pm b/Bugzilla/Config/Auth.pm
index a61cab5a2..d70c1f81e 100644
--- a/Bugzilla/Config/Auth.pm
+++ b/Bugzilla/Config/Auth.pm
@@ -97,6 +97,12 @@ sub get_param_list {
},
{
+ name => 'webservice_email_filter',
+ type => 'b',
+ default => 0
+ },
+
+ {
name => 'emailregexp',
type => 't',
default => q:^[\\w\\.\\+\\-=]+@[\\w\\.\\-]+\\.[\\w\\-]+$:,
diff --git a/Bugzilla/Constants.pm b/Bugzilla/Constants.pm
index 8056706b1..0658244a1 100644
--- a/Bugzilla/Constants.pm
+++ b/Bugzilla/Constants.pm
@@ -105,7 +105,7 @@ use Memoize;
POS_EVENTS
EVT_OTHER EVT_ADDED_REMOVED EVT_COMMENT EVT_ATTACHMENT EVT_ATTACHMENT_DATA
EVT_PROJ_MANAGEMENT EVT_OPENED_CLOSED EVT_KEYWORD EVT_CC EVT_DEPEND_BLOCK
- EVT_BUG_CREATED
+ EVT_BUG_CREATED EVT_COMPONENT
NEG_EVENTS
EVT_UNCONFIRMED EVT_CHANGED_BY_ME
@@ -262,7 +262,8 @@ use constant AUTH_NO_SUCH_USER => 5;
use constant AUTH_LOCKOUT => 6;
# The minimum length a password must have.
-use constant USER_PASSWORD_MIN_LENGTH => 6;
+# BMO uses 8 characters.
+use constant USER_PASSWORD_MIN_LENGTH => 8;
use constant LOGIN_OPTIONAL => 0;
use constant LOGIN_NORMAL => 1;
@@ -355,11 +356,13 @@ use constant EVT_KEYWORD => 7;
use constant EVT_CC => 8;
use constant EVT_DEPEND_BLOCK => 9;
use constant EVT_BUG_CREATED => 10;
+use constant EVT_COMPONENT => 11;
use constant POS_EVENTS => EVT_OTHER, EVT_ADDED_REMOVED, EVT_COMMENT,
EVT_ATTACHMENT, EVT_ATTACHMENT_DATA,
EVT_PROJ_MANAGEMENT, EVT_OPENED_CLOSED, EVT_KEYWORD,
- EVT_CC, EVT_DEPEND_BLOCK, EVT_BUG_CREATED;
+ EVT_CC, EVT_DEPEND_BLOCK, EVT_BUG_CREATED,
+ EVT_COMPONENT;
use constant EVT_UNCONFIRMED => 50;
use constant EVT_CHANGED_BY_ME => 51;
@@ -431,8 +434,8 @@ use constant MAX_LOGIN_ATTEMPTS => 5;
use constant LOGIN_LOCKOUT_INTERVAL => 30;
# The maximum number of seconds the Strict-Transport-Security header
-# will remain valid. Default is one week.
-use constant MAX_STS_AGE => 604800;
+# will remain valid. BMO uses one month.
+use constant MAX_STS_AGE => 2629744;
# Protocols which are considered as safe.
use constant SAFE_PROTOCOLS => ('afs', 'cid', 'ftp', 'gopher', 'http', 'https',
@@ -445,15 +448,16 @@ use constant LEGAL_CONTENT_TYPES => ('application', 'audio', 'image', 'message',
use constant contenttypes =>
{
- "html"=> "text/html" ,
- "rdf" => "application/rdf+xml" ,
- "atom"=> "application/atom+xml" ,
- "xml" => "application/xml" ,
- "js" => "application/x-javascript" ,
- "json"=> "application/json" ,
- "csv" => "text/csv" ,
- "png" => "image/png" ,
- "ics" => "text/calendar" ,
+ "html" => "text/html" ,
+ "rdf" => "application/rdf+xml" ,
+ "atom" => "application/atom+xml" ,
+ "xml" => "application/xml" ,
+ "dtd" => "application/xml-dtd" ,
+ "js" => "application/x-javascript" ,
+ "json" => "application/json" ,
+ "csv" => "text/csv" ,
+ "png" => "image/png" ,
+ "ics" => "text/calendar" ,
};
# Usage modes. Default USAGE_MODE_BROWSER. Use with Bugzilla->usage_mode.
diff --git a/Bugzilla/DB.pm b/Bugzilla/DB.pm
index 0c841632f..2f708d065 100644
--- a/Bugzilla/DB.pm
+++ b/Bugzilla/DB.pm
@@ -159,7 +159,7 @@ sub _handle_error {
# Cut down the error string to a reasonable size
$_[0] = substr($_[0], 0, 2000) . ' ... ' . substr($_[0], -2000)
if length($_[0]) > 4000;
- $_[0] = Carp::longmess($_[0]);
+ # BMO: stracktrace disabled: $_[0] = Carp::longmess($_[0]);
return 0; # Now let DBI handle raising the error
}
diff --git a/Bugzilla/DB/Schema.pm b/Bugzilla/DB/Schema.pm
index 1e598c61e..23e484464 100644
--- a/Bugzilla/DB/Schema.pm
+++ b/Bugzilla/DB/Schema.pm
@@ -342,6 +342,8 @@ use constant ABSTRACT_SCHEMA => {
bugs_activity => {
FIELDS => [
+ id => {TYPE => 'INTSERIAL', NOTNULL => 1,
+ PRIMARYKEY => 1},
bug_id => {TYPE => 'INT3', NOTNULL => 1,
REFERENCES => {TABLE => 'bugs',
COLUMN => 'bug_id',
@@ -358,8 +360,8 @@ use constant ABSTRACT_SCHEMA => {
REFERENCES => {TABLE => 'fielddefs',
COLUMN => 'id'}},
added => {TYPE => 'varchar(255)'},
- removed => {TYPE => 'TINYTEXT'},
- comment_id => {TYPE => 'INT3',
+ removed => {TYPE => 'varchar(255)'},
+ comment_id => {TYPE => 'INT4',
REFERENCES => { TABLE => 'longdescs',
COLUMN => 'comment_id',
DELETE => 'CASCADE'}},
@@ -370,6 +372,7 @@ use constant ABSTRACT_SCHEMA => {
bugs_activity_bug_when_idx => ['bug_when'],
bugs_activity_fieldid_idx => ['fieldid'],
bugs_activity_added_idx => ['added'],
+ bugs_activity_removed_idx => ['removed'],
],
},
@@ -393,7 +396,7 @@ use constant ABSTRACT_SCHEMA => {
longdescs => {
FIELDS => [
- comment_id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1,
+ comment_id => {TYPE => 'INTSERIAL', NOTNULL => 1,
PRIMARYKEY => 1},
bug_id => {TYPE => 'INT3', NOTNULL => 1,
REFERENCES => {TABLE => 'bugs',
@@ -433,7 +436,8 @@ use constant ABSTRACT_SCHEMA => {
DELETE => 'CASCADE'}},
],
INDEXES => [
- dependencies_blocked_idx => ['blocked'],
+ dependencies_blocked_idx => {FIELDS => [qw(blocked dependson)],
+ TYPE => 'UNIQUE'},
dependencies_dependson_idx => ['dependson'],
],
},
@@ -915,6 +919,8 @@ use constant ABSTRACT_SCHEMA => {
profiles_activity => {
FIELDS => [
+ id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1,
+ PRIMARYKEY => 1},
userid => {TYPE => 'INT3', NOTNULL => 1,
REFERENCES => {TABLE => 'profiles',
COLUMN => 'userid',
diff --git a/Bugzilla/Error.pm b/Bugzilla/Error.pm
index 178f6f90c..e49f466d6 100644
--- a/Bugzilla/Error.pm
+++ b/Bugzilla/Error.pm
@@ -26,8 +26,9 @@ package Bugzilla::Error;
use strict;
use base qw(Exporter);
-@Bugzilla::Error::EXPORT = qw(ThrowCodeError ThrowTemplateError ThrowUserError);
+@Bugzilla::Error::EXPORT = qw(ThrowCodeError ThrowTemplateError ThrowUserError ThrowErrorPage);
+use Bugzilla::Arecibo;
use Bugzilla::Constants;
use Bugzilla::WebService::Constants;
use Bugzilla::Util;
@@ -93,6 +94,7 @@ sub _throw_error {
my $template = Bugzilla->template;
my $message;
+
# There are some tests that throw and catch a lot of errors,
# and calling $template->process over and over for those errors
# is too slow. So instead, we just "die" with a dump of the arguments.
@@ -108,8 +110,22 @@ sub _throw_error {
message => \$message });
if (Bugzilla->error_mode == ERROR_MODE_WEBPAGE) {
+ if (arecibo_should_notify($vars->{error})) {
+ $vars->{maintainers_notified} = 1;
+ $vars->{uid} = arecibo_generate_id();
+ $vars->{processed} = {};
+ } else {
+ $vars->{maintainers_notified} = 0;
+ }
+
print Bugzilla->cgi->header();
- print $message;
+ $template->process($name, $vars)
+ || ThrowTemplateError($template->error());
+
+ if ($vars->{maintainers_notified}) {
+ arecibo_handle_error(
+ $vars->{error}, $vars->{processed}->{error_message}, $vars->{uid});
+ }
}
elsif (Bugzilla->error_mode == ERROR_MODE_TEST) {
die Dumper($vars);
@@ -183,40 +199,85 @@ sub ThrowTemplateError {
die("error: template error: $template_err");
}
+ # mod_perl overrides exit to call die with this string
+ # we never want to display this to the user
+ exit if $template_err =~ /\bModPerl::Util::exit\b/;
+
$vars->{'template_error_msg'} = $template_err;
$vars->{'error'} = "template_error";
+ $vars->{'uid'} = arecibo_generate_id();
+ arecibo_handle_error('error', $template_err, $vars->{'uid'});
+ $vars->{'template_error_msg'} =~ s/ at \S+ line \d+\.\s*$//;
+
my $template = Bugzilla->template;
# Try a template first; but if this one fails too, fall back
# on plain old print statements.
if (!$template->process("global/code-error.html.tmpl", $vars)) {
- my $maintainer = Bugzilla->params->{'maintainer'};
+ my $maintainer = html_quote(Bugzilla->params->{'maintainer'});
my $error = html_quote($vars->{'template_error_msg'});
my $error2 = html_quote($template->error());
+ my $uid = html_quote($vars->{'uid'});
print <<END;
<tt>
<p>
- Bugzilla has suffered an internal error. Please save this page and
- send it to $maintainer with details of what you were doing at the
- time this message appeared.
+ Bugzilla has suffered an internal error:
+ </p>
+ <p>
+ $error
+ </p>
+ <!-- template error, no real need to show this to the user
+ $error2
+ -->
+ <p>
+ The <a href="mailto:$maintainer">Bugzilla maintainers</a> have
+ been notified of this error [#$uid].
</p>
- <script type="text/javascript"> <!--
- document.write("<p>URL: " +
- document.location.href.replace(/&/g,"&amp;")
- .replace(/</g,"&lt;")
- .replace(/>/g,"&gt;") + "</p>");
- // -->
- </script>
- <p>Template->process() failed twice.<br>
- First error: $error<br>
- Second error: $error2</p>
</tt>
END
}
exit;
}
+sub ThrowErrorPage {
+ # BMO customisation for bug 659231
+ my ($template_name, $message) = @_;
+
+ my $dbh = Bugzilla->dbh;
+ $dbh->bz_rollback_transaction() if $dbh->bz_in_transaction();
+
+ if (Bugzilla->error_mode == ERROR_MODE_DIE) {
+ die("error: $message");
+ }
+
+ if (Bugzilla->error_mode == ERROR_MODE_DIE_SOAP_FAULT
+ || Bugzilla->error_mode == ERROR_MODE_JSON_RPC)
+ {
+ my $code = ERROR_UNKNOWN_TRANSIENT;
+ if (Bugzilla->error_mode == ERROR_MODE_DIE_SOAP_FAULT) {
+ die SOAP::Fault->faultcode($code)->faultstring($message);
+ } else {
+ my $server = Bugzilla->_json_server;
+ $server->raise_error(code => 100000 + $code,
+ message => $message,
+ id => $server->{_bz_request_id},
+ version => $server->version);
+ die if _in_eval();
+ $server->response($server->error_response_header);
+ }
+ } else {
+ my $cgi = Bugzilla->cgi;
+ my $template = Bugzilla->template;
+ my $vars = {};
+ $vars->{message} = $message;
+ print $cgi->header();
+ $template->process($template_name, $vars)
+ || ThrowTemplateError($template->error());
+ exit;
+ }
+}
+
1;
__END__
diff --git a/Bugzilla/Field.pm b/Bugzilla/Field.pm
index 81677c7ea..8ebf08672 100644
--- a/Bugzilla/Field.pm
+++ b/Bugzilla/Field.pm
@@ -78,6 +78,8 @@ use Bugzilla::Constants;
use Bugzilla::Error;
use Bugzilla::Util;
use List::MoreUtils qw(any);
+use Bugzilla::Config qw(SetParam write_params);
+use Bugzilla::Hook;
use Scalar::Util qw(blessed);
@@ -918,53 +920,64 @@ sub remove_from_db {
ThrowUserError('customfield_not_obsolete', {'name' => $self->name });
}
- $dbh->bz_start_transaction();
+ # BMO: disable bug updates during field creation
+ # using an eval as try/finally
+ eval {
+ SetParam('disable_bug_updates', 1);
+ write_params();
- # Check to see if bug activity table has records (should be fast with index)
- my $has_activity = $dbh->selectrow_array("SELECT COUNT(*) FROM bugs_activity
- WHERE fieldid = ?", undef, $self->id);
- if ($has_activity) {
- ThrowUserError('customfield_has_activity', {'name' => $name });
- }
+ $dbh->bz_start_transaction();
- # Check to see if bugs table has records (slow)
- my $bugs_query = "";
+ # Check to see if bug activity table has records (should be fast with index)
+ my $has_activity = $dbh->selectrow_array("SELECT COUNT(*) FROM bugs_activity
+ WHERE fieldid = ?", undef, $self->id);
+ if ($has_activity) {
+ ThrowUserError('customfield_has_activity', {'name' => $name });
+ }
- if ($self->type == FIELD_TYPE_MULTI_SELECT) {
- $bugs_query = "SELECT COUNT(*) FROM bug_$name";
- }
- else {
- $bugs_query = "SELECT COUNT(*) FROM bugs WHERE $name IS NOT NULL";
- if ($self->type != FIELD_TYPE_BUG_ID && $self->type != FIELD_TYPE_DATETIME) {
- $bugs_query .= " AND $name != ''";
+ # Check to see if bugs table has records (slow)
+ my $bugs_query = "";
+
+ if ($self->type == FIELD_TYPE_MULTI_SELECT) {
+ $bugs_query = "SELECT COUNT(*) FROM bug_$name";
}
- # Ignore the default single select value
- if ($self->type == FIELD_TYPE_SINGLE_SELECT) {
- $bugs_query .= " AND $name != '---'";
+ else {
+ $bugs_query = "SELECT COUNT(*) FROM bugs WHERE $name IS NOT NULL";
+ if ($self->type != FIELD_TYPE_BUG_ID && $self->type != FIELD_TYPE_DATETIME) {
+ $bugs_query .= " AND $name != ''";
+ }
+ # Ignore the default single select value
+ if ($self->type == FIELD_TYPE_SINGLE_SELECT) {
+ $bugs_query .= " AND $name != '---'";
+ }
}
- }
- my $has_bugs = $dbh->selectrow_array($bugs_query);
- if ($has_bugs) {
- ThrowUserError('customfield_has_contents', {'name' => $name });
- }
+ my $has_bugs = $dbh->selectrow_array($bugs_query);
+ if ($has_bugs) {
+ ThrowUserError('customfield_has_contents', {'name' => $name });
+ }
- # Once we reach here, we should be OK to delete.
- $dbh->do('DELETE FROM fielddefs WHERE id = ?', undef, $self->id);
+ # Once we reach here, we should be OK to delete.
+ $dbh->do('DELETE FROM fielddefs WHERE id = ?', undef, $self->id);
- my $type = $self->type;
+ my $type = $self->type;
- # the values for multi-select are stored in a seperate table
- if ($type != FIELD_TYPE_MULTI_SELECT) {
- $dbh->bz_drop_column('bugs', $name);
- }
+ # the values for multi-select are stored in a seperate table
+ if ($type != FIELD_TYPE_MULTI_SELECT) {
+ $dbh->bz_drop_column('bugs', $name);
+ }
- if ($self->is_select) {
- # Delete the table that holds the legal values for this field.
- $dbh->bz_drop_field_tables($self);
- }
+ if ($self->is_select) {
+ # Delete the table that holds the legal values for this field.
+ $dbh->bz_drop_field_tables($self);
+ }
- $dbh->bz_commit_transaction()
+ $dbh->bz_commit_transaction();
+ };
+ my $error = "$@";
+ SetParam('disable_bug_updates', 0);
+ write_params();
+ die $error if $error;
}
=pod
@@ -1012,48 +1025,67 @@ sub create {
my ($params) = @_;
my $dbh = Bugzilla->dbh;
- # This makes sure the "sortkey" validator runs, even if
- # the parameter isn't sent to create().
- $params->{sortkey} = undef if !exists $params->{sortkey};
- $params->{type} ||= 0;
- # We mark the custom field as obsolete till it has been fully created,
- # to avoid race conditions when viewing bugs at the same time.
- my $is_obsolete = $params->{obsolete};
- $params->{obsolete} = 1 if $params->{custom};
-
- $dbh->bz_start_transaction();
- $class->check_required_create_fields(@_);
- my $field_values = $class->run_create_validators($params);
- my $visibility_values = delete $field_values->{visibility_values};
- my $field = $class->insert_create_data($field_values);
-
- $field->set_visibility_values($visibility_values);
- $field->_update_visibility_values();
+ # BMO: disable bug updates during field creation
+ # using an eval as try/finally
+ my $field;
+ eval {
+ if ($params->{'custom'}) {
+ SetParam('disable_bug_updates', 1);
+ write_params();
+ }
- $dbh->bz_commit_transaction();
+ # This makes sure the "sortkey" validator runs, even if
+ # the parameter isn't sent to create().
+ $params->{sortkey} = undef if !exists $params->{sortkey};
+ $params->{type} ||= 0;
+ # We mark the custom field as obsolete till it has been fully created,
+ # to avoid race conditions when viewing bugs at the same time.
+ my $is_obsolete = $params->{obsolete};
+ $params->{obsolete} = 1 if $params->{custom};
+
+ $dbh->bz_start_transaction();
+ $class->check_required_create_fields(@_);
+ my $field_values = $class->run_create_validators($params);
+ my $visibility_values = delete $field_values->{visibility_values};
+ my $field = $class->insert_create_data($field_values);
+
+ $field->set_visibility_values($visibility_values);
+ $field->_update_visibility_values();
+
+ $dbh->bz_commit_transaction();
+
+ if ($field->custom) {
+ my $name = $field->name;
+ my $type = $field->type;
+ if (SQL_DEFINITIONS->{$type}) {
+ # Create the database column that stores the data for this field.
+ $dbh->bz_add_column('bugs', $name, SQL_DEFINITIONS->{$type});
+ }
- if ($field->custom) {
- my $name = $field->name;
- my $type = $field->type;
- if (SQL_DEFINITIONS->{$type}) {
- # Create the database column that stores the data for this field.
- $dbh->bz_add_column('bugs', $name, SQL_DEFINITIONS->{$type});
- }
+ if ($field->is_select) {
+ # Create the table that holds the legal values for this field.
+ $dbh->bz_add_field_tables($field);
+ }
- if ($field->is_select) {
- # Create the table that holds the legal values for this field.
- $dbh->bz_add_field_tables($field);
- }
+ if ($type == FIELD_TYPE_SINGLE_SELECT) {
+ # Insert a default value of "---" into the legal values table.
+ $dbh->do("INSERT INTO $name (value) VALUES ('---')");
+ }
- if ($type == FIELD_TYPE_SINGLE_SELECT) {
- # Insert a default value of "---" into the legal values table.
- $dbh->do("INSERT INTO $name (value) VALUES ('---')");
+ # Restore the original obsolete state of the custom field.
+ $dbh->do('UPDATE fielddefs SET obsolete = 0 WHERE id = ?', undef, $field->id)
+ unless $is_obsolete;
}
+ };
- # Restore the original obsolete state of the custom field.
- $dbh->do('UPDATE fielddefs SET obsolete = 0 WHERE id = ?', undef, $field->id)
- unless $is_obsolete;
+ my $error = "$@";
+ if ($params->{'custom'}) {
+ SetParam('disable_bug_updates', 0);
+ write_params();
}
+ die $error if $error;
+
+ Bugzilla::Hook::process("field_end_of_create", { field => $field });
return $field;
}
diff --git a/Bugzilla/Flag.pm b/Bugzilla/Flag.pm
index a727532a6..ba91af85c 100644
--- a/Bugzilla/Flag.pm
+++ b/Bugzilla/Flag.pm
@@ -79,17 +79,23 @@ use constant AUDIT_CREATES => 0;
use constant AUDIT_UPDATES => 0;
use constant AUDIT_REMOVES => 0;
-use constant SKIP_REQUESTEE_ON_ERROR => 1;
+use constant SKIP_REQUESTEE_ON_ERROR => 0;
-use constant DB_COLUMNS => qw(
- id
- type_id
- bug_id
- attach_id
- requestee_id
- setter_id
- status
-);
+sub DB_COLUMNS {
+ my $dbh = Bugzilla->dbh;
+ return qw(
+ id
+ type_id
+ bug_id
+ attach_id
+ requestee_id
+ setter_id
+ status),
+ $dbh->sql_date_format('creation_date', '%Y.%m.%d %H:%i:%s') .
+ ' AS creation_date',
+ $dbh->sql_date_format('modification_date', '%Y.%m.%d %H:%i:%s') .
+ ' AS modification_date';
+}
use constant UPDATE_COLUMNS => qw(
requestee_id
@@ -134,6 +140,14 @@ Returns the ID of the attachment this flag belongs to, if any.
Returns the status '+', '-', '?' of the flag.
+=item C<creation_date>
+
+Returns the timestamp when the flag was created.
+
+=item C<modification_date>
+
+Returns the timestamp when the flag was last modified.
+
=back
=cut
@@ -146,6 +160,8 @@ sub attach_id { return $_[0]->{'attach_id'}; }
sub status { return $_[0]->{'status'}; }
sub setter_id { return $_[0]->{'setter_id'}; }
sub requestee_id { return $_[0]->{'requestee_id'}; }
+sub creation_date { return $_[0]->{'creation_date'}; }
+sub modification_date { return $_[0]->{'modification_date'}; }
###############################
#### Methods ####
@@ -284,7 +300,7 @@ sub count {
sub set_flag {
my ($class, $obj, $params) = @_;
- my ($bug, $attachment);
+ my ($bug, $attachment, $obj_flag, $requestee_changed);
if (blessed($obj) && $obj->isa('Bugzilla::Attachment')) {
$attachment = $obj;
$bug = $attachment->bug;
@@ -322,13 +338,14 @@ sub set_flag {
($obj_flagtype) = grep { $_->id == $flag->type_id } @{$obj->flag_types};
push(@{$obj_flagtype->{flags}}, $flag);
}
- my ($obj_flag) = grep { $_->id == $flag->id } @{$obj_flagtype->{flags}};
+ ($obj_flag) = grep { $_->id == $flag->id } @{$obj_flagtype->{flags}};
# If the flag has the correct type but cannot be found above, this means
# the flag is going to be removed (e.g. because this is a pending request
# and the attachment is being marked as obsolete).
return unless $obj_flag;
- $class->_validate($obj_flag, $obj_flagtype, $params, $bug, $attachment);
+ ($obj_flag, $requestee_changed) =
+ $class->_validate($obj_flag, $obj_flagtype, $params, $bug, $attachment);
}
# Create a new flag.
elsif ($params->{type_id}) {
@@ -360,12 +377,21 @@ sub set_flag {
}
}
- $class->_validate(undef, $obj_flagtype, $params, $bug, $attachment);
+ ($obj_flag, $requestee_changed) =
+ $class->_validate(undef, $obj_flagtype, $params, $bug, $attachment);
}
else {
ThrowCodeError('param_required', { function => $class . '->set_flag',
param => 'id/type_id' });
}
+
+ if ($obj_flag
+ && $requestee_changed
+ && $obj_flag->requestee_id
+ && $obj_flag->requestee->setting('requestee_cc') eq 'on')
+ {
+ $bug->add_cc($obj_flag->requestee);
+ }
}
sub _validate {
@@ -385,23 +411,25 @@ sub _validate {
$obj_flag->_set_status($params->{status});
$obj_flag->_set_requestee($params->{requestee}, $attachment, $params->{skip_roe});
+ # The requestee ID can be undefined.
+ my $requestee_changed = ($obj_flag->requestee_id || 0) != ($old_requestee_id || 0);
+
# The setter field MUST NOT be updated if neither the status
# nor the requestee fields changed.
- if (($obj_flag->status ne $old_status)
- # The requestee ID can be undefined.
- || (($obj_flag->requestee_id || 0) != ($old_requestee_id || 0)))
- {
+ if (($obj_flag->status ne $old_status) || $requestee_changed) {
$obj_flag->_set_setter($params->{setter});
}
# If the flag is deleted, remove it from the list.
if ($obj_flag->status eq 'X') {
@{$flag_type->{flags}} = grep { $_->id != $obj_flag->id } @{$flag_type->{flags}};
+ return;
}
# Add the newly created flag to the list.
elsif (!$obj_flag->id) {
push(@{$flag_type->{flags}}, $obj_flag);
}
+ return wantarray ? ($obj_flag, $requestee_changed) : $obj_flag;
}
=pod
@@ -418,10 +446,14 @@ Creates a flag record in the database.
sub create {
my ($class, $flag, $timestamp) = @_;
- $timestamp ||= Bugzilla->dbh->selectrow_array('SELECT NOW()');
+ $timestamp ||= Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
my $params = {};
my @columns = grep { $_ ne 'id' } $class->_get_db_columns;
+
+ # Some columns use date formatting so use alias instead
+ @columns = map { /\s+AS\s+(.*)$/ ? $1 : $_ } @columns;
+
$params->{$_} = $flag->{$_} foreach @columns;
$params->{creation_date} = $params->{modification_date} = $timestamp;
@@ -440,6 +472,7 @@ sub update {
if (scalar(keys %$changes)) {
$dbh->do('UPDATE flags SET modification_date = ? WHERE id = ?',
undef, ($timestamp, $self->id));
+ $self->{'modification_date'} = format_time($timestamp, '%Y.%m.%d %T');
}
return $changes;
}
diff --git a/Bugzilla/FlagType.pm b/Bugzilla/FlagType.pm
index 811530c42..617ea54b7 100644
--- a/Bugzilla/FlagType.pm
+++ b/Bugzilla/FlagType.pm
@@ -601,7 +601,7 @@ sub match {
$tables = join(' ', @$tables);
$criteria = join(' AND ', @criteria);
- my $flagtype_ids = $dbh->selectcol_arrayref("SELECT id FROM $tables WHERE $criteria");
+ my $flagtype_ids = $dbh->selectcol_arrayref("SELECT flagtypes.id FROM $tables WHERE $criteria");
return Bugzilla::FlagType->new_from_list($flagtype_ids);
}
@@ -679,6 +679,11 @@ sub sqlify_criteria {
my $is_active = $criteria->{is_active} ? "1" : "0";
push(@criteria, "flagtypes.is_active = $is_active");
}
+ if (exists($criteria->{active_or_has_flags}) && $criteria->{active_or_has_flags} =~ /^\d+$/) {
+ push(@$tables, "LEFT JOIN flags AS f ON flagtypes.id = f.type_id " .
+ "AND f.bug_id = " . $criteria->{active_or_has_flags});
+ push(@criteria, "(flagtypes.is_active = 1 OR f.id IS NOT NULL)");
+ }
if ($criteria->{product_id}) {
my $product_id = $criteria->{product_id};
detaint_natural($product_id)
diff --git a/Bugzilla/Group.pm b/Bugzilla/Group.pm
index 382407748..109f06d7f 100644
--- a/Bugzilla/Group.pm
+++ b/Bugzilla/Group.pm
@@ -119,9 +119,10 @@ sub _get_members {
}
sub flag_types {
- my $self = shift;
+ my ($self, $params) = @_;
+ $params ||= {};
require Bugzilla::FlagType;
- $self->{flag_types} ||= Bugzilla::FlagType::match({ group => $self->id });
+ $self->{flag_types} ||= Bugzilla::FlagType::match({ group => $self->id, %$params });
return $self->{flag_types};
}
diff --git a/Bugzilla/Hook.pm b/Bugzilla/Hook.pm
index c658989a0..27d70e7f5 100644
--- a/Bugzilla/Hook.pm
+++ b/Bugzilla/Hook.pm
@@ -1289,6 +1289,22 @@ your template.
=back
+=head2 path_info_whitelist
+
+By default, Bugzilla removes the Path-Info information from URLs before
+passing data to CGI scripts. If this information is needed for your
+customizations, you can enumerate the pages you want to whitelist here.
+
+Params:
+
+=over
+
+=item C<whitelist>
+
+An array of script names that will not have their Path-Info automatically
+removed.
+
+=back
=head2 post_bug_after_creation
diff --git a/Bugzilla/Install.pm b/Bugzilla/Install.pm
index ce8fe6bad..6019c9d18 100644
--- a/Bugzilla/Install.pm
+++ b/Bugzilla/Install.pm
@@ -93,6 +93,10 @@ sub SETTINGS {
# 2011-06-21 glob@mozilla.com -- Bug 589128
email_format => { options => ['html', 'text_only'],
default => 'html' },
+ # 2011-06-16 glob@mozilla.com -- Bug 663747
+ bugmail_new_prefix => { options => ['on', 'off'], default => 'on' },
+ # 2011-10-11 glob@mozilla.com -- Bug 301656
+ requestee_cc => { options => ['on', 'off'], default => 'on' },
}
};
diff --git a/Bugzilla/Install/DB.pm b/Bugzilla/Install/DB.pm
index 6b9dd65cd..d86d6e177 100644
--- a/Bugzilla/Install/DB.pm
+++ b/Bugzilla/Install/DB.pm
@@ -398,7 +398,7 @@ sub update_table_definitions {
"WHERE initialqacontact = 0");
_migrate_email_prefs_to_new_table();
- _initialize_dependency_tree_changes_email_pref();
+ _initialize_new_email_prefs();
_change_all_mysql_booleans_to_tinyint();
# make classification_id field type be consistent with DB:Schema
@@ -455,7 +455,7 @@ sub update_table_definitions {
# 2005-12-07 altlst@sonic.net -- Bug 225221
$dbh->bz_add_column('longdescs', 'comment_id',
- {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1});
+ {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1});
_stop_storing_inactive_flags();
_change_short_desc_from_mediumtext_to_varchar();
@@ -607,7 +607,7 @@ sub update_table_definitions {
_fix_series_creator_fk();
# 2009-11-14 dkl@redhat.com - Bug 310450
- $dbh->bz_add_column('bugs_activity', 'comment_id', {TYPE => 'INT3'});
+ $dbh->bz_add_column('bugs_activity', 'comment_id', {TYPE => 'INT4'});
# 2010-04-07 LpSolit@gmail.com - Bug 69621
$dbh->bz_drop_column('bugs', 'keywords');
@@ -669,6 +669,30 @@ sub update_table_definitions {
$dbh->bz_add_index('profile_search', 'profile_search_user_id_idx', [qw(user_id)]);
}
+ # 2012-06-06 dkl@mozilla.com - Bug 762288
+ $dbh->bz_alter_column('bugs_activity', 'removed',
+ { TYPE => 'varchar(255)' });
+ $dbh->bz_add_index('bugs_activity', 'bugs_activity_removed_idx', ['removed']);
+
+ # 2012-06-13 dkl@mozilla.com - Bug 764457
+ $dbh->bz_add_column('bugs_activity', 'id',
+ {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1});
+
+ # 2012-06-13 dkl@mozilla.com - Bug 764466
+ $dbh->bz_add_column('profiles_activity', 'id',
+ {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1});
+
+ # 2012-07-24 dkl@mozilla.com - Bug 776972
+ $dbh->bz_alter_column('bugs_activity', 'id',
+ {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1});
+
+
+ # 2012-07-24 dkl@mozilla.com - Bug 776982
+ _fix_longdescs_primary_key();
+
+ # 2012-08-02 dkl@mozilla.com - Bug 756953
+ _fix_dependencies_dupes();
+
################################################################
# New --TABLE-- changes should go *** A B O V E *** this point #
################################################################
@@ -2396,13 +2420,16 @@ sub _migrate_email_prefs_to_new_table {
}
}
-sub _initialize_dependency_tree_changes_email_pref {
+sub _initialize_new_email_prefs {
my $dbh = Bugzilla->dbh;
# Check for any "new" email settings that wouldn't have been ported over
# during the block above. Since these settings would have otherwise
# fallen under EVT_OTHER, we'll just clone those settings. That way if
# folks have already disabled all of that mail, there won't be any change.
- my %events = ("Dependency Tree Changes" => EVT_DEPEND_BLOCK);
+ my %events = (
+ "Dependency Tree Changes" => EVT_DEPEND_BLOCK,
+ "Product/Component Changes" => EVT_COMPONENT,
+ );
foreach my $desc (keys %events) {
my $event = $events{$desc};
@@ -3220,6 +3247,11 @@ sub _populate_bugs_fulltext {
print "Populating bugs_fulltext with $num_bugs entries...";
print " (this can take a long time.)\n";
}
+
+ # As recommended by Monty Widenius for GNOME's upgrade.
+ # mkanat and justdave concur it'll be helpful for bmo, too.
+ $dbh->do('SET SESSION myisam_sort_buffer_size = 3221225472');
+
my $newline = $dbh->quote("\n");
$dbh->do(
qq{$command INTO bugs_fulltext (bug_id, short_desc, comments,
@@ -3687,6 +3719,41 @@ sub _fix_notnull_defaults {
}
}
+sub _fix_longdescs_primary_key {
+ my $dbh = Bugzilla->dbh;
+ if ($dbh->bz_column_info('longdescs', 'comment_id')->{TYPE} ne 'INTSERIAL') {
+ $dbh->bz_drop_related_fks('longdescs', 'comment_id');
+ $dbh->bz_alter_column('bugs_activity', 'comment_id', {TYPE => 'INT4'});
+ $dbh->bz_alter_column('longdescs', 'comment_id',
+ {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1});
+ }
+}
+
+sub _fix_dependencies_dupes {
+ my $dbh = Bugzilla->dbh;
+ my $blocked_idx = $dbh->bz_index_info('dependencies', 'dependencies_blocked_idx');
+ if ($blocked_idx && scalar @{$blocked_idx->{'FIELDS'}} < 2) {
+ # Remove duplicated entries
+ my $dupes = $dbh->selectall_arrayref("
+ SELECT blocked, dependson, COUNT(*) AS count
+ FROM dependencies " .
+ $dbh->sql_group_by('blocked, dependson') . "
+ HAVING COUNT(*) > 1",
+ { Slice => {} });
+ print "Removing duplicated entries from the 'dependencies' table...\n" if @$dupes;
+ foreach my $dupe (@$dupes) {
+ $dbh->do("DELETE FROM dependencies
+ WHERE blocked = ? AND dependson = ?",
+ undef, $dupe->{blocked}, $dupe->{dependson});
+ $dbh->do("INSERT INTO dependencies (blocked, dependson) VALUES (?, ?)",
+ undef, $dupe->{blocked}, $dupe->{dependson});
+ }
+ $dbh->bz_drop_index('dependencies', 'dependencies_blocked_idx');
+ $dbh->bz_add_index('dependencies', 'dependencies_blocked_idx',
+ { FIELDS => [qw(blocked dependson)], TYPE => 'UNIQUE' });
+ }
+}
+
1;
__END__
diff --git a/Bugzilla/Install/Filesystem.pm b/Bugzilla/Install/Filesystem.pm
index c5215ecfa..c3f103aaa 100644
--- a/Bugzilla/Install/Filesystem.pm
+++ b/Bugzilla/Install/Filesystem.pm
@@ -170,6 +170,7 @@ sub FILESYSTEM {
'contrib/README' => { perms => OWNER_WRITE },
'contrib/*/README' => { perms => OWNER_WRITE },
+ 'contrib/sendunsentbugmail.pl' => { perms => WS_EXECUTE },
'docs/bugzilla.ent' => { perms => OWNER_WRITE },
'docs/makedocs.pl' => { perms => OWNER_EXECUTE },
'docs/style.css' => { perms => WS_SERVE },
@@ -184,8 +185,10 @@ sub FILESYSTEM {
# Directories that we want to set the perms on, but not
# recurse through. These are directories we didn't create
# in checkesetup.pl.
+ #
+ # Purpose of BMO change: unknown.
my %non_recurse_dirs = (
- '.' => DIR_WS_SERVE,
+ '.' => 0755,
docs => DIR_WS_SERVE,
);
@@ -243,10 +246,13 @@ sub FILESYSTEM {
dirs => DIR_WS_SERVE },
"$extensionsdir/*/web" => { files => WS_SERVE,
dirs => DIR_WS_SERVE },
-
+
+ # Purpose: allow webserver to read .bzr so we execute bzr commands
+ # in backticks and look at the result over the web. Used to show
+ # bzr history.
+ '.bzr' => { files => WS_SERVE,
+ dirs => DIR_WS_SERVE },
# Directories only for the owner, not for the webserver.
- '.bzr' => { files => OWNER_WRITE,
- dirs => DIR_OWNER_WRITE },
t => { files => OWNER_WRITE,
dirs => DIR_OWNER_WRITE },
xt => { files => OWNER_WRITE,
diff --git a/Bugzilla/Mailer.pm b/Bugzilla/Mailer.pm
index 7e42cb609..d27f79155 100644
--- a/Bugzilla/Mailer.pm
+++ b/Bugzilla/Mailer.pm
@@ -49,6 +49,7 @@ use Encode::MIME::Header;
use Email::Address;
use Email::MIME;
use Email::Send;
+use Sys::Hostname;
sub MessageToMTA {
my ($msg, $send_now) = (@_);
@@ -87,29 +88,6 @@ sub MessageToMTA {
# thus to hopefully avoid auto replies.
$email->header_set('Auto-Submitted', 'auto-generated');
- $email->walk_parts(sub {
- my ($part) = @_;
- return if $part->parts > 1; # Top-level
- my $content_type = $part->content_type || '';
- $content_type =~ /charset=['"](.+)['"]/;
- # If no charset is defined or is the default us-ascii,
- # then we encode the email to UTF-8 if Bugzilla has utf8 enabled.
- # XXX - This is a hack to workaround bug 723944.
- if (!$1 || $1 eq 'us-ascii') {
- my $body = $part->body;
- if (Bugzilla->params->{'utf8'}) {
- $part->charset_set('UTF-8');
- # encoding_set works only with bytes, not with utf8 strings.
- my $raw = $part->body_raw;
- if (utf8::is_utf8($raw)) {
- utf8::encode($raw);
- $part->body_set($raw);
- }
- }
- $part->encoding_set('quoted-printable') if !is_7bit_clean($body);
- }
- });
-
# MIME-Version must be set otherwise some mailsystems ignore the charset
$email->header_set('MIME-Version', '1.0') if !$email->header('MIME-Version');
@@ -134,7 +112,9 @@ sub MessageToMTA {
my $from = $email->header('From');
my ($hostname, @args);
+ my $mailer_class = $method;
if ($method eq "Sendmail") {
+ $mailer_class = 'Bugzilla::Send::Sendmail';
if (ON_WINDOWS) {
$Email::Send::Sendmail::SENDMAIL = SENDMAIL_EXE;
}
@@ -163,6 +143,12 @@ sub MessageToMTA {
}
}
+ # For tracking/diagnostic purposes, add our hostname
+ my $generated_by = $email->header('X-Generated-By') || '';
+ if ($generated_by =~ tr/\/// < 3) {
+ $email->header_set('X-Generated-By' => $generated_by . '/' . hostname() . "($$)");
+ }
+
if ($method eq "SMTP") {
push @args, Host => Bugzilla->params->{"smtpserver"},
username => Bugzilla->params->{"smtp_username"},
@@ -174,6 +160,29 @@ sub MessageToMTA {
Bugzilla::Hook::process('mailer_before_send',
{ email => $email, mailer_args => \@args });
+ $email->walk_parts(sub {
+ my ($part) = @_;
+ return if $part->parts > 1; # Top-level
+ my $content_type = $part->content_type || '';
+ $content_type =~ /charset=['"](.+)['"]/;
+ # If no charset is defined or is the default us-ascii,
+ # then we encode the email to UTF-8 if Bugzilla has utf8 enabled.
+ # XXX - This is a hack to workaround bug 723944.
+ if (!$1 || $1 eq 'us-ascii') {
+ my $body = $part->body;
+ if (Bugzilla->params->{'utf8'}) {
+ $part->charset_set('UTF-8');
+ # encoding_set works only with bytes, not with utf8 strings.
+ my $raw = $part->body_raw;
+ if (utf8::is_utf8($raw)) {
+ utf8::encode($raw);
+ $part->body_set($raw);
+ }
+ }
+ $part->encoding_set('quoted-printable') if !is_7bit_clean($body);
+ }
+ });
+
if ($method eq "Test") {
my $filename = bz_locations()->{'datadir'} . '/mailer.testfile';
open TESTFILE, '>>', $filename;
@@ -184,7 +193,7 @@ sub MessageToMTA {
else {
# This is useful for both Sendmail and Qmail, so we put it out here.
local $ENV{PATH} = SENDMAIL_PATH;
- my $mailer = Email::Send->new({ mailer => $method,
+ my $mailer = Email::Send->new({ mailer => $mailer_class,
mailer_args => \@args });
my $retval = $mailer->send($email);
ThrowCodeError('mail_send_error', { msg => $retval, mail => $email })
diff --git a/Bugzilla/Object.pm b/Bugzilla/Object.pm
index d4574abd2..8089c6ccc 100644
--- a/Bugzilla/Object.pm
+++ b/Bugzilla/Object.pm
@@ -228,8 +228,11 @@ sub match {
}
next;
}
-
- $class->_check_field($field, 'match');
+
+ # It's always safe to use the field defined by classes as being
+ # their ID field. In particular, this means that new_from_list()
+ # is exempted from this check.
+ $class->_check_field($field, 'match') unless $field eq $class->ID_FIELD;
if (ref $value eq 'ARRAY') {
# IN () is invalid SQL, and if we have an empty list
@@ -332,12 +335,17 @@ sub set_all {
my %field_values = %$params;
my @sorted_names = $self->_sort_by_dep(keys %field_values);
+
foreach my $key (@sorted_names) {
# It's possible for one set_ method to delete a key from $params
# for another set method, so if that's happened, we don't call the
# other set method.
next if !exists $field_values{$key};
my $method = "set_$key";
+ if (!$self->can($method)) {
+ my $class = ref($self) || $self;
+ ThrowCodeError("unknown_method", { method => "${class}::${method}" });
+ }
$self->$method($field_values{$key}, \%field_values);
}
Bugzilla::Hook::process('object_end_of_set_all',
diff --git a/Bugzilla/PatchReader.pm b/Bugzilla/PatchReader.pm
new file mode 100644
index 000000000..b5c3b957b
--- /dev/null
+++ b/Bugzilla/PatchReader.pm
@@ -0,0 +1,117 @@
+package Bugzilla::PatchReader;
+
+use strict;
+
+=head1 NAME
+
+PatchReader - Utilities to read and manipulate patches and CVS
+
+=head1 SYNOPSIS
+
+ # Script that reads in a patch (in any known format), and prints
+ # out some information about it. Other common operations are
+ # outputting the patch in a raw unified diff format, outputting
+ # the patch information to Template::Toolkit templates, adding
+ # context to a patch from CVS, and narrowing the patch down to
+ # apply only to a single file or set of files.
+
+ use PatchReader::Raw;
+ use PatchReader::PatchInfoGrabber;
+ my $filename = 'filename.patch';
+
+ # Create the reader that parses the patch and the object that
+ # extracts info from the reader's datastream
+ my $reader = new PatchReader::Raw();
+ my $patch_info_grabber = new PatchReader::PatchInfoGrabber();
+ $reader->sends_data_to($patch_info_grabber);
+
+ # Iterate over the file
+ $reader->iterate_file($filename);
+
+ # Print the output
+ my $patch_info = $patch_info_grabber->patch_info();
+ print "Summary of Changed Files:\n";
+ while (my ($file, $info) = each %{$patch_info->{files}}) {
+ print "$file: +$info->{plus_lines} -$info->{minus_lines}\n";
+ }
+
+=head1 ABSTRACT
+
+This perl library allows you to manipulate patches programmatically by
+chaining together a variety of objects that read, manipulate, and output
+patch information:
+
+=over
+
+=item PatchReader::Raw
+
+Parse a patch in any format known to this author (unified, normal, cvs diff,
+among others)
+
+=item PatchReader::PatchInfoGrabber
+
+Grab summary info for sections of a patch in a nice hash
+
+=item PatchReader::AddCVSContext
+
+Add context to the patch by grabbing the original files from CVS
+
+=item PatchReader::NarrowPatch
+
+Narrow a patch down to only apply to a specific set of files
+
+=item PatchReader::DiffPrinter::raw
+
+Output the parsed patch in raw unified diff format
+
+=item PatchReader::DiffPrinter::template
+
+Output the parsed patch to L<Template::Toolkit> templates (can be used to make
+HTML output or anything else you please)
+
+=back
+
+Additionally, it is designed so that you can plug in your own objects that
+read the parsed data while it is being parsed (no need for the performance or
+memory problems that can come from reading in the entire patch all at once).
+You can do this by mimicking one of the existing readers (such as
+PatchInfoGrabber) and overriding the methods start_patch, start_file, section,
+end_file and end_patch.
+
+=head1 AUTHORS
+
+ John Keiser <jkeiser@cpan.org>
+ Teemu Mannermaa <tmannerm@cpan.org>
+
+=head1 COPYRIGHT AND LICENSE
+
+ Copyright (C) 2003-2004, John Keiser and
+ Copyright (C) 2011-2012, Teemu Mannermaa.
+
+This module is free software; you can redistribute it and/or modify it under
+the terms of the Artistic License 1.0. For details, see the full text of the
+license at
+ <http://www.perlfoundation.org/artistic_license_1_0>.
+
+This module is distributed in the hope that it will be useful, but it is
+provided “as is” and without any warranty; without even the implied warranty
+of merchantability or fitness for a particular purpose.
+
+Files with different licenses or copyright holders:
+
+=over
+
+=item F<lib/PatchReader/CVSClient.pm>
+
+Portions created by Netscape are
+Copyright (C) 2003, Netscape Communications Corporation. All rights reserved.
+
+This file is subject to the terms of the Mozilla Public License, v. 2.0.
+
+=back
+
+=cut
+
+$Bugzilla::PatchReader::VERSION = '0.9.7';
+
+1
diff --git a/Bugzilla/PatchReader/AddCVSContext.pm b/Bugzilla/PatchReader/AddCVSContext.pm
new file mode 100644
index 000000000..910e45669
--- /dev/null
+++ b/Bugzilla/PatchReader/AddCVSContext.pm
@@ -0,0 +1,226 @@
+package Bugzilla::PatchReader::AddCVSContext;
+
+use Bugzilla::PatchReader::FilterPatch;
+use Bugzilla::PatchReader::CVSClient;
+use Cwd;
+use File::Temp;
+
+use strict;
+
+@Bugzilla::PatchReader::AddCVSContext::ISA = qw(Bugzilla::PatchReader::FilterPatch);
+
+# XXX If you need to, get the entire patch worth of files and do a single
+# cvs update of all files as soon as you find a file where you need to do a
+# cvs update, to avoid the significant connect overhead
+sub new {
+ my $class = shift;
+ $class = ref($class) || $class;
+ my $this = $class->SUPER::new();
+ bless $this, $class;
+
+ $this->{CONTEXT} = $_[0];
+ $this->{CVSROOT} = $_[1];
+
+ return $this;
+}
+
+sub my_rmtree {
+ my ($this, $dir) = @_;
+ foreach my $file (glob("$dir/*")) {
+ if (-d $file) {
+ $this->my_rmtree($file);
+ } else {
+ trick_taint($file);
+ unlink $file;
+ }
+ }
+ trick_taint($dir);
+ rmdir $dir;
+}
+
+sub end_patch {
+ my $this = shift;
+ if (exists($this->{TMPDIR})) {
+ # Set as variable to get rid of taint
+ # One would like to use rmtree here, but that is not taint-safe.
+ $this->my_rmtree($this->{TMPDIR});
+ }
+}
+
+sub start_file {
+ my $this = shift;
+ my ($file) = @_;
+ $this->{HAS_CVS_CONTEXT} = !$file->{is_add} && !$file->{is_remove} &&
+ $file->{old_revision};
+ $this->{REVISION} = $file->{old_revision};
+ $this->{FILENAME} = $file->{filename};
+ $this->{SECTION_END} = -1;
+ $this->{TARGET}->start_file(@_) if $this->{TARGET};
+}
+
+sub end_file {
+ my $this = shift;
+ $this->flush_section();
+
+ if ($this->{FILE}) {
+ close $this->{FILE};
+ unlink $this->{FILE}; # If it fails, it fails ...
+ delete $this->{FILE};
+ }
+ $this->{TARGET}->end_file(@_) if $this->{TARGET};
+}
+
+sub next_section {
+ my $this = shift;
+ my ($section) = @_;
+ $this->{NEXT_PATCH_LINE} = $section->{old_start};
+ $this->{NEXT_NEW_LINE} = $section->{new_start};
+ foreach my $line (@{$section->{lines}}) {
+ # If this is a line requiring context ...
+ if ($line =~ /^[-\+]/) {
+ # Determine how much context is needed for both the previous section line
+ # and this one:
+ # - If there is no old line, start new section
+ # - If this is file context, add (old section end to new line) context to
+ # the existing section
+ # - If old end context line + 1 < new start context line, there is an empty
+ # space and therefore we end the old section and start the new one
+ # - Else we add (old start context line through new line) context to
+ # existing section
+ if (! exists($this->{SECTION})) {
+ $this->_start_section();
+ } elsif ($this->{CONTEXT} eq "file") {
+ $this->push_context_lines($this->{SECTION_END} + 1,
+ $this->{NEXT_PATCH_LINE} - 1);
+ } else {
+ my $start_context = $this->{NEXT_PATCH_LINE} - $this->{CONTEXT};
+ $start_context = $start_context > 0 ? $start_context : 0;
+ if (($this->{SECTION_END} + $this->{CONTEXT} + 1) < $start_context) {
+ $this->flush_section();
+ $this->_start_section();
+ } else {
+ $this->push_context_lines($this->{SECTION_END} + 1,
+ $this->{NEXT_PATCH_LINE} - 1);
+ }
+ }
+ push @{$this->{SECTION}{lines}}, $line;
+ if (substr($line, 0, 1) eq "+") {
+ $this->{SECTION}{plus_lines}++;
+ $this->{SECTION}{new_lines}++;
+ $this->{NEXT_NEW_LINE}++;
+ } else {
+ $this->{SECTION_END}++;
+ $this->{SECTION}{minus_lines}++;
+ $this->{SECTION}{old_lines}++;
+ $this->{NEXT_PATCH_LINE}++;
+ }
+ } else {
+ $this->{NEXT_PATCH_LINE}++;
+ $this->{NEXT_NEW_LINE}++;
+ }
+ # If this is context, for now lose it (later we should try and determine if
+ # we can just use it instead of pulling the file all the time)
+ }
+}
+
+sub determine_start {
+ my ($this, $line) = @_;
+ return 0 if $line < 0;
+ if ($this->{CONTEXT} eq "file") {
+ return 1;
+ } else {
+ my $start = $line - $this->{CONTEXT};
+ $start = $start > 0 ? $start : 1;
+ return $start;
+ }
+}
+
+sub _start_section {
+ my $this = shift;
+
+ # Add the context to the beginning
+ $this->{SECTION}{old_start} = $this->determine_start($this->{NEXT_PATCH_LINE});
+ $this->{SECTION}{new_start} = $this->determine_start($this->{NEXT_NEW_LINE});
+ $this->{SECTION}{old_lines} = 0;
+ $this->{SECTION}{new_lines} = 0;
+ $this->{SECTION}{minus_lines} = 0;
+ $this->{SECTION}{plus_lines} = 0;
+ $this->{SECTION_END} = $this->{SECTION}{old_start} - 1;
+ $this->push_context_lines($this->{SECTION}{old_start},
+ $this->{NEXT_PATCH_LINE} - 1);
+}
+
+sub flush_section {
+ my $this = shift;
+
+ if ($this->{SECTION}) {
+ # Add the necessary context to the end
+ if ($this->{CONTEXT} eq "file") {
+ $this->push_context_lines($this->{SECTION_END} + 1, "file");
+ } else {
+ $this->push_context_lines($this->{SECTION_END} + 1,
+ $this->{SECTION_END} + $this->{CONTEXT});
+ }
+ # Send the section and line notifications
+ $this->{TARGET}->next_section($this->{SECTION}) if $this->{TARGET};
+ delete $this->{SECTION};
+ $this->{SECTION_END} = 0;
+ }
+}
+
+sub push_context_lines {
+ my $this = shift;
+ # Grab from start to end
+ my ($start, $end) = @_;
+ return if $end ne "file" && $start > $end;
+
+ # If it's an added / removed file, don't do anything
+ return if ! $this->{HAS_CVS_CONTEXT};
+
+ # Get and open the file if necessary
+ if (!$this->{FILE}) {
+ my $olddir = getcwd();
+ if (! exists($this->{TMPDIR})) {
+ $this->{TMPDIR} = File::Temp::tempdir();
+ if (! -d $this->{TMPDIR}) {
+ die "Could not get temporary directory";
+ }
+ }
+ chdir($this->{TMPDIR}) or die "Could not cd $this->{TMPDIR}";
+ if (Bugzilla::PatchReader::CVSClient::cvs_co_rev($this->{CVSROOT}, $this->{REVISION}, $this->{FILENAME})) {
+ die "Could not check out $this->{FILENAME} r$this->{REVISION} from $this->{CVSROOT}";
+ }
+ open my $fh, $this->{FILENAME} or die "Could not open $this->{FILENAME}";
+ $this->{FILE} = $fh;
+ $this->{NEXT_FILE_LINE} = 1;
+ trick_taint($olddir); # $olddir comes from getcwd()
+ chdir($olddir) or die "Could not cd back to $olddir";
+ }
+
+ # Read through the file to reach the line we need
+ die "File read too far!" if $this->{NEXT_FILE_LINE} && $this->{NEXT_FILE_LINE} > $start;
+ my $fh = $this->{FILE};
+ while ($this->{NEXT_FILE_LINE} < $start) {
+ my $dummy = <$fh>;
+ $this->{NEXT_FILE_LINE}++;
+ }
+ my $i = $start;
+ for (; $end eq "file" || $i <= $end; $i++) {
+ my $line = <$fh>;
+ last if !defined($line);
+ $line =~ s/\r\n/\n/g;
+ push @{$this->{SECTION}{lines}}, " $line";
+ $this->{NEXT_FILE_LINE}++;
+ $this->{SECTION}{old_lines}++;
+ $this->{SECTION}{new_lines}++;
+ }
+ $this->{SECTION_END} = $i - 1;
+}
+
+sub trick_taint {
+ $_[0] =~ /^(.*)$/s;
+ $_[0] = $1;
+ return (defined($_[0]));
+}
+
+1;
diff --git a/Bugzilla/PatchReader/Base.pm b/Bugzilla/PatchReader/Base.pm
new file mode 100644
index 000000000..f2fd69a68
--- /dev/null
+++ b/Bugzilla/PatchReader/Base.pm
@@ -0,0 +1,23 @@
+package Bugzilla::PatchReader::Base;
+
+use strict;
+
+sub new {
+ my $class = shift;
+ $class = ref($class) || $class;
+ my $this = {};
+ bless $this, $class;
+
+ return $this;
+}
+
+sub sends_data_to {
+ my $this = shift;
+ if (defined($_[0])) {
+ $this->{TARGET} = $_[0];
+ } else {
+ return $this->{TARGET};
+ }
+}
+
+1
diff --git a/Bugzilla/PatchReader/CVSClient.pm b/Bugzilla/PatchReader/CVSClient.pm
new file mode 100644
index 000000000..2f76fc18d
--- /dev/null
+++ b/Bugzilla/PatchReader/CVSClient.pm
@@ -0,0 +1,48 @@
+# -*- Mode: perl; indent-tabs-mode: nil -*-
+#
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::PatchReader::CVSClient;
+
+use strict;
+
+sub parse_cvsroot {
+ my $cvsroot = $_[0];
+ # Format: :method:[user[:password]@]server[:[port]]/path
+ if ($cvsroot =~ /^:([^:]*):(.*?)(\/.*)$/) {
+ my %retval;
+ $retval{protocol} = $1;
+ $retval{rootdir} = $3;
+ my $remote = $2;
+ if ($remote =~ /^(([^\@:]*)(:([^\@]*))?\@)?([^:]*)(:(.*))?$/) {
+ $retval{user} = $2;
+ $retval{password} = $4;
+ $retval{server} = $5;
+ $retval{port} = $7;
+ return %retval;
+ }
+ }
+
+ return (
+ rootdir => $cvsroot
+ );
+}
+
+sub cvs_co {
+ my ($cvsroot, @files) = @_;
+ my $cvs = $::cvsbin || "cvs";
+ return system($cvs, "-Q", "-d$cvsroot", "co", @files);
+}
+
+sub cvs_co_rev {
+ my ($cvsroot, $rev, @files) = @_;
+ my $cvs = $::cvsbin || "cvs";
+ return system($cvs, "-Q", "-d$cvsroot", "co", "-r$rev", @files);
+}
+
+1
diff --git a/Bugzilla/PatchReader/DiffPrinter/raw.pm b/Bugzilla/PatchReader/DiffPrinter/raw.pm
new file mode 100644
index 000000000..ceb425800
--- /dev/null
+++ b/Bugzilla/PatchReader/DiffPrinter/raw.pm
@@ -0,0 +1,61 @@
+package Bugzilla::PatchReader::DiffPrinter::raw;
+
+use strict;
+
+sub new {
+ my $class = shift;
+ $class = ref($class) || $class;
+ my $this = {};
+ bless $this, $class;
+
+ $this->{OUTFILE} = @_ ? $_[0] : *STDOUT;
+ my $fh = $this->{OUTFILE};
+
+ return $this;
+}
+
+sub start_patch {
+}
+
+sub end_patch {
+}
+
+sub start_file {
+ my $this = shift;
+ my ($file) = @_;
+
+ my $fh = $this->{OUTFILE};
+ if ($file->{rcs_filename}) {
+ print $fh "Index: $file->{filename}\n";
+ print $fh "===================================================================\n";
+ print $fh "RCS file: $file->{rcs_filename}\n";
+ }
+ my $old_file = $file->{is_add} ? "/dev/null" : $file->{filename};
+ my $old_date = $file->{old_date_str} || "";
+ print $fh "--- $old_file\t$old_date";
+ print $fh "\t$file->{old_revision}" if $file->{old_revision};
+ print $fh "\n";
+ my $new_file = $file->{is_remove} ? "/dev/null" : $file->{filename};
+ my $new_date = $file->{new_date_str} || "";
+ print $fh "+++ $new_file\t$new_date";
+ print $fh "\t$file->{new_revision}" if $file->{new_revision};
+ print $fh "\n";
+}
+
+sub end_file {
+}
+
+sub next_section {
+ my $this = shift;
+ my ($section) = @_;
+
+ return unless $section->{old_start} || $section->{new_start};
+ my $fh = $this->{OUTFILE};
+ print $fh "@@ -$section->{old_start},$section->{old_lines} +$section->{new_start},$section->{new_lines} @@ $section->{func_info}\n";
+ foreach my $line (@{$section->{lines}}) {
+ $line =~ s/(\r?\n?)$/\n/;
+ print $fh $line;
+ }
+}
+
+1
diff --git a/Bugzilla/PatchReader/DiffPrinter/template.pm b/Bugzilla/PatchReader/DiffPrinter/template.pm
new file mode 100644
index 000000000..6545e9336
--- /dev/null
+++ b/Bugzilla/PatchReader/DiffPrinter/template.pm
@@ -0,0 +1,119 @@
+package Bugzilla::PatchReader::DiffPrinter::template;
+
+use strict;
+
+sub new {
+ my $class = shift;
+ $class = ref($class) || $class;
+ my $this = {};
+ bless $this, $class;
+
+ $this->{TEMPLATE_PROCESSOR} = $_[0];
+ $this->{HEADER_TEMPLATE} = $_[1];
+ $this->{FILE_TEMPLATE} = $_[2];
+ $this->{FOOTER_TEMPLATE} = $_[3];
+ $this->{ARGS} = $_[4] || {};
+
+ $this->{ARGS}{file_count} = 0;
+ return $this;
+}
+
+sub start_patch {
+ my $this = shift;
+ $this->{TEMPLATE_PROCESSOR}->process($this->{HEADER_TEMPLATE}, $this->{ARGS})
+ || ::ThrowTemplateError($this->{TEMPLATE_PROCESSOR}->error());
+}
+
+sub end_patch {
+ my $this = shift;
+ $this->{TEMPLATE_PROCESSOR}->process($this->{FOOTER_TEMPLATE}, $this->{ARGS})
+ || ::ThrowTemplateError($this->{TEMPLATE_PROCESSOR}->error());
+}
+
+sub start_file {
+ my $this = shift;
+ $this->{ARGS}{file_count}++;
+ $this->{ARGS}{file} = shift;
+ $this->{ARGS}{file}{plus_lines} = 0;
+ $this->{ARGS}{file}{minus_lines} = 0;
+ @{$this->{ARGS}{sections}} = ();
+}
+
+sub end_file {
+ my $this = shift;
+ my $file = $this->{ARGS}{file};
+ if ($file->{canonical} && $file->{old_revision} && $this->{ARGS}{bonsai_url}) {
+ $this->{ARGS}{bonsai_prefix} = "$this->{ARGS}{bonsai_url}/cvsblame.cgi?file=$file->{filename}&amp;rev=$file->{old_revision}";
+ }
+ if ($file->{canonical} && $this->{ARGS}{lxr_url}) {
+ # Cut off the lxr root, if any
+ my $filename = $file->{filename};
+ $filename = substr($filename, length($this->{ARGS}{lxr_root}));
+ $this->{ARGS}{lxr_prefix} = "$this->{ARGS}{lxr_url}/source/$filename";
+ }
+
+ $this->{TEMPLATE_PROCESSOR}->process($this->{FILE_TEMPLATE}, $this->{ARGS})
+ || ::ThrowTemplateError($this->{TEMPLATE_PROCESSOR}->error());
+ @{$this->{ARGS}{sections}} = ();
+ delete $this->{ARGS}{file};
+}
+
+sub next_section {
+ my $this = shift;
+ my ($section) = @_;
+
+ $this->{ARGS}{file}{plus_lines} += $section->{plus_lines};
+ $this->{ARGS}{file}{minus_lines} += $section->{minus_lines};
+
+ # Get groups of lines and print them
+ my $last_line_char = '';
+ my $context_lines = [];
+ my $plus_lines = [];
+ my $minus_lines = [];
+ foreach my $line (@{$section->{lines}}) {
+ $line =~ s/\r?\n?$//;
+ if ($line =~ /^ /) {
+ if ($last_line_char ne ' ') {
+ push @{$section->{groups}}, {context => $context_lines,
+ plus => $plus_lines,
+ minus => $minus_lines};
+ $context_lines = [];
+ $plus_lines = [];
+ $minus_lines = [];
+ }
+ $last_line_char = ' ';
+ push @{$context_lines}, substr($line, 1);
+ } elsif ($line =~ /^\+/) {
+ if ($last_line_char eq ' ' || $last_line_char eq '-' && @{$plus_lines}) {
+ push @{$section->{groups}}, {context => $context_lines,
+ plus => $plus_lines,
+ minus => $minus_lines};
+ $context_lines = [];
+ $plus_lines = [];
+ $minus_lines = [];
+ $last_line_char = '';
+ }
+ $last_line_char = '+';
+ push @{$plus_lines}, substr($line, 1);
+ } elsif ($line =~ /^-/) {
+ if ($last_line_char eq '+' && @{$minus_lines}) {
+ push @{$section->{groups}}, {context => $context_lines,
+ plus => $plus_lines,
+ minus => $minus_lines};
+ $context_lines = [];
+ $plus_lines = [];
+ $minus_lines = [];
+ $last_line_char = '';
+ }
+ $last_line_char = '-';
+ push @{$minus_lines}, substr($line, 1);
+ }
+ }
+
+ push @{$section->{groups}}, {context => $context_lines,
+ plus => $plus_lines,
+ minus => $minus_lines};
+ push @{$this->{ARGS}{sections}}, $section;
+}
+
+1
diff --git a/Bugzilla/PatchReader/FilterPatch.pm b/Bugzilla/PatchReader/FilterPatch.pm
new file mode 100644
index 000000000..dfe42e750
--- /dev/null
+++ b/Bugzilla/PatchReader/FilterPatch.pm
@@ -0,0 +1,43 @@
+package Bugzilla::PatchReader::FilterPatch;
+
+use strict;
+
+use Bugzilla::PatchReader::Base;
+
+@Bugzilla::PatchReader::FilterPatch::ISA = qw(Bugzilla::PatchReader::Base);
+
+sub new {
+ my $class = shift;
+ $class = ref($class) || $class;
+ my $this = $class->SUPER::new();
+ bless $this, $class;
+
+ return $this;
+}
+
+sub start_patch {
+ my $this = shift;
+ $this->{TARGET}->start_patch(@_) if $this->{TARGET};
+}
+
+sub end_patch {
+ my $this = shift;
+ $this->{TARGET}->end_patch(@_) if $this->{TARGET};
+}
+
+sub start_file {
+ my $this = shift;
+ $this->{TARGET}->start_file(@_) if $this->{TARGET};
+}
+
+sub end_file {
+ my $this = shift;
+ $this->{TARGET}->end_file(@_) if $this->{TARGET};
+}
+
+sub next_section {
+ my $this = shift;
+ $this->{TARGET}->next_section(@_) if $this->{TARGET};
+}
+
+1
diff --git a/Bugzilla/PatchReader/FixPatchRoot.pm b/Bugzilla/PatchReader/FixPatchRoot.pm
new file mode 100644
index 000000000..e67fb2796
--- /dev/null
+++ b/Bugzilla/PatchReader/FixPatchRoot.pm
@@ -0,0 +1,130 @@
+package Bugzilla::PatchReader::FixPatchRoot;
+
+use Bugzilla::PatchReader::FilterPatch;
+use Bugzilla::PatchReader::CVSClient;
+
+use strict;
+
+@Bugzilla::PatchReader::FixPatchRoot::ISA = qw(Bugzilla::PatchReader::FilterPatch);
+
+sub new {
+ my $class = shift;
+ $class = ref($class) || $class;
+ my $this = $class->SUPER::new();
+ bless $this, $class;
+
+ my %parsed = Bugzilla::PatchReader::CVSClient::parse_cvsroot($_[0]);
+ $this->{REPOSITORY_ROOT} = $parsed{rootdir};
+ $this->{REPOSITORY_ROOT} .= "/" if substr($this->{REPOSITORY_ROOT}, -1) ne "/";
+
+ return $this;
+}
+
+sub diff_root {
+ my $this = shift;
+ if (@_) {
+ $this->{DIFF_ROOT} = $_[0];
+ } else {
+ return $this->{DIFF_ROOT};
+ }
+}
+
+sub flush_delayed_commands {
+ my $this = shift;
+ return if ! $this->{DELAYED_COMMANDS};
+
+ my $commands = $this->{DELAYED_COMMANDS};
+ delete $this->{DELAYED_COMMANDS};
+ $this->{FORCE_COMMANDS} = 1;
+ foreach my $command_arr (@{$commands}) {
+ my $command = $command_arr->[0];
+ my $arg = $command_arr->[1];
+ if ($command eq "start_file") {
+ $this->start_file($arg);
+ } elsif ($command eq "end_file") {
+ $this->end_file($arg);
+ } elsif ($command eq "section") {
+ $this->next_section($arg);
+ }
+ }
+}
+
+sub end_patch {
+ my $this = shift;
+ $this->flush_delayed_commands();
+ $this->{TARGET}->end_patch(@_) if $this->{TARGET};
+}
+
+sub start_file {
+ my $this = shift;
+ my ($file) = @_;
+ # If the file is new, it will not have a filename that fits the repository
+ # root and therefore needs to be fixed up to have the same root as everyone
+ # else. At the same time we need to fix DIFF_ROOT too.
+ if (exists($this->{DIFF_ROOT})) {
+ # XXX Return error if there are multiple roots in the patch by verifying
+ # that the DIFF_ROOT is not different from the calculated diff root on this
+ # filename
+
+ $file->{filename} = $this->{DIFF_ROOT} . $file->{filename};
+
+ $file->{canonical} = 1;
+ } elsif ($file->{rcs_filename} &&
+ substr($file->{rcs_filename}, 0, length($this->{REPOSITORY_ROOT})) eq
+ $this->{REPOSITORY_ROOT}) {
+ # Since we know the repository we can determine where the user was in the
+ # repository when they did the diff by chopping off the repository root
+ # from the rcs filename
+ $this->{DIFF_ROOT} = substr($file->{rcs_filename},
+ length($this->{REPOSITORY_ROOT}));
+ $this->{DIFF_ROOT} =~ s/,v$//;
+ # If the RCS file exists in the Attic then we need to correct for
+ # this, stripping off the '/Attic' suffix in order to reduce the name
+ # to just the CVS root.
+ if ($this->{DIFF_ROOT} =~ m/Attic/) {
+ $this->{DIFF_ROOT} = substr($this->{DIFF_ROOT}, 0, -6);
+ }
+ # XXX More error checking--that filename exists and that it is in fact
+ # part of the rcs filename
+ $this->{DIFF_ROOT} = substr($this->{DIFF_ROOT}, 0,
+ -length($file->{filename}));
+ $this->flush_delayed_commands();
+
+ $file->{filename} = $this->{DIFF_ROOT} . $file->{filename};
+
+ $file->{canonical} = 1;
+ } else {
+ # DANGER Will Robinson. The first file in the patch is new. We will try
+ # "delayed command mode"
+ #
+ # (if force commands is on we are already in delayed command mode, and sadly
+ # this means the entire patch was unintelligible to us, so we just output
+ # whatever the hell was in the patch)
+
+ if (!$this->{FORCE_COMMANDS}) {
+ push @{$this->{DELAYED_COMMANDS}}, [ "start_file", { %{$file} } ];
+ return;
+ }
+ }
+ $this->{TARGET}->start_file($file) if $this->{TARGET};
+}
+
+sub end_file {
+ my $this = shift;
+ if (exists($this->{DELAYED_COMMANDS})) {
+ push @{$this->{DELAYED_COMMANDS}}, [ "end_file", { %{$_[0]} } ];
+ } else {
+ $this->{TARGET}->end_file(@_) if $this->{TARGET};
+ }
+}
+
+sub next_section {
+ my $this = shift;
+ if (exists($this->{DELAYED_COMMANDS})) {
+ push @{$this->{DELAYED_COMMANDS}}, [ "section", { %{$_[0]} } ];
+ } else {
+ $this->{TARGET}->next_section(@_) if $this->{TARGET};
+ }
+}
+
+1
diff --git a/Bugzilla/PatchReader/NarrowPatch.pm b/Bugzilla/PatchReader/NarrowPatch.pm
new file mode 100644
index 000000000..b6502f2f3
--- /dev/null
+++ b/Bugzilla/PatchReader/NarrowPatch.pm
@@ -0,0 +1,44 @@
+package Bugzilla::PatchReader::NarrowPatch;
+
+use Bugzilla::PatchReader::FilterPatch;
+
+use strict;
+
+@Bugzilla::PatchReader::NarrowPatch::ISA = qw(Bugzilla::PatchReader::FilterPatch);
+
+sub new {
+ my $class = shift;
+ $class = ref($class) || $class;
+ my $this = $class->SUPER::new();
+ bless $this, $class;
+
+ $this->{INCLUDE_FILES} = [@_];
+
+ return $this;
+}
+
+sub start_file {
+ my $this = shift;
+ my ($file) = @_;
+ if (grep { $_ eq substr($file->{filename}, 0, length($_)) } @{$this->{INCLUDE_FILES}}) {
+ $this->{IS_INCLUDED} = 1;
+ $this->{TARGET}->start_file(@_) if $this->{TARGET};
+ }
+}
+
+sub end_file {
+ my $this = shift;
+ if ($this->{IS_INCLUDED}) {
+ $this->{TARGET}->end_file(@_) if $this->{TARGET};
+ $this->{IS_INCLUDED} = 0;
+ }
+}
+
+sub next_section {
+ my $this = shift;
+ if ($this->{IS_INCLUDED}) {
+ $this->{TARGET}->next_section(@_) if $this->{TARGET};
+ }
+}
+
+1
diff --git a/Bugzilla/PatchReader/PatchInfoGrabber.pm b/Bugzilla/PatchReader/PatchInfoGrabber.pm
new file mode 100644
index 000000000..8c52931ba
--- /dev/null
+++ b/Bugzilla/PatchReader/PatchInfoGrabber.pm
@@ -0,0 +1,45 @@
+package Bugzilla::PatchReader::PatchInfoGrabber;
+
+use Bugzilla::PatchReader::FilterPatch;
+
+use strict;
+
+@Bugzilla::PatchReader::PatchInfoGrabber::ISA = qw(Bugzilla::PatchReader::FilterPatch);
+
+sub new {
+ my $class = shift;
+ $class = ref($class) || $class;
+ my $this = $class->SUPER::new();
+ bless $this, $class;
+
+ return $this;
+}
+
+sub patch_info {
+ my $this = shift;
+ return $this->{PATCH_INFO};
+}
+
+sub start_patch {
+ my $this = shift;
+ $this->{PATCH_INFO} = {};
+ $this->{TARGET}->start_patch(@_) if $this->{TARGET};
+}
+
+sub start_file {
+ my $this = shift;
+ my ($file) = @_;
+ $this->{PATCH_INFO}{files}{$file->{filename}} = { %{$file} };
+ $this->{FILE} = { %{$file} };
+ $this->{TARGET}->start_file(@_) if $this->{TARGET};
+}
+
+sub next_section {
+ my $this = shift;
+ my ($section) = @_;
+ $this->{PATCH_INFO}{files}{$this->{FILE}{filename}}{plus_lines} += $section->{plus_lines};
+ $this->{PATCH_INFO}{files}{$this->{FILE}{filename}}{minus_lines} += $section->{minus_lines};
+ $this->{TARGET}->next_section(@_) if $this->{TARGET};
+}
+
+1
diff --git a/Bugzilla/PatchReader/Raw.pm b/Bugzilla/PatchReader/Raw.pm
new file mode 100644
index 000000000..b58ed3a2d
--- /dev/null
+++ b/Bugzilla/PatchReader/Raw.pm
@@ -0,0 +1,268 @@
+package Bugzilla::PatchReader::Raw;
+
+#
+# USAGE:
+# use PatchReader::Raw;
+# my $parser = new PatchReader::Raw();
+# $parser->sends_data_to($my_target);
+# $parser->start_lines();
+# open FILE, "mypatch.patch";
+# while (<FILE>) {
+# $parser->next_line($_);
+# }
+# $parser->end_lines();
+#
+
+use strict;
+
+use Bugzilla::PatchReader::Base;
+
+@Bugzilla::PatchReader::Raw::ISA = qw(Bugzilla::PatchReader::Base);
+
+sub new {
+ my $class = shift;
+ $class = ref($class) || $class;
+ my $this = $class->SUPER::new();
+ bless $this, $class;
+
+ return $this;
+}
+
+# We send these notifications:
+# start_patch({ patchname })
+# start_file({ filename, rcs_filename, old_revision, old_date_str, new_revision, new_date_str, is_add, is_remove })
+# next_section({ old_start, new_start, old_lines, new_lines, @lines })
+# end_patch
+# end_file
+sub next_line {
+ my $this = shift;
+ my ($line) = @_;
+
+ return if $line =~ /^\?/;
+
+ # patch header parsing
+ if ($line =~ /^---\s*([\S ]+)\s*\t([^\t\r\n]*)\s*(\S*)/) {
+ $this->_maybe_end_file();
+
+ if ($1 eq "/dev/null") {
+ $this->{FILE_STATE}{is_add} = 1;
+ } else {
+ $this->{FILE_STATE}{filename} = $1;
+ }
+ $this->{FILE_STATE}{old_date_str} = $2;
+ $this->{FILE_STATE}{old_revision} = $3 if $3;
+
+ $this->{IN_HEADER} = 1;
+
+ } elsif ($line =~ /^\+\+\+\s*([\S ]+)\s*\t([^\t\r\n]*)(\S*)/) {
+ if ($1 eq "/dev/null") {
+ $this->{FILE_STATE}{is_remove} = 1;
+ }
+ $this->{FILE_STATE}{new_date_str} = $2;
+ $this->{FILE_STATE}{new_revision} = $3 if $3;
+
+ $this->{IN_HEADER} = 1;
+
+ } elsif ($line =~ /^RCS file: ([\S ]+)/) {
+ $this->{FILE_STATE}{rcs_filename} = $1;
+
+ $this->{IN_HEADER} = 1;
+
+ } elsif ($line =~ /^retrieving revision (\S+)/) {
+ $this->{FILE_STATE}{old_revision} = $1;
+
+ $this->{IN_HEADER} = 1;
+
+ } elsif ($line =~ /^Index:\s*([\S ]+)/) {
+ $this->_maybe_end_file();
+
+ $this->{FILE_STATE}{filename} = $1;
+
+ $this->{IN_HEADER} = 1;
+
+ } elsif ($line =~ /^diff\s*(-\S+\s*)*(\S+)\s*(\S*)/ && $3) {
+ # Simple diff <dir> <dir>
+ $this->_maybe_end_file();
+ $this->{FILE_STATE}{filename} = $2;
+
+ $this->{IN_HEADER} = 1;
+
+ # section parsing
+ } elsif ($line =~ /^@@\s*-(\d+),?(\d*)\s*\+(\d+),?(\d*)\s*(?:@@\s*(.*))?/) {
+ $this->{IN_HEADER} = 0;
+
+ $this->_maybe_start_file();
+ $this->_maybe_end_section();
+ $2 = 0 if !defined($2);
+ $4 = 0 if !defined($4);
+ $this->{SECTION_STATE} = { old_start => $1, old_lines => $2,
+ new_start => $3, new_lines => $4,
+ func_info => $5,
+ minus_lines => 0, plus_lines => 0,
+ };
+
+ } elsif ($line =~ /^(\d+),?(\d*)([acd])(\d+),?(\d*)/) {
+ # Non-universal diff. Calculate as though it were universal.
+ $this->{IN_HEADER} = 0;
+
+ $this->_maybe_start_file();
+ $this->_maybe_end_section();
+
+ my $old_start;
+ my $old_lines;
+ my $new_start;
+ my $new_lines;
+ if ($3 eq 'a') {
+ # 'a' has the old number one off from diff -u ("insert after this line"
+ # vs. "insert at this line")
+ $old_start = $1 + 1;
+ $old_lines = 0;
+ } else {
+ $old_start = $1;
+ $old_lines = $2 ? ($2 - $1 + 1) : 1;
+ }
+ if ($3 eq 'd') {
+ # 'd' has the new number one off from diff -u ("delete after this line"
+ # vs. "delete at this line")
+ $new_start = $4 + 1;
+ $new_lines = 0;
+ } else {
+ $new_start = $4;
+ $new_lines = $5 ? ($5 - $4 + 1) : 1;
+ }
+
+ $this->{SECTION_STATE} = { old_start => $old_start, old_lines => $old_lines,
+ new_start => $new_start, new_lines => $new_lines,
+ minus_lines => 0, plus_lines => 0
+ };
+ }
+
+ # line parsing (only when inside a section)
+ return if $this->{IN_HEADER};
+ if ($line =~ /^ /) {
+ push @{$this->{SECTION_STATE}{lines}}, $line;
+ } elsif ($line =~ /^-/) {
+ $this->{SECTION_STATE}{minus_lines}++;
+ push @{$this->{SECTION_STATE}{lines}}, $line;
+ } elsif ($line =~ /^\+/) {
+ $this->{SECTION_STATE}{plus_lines}++;
+ push @{$this->{SECTION_STATE}{lines}}, $line;
+ } elsif ($line =~ /^< /) {
+ $this->{SECTION_STATE}{minus_lines}++;
+ push @{$this->{SECTION_STATE}{lines}}, "-" . substr($line, 2);
+ } elsif ($line =~ /^> /) {
+ $this->{SECTION_STATE}{plus_lines}++;
+ push @{$this->{SECTION_STATE}{lines}}, "+" . substr($line, 2);
+ }
+}
+
+sub start_lines {
+ my $this = shift;
+ die "No target specified: call sends_data_to!" if !$this->{TARGET};
+ delete $this->{FILE_STARTED};
+ delete $this->{FILE_STATE};
+ delete $this->{SECTION_STATE};
+ $this->{FILE_NEVER_STARTED} = 1;
+
+ $this->{TARGET}->start_patch(@_);
+}
+
+sub end_lines {
+ my $this = shift;
+ $this->_maybe_end_file();
+ $this->{TARGET}->end_patch(@_);
+}
+
+sub _init_state {
+ my $this = shift;
+ $this->{SECTION_STATE}{minus_lines} ||= 0;
+ $this->{SECTION_STATE}{plus_lines} ||= 0;
+}
+
+sub _maybe_start_file {
+ my $this = shift;
+ $this->_init_state();
+ if (exists($this->{FILE_STATE}) && !$this->{FILE_STARTED} ||
+ $this->{FILE_NEVER_STARTED}) {
+ $this->_start_file();
+ }
+}
+
+sub _maybe_end_file {
+ my $this = shift;
+ $this->_init_state();
+ return if $this->{IN_HEADER};
+
+ $this->_maybe_end_section();
+ if (exists($this->{FILE_STATE})) {
+ # Handle empty patch sections (if the file has not been started and we're
+ # already trying to end it, start it first!)
+ if (!$this->{FILE_STARTED}) {
+ $this->_start_file();
+ }
+
+ # Send end notification and set state
+ $this->{TARGET}->end_file($this->{FILE_STATE});
+ delete $this->{FILE_STATE};
+ delete $this->{FILE_STARTED};
+ }
+}
+
+sub _start_file {
+ my $this = shift;
+
+ # Send start notification and set state
+ if (!$this->{FILE_STATE}) {
+ $this->{FILE_STATE} = { filename => "file_not_specified_in_diff" };
+ }
+
+ # Send start notification and set state
+ $this->{TARGET}->start_file($this->{FILE_STATE});
+ $this->{FILE_STARTED} = 1;
+ delete $this->{FILE_NEVER_STARTED};
+}
+
+sub _maybe_end_section {
+ my $this = shift;
+ if (exists($this->{SECTION_STATE})) {
+ $this->{TARGET}->next_section($this->{SECTION_STATE});
+ delete $this->{SECTION_STATE};
+ }
+}
+
+sub iterate_file {
+ my $this = shift;
+ my ($filename) = @_;
+
+ open FILE, $filename or die "Could not open $filename: $!";
+ $this->start_lines($filename);
+ while (<FILE>) {
+ $this->next_line($_);
+ }
+ $this->end_lines($filename);
+ close FILE;
+}
+
+sub iterate_fh {
+ my $this = shift;
+ my ($fh, $filename) = @_;
+
+ $this->start_lines($filename);
+ while (<$fh>) {
+ $this->next_line($_);
+ }
+ $this->end_lines($filename);
+}
+
+sub iterate_string {
+ my $this = shift;
+ my ($id, $data) = @_;
+
+ $this->start_lines($id);
+ while ($data =~ /([^\n]*(\n|$))/g) {
+ $this->next_line($1);
+ }
+ $this->end_lines($id);
+}
+
+1
diff --git a/Bugzilla/Product.pm b/Bugzilla/Product.pm
index a0079a033..79af9cbf5 100644
--- a/Bugzilla/Product.pm
+++ b/Bugzilla/Product.pm
@@ -114,7 +114,7 @@ sub create {
# for each product in the list, particularly with hundreds or thousands
# of products.
sub preload {
- my ($products, $preload_flagtypes) = @_;
+ my ($products, $preload_flagtypes, $flagtypes_params) = @_;
my %prods = map { $_->id => $_ } @$products;
my @prod_ids = keys %prods;
return unless @prod_ids;
@@ -132,7 +132,7 @@ sub preload {
}
}
if ($preload_flagtypes) {
- $_->flag_types foreach @$products;
+ $_->flag_types($flagtypes_params) foreach @$products;
}
}
@@ -779,7 +779,8 @@ sub user_has_access {
}
sub flag_types {
- my $self = shift;
+ my ($self, $params) = @_;
+ $params ||= {};
return $self->{'flag_types'} if defined $self->{'flag_types'};
@@ -787,7 +788,7 @@ sub flag_types {
my $cache = Bugzilla->request_cache->{flag_types_per_product} ||= {};
$self->{flag_types} = {};
my $prod_id = $self->id;
- my $flagtypes = Bugzilla::FlagType::match({ product_id => $prod_id });
+ my $flagtypes = Bugzilla::FlagType::match({ product_id => $prod_id, %$params });
foreach my $type ('bug', 'attachment') {
my @flags = grep { $_->target_type eq $type } @$flagtypes;
diff --git a/Bugzilla/Search.pm b/Bugzilla/Search.pm
index 95f03a6ae..542b01045 100644
--- a/Bugzilla/Search.pm
+++ b/Bugzilla/Search.pm
@@ -2887,7 +2887,8 @@ sub _changed_security_check {
sub IsValidQueryType
{
my ($queryType) = @_;
- if (grep { $_ eq $queryType } qw(specific advanced)) {
+ # BMO: Added google and instant
+ if (grep { $_ eq $queryType } qw(specific advanced google instant)) {
return 1;
}
return 0;
diff --git a/Bugzilla/Search/Quicksearch.pm b/Bugzilla/Search/Quicksearch.pm
index 7424f831f..215cc842e 100644
--- a/Bugzilla/Search/Quicksearch.pm
+++ b/Bugzilla/Search/Quicksearch.pm
@@ -161,6 +161,8 @@ sub quicksearch {
ThrowUserError('quicksearch_invalid_query')
if ($words[0] =~ /^(?:AND|OR)$/ || $words[$#words] =~ /^(?:AND|OR|NOT)$/);
+ $fulltext = Bugzilla->user->setting('quicksearch_fulltext') eq 'on' ? 1 : 0;
+
my (@qswords, @or_group);
while (scalar @words) {
my $word = shift @words;
@@ -187,6 +189,10 @@ sub quicksearch {
}
unshift(@words, "-$word");
}
+ # --comment and ++comment disable or enable fulltext searching
+ elsif ($word =~ /^(--|\+\+)comments?$/i) {
+ $fulltext = $1 eq '--' ? 0 : 1;
+ }
else {
# OR groups words together, as OR has higher precedence than AND.
push(@or_group, $word);
@@ -203,7 +209,6 @@ sub quicksearch {
shift(@qswords) if $bug_status_set;
my (@unknownFields, %ambiguous_fields);
- $fulltext = Bugzilla->user->setting('quicksearch_fulltext') eq 'on' ? 1 : 0;
# Loop over all main-level QuickSearch words.
foreach my $qsword (@qswords) {
@@ -530,6 +535,9 @@ sub _default_quicksearch_word {
addChart('short_desc', 'substring', $word, $negate);
addChart('status_whiteboard', 'substring', $word, $negate);
addChart('content', 'matches', _matches_phrase($word), $negate) if $fulltext;
+
+ # BMO Bug 664124 - Include the crash signature (sig:) field in default quicksearches
+ addChart('cf_crash_signature', 'substring', $word, $negate);
}
sub _handle_urls {
diff --git a/Bugzilla/Search/Recent.pm b/Bugzilla/Search/Recent.pm
index 5f04b180b..125850e85 100644
--- a/Bugzilla/Search/Recent.pm
+++ b/Bugzilla/Search/Recent.pm
@@ -65,12 +65,13 @@ sub create {
my $user_id = $search->user_id;
# Enforce there only being SAVE_NUM_SEARCHES per user.
- my $min_id = $dbh->selectrow_array(
- 'SELECT id FROM profile_search WHERE user_id = ? ORDER BY id DESC '
- . $dbh->sql_limit(1, SAVE_NUM_SEARCHES), undef, $user_id);
- if ($min_id) {
- $dbh->do('DELETE FROM profile_search WHERE user_id = ? AND id <= ?',
- undef, ($user_id, $min_id));
+ my @ids = @{ $dbh->selectcol_arrayref(
+ "SELECT id FROM profile_search WHERE user_id = ? ORDER BY id",
+ undef, $user_id) };
+ if (scalar(@ids) > SAVE_NUM_SEARCHES) {
+ splice(@ids, - SAVE_NUM_SEARCHES);
+ $dbh->do(
+ "DELETE FROM profile_search WHERE id IN (" . join(',', @ids) . ")");
}
$dbh->bz_commit_transaction();
return $search;
diff --git a/Bugzilla/Send/Sendmail.pm b/Bugzilla/Send/Sendmail.pm
new file mode 100644
index 000000000..9513134f4
--- /dev/null
+++ b/Bugzilla/Send/Sendmail.pm
@@ -0,0 +1,95 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Send::Sendmail;
+
+use strict;
+
+use base qw(Email::Send::Sendmail);
+
+use Return::Value;
+use Symbol qw(gensym);
+
+sub send {
+ my ($class, $message, @args) = @_;
+ my $mailer = $class->_find_sendmail;
+
+ return failure "Couldn't find 'sendmail' executable in your PATH"
+ ." and Email::Send::Sendmail::SENDMAIL is not set"
+ unless $mailer;
+
+ return failure "Found $mailer but cannot execute it"
+ unless -x $mailer;
+
+ local $SIG{'CHLD'} = 'DEFAULT';
+
+ my $pipe = gensym;
+
+ open($pipe, "| $mailer -t -oi @args")
+ || return failure "Error executing $mailer: $!";
+ print($pipe $message->as_string)
+ || return failure "Error printing via pipe to $mailer: $!";
+ unless (close $pipe) {
+ return failure "error when closing pipe to $mailer: $!" if $!;
+ my ($error_message, $is_transient) = _map_exitcode($? >> 8);
+ if (Bugzilla->params->{'use_mailer_queue'}) {
+ # Return success for errors which are fatal so Bugzilla knows to
+ # remove them from the queue
+ if ($is_transient) {
+ return failure "error when closing pipe to $mailer: $error_message";
+ } else {
+ warn "error when closing pipe to $mailer: $error_message\n";
+ return success;
+ }
+ } else {
+ return failure "error when closing pipe to $mailer: $error_message";
+ }
+ }
+ return success;
+}
+
+sub _map_exitcode {
+ # Returns (error message, is_transient)
+ # from the sendmail source (sendmail/sysexit.h)
+ my $code = shift;
+ if ($code == 64) {
+ return ("Command line usage error (EX_USAGE)", 1);
+ } elsif ($code == 65) {
+ return ("Data format error (EX_DATAERR)", 1);
+ } elsif ($code == 66) {
+ return ("Cannot open input (EX_NOINPUT)", 1);
+ } elsif ($code == 67) {
+ return ("Addressee unknown (EX_NOUSER)", 0);
+ } elsif ($code == 68) {
+ return ("Host name unknown (EX_NOHOST)", 0);
+ } elsif ($code == 69) {
+ return ("Service unavailable (EX_UNAVAILABLE)", 1);
+ } elsif ($code == 70) {
+ return ("Internal software error (EX_SOFTWARE)", 1);
+ } elsif ($code == 71) {
+ return ("System error (EX_OSERR)", 1);
+ } elsif ($code == 72) {
+ return ("Critical OS file missing (EX_OSFILE)", 1);
+ } elsif ($code == 73) {
+ return ("Can't create output file (EX_CANTCREAT)", 1);
+ } elsif ($code == 74) {
+ return ("Input/output error (EX_IOERR)", 1);
+ } elsif ($code == 75) {
+ return ("Temp failure (EX_TEMPFAIL)", 1);
+ } elsif ($code == 76) {
+ return ("Remote error in protocol (EX_PROTOCOL)", 1);
+ } elsif ($code == 77) {
+ return ("Permission denied (EX_NOPERM)", 1);
+ } elsif ($code == 78) {
+ return ("Configuration error (EX_CONFIG)", 1);
+ } else {
+ return ("Unknown Error ($code)", 1);
+ }
+}
+
+1;
+
diff --git a/Bugzilla/Template.pm b/Bugzilla/Template.pm
index 4e51036a6..b35a9d269 100644
--- a/Bugzilla/Template.pm
+++ b/Bugzilla/Template.pm
@@ -235,7 +235,8 @@ sub quoteUrls {
~<a href=\"mailto:$2\">$1$2</a>~igx;
# attachment links
- $text =~ s~\b(attachment\s*\#?\s*(\d+)(?:\s+\[details\])?)
+ # BMO: Bug 652332 dkl@mozilla.com 2011-07-20
+ $text =~ s~\b(attachment\s*\#?\s*(\d+)(?:\s+\[diff\])?(?:\s+\[details\])?)
~($things[$count++] = get_attachment_link($2, $1, $user)) &&
("\0\0" . ($count-1) . "\0\0")
~egmxi;
@@ -297,19 +298,21 @@ sub get_attachment_link {
$title = html_quote(clean_text($title));
$link_text =~ s/ \[details\]$//;
+ $link_text =~ s/ \[diff\]$//;
my $linkval = "attachment.cgi?id=$attachid";
- # If the attachment is a patch, try to link to the diff rather
- # than the text, by default.
+ # If the attachment is a patch and patch_viewer feature is
+ # enabled, add link to the diff.
my $patchlink = "";
if ($attachment->ispatch and Bugzilla->feature('patch_viewer')) {
- $patchlink = '&amp;action=diff';
+ $patchlink = qq| <a href="${linkval}&amp;action=diff" title="$title">[diff]</a>|;
}
# Whitespace matters here because these links are in <pre> tags.
return qq|<span class="$className">|
- . qq|<a href="${linkval}${patchlink}" name="attach_${attachid}" title="$title">$link_text</a>|
+ . qq|<a href="${linkval}" name="attach_${attachid}" title="$title">$link_text</a>|
. qq| <a href="${linkval}&amp;action=edit" title="$title">[details]</a>|
+ . qq|${patchlink}|
. qq|</span>|;
}
else {
@@ -665,6 +668,18 @@ sub create {
$var =~ s/>/\\x3e/g;
return $var;
},
+
+ # Sadly, different to the above. See http://www.json.org/
+ # for details.
+ json => sub {
+ my ($var) = @_;
+ $var =~ s/([\\\"\/])/\\$1/g;
+ $var =~ s/\n/\\n/g;
+ $var =~ s/\r/\\r/g;
+ $var =~ s/\f/\\f/g;
+ $var =~ s/\t/\\t/g;
+ return $var;
+ },
# Converts data to base64
base64 => sub {
@@ -927,7 +942,15 @@ sub create {
Bugzilla->fields({ by_name => 1 });
return $cache->{template_bug_fields};
},
-
+
+ # A general purpose cache to store rendered templates for reuse.
+ # Make sure to not mix language-specific data.
+ 'template_cache' => sub {
+ my $cache = Bugzilla->request_cache->{template_cache} ||= {};
+ $cache->{users} ||= {};
+ return $cache;
+ },
+
'css_files' => \&css_files,
yui_resolve_deps => \&yui_resolve_deps,
@@ -974,6 +997,12 @@ sub create {
'default_authorizer' => new Bugzilla::Auth(),
},
};
+ # Use a per-process provider to cache compiled templates in memory across
+ # requests.
+ my $provider_key = join(':', @{ $config->{INCLUDE_PATH} });
+ my $shared_providers = Bugzilla->process_cache->{shared_providers} ||= {};
+ $shared_providers->{$provider_key} ||= Template::Provider->new($config);
+ $config->{LOAD_TEMPLATES} = [ $shared_providers->{$provider_key} ];
local $Template::Config::CONTEXT = 'Bugzilla::Template::Context';
@@ -1055,6 +1084,9 @@ sub precompile_templates {
# If anything created a Template object before now, clear it out.
delete Bugzilla->request_cache->{template};
+ # Clear out the cached Provider object
+ Bugzilla->process_cache->{shared_providers} = undef;
+
print install_string('done') . "\n" if $output;
}
diff --git a/Bugzilla/Token.pm b/Bugzilla/Token.pm
index 2bb68e721..4804851bb 100644
--- a/Bugzilla/Token.pm
+++ b/Bugzilla/Token.pm
@@ -109,6 +109,8 @@ sub IssueEmailChangeToken {
$vars->{'newemailaddress'} = $new_email . $email_suffix;
$vars->{'expiration_ts'} = ctime($token_ts + MAX_TOKEN_AGE * 86400);
$vars->{'token'} = $token;
+ # For SecureMail extension
+ $vars->{'to_user'} = $user;
$vars->{'emailaddress'} = $old_email . $email_suffix;
my $message;
diff --git a/Bugzilla/User.pm b/Bugzilla/User.pm
index 713de3649..0b4c1c867 100644
--- a/Bugzilla/User.pm
+++ b/Bugzilla/User.pm
@@ -50,6 +50,7 @@ use Bugzilla::Product;
use Bugzilla::Classification;
use Bugzilla::Field;
use Bugzilla::Group;
+use Bugzilla::Hook;
use DateTime::TimeZone;
use List::Util qw(max);
@@ -707,8 +708,8 @@ sub bless_groups {
return $self->{'bless_groups'} if defined $self->{'bless_groups'};
return [] unless $self->id;
- if ($self->in_group('editusers')) {
- # Users having editusers permissions may bless all groups.
+ if ($self->in_group('admin')) {
+ # Users having admin permissions may bless all groups.
$self->{'bless_groups'} = [Bugzilla::Group->get_all];
return $self->{'bless_groups'};
}
@@ -778,6 +779,15 @@ sub in_group_id {
return grep($_->id == $id, @{ $self->groups }) ? 1 : 0;
}
+# This is a helper to get all groups which have an icon to be displayed
+# besides the name of the commenter.
+sub groups_with_icon {
+ my $self = shift;
+
+ my @groups = grep { $_->icon_url } @{ $self->direct_group_membership };
+ return \@groups;
+}
+
sub get_products_by_permission {
my ($self, $group) = @_;
# Make sure $group exists on a per-product basis.
@@ -1635,7 +1645,9 @@ our %names_to_events = (
'attachments.mimetype' => EVT_ATTACHMENT_DATA,
'attachments.ispatch' => EVT_ATTACHMENT_DATA,
'dependson' => EVT_DEPEND_BLOCK,
- 'blocked' => EVT_DEPEND_BLOCK);
+ 'blocked' => EVT_DEPEND_BLOCK,
+ 'product' => EVT_COMPONENT,
+ 'component' => EVT_COMPONENT);
# Returns true if the user wants mail for a given bug change.
# Note: the "+" signs before the constants suppress bareword quoting.
@@ -1654,7 +1666,7 @@ sub wants_bug_mail {
}
else {
# Catch-all for any change not caught by a more specific event
- $events{+EVT_OTHER} = 1;
+ $events{+EVT_OTHER} = 1;
}
# If the user is in a particular role and the value of that role
@@ -2334,7 +2346,7 @@ Determines whether or not a user is in the given group by id.
Returns an arrayref of L<Bugzilla::Group> objects.
The arrayref consists of the groups the user can bless, taking into account
-that having editusers permissions means that you can bless all groups, and
+that having admin permissions means that you can bless all groups, and
that you need to be able to see a group in order to bless it.
=item C<get_products_by_permission($group)>
diff --git a/Bugzilla/UserAgent.pm b/Bugzilla/UserAgent.pm
new file mode 100644
index 000000000..07b05b99c
--- /dev/null
+++ b/Bugzilla/UserAgent.pm
@@ -0,0 +1,249 @@
+# -*- 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 <terry@mozilla.org>
+# Dave Miller <justdave@syndicomm.com>
+# Joe Robins <jmrobins@tgix.com>
+# Gervase Markham <gerv@gerv.net>
+# Shane H. W. Travis <travis@sedsystems.ca>
+# Nitish Bezzala <nbezzala@yahoo.com>
+# Byron Jones <glob@mozilla.com>
+
+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"],
+ qr/\(.*Android.*\)/ => ["Android"],
+ # Windows
+ qr/\(.*Windows XP.*\)/ => ["Windows XP"],
+ qr/\(.*Windows NT 6\.2.*\)/ => ["Windows 8"],
+ 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<detect_platform>
+
+This function attempts to detect the remote client's platform from the
+presented user-agent. If a suitable value on the I<platform> field is found,
+that field value will be returned. If no suitable value is detected,
+C<detect_platform> returns I<Other>.
+
+=item C<detect_op_sys>
+
+This function attempts to detect the remote client's operating system from the
+presented user-agent. If a suitable value on the I<op_sys> field is found, that
+field value will be returned. If no suitable value is detected,
+C<detect_op_sys> returns I<Other>.
+
+=back
+
diff --git a/Bugzilla/Util.pm b/Bugzilla/Util.pm
index c2dbdc97d..9c8f80dcf 100644
--- a/Bugzilla/Util.pm
+++ b/Bugzilla/Util.pm
@@ -44,7 +44,7 @@ use base qw(Exporter);
bz_crypt generate_random_password
validate_email_syntax clean_text
get_text template_var disable_utf8
- detect_encoding);
+ detect_encoding email_filter);
use Bugzilla::Constants;
use Bugzilla::RNG qw(irand);
@@ -119,6 +119,9 @@ sub html_quote {
sub html_light_quote {
my ($text) = @_;
+ # admin/table.html.tmpl calls |FILTER html_light| many times.
+ # There is no need to recreate the HTML::Scrubber object again and again.
+ my $scrubber = Bugzilla->process_cache->{html_scrubber};
# List of allowed HTML elements having no attributes.
my @allow = qw(b strong em i u p br abbr acronym ins del cite code var
@@ -140,7 +143,7 @@ sub html_light_quote {
$text =~ s#$chr($safe)$chr#<$1>#go;
return $text;
}
- else {
+ elsif (!$scrubber) {
# We can be less restrictive. We can accept elements with attributes.
push(@allow, qw(a blockquote q span));
@@ -183,14 +186,14 @@ sub html_light_quote {
},
);
- my $scrubber = HTML::Scrubber->new(default => \@default,
- allow => \@allow,
- rules => \@rules,
- comment => 0,
- process => 0);
-
- return $scrubber->scrub($text);
+ Bugzilla->process_cache->{html_scrubber} = $scrubber =
+ HTML::Scrubber->new(default => \@default,
+ allow => \@allow,
+ rules => \@rules,
+ comment => 0,
+ process => 0);
}
+ return $scrubber->scrub($text);
}
sub email_filter {
diff --git a/Bugzilla/WebService.pm b/Bugzilla/WebService.pm
index 166707626..8e0bfd9c9 100644
--- a/Bugzilla/WebService.pm
+++ b/Bugzilla/WebService.pm
@@ -79,6 +79,11 @@ A floating-point number. May be null.
A string. May be null.
+=item C<email>
+
+A string representing an email address. This value, when returned,
+may be filtered based on if the user is logged in or not. May be null.
+
=item C<dateTime>
A date/time. Represented differently in different interfaces to this API.
diff --git a/Bugzilla/WebService/Bug.pm b/Bugzilla/WebService/Bug.pm
index 5d5f49b26..1722086cd 100644
--- a/Bugzilla/WebService/Bug.pm
+++ b/Bugzilla/WebService/Bug.pm
@@ -82,6 +82,8 @@ BEGIN {
sub fields {
my ($self, $params) = validate(@_, 'ids', 'names');
+ Bugzilla->switch_to_shadow_db();
+
my @fields;
if (defined $params->{ids}) {
my $ids = $params->{ids};
@@ -117,11 +119,12 @@ sub fields {
my (@values, $has_values);
if ( ($field->is_select and $field->name ne 'product')
- or grep($_ eq $field->name, PRODUCT_SPECIFIC_FIELDS))
+ or grep($_ eq $field->name, PRODUCT_SPECIFIC_FIELDS)
+ or $field->name eq 'keywords')
{
$has_values = 1;
@values = @{ $self->_legal_field_values({ field => $field }) };
- }
+ }
if (grep($_ eq $field->name, PRODUCT_SPECIFIC_FIELDS)) {
$value_field = 'product';
@@ -211,6 +214,15 @@ sub _legal_field_values {
}
}
+ elsif ($field_name eq 'keywords') {
+ my @legal_keywords = Bugzilla::Keyword->get_all;
+ foreach my $value (@legal_keywords) {
+ push (@result, {
+ name => $self->type('string', $value->name),
+ description => $self->type('string', $value->description),
+ });
+ }
+ }
else {
my @values = Bugzilla::Field::Choice->type($field)->get_all();
foreach my $value (@values) {
@@ -242,7 +254,7 @@ sub comments {
my $bug_ids = $params->{ids} || [];
my $comment_ids = $params->{comment_ids} || [];
- my $dbh = Bugzilla->dbh;
+ my $dbh = Bugzilla->switch_to_shadow_db();
my $user = Bugzilla->user;
my %bugs;
@@ -297,9 +309,10 @@ sub _translate_comment {
return filter $filters, {
id => $self->type('int', $comment->id),
bug_id => $self->type('int', $comment->bug_id),
- creator => $self->type('string', $comment->author->login),
- author => $self->type('string', $comment->author->login),
+ creator => $self->type('email', $comment->author->login),
+ author => $self->type('email', $comment->author->login),
time => $self->type('dateTime', $comment->creation_ts),
+ creation_time => $self->type('dateTime', $comment->creation_ts),
is_private => $self->type('boolean', $comment->is_private),
text => $self->type('string', $comment->body_full),
attachment_id => $self->type('int', $attach_id),
@@ -309,6 +322,8 @@ sub _translate_comment {
sub get {
my ($self, $params) = validate(@_, 'ids');
+ Bugzilla->switch_to_shadow_db();
+
my $ids = $params->{ids};
defined $ids || ThrowCodeError('param_required', { param => 'ids' });
@@ -343,11 +358,15 @@ sub get {
sub history {
my ($self, $params) = validate(@_, 'ids');
+ Bugzilla->switch_to_shadow_db();
+
my $ids = $params->{ids};
defined $ids || ThrowCodeError('param_required', { param => 'ids' });
- my @return;
+ my %api_name = reverse %{ Bugzilla::Bug::FIELD_MAP() };
+ $api_name{'bug_group'} = 'groups';
+ my @return;
foreach my $bug_id (@$ids) {
my %item;
my $bug = Bugzilla::Bug->check($bug_id);
@@ -363,14 +382,15 @@ sub history {
$bug_history{who} = $self->type('string', $changeset->{who});
$bug_history{changes} = [];
foreach my $change (@{ $changeset->{changes} }) {
+ my $api_field = $api_name{$change->{fieldname}} || $change->{fieldname};
my $attach_id = delete $change->{attachid};
if ($attach_id) {
$change->{attachment_id} = $self->type('int', $attach_id);
}
$change->{removed} = $self->type('string', $change->{removed});
$change->{added} = $self->type('string', $change->{added});
- $change->{field_name} = $self->type('string',
- delete $change->{fieldname});
+ $change->{field_name} = $self->type('string', $api_field);
+ delete $change->{fieldname};
push (@{$bug_history{changes}}, $change);
}
@@ -399,7 +419,9 @@ sub history {
sub search {
my ($self, $params) = @_;
-
+
+ Bugzilla->switch_to_shadow_db();
+
if ( defined($params->{offset}) and !defined($params->{limit}) ) {
ThrowCodeError('param_required',
{ param => 'limit', function => 'Bug.search()' });
@@ -439,16 +461,25 @@ sub search {
delete $match_params{'include_fields'};
delete $match_params{'exclude_fields'};
+ my $count_only = delete $match_params{count_only};
+
my $bugs = Bugzilla::Bug->match(\%match_params);
my $visible = Bugzilla->user->visible_bugs($bugs);
- my @hashes = map { $self->_bug_to_hash($_, $params) } @$visible;
- return { bugs => \@hashes };
+ if ($count_only) {
+ return { bug_count => scalar @$visible };
+ }
+ else {
+ my @hashes = map { $self->_bug_to_hash($_, $params) } @$visible;
+ return { bugs => \@hashes };
+ }
}
sub possible_duplicates {
my ($self, $params) = validate(@_, 'product');
my $user = Bugzilla->user;
+ Bugzilla->switch_to_shadow_db();
+
# Undo the array-ification that validate() does, for "summary".
$params->{summary} || ThrowCodeError('param_required',
{ function => 'Bug.possible_duplicates', param => 'summary' });
@@ -469,6 +500,12 @@ sub possible_duplicates {
sub update {
my ($self, $params) = validate(@_, 'ids');
+ # BMO: Don't allow updating of bugs if disabled
+ if (Bugzilla->params->{disable_bug_updates}) {
+ ThrowErrorPage('bug/process/updates-disabled.html.tmpl',
+ 'Bug updates are currently disabled.');
+ }
+
my $user = Bugzilla->login(LOGIN_REQUIRED);
my $dbh = Bugzilla->dbh;
@@ -563,6 +600,13 @@ sub update {
sub create {
my ($self, $params) = @_;
+
+ # BMO: Don't allow updating of bugs if disabled
+ if (Bugzilla->params->{disable_bug_updates}) {
+ ThrowErrorPage('bug/process/updates-disabled.html.tmpl',
+ 'Bug updates are currently disabled.');
+ }
+
Bugzilla->login(LOGIN_REQUIRED);
$params = Bugzilla::Bug::map_fields($params);
my $bug = Bugzilla::Bug->create($params);
@@ -573,6 +617,8 @@ sub create {
sub legal_values {
my ($self, $params) = @_;
+ Bugzilla->switch_to_shadow_db();
+
defined $params->{field}
or ThrowCodeError('param_required', { param => 'field' });
@@ -625,6 +671,12 @@ sub add_attachment {
my ($self, $params) = validate(@_, 'ids');
my $dbh = Bugzilla->dbh;
+ # BMO: Don't allow updating of bugs if disabled
+ if (Bugzilla->params->{disable_bug_updates}) {
+ ThrowErrorPage('bug/process/updates-disabled.html.tmpl',
+ 'Bug updates are currently disabled.');
+ }
+
Bugzilla->login(LOGIN_REQUIRED);
defined $params->{ids}
|| ThrowCodeError('param_required', { param => 'ids' });
@@ -673,6 +725,12 @@ sub add_attachment {
sub add_comment {
my ($self, $params) = @_;
+ # BMO: Don't allow updating of bugs if disabled
+ if (Bugzilla->params->{disable_bug_updates}) {
+ ThrowErrorPage('bug/process/updates-disabled.html.tmpl',
+ 'Bug updates are currently disabled.');
+ }
+
#The user must login in order add a comment
Bugzilla->login(LOGIN_REQUIRED);
@@ -717,6 +775,12 @@ sub add_comment {
sub update_see_also {
my ($self, $params) = @_;
+ # BMO: Don't allow updating of bugs if disabled
+ if (Bugzilla->params->{disable_bug_updates}) {
+ ThrowErrorPage('bug/process/updates-disabled.html.tmpl',
+ 'Bug updates are currently disabled.');
+ }
+
my $user = Bugzilla->login(LOGIN_REQUIRED);
# Check parameters
@@ -764,6 +828,8 @@ sub update_see_also {
sub attachments {
my ($self, $params) = validate(@_, 'ids', 'attachment_ids');
+ Bugzilla->switch_to_shadow_db();
+
if (!(defined $params->{ids}
or defined $params->{attachment_ids}))
{
@@ -842,18 +908,18 @@ sub _bug_to_hash {
# We don't do the SQL calls at all if the filter would just
# eliminate them anyway.
if (filter_wants $params, 'assigned_to') {
- $item{'assigned_to'} = $self->type('string', $bug->assigned_to->login);
+ $item{'assigned_to'} = $self->type('email', $bug->assigned_to->login);
}
if (filter_wants $params, 'blocks') {
my @blocks = map { $self->type('int', $_) } @{ $bug->blocked };
$item{'blocks'} = \@blocks;
}
if (filter_wants $params, 'cc') {
- my @cc = map { $self->type('string', $_) } @{ $bug->cc || [] };
+ my @cc = map { $self->type('email', $_) } @{ $bug->cc || [] };
$item{'cc'} = \@cc;
}
if (filter_wants $params, 'creator') {
- $item{'creator'} = $self->type('string', $bug->reporter->login);
+ $item{'creator'} = $self->type('email', $bug->reporter->login);
}
if (filter_wants $params, 'depends_on') {
my @depends_on = map { $self->type('int', $_) } @{ $bug->dependson };
@@ -877,13 +943,16 @@ sub _bug_to_hash {
}
if (filter_wants $params, 'qa_contact') {
my $qa_login = $bug->qa_contact ? $bug->qa_contact->login : '';
- $item{'qa_contact'} = $self->type('string', $qa_login);
+ $item{'qa_contact'} = $self->type('email', $qa_login);
}
if (filter_wants $params, 'see_also') {
my @see_also = map { $self->type('string', $_->name) }
@{ $bug->see_also };
$item{'see_also'} = \@see_also;
}
+ if (filter_wants $params, 'flags') {
+ $item{'flags'} = [ map { $self->_flag_to_hash($_) } @{$bug->flags} ];
+ }
# And now custom fields
my @custom_fields = Bugzilla->active_custom_fields;
@@ -912,6 +981,7 @@ sub _bug_to_hash {
# No need to format $bug->deadline specially, because Bugzilla::Bug
# already does it for us.
$item{'deadline'} = $self->type('string', $bug->deadline);
+ $item{'actual_time'} = $self->type('double', $bug->actual_time);
}
if (Bugzilla->user->id) {
@@ -932,9 +1002,6 @@ sub _bug_to_hash {
sub _attachment_to_hash {
my ($self, $attach, $filters) = @_;
- # Skipping attachment flags for now.
- delete $attach->{flags};
-
my $item = filter $filters, {
creation_time => $self->type('dateTime', $attach->attached),
last_change_time => $self->type('dateTime', $attach->modification_time),
@@ -953,7 +1020,7 @@ sub _attachment_to_hash {
# the filter wants them.
foreach my $field (qw(creator attacher)) {
if (filter_wants $filters, $field) {
- $item->{$field} = $self->type('string', $attach->attacher->login);
+ $item->{$field} = $self->type('email', $attach->attacher->login);
}
}
@@ -961,6 +1028,31 @@ sub _attachment_to_hash {
$item->{'data'} = $self->type('base64', $attach->data);
}
+ if (filter_wants $filters, 'flags') {
+ $item->{'flags'} = [ map { $self->_flag_to_hash($_) } @{$attach->flags} ];
+ }
+
+ return $item;
+}
+
+sub _flag_to_hash {
+ my ($self, $flag) = @_;
+
+ my $item = {
+ id => $self->type('int', $flag->id),
+ name => $self->type('string', $flag->name),
+ type_id => $self->type('int', $flag->type_id),
+ creation_date => $self->type('dateTime', $flag->creation_date),
+ modification_date => $self->type('dateTime', $flag->modification_date),
+ status => $self->type('string', $flag->status)
+ };
+
+ foreach my $field (qw(setter requestee)) {
+ my $field_id = $field . "_id";
+ $item->{$field} = $self->type('email', $flag->$field->login)
+ if $flag->$field_id;
+ }
+
return $item;
}
@@ -1099,7 +1191,7 @@ values of the field are shown in the user interface. Can be null.
This is an array of hashes, representing the legal values for
select-type (drop-down and multiple-selection) fields. This is also
-populated for the C<component>, C<version>, and C<target_milestone>
+populated for the C<component>, C<version>, C<target_milestone>, and C<keywords>
fields, but not for the C<product> field (you must use
L<Product.get_accessible_products|Bugzilla::WebService::Product/get_accessible_products>
for that.
@@ -1132,6 +1224,11 @@ if the C<value_field> is set to one of the values listed in this array.
Note that for per-product fields, C<value_field> is set to C<'product'>
and C<visibility_values> will reflect which product(s) this value appears in.
+=item C<description>
+
+C<string> The description of the value. This item is only included for the
+C<keywords> field.
+
=item C<is_open>
C<boolean> For C<bug_status> values, determines whether this status
@@ -1361,6 +1458,48 @@ Also returned as C<attacher>, for backwards-compatibility with older
Bugzillas. (However, this backwards-compatibility will go away in Bugzilla
5.0.)
+=item C<flags>
+
+An array of hashes containing the information about flags currently set
+for each attachment. Each flag hash contains the following items:
+
+=over
+
+=item C<id>
+
+C<int> The id of the flag.
+
+=item C<name>
+
+C<string> The name of the flag.
+
+=item C<type_id>
+
+C<int> The type id of the flag.
+
+=item C<creation_date>
+
+C<dateTime> The timestamp when this flag was originally created.
+
+=item C<modification_date>
+
+C<dateTime> The timestamp when the flag was last modified.
+
+=item C<status>
+
+C<string> The current status of the flag.
+
+=item C<setter>
+
+C<string> The login name of the user who created or last modified the flag.
+
+=item C<requestee>
+
+C<string> The login name of the user this flag has been requested to be granted or denied.
+Note, this field is only returned if a requestee is set.
+
+=back
+
=back
=item B<Errors>
@@ -1397,6 +1536,8 @@ C<summary>.
=back
+=item The C<flags> array was added in Bugzilla B<4.4>.
+
=back
@@ -1501,6 +1642,13 @@ Bugzillas. (However, this backwards-compatibility will go away in Bugzilla
C<dateTime> The time (in Bugzilla's timezone) that the comment was added.
+=item creation_time
+
+C<dateTime> This is exactly same as the C<time> key. Use this field instead of
+C<time> for consistency with other methods including L</get> and L</attachments>.
+For compatibility, C<time> is still usable. However, please note that C<time>
+may be deprecated and removed in a future release.
+
=item is_private
C<boolean> True if this comment is private (only visible to a certain
@@ -1542,6 +1690,8 @@ C<creator>.
=back
+=item C<creation_time> was added in Bugzilla B<4.4>.
+
=back
@@ -1601,6 +1751,13 @@ the valid ids. Each hash contains the following items:
=over
+=item C<actual_time>
+
+C<double> The total number of hours that this bug has taken (so far).
+
+If you are not in the time-tracking group, this field will not be included
+in the return value.
+
=item C<alias>
C<string> The unique alias of this bug.
@@ -1659,6 +1816,48 @@ take.
If you are not in the time-tracking group, this field will not be included
in the return value.
+=item C<flags>
+
+An array of hashes containing the information about flags currently set
+for the bug. Each flag hash contains the following items:
+
+=over
+
+=item C<id>
+
+C<int> The id of the flag.
+
+=item C<name>
+
+C<string> The name of the flag.
+
+=item C<type_id>
+
+C<int> The type id of the flag.
+
+=item C<creation_date>
+
+C<dateTime> The timestamp when this flag was originally created.
+
+=item C<modification_date>
+
+C<dateTime> The timestamp when the flag was last modified.
+
+=item C<status>
+
+C<string> The current status of the flag.
+
+=item C<setter>
+
+C<string> The login name of the user who created or last modified the flag.
+
+=item C<requestee>
+
+C<string> The login name of the user this flag has been requested to be granted or denied.
+Note, this field is only returned if a requestee is set.
+
+=back
+
=item C<groups>
C<array> of C<string>s. The names of all the groups that this bug is in.
@@ -1886,8 +2085,12 @@ C<op_sys>, C<platform>, C<qa_contact>, C<remaining_time>, C<see_also>,
C<target_milestone>, C<update_token>, C<url>, C<version>, C<whiteboard>,
and all custom fields.
-=back
+=item The C<flags> array was added in Bugzilla B<4.4>.
+
+=item The C<actual_time> item was added to the C<bugs> return value
+in Bugzilla B<4.4>.
+=back
=back
@@ -1993,6 +2196,10 @@ The same as L</get>.
=item Added in Bugzilla B<3.4>.
+=item Field names changed to be more consistent with other methods in Bugzilla B<4.4>.
+
+=item As of Bugzilla B<4.4>, field names now match names used by L<Bug.update|/"update"> for consistency.
+
=back
=back
@@ -2153,6 +2360,11 @@ C<string> Search the "Status Whiteboard" field on bugs for a substring.
Works the same as the C<summary> field described above, but searches the
Status Whiteboard field.
+=item C<count_only>
+
+C<boolean> If count_only set to true, only a single hash key called C<bug_count>
+will be returned which is the number of bugs that matched the search.
+
=back
=item B<Returns>
diff --git a/Bugzilla/WebService/Product.pm b/Bugzilla/WebService/Product.pm
index 3cd0d0a6c..7d31f2c38 100644
--- a/Bugzilla/WebService/Product.pm
+++ b/Bugzilla/WebService/Product.pm
@@ -47,23 +47,28 @@ BEGIN { *get_products = \&get }
# Get the ids of the products the user can search
sub get_selectable_products {
+ Bugzilla->switch_to_shadow_db();
return {ids => [map {$_->id} @{Bugzilla->user->get_selectable_products}]};
}
# Get the ids of the products the user can enter bugs against
sub get_enterable_products {
+ Bugzilla->switch_to_shadow_db();
return {ids => [map {$_->id} @{Bugzilla->user->get_enterable_products}]};
}
# Get the union of the products the user can search and enter bugs against.
sub get_accessible_products {
+ Bugzilla->switch_to_shadow_db();
return {ids => [map {$_->id} @{Bugzilla->user->get_accessible_products}]};
}
# Get a list of actual products, based on list of ids or names
sub get {
my ($self, $params) = validate(@_, 'ids', 'names');
-
+
+ Bugzilla->switch_to_shadow_db();
+
# Only products that are in the users accessible products,
# can be allowed to be returned
my $accessible_products = Bugzilla->user->get_accessible_products;
@@ -167,11 +172,11 @@ sub _component_to_hash {
name =>
$self->type('string', $component->name),
description =>
- $self->type('string' , $component->description),
+ $self->type('string', $component->description),
default_assigned_to =>
- $self->type('string' , $component->default_assignee->login),
+ $self->type('email', $component->default_assignee->login),
default_qa_contact =>
- $self->type('string' , $component->default_qa_contact->login),
+ $self->type('email', $component->default_qa_contact->login),
sort_key => # sort_key is returned to match Bug.fields
0,
is_active =>
diff --git a/Bugzilla/WebService/Server/JSONRPC.pm b/Bugzilla/WebService/Server/JSONRPC.pm
index cec1c29ea..63e9ca335 100644
--- a/Bugzilla/WebService/Server/JSONRPC.pm
+++ b/Bugzilla/WebService/Server/JSONRPC.pm
@@ -38,7 +38,7 @@ BEGIN {
use Bugzilla::Error;
use Bugzilla::WebService::Constants;
use Bugzilla::WebService::Util qw(taint_data);
-use Bugzilla::Util qw(correct_urlbase trim disable_utf8);
+use Bugzilla::Util;
use HTTP::Message;
use MIME::Base64 qw(decode_base64 encode_base64);
@@ -221,6 +221,9 @@ sub type {
utf8::encode($value) if utf8::is_utf8($value);
$retval = encode_base64($value, '');
}
+ elsif ($type eq 'email' && Bugzilla->params->{'webservice_email_filter'}) {
+ $retval = email_filter($value);
+ }
return $retval;
}
diff --git a/Bugzilla/WebService/Server/XMLRPC.pm b/Bugzilla/WebService/Server/XMLRPC.pm
index 025fb8f19..824f6ee2d 100644
--- a/Bugzilla/WebService/Server/XMLRPC.pm
+++ b/Bugzilla/WebService/Server/XMLRPC.pm
@@ -30,6 +30,7 @@ if ($ENV{MOD_PERL}) {
}
use Bugzilla::WebService::Constants;
+use Bugzilla::Util;
# Allow WebService methods to call XMLRPC::Lite's type method directly
BEGIN {
@@ -41,6 +42,12 @@ BEGIN {
$value = Bugzilla::WebService::Server->datetime_format_outbound($value);
$value =~ s/-//g;
}
+ elsif ($type eq 'email') {
+ $type = 'string';
+ if (Bugzilla->params->{'webservice_email_filter'}) {
+ $value = email_filter($value);
+ }
+ }
return XMLRPC::Data->type($type)->value($value);
};
}
diff --git a/Bugzilla/WebService/User.pm b/Bugzilla/WebService/User.pm
index deb7518ec..758c69aa8 100644
--- a/Bugzilla/WebService/User.pm
+++ b/Bugzilla/WebService/User.pm
@@ -29,6 +29,7 @@ use Bugzilla::Group;
use Bugzilla::User;
use Bugzilla::Util qw(trim);
use Bugzilla::WebService::Util qw(filter validate);
+use Bugzilla::Hook;
# Don't need auth to login
use constant LOGIN_EXEMPT => {
@@ -126,6 +127,8 @@ sub create {
sub get {
my ($self, $params) = validate(@_, 'names', 'ids');
+ Bugzilla->switch_to_shadow_db();
+
defined($params->{names}) || defined($params->{ids})
|| defined($params->{match})
|| ThrowCodeError('params_required',
@@ -154,8 +157,8 @@ sub get {
\@user_objects, $params);
@users = map {filter $params, {
id => $self->type('int', $_->id),
- real_name => $self->type('string', $_->name),
- name => $self->type('string', $_->login),
+ real_name => $self->type('string', $_->name),
+ name => $self->type('email', $_->login),
}} @$in_group;
return { users => \@users };
@@ -196,33 +199,39 @@ sub get {
}
}
}
-
+
my $in_group = $self->_filter_users_by_group(
\@user_objects, $params);
if (Bugzilla->user->in_group('editusers')) {
- @users =
+ @users =
map {filter $params, {
id => $self->type('int', $_->id),
real_name => $self->type('string', $_->name),
- name => $self->type('string', $_->login),
- email => $self->type('string', $_->email),
+ name => $self->type('email', $_->login),
+ email => $self->type('email', $_->email),
can_login => $self->type('boolean', $_->is_enabled ? 1 : 0),
+ groups => $self->_filter_bless_groups($_->groups),
email_enabled => $self->type('boolean', $_->email_enabled),
login_denied_text => $self->type('string', $_->disabledtext),
+ saved_searches => [map { $self->_query_to_hash($_) } @{ $_->queries }],
}} @$in_group;
-
}
else {
@users =
map {filter $params, {
id => $self->type('int', $_->id),
real_name => $self->type('string', $_->name),
- name => $self->type('string', $_->login),
- email => $self->type('string', $_->email),
+ name => $self->type('email', $_->login),
+ email => $self->type('email', $_->email),
can_login => $self->type('boolean', $_->is_enabled ? 1 : 0),
+ groups => $self->_filter_bless_groups($_->groups),
+ saved_searches => [map { $self->_query_to_hash($_) } @{ $_->queries }],
}} @$in_group;
}
+ Bugzilla::Hook::process('webservice_user_get',
+ { webservice => $self, params => $params, users => \@users });
+
return { users => \@users };
}
@@ -259,6 +268,40 @@ sub _user_in_any_group {
return 0;
}
+sub _filter_bless_groups {
+ my ($self, $groups) = @_;
+ my $user = Bugzilla->user;
+
+ my @filtered_groups;
+ foreach my $group (@$groups) {
+ next unless ($user->in_group('editusers') || $user->can_bless($group->id));
+ push(@filtered_groups, $self->_group_to_hash($group));
+ }
+
+ return \@filtered_groups;
+}
+
+sub _group_to_hash {
+ my ($self, $group) = @_;
+ my $item = {
+ id => $self->type('int', $group->id),
+ name => $self->type('string', $group->name),
+ description => $self->type('string', $group->description),
+ };
+ return $item;
+}
+
+sub _query_to_hash {
+ my ($self, $query) = @_;
+ my $item = {
+ id => $self->type('int', $query->id),
+ name => $self->type('string', $query->name),
+ url => $self->type('string', $query->url),
+ };
+
+ return $item;
+}
+
1;
__END__
@@ -581,10 +624,60 @@ C<string> A text field that holds the reason for disabling a user from logging
into bugzilla, if empty then the user account is enabled. Otherwise it is
disabled/closed.
+=item groups
+
+C<array> An array of group hashes the user is a member of. Each hash describes
+the group and contains the following items:
+
+=over
+
+=item id
+
+C<int> The group id
+
+=item name
+
+C<string> The name of the group
+
+=item description
+
+C<string> The description for the group
+
+=back
+
+=over
+
+=item saved_searches
+
+C<array> An array of hashes, each of which represents a user's saved search and has
+the following keys:
+
+=over
+
+=item id
+
+C<int> An integer id uniquely identifying the saved search.
+
+=item name
+
+C<string> The name of the saved search.
+
+=item url
+
+C<string> The CGI parameters for the saved search.
+
+=back
+
+B<Note>: The elements of the returned array (i.e. hashes) are ordered by the
+name of each saved search.
+
+=back
+
B<Note>: If you are not logged in to Bugzilla when you call this function, you
will only be returned the C<id>, C<name>, and C<real_name> items. If you are
logged in and not in editusers group, you will only be returned the C<id>, C<name>,
-C<real_name>, C<email>, and C<can_login> items.
+C<real_name>, C<email>, and C<can_login> items. The groups returned are filtered
+based on your permission to bless each group.
=back
@@ -625,6 +718,10 @@ exist or you do not belong to it.
=item C<include_disabled> added in Bugzilla B<4.0>. Default behavior
for C<match> has changed to only returning enabled accounts.
+=item C<groups> Added in Bugzilla B<4.4>.
+
+=item C<saved_searches> Added in Bugzilla B<4.4>.
+
=item Error 804 has been added in Bugzilla 4.0.9 and 4.2.4. It's now
illegal to pass a group name you don't belong to.
diff --git a/Bugzilla/WebService/Util.pm b/Bugzilla/WebService/Util.pm
index fe4105ca2..feefd47af 100644
--- a/Bugzilla/WebService/Util.pm
+++ b/Bugzilla/WebService/Util.pm
@@ -34,27 +34,30 @@ our @EXPORT_OK = qw(
validate
);
-sub filter ($$) {
- my ($params, $hash) = @_;
+sub filter ($$;$) {
+ my ($params, $hash, $prefix) = @_;
my %newhash = %$hash;
foreach my $key (keys %$hash) {
- delete $newhash{$key} if !filter_wants($params, $key);
+ delete $newhash{$key} if !filter_wants($params, $key, $prefix);
}
return \%newhash;
}
-sub filter_wants ($$) {
- my ($params, $field) = @_;
+sub filter_wants ($$;$) {
+ my ($params, $field, $prefix) = @_;
my %include = map { $_ => 1 } @{ $params->{'include_fields'} || [] };
my %exclude = map { $_ => 1 } @{ $params->{'exclude_fields'} || [] };
+ my $field_temp;
+
+ $field = "${prefix}.${field}" if $prefix;
if (defined $params->{include_fields}) {
- return 0 if !$include{$field};
+ return 0 if !$include{$field_temp};
}
if (defined $params->{exclude_fields}) {
- return 0 if $exclude{$field};
+ return 0 if $exclude{$field_temp};
}
return 1;
@@ -136,6 +139,13 @@ of WebService methods. Given a hash (the second argument to this subroutine),
this will remove any keys that are I<not> in C<include_fields> and then remove
any keys that I<are> in C<exclude_fields>.
+An optional third option can be passed that prefixes the field name to allow
+filtering of data two or more levels deep.
+
+For example, if you want to filter out the C<id> key/value in components returned
+by Product.get, you would use the value C<component.id> in your C<exclude_fields>
+list.
+
=head2 filter_wants
Returns C<1> if a filter would preserve the specified field when passing
diff --git a/attachment.cgi b/attachment.cgi
index 64f78dc36..985430d85 100755
--- a/attachment.cgi
+++ b/attachment.cgi
@@ -76,6 +76,12 @@ local our $vars = {};
my $action = $cgi->param('action') || 'view';
my $format = $cgi->param('format') || '';
+# BMO: Don't allow updating of bugs if disabled
+if (Bugzilla->params->{disable_bug_updates} && $cgi->request_method eq 'POST') {
+ ThrowErrorPage('bug/process/updates-disabled.html.tmpl',
+ 'Bug updates are currently disabled.');
+}
+
# You must use the appropriate urlbase/sslbase param when doing anything
# but viewing an attachment, or a raw diff.
if ($action ne 'view'
@@ -496,7 +502,8 @@ sub enter {
my $flag_types = Bugzilla::FlagType::match({'target_type' => 'attachment',
'product_id' => $bug->product_id,
- 'component_id' => $bug->component_id});
+ 'component_id' => $bug->component_id,
+ 'is_active' => 1});
$vars->{'flag_types'} = $flag_types;
$vars->{'any_flags_requesteeble'} =
grep { $_->is_requestable && $_->is_requesteeble } @$flag_types;
@@ -617,8 +624,6 @@ sub edit {
my $bugattachments =
Bugzilla::Attachment->get_attachments_by_bug($attachment->bug_id);
- # We only want attachment IDs.
- @$bugattachments = map { $_->id } @$bugattachments;
my $any_flags_requesteeble =
grep { $_->is_requestable && $_->is_requesteeble } @{$attachment->flag_types};
@@ -774,7 +779,6 @@ sub delete_attachment {
# The token is valid. Delete the content of the attachment.
my $msg;
$vars->{'attachment'} = $attachment;
- $vars->{'date'} = $date;
$vars->{'reason'} = clean_text($cgi->param('reason') || '');
$template->process("attachment/delete_reason.txt.tmpl", $vars, \$msg)
diff --git a/buglist.cgi b/buglist.cgi
index 7439b78ee..e3c56cd24 100755
--- a/buglist.cgi
+++ b/buglist.cgi
@@ -51,6 +51,7 @@ use Bugzilla::Status;
use Bugzilla::Token;
use Date::Parse;
+use Time::HiRes qw(gettimeofday tv_interval);
my $cgi = Bugzilla->cgi;
my $dbh = Bugzilla->dbh;
@@ -830,8 +831,10 @@ $::SIG{TERM} = 'DEFAULT';
$::SIG{PIPE} = 'DEFAULT';
# Execute the query.
+my $start_time = [gettimeofday()];
my $buglist_sth = $dbh->prepare($query);
$buglist_sth->execute();
+$vars->{query_time} = tv_interval($start_time);
################################################################################
diff --git a/bzr-update.sh b/bzr-update.sh
new file mode 100644
index 000000000..e1d88e5d7
--- /dev/null
+++ b/bzr-update.sh
@@ -0,0 +1,9 @@
+#!/bin/bash
+HOST=`hostname -s`
+TAG="current-staging"
+[ "$HOST" == "mradm02" -o "$HOST" == "ip-admin02" ] && TAG="current-production"
+echo "+ bzr pull --overwrite -rtag:$TAG"
+output=`bzr pull --overwrite -rtag:$TAG 2>&1`
+echo "$output"
+echo "$output" | grep "Now on revision" | sed -e 's/Now on revision //' -e 's/\.$//' | xargs -i{} echo bzr pull --overwrite -r{} \# `date` >> `dirname $0`/cvs-update.log
+contrib/fixperms.pl
diff --git a/chart.cgi b/chart.cgi
index e7a0f5e8b..89fefbc3f 100755
--- a/chart.cgi
+++ b/chart.cgi
@@ -274,7 +274,8 @@ sub assertCanCreate {
# Check permission for frequency
my $min_freq = 7;
- if ($cgi->param('frequency') < $min_freq && !$user->in_group("admin")) {
+ # Upstreaming: denied, as this min_freq feature is going away.
+ if ($cgi->param('frequency') < $min_freq && !$user->in_group("bz_canusewhines")) {
ThrowUserError("illegal_frequency", { 'minimum' => $min_freq });
}
}
diff --git a/config.cgi b/config.cgi
index 2c82fdc59..963224638 100755
--- a/config.cgi
+++ b/config.cgi
@@ -82,7 +82,7 @@ if ($cgi->param('product')) {
}
# We set the 2nd argument to 1 to also preload flag types.
-Bugzilla::Product::preload($vars->{'products'}, 1);
+Bugzilla::Product::preload($vars->{'products'}, 1, { is_active => 1 });
# Allow consumers to specify whether or not they want flag data.
if (defined $cgi->param('flags')) {
diff --git a/contrib/addcustomfield.pl b/contrib/addcustomfield.pl
new file mode 100755
index 000000000..c7f93c297
--- /dev/null
+++ b/contrib/addcustomfield.pl
@@ -0,0 +1,62 @@
+#!/usr/bin/perl -wT
+# -*- 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.
+#
+# Contributor(s): Frédéric Buclin <LpSolit@gmail.com>
+# David Miller <justdave@mozilla.com>
+
+use strict;
+use lib qw(. lib);
+
+use Bugzilla;
+use Bugzilla::Constants;
+use Bugzilla::Field;
+
+Bugzilla->usage_mode(USAGE_MODE_CMDLINE);
+
+my %types = (
+ 'freetext' => FIELD_TYPE_FREETEXT,
+ 'single_select' => FIELD_TYPE_SINGLE_SELECT,
+ 'multi_select' => FIELD_TYPE_MULTI_SELECT,
+ 'textarea' => FIELD_TYPE_TEXTAREA,
+ 'datetime' => FIELD_TYPE_DATETIME,
+ 'bug_id' => FIELD_TYPE_BUG_ID,
+ 'bug_urls' => FIELD_TYPE_BUG_URLS,
+ 'keywords' => FIELD_TYPE_KEYWORDS,
+);
+
+my $syntax =
+ "syntax: addcustomfield.pl <field name> [field type]\n\n" .
+ "valid field types:\n " . join("\n ", sort keys %types) . "\n\n" .
+ "the default field type is single_select\n";
+
+my $name = shift || die $syntax;
+my $type = lc(shift || 'single_select');
+exists $types{$type} || die "Invalid field type '$type'.\n\n$syntax";
+$type = $types{$type};
+
+Bugzilla::Field->create({
+ name => $name,
+ description => 'Please give me a description!',
+ type => $type,
+ mailhead => 0,
+ enter_bug => 0,
+ obsolete => 1,
+ custom => 1,
+ buglist => 1,
+});
+print "Done!\n";
+
+my $urlbase = Bugzilla->params->{urlbase};
+print "Please visit ${urlbase}editfields.cgi?action=edit&name=$name to finish setting up this field.\n";
diff --git a/contrib/fix_comment_text.pl b/contrib/fix_comment_text.pl
new file mode 100755
index 000000000..f17bbc3d4
--- /dev/null
+++ b/contrib/fix_comment_text.pl
@@ -0,0 +1,75 @@
+#!/usr/bin/perl
+# -*- 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 Initial Developer of the Original Code is Mozilla Foundation
+# Portions created by the Initial Developer are Copyright (C) 2011 the
+# Initial Developer. All Rights Reserved.
+#
+#===============================================================================
+#
+# FILE: fix_comment_text.pl
+#
+# USAGE: ./fix_comment_text.pl <comment_id>
+#
+# DESCRIPTION: Updates a comment in Bugzilla with the text after __DATA__
+#
+# OPTIONS: <comment_id> - The comment id from longdescs with the comment
+# to be replaced.
+# REQUIREMENTS: ---
+# BUGS: ---
+# NOTES: ---
+# AUTHOR: David Lawrence (:dkl), dkl@mozilla.com
+# COMPANY: Mozilla Foundation
+# VERSION: 1.0
+# CREATED: 06/20/2011 03:40:22 PM
+# REVISION: ---
+#===============================================================================
+
+use strict;
+use warnings;
+
+use lib ".";
+
+use Bugzilla;
+use Bugzilla::Util qw(detaint_natural);
+
+my $comment_id = shift;
+
+if (!detaint_natural($comment_id)) {
+ print "Error: invalid comment id or comment id not provided.\n" .
+ "Usage: ./fix_comment_text.pl <comment_id>\n";
+ exit(1);
+}
+
+my $dbh = Bugzilla->dbh;
+
+my $comment = join("", <DATA>);
+
+if ($comment =~ /ENTER NEW COMMENT TEXT HERE/) {
+ print "Please enter the new comment text in the script " .
+ "after the __DATA__ marker.\n";
+ exit(1);
+}
+
+$dbh->bz_start_transaction;
+
+Bugzilla->dbh->do(
+ "UPDATE longdescs SET thetext = ? WHERE comment_id = ?",
+ undef, $comment, $comment_id);
+
+$dbh->bz_commit_transaction;
+
+exit(0);
+
+__DATA__
+ENTER NEW COMMENT TEXT HERE BELOW THE __DATA__ MARKER!
diff --git a/contrib/moco-ldap-check.pl b/contrib/moco-ldap-check.pl
new file mode 100755
index 000000000..7a3a6ca8c
--- /dev/null
+++ b/contrib/moco-ldap-check.pl
@@ -0,0 +1,542 @@
+#!/usr/bin/perl
+
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+use strict;
+use warnings;
+
+use FindBin qw($Bin);
+use lib "$Bin/..";
+use lib "$Bin/../lib";
+
+use Bugzilla;
+use Bugzilla::Constants;
+use Bugzilla::Group;
+use Bugzilla::Mailer;
+use Data::Dumper;
+use File::Slurp;
+use Getopt::Long;
+use Net::LDAP;
+use Safe;
+
+#
+
+use constant BUGZILLA_IGNORE => <<'EOF';
+ infra+bot@mozilla.com # Mozilla Infrastructure Bot
+ qa-auto@mozilla.com # QA Desktop Automation
+ qualys@mozilla.com # Qualys Security Scanner
+ recruiting@mozilla.com # Recruiting
+ release@mozilla.com # Mozilla RelEng Bot
+ sumo-dev@mozilla.com # SUMOdev [:sumodev]
+ airmozilla@mozilla.com # Air Mozilla
+ ux-review@mozilla.com
+ release-mgmt@mozilla.com
+ reps@mozilla.com
+ moz_bug_r_a4@mozilla.com # Security contractor
+ nightwatch@mozilla.com # Security distribution list for whines
+EOF
+
+use constant LDAP_IGNORE => <<'EOF';
+ airmozilla@mozilla.com # Air Mozilla
+EOF
+
+# REPORT_SENDER has to be a valid @mozilla.com LDAP account
+use constant REPORT_SENDER => 'bjones@mozilla.com';
+
+use constant BMO_RECIPIENTS => qw(
+ glob@mozilla.com
+ dkl@mozilla.com
+);
+
+use constant SUPPORT_RECIPIENTS => qw(
+ desktop@mozilla.com
+);
+
+#
+
+my ($ldap_host, $ldap_user, $ldap_pass, $debug, $no_update);
+GetOptions('h=s' => \$ldap_host,
+ 'u=s' => \$ldap_user,
+ 'p=s' => \$ldap_pass,
+ 'd' => \$debug,
+ 'n' => \$no_update);
+die "syntax: -h ldap_host -u ldap_user -p ldap_pass\n"
+ unless $ldap_host && $ldap_user && $ldap_pass;
+
+my $data_dir = bz_locations()->{'datadir'} . '/moco-ldap-check';
+mkdir($data_dir) unless -d $data_dir;
+
+if ($ldap_user !~ /,/) {
+ $ldap_user = "mail=$ldap_user,o=com,dc=mozilla";
+}
+
+#
+# group members
+#
+
+my @bugzilla_ignore;
+foreach my $line (split(/\n/, BUGZILLA_IGNORE)) {
+ $line =~ s/^([^#]+)#.*$/$1/;
+ $line =~ s/(^\s+|\s+$)//g;
+ push @bugzilla_ignore, clean_email($line);
+}
+
+my @bugzilla_moco;
+if ($no_update && -s "$data_dir/bugzilla_moco.last") {
+ $debug && print "Using cached user list from Bugzilla...\n";
+ my $ra = deserialise("$data_dir/bugzilla_moco.last");
+ @bugzilla_moco = @$ra;
+} else {
+ $debug && print "Getting user list from Bugzilla...\n";
+
+ my $group = Bugzilla::Group->new({ name => 'mozilla-corporation' })
+ or die "Failed to find group mozilla-corporation\n";
+
+ foreach my $user (@{ $group->members_non_inherited }) {
+ next unless $user->is_enabled;
+ my $mail = clean_email($user->login);
+ my $name = trim($user->name);
+ $name =~ s/\s+/ /g;
+ next if grep { $mail eq $_ } @bugzilla_ignore;
+ push @bugzilla_moco, {
+ mail => $user->login,
+ canon => $mail,
+ name => $name,
+ };
+ }
+
+ @bugzilla_moco = sort { $a->{mail} cmp $b->{mail} } @bugzilla_moco;
+ serialise("$data_dir/bugzilla_moco.last", \@bugzilla_moco);
+}
+
+#
+# build list of current mo-co bugmail accounts
+#
+
+my @ldap_ignore;
+foreach my $line (split(/\n/, LDAP_IGNORE)) {
+ $line =~ s/^([^#]+)#.*$/$1/;
+ $line =~ s/(^\s+|\s+$)//g;
+ push @ldap_ignore, canon_email($line);
+}
+
+my %ldap;
+if ($no_update && -s "$data_dir/ldap.last") {
+ $debug && print "Using cached user list from LDAP...\n";
+ my $rh = deserialise("$data_dir/ldap.last");
+ %ldap = %$rh;
+} else {
+ $debug && print "Logging into LDAP as $ldap_user...\n";
+ my $ldap = Net::LDAP->new($ldap_host,
+ scheme => 'ldaps', onerror => 'die') or die "$@";
+ $ldap->bind($ldap_user, password => $ldap_pass);
+ foreach my $ldap_base ('o=com,dc=mozilla', 'o=org,dc=mozilla') {
+ $debug && print "Getting user list from LDAP $ldap_base...\n";
+ my $result = $ldap->search(
+ base => $ldap_base,
+ scope => 'sub',
+ filter => '(mail=*)',
+ attrs => ['mail', 'bugzillaEmail', 'emailAlias', 'cn', 'employeeType'],
+ );
+ foreach my $entry ($result->entries) {
+ my ($name, $bugMail, $mail, $type) =
+ map { $entry->get_value($_) || '' }
+ qw(cn bugzillaEmail mail employeeType);
+ next if $type eq 'DISABLED';
+ $mail = lc $mail;
+ next if grep { $_ eq canon_email($mail) } @ldap_ignore;
+ $bugMail = '' if $bugMail !~ /\@/;
+ $bugMail =~ s/(^\s+|\s+$)//g;
+ if ($bugMail =~ / /) {
+ $bugMail = (grep { /\@/ } split / /, $bugMail)[0];
+ }
+ $name =~ s/\s+/ /g;
+ $ldap{$mail}{name} = trim($name);
+ $ldap{$mail}{bugmail} = $bugMail;
+ $ldap{$mail}{bugmail_canon} = canon_email($bugMail);
+ $ldap{$mail}{aliases} = [];
+ foreach my $alias (
+ @{$entry->get_value('emailAlias', asref => 1) || []}
+ ) {
+ push @{$ldap{$mail}{aliases}}, canon_email($alias);
+ }
+ }
+ $debug && printf "Found %s entries\n", scalar($result->entries);
+ }
+ serialise("$data_dir/ldap.last", \%ldap);
+}
+
+#
+# validate all bugmail entries from the phonebook
+#
+
+my %bugzilla_login;
+if ($no_update && -s "$data_dir/bugzilla_login.last") {
+ $debug && print "Using cached bugzilla checks...\n";
+ my $rh = deserialise("$data_dir/bugzilla_login.last");
+ %bugzilla_login = %$rh;
+} else {
+ my %logins;
+ foreach my $mail (keys %ldap) {
+ $logins{$mail} = 1;
+ $logins{$ldap{$mail}{bugmail}} = 1 if $ldap{$mail}{bugmail};
+ }
+ my @logins = sort keys %logins;
+ $debug && print "Checking " . scalar(@logins) . " bugmail accounts...\n";
+
+ foreach my $login (@logins) {
+ if (Bugzilla::User->new({ name => $login })) {
+ $bugzilla_login{$login} = 1;
+ }
+ }
+ serialise("$data_dir/bugzilla_login.last", \%bugzilla_login);
+}
+
+#
+# load previous ldap list
+#
+
+my %ldap_old;
+{
+ my $rh = deserialise("$data_dir/ldap.data");
+ %ldap_old = %$rh if $rh;
+}
+
+#
+# save current ldap list
+#
+
+{
+ serialise("$data_dir/ldap.data", \%ldap);
+}
+
+#
+# new ldap accounts
+#
+
+my @new_ldap;
+{
+ foreach my $mail (sort keys %ldap) {
+ next if exists $ldap_old{$mail};
+ push @new_ldap, {
+ mail => $mail,
+ name => $ldap{$mail}{name},
+ bugmail => $ldap{$mail}{bugmail},
+ };
+ }
+}
+
+#
+# deleted ldap accounts
+#
+
+my @gone_ldap_bmo;
+my @gone_ldap_no_bmo;
+{
+ foreach my $mail (sort keys %ldap_old) {
+ next if exists $ldap{$mail};
+ if ($ldap_old{$mail}{bugmail}) {
+ push @gone_ldap_bmo, {
+ mail => $mail,
+ name => $ldap_old{$mail}{name},
+ bugmail => $ldap_old{$mail}{bugmail},
+ }
+ } else {
+ push @gone_ldap_no_bmo, {
+ mail => $mail,
+ name => $ldap_old{$mail}{name},
+ }
+ }
+ }
+}
+
+#
+# check bugmail entry for all users in bmo/moco group
+#
+
+my @suspect_bugzilla;
+my @invalid_bugzilla;
+foreach my $rh (@bugzilla_moco) {
+ my @check = ($rh->{mail}, $rh->{canon});
+ if ($rh->{mail} =~ /^([^\@]+)\@mozilla\.org$/) {
+ push @check, "$1\@mozilla.com";
+ }
+
+ my $exists;
+ foreach my $check (@check) {
+ $exists = 0;
+
+ # don't complain about deleted accounts
+ if (grep { $_->{mail} eq $check } (@gone_ldap_bmo, @gone_ldap_no_bmo)) {
+ $exists = 1;
+ last;
+ }
+
+ # check for matching bugmail entry
+ foreach my $mail (sort keys %ldap) {
+ next unless $ldap{$mail}{bugmail_canon} eq $check;
+ $exists = 1;
+ last;
+ }
+ last if $exists;
+
+ # check for matching mail
+ $exists = 0;
+ foreach my $mail (sort keys %ldap) {
+ next unless $mail eq $check;
+ $exists = 1;
+ last;
+ }
+ last if $exists;
+
+ # check for matching email alias
+ $exists = 0;
+ foreach my $mail (sort keys %ldap) {
+ next unless grep { $check eq $_ } @{$ldap{$mail}{aliases}};
+ $exists = 1;
+ last;
+ }
+ last if $exists;
+ }
+
+ if (!$exists) {
+ # flag the account
+ if ($rh->{mail} =~ /\@mozilla\.(com|org)$/i) {
+ push @invalid_bugzilla, {
+ mail => $rh->{mail},
+ name => $rh->{name},
+ };
+ } else {
+ push @suspect_bugzilla, {
+ mail => $rh->{mail},
+ name => $rh->{name},
+ };
+ }
+ }
+}
+
+#
+# check bugmail entry for ldap users
+#
+
+my @ldap_unblessed;
+my @invalid_ldap;
+my @invalid_bugmail;
+foreach my $mail (sort keys %ldap) {
+ # try to find the bmo account
+ my $found;
+ foreach my $address ($ldap{$mail}{bugmail}, $ldap{$mail}{bugmail_canon}, $mail, @{$ldap{$mail}{aliases}}) {
+ if (exists $bugzilla_login{$address}) {
+ $found = $address;
+ last;
+ }
+ }
+
+ # not on bmo
+ if (!$found) {
+ # if they have specified a bugmail account, warn, otherwise ignore
+ if ($ldap{$mail}{bugmail}) {
+ if (grep { $_->{canon} eq $ldap{$mail}{bugmail_canon} } @bugzilla_moco) {
+ push @invalid_bugmail, {
+ mail => $mail,
+ name => $ldap{$mail}{name},
+ bugmail => $ldap{$mail}{bugmail},
+ };
+ } else {
+ push @invalid_ldap, {
+ mail => $mail,
+ name => $ldap{$mail}{name},
+ bugmail => $ldap{$mail}{bugmail},
+ };
+ }
+ }
+ next;
+ }
+
+ # warn about mismatches
+ if ($ldap{$mail}{bugmail} && $found ne $ldap{$mail}{bugmail}) {
+ push @invalid_bugmail, {
+ mail => $mail,
+ name => $ldap{$mail}{name},
+ bugmail => $ldap{$mail}{bugmail},
+ };
+ }
+
+ # warn about unblessed accounts
+ if ($mail =~ /\@mozilla\.com$/) {
+ unless (grep { $_->{mail} eq $found || $_->{canon} eq canon_email($found) } @bugzilla_moco) {
+ push @ldap_unblessed, {
+ mail => $mail,
+ name => $ldap{$mail}{name},
+ bugmail => $ldap{$mail}{bugmail} || $mail,
+ };
+ }
+ }
+}
+
+#
+# reports
+#
+
+my @bmo_report;
+push @bmo_report, generate_report(
+ 'new ldap accounts',
+ 'no action required',
+ @new_ldap);
+
+push @bmo_report, generate_report(
+ 'deleted ldap accounts',
+ 'disable bmo account',
+ @gone_ldap_bmo);
+
+push @bmo_report, generate_report(
+ 'deleted ldap accounts',
+ 'no action required (no bmo account)',
+ @gone_ldap_no_bmo);
+
+push @bmo_report, generate_report(
+ 'suspect bugzilla accounts',
+ 'remove from mo-co if required',
+ @suspect_bugzilla);
+
+push @bmo_report, generate_report(
+ 'miss-configured bugzilla accounts',
+ 'ask owner to update phonebook, disable if not on phonebook',
+ @invalid_bugzilla);
+
+push @bmo_report, generate_report(
+ 'ldap accounts without mo-co group',
+ 'verify, and add mo-co group to bmo account',
+ @ldap_unblessed);
+
+push @bmo_report, generate_report(
+ 'missmatched bugmail entries on ldap accounts',
+ 'ask owner to update phonebook',
+ @invalid_bugmail);
+
+push @bmo_report, generate_report(
+ 'invalid bugmail entries on ldap accounts',
+ 'ask owner to update phonebook',
+ @invalid_ldap);
+
+if (!scalar @bmo_report) {
+ push @bmo_report, '**';
+ push @bmo_report, '** nothing to report \o/';
+ push @bmo_report, '**';
+}
+
+email_report(\@bmo_report, 'moco-ldap-check', BMO_RECIPIENTS);
+
+my @support_report;
+
+push @support_report, generate_report(
+ 'Missmatched "Bugzilla Email" entries on LDAP accounts',
+ 'Ask owner to update phonebook, or update directly',
+ @invalid_bugmail);
+
+push @support_report, generate_report(
+ 'Invalid "Bugzilla Email" entries on LDAP accounts',
+ 'Ask owner to update phonebook',
+ @invalid_ldap);
+
+if (scalar @support_report) {
+ email_report(\@support_report, 'Invalid "Bugzilla Email" entries in LDAP', SUPPORT_RECIPIENTS);
+}
+
+#
+#
+#
+
+sub generate_report {
+ my ($title, $action, @lines) = @_;
+
+ my $count = scalar @lines;
+ return unless $count;
+
+ my @report;
+ push @report, '';
+ push @report, '**';
+ push @report, "** $title ($count)";
+ push @report, "** [ $action ]";
+ push @report, '**';
+ push @report, '';
+
+ my $max_length = 0;
+ foreach my $rh (@lines) {
+ $max_length = length($rh->{mail}) if length($rh->{mail}) > $max_length;
+ }
+
+ foreach my $rh (@lines) {
+ my $template = "%-${max_length}s %s";
+ my @fields = ($rh->{mail}, $rh->{name});
+
+ if ($rh->{bugmail}) {
+ $template .= ' (%s)';
+ push @fields, $rh->{bugmail};
+ };
+
+ push @report, sprintf($template, @fields);
+ }
+
+ return @report;
+}
+
+sub email_report {
+ my ($report, $subject, @recipients) = @_;
+ unshift @$report, (
+ "Subject: $subject",
+ 'X-Bugzilla-Type: moco-ldap-check',
+ 'From: ' . REPORT_SENDER,
+ 'To: ' . join(',', @recipients),
+ );
+ if ($debug) {
+ print "\n", join("\n", @$report), "\n";
+ } else {
+ MessageToMTA(join("\n", @$report));
+ }
+}
+
+sub clean_email {
+ my $email = shift;
+ $email = trim($email);
+ $email = $1 if $email =~ /^(\S+)/;
+ $email =~ s/&#64;/@/;
+ $email = lc $email;
+ return $email;
+}
+
+sub canon_email {
+ my $email = shift;
+ $email = clean_email($email);
+ $email =~ s/^([^\+]+)\+[^\@]+(\@.+)$/$1$2/;
+ return $email;
+}
+
+sub trim {
+ my $value = shift;
+ $value =~ s/(^\s+|\s+$)//g;
+ return $value;
+}
+
+sub serialise {
+ my ($filename, $ref) = @_;
+ local $Data::Dumper::Purity = 1;
+ local $Data::Dumper::Deepcopy = 1;
+ local $Data::Dumper::Sortkeys = 1;
+ write_file($filename, Dumper($ref));
+}
+
+sub deserialise {
+ my ($filename) = @_;
+ return unless -s $filename;
+ my $cpt = Safe->new();
+ $cpt->reval('our ' . read_file($filename))
+ || die "$!";
+ return ${$cpt->varglob('VAR1')};
+}
+
diff --git a/contrib/recode.pl b/contrib/recode.pl
index f8de12eb1..e74e06c07 100755
--- a/contrib/recode.pl
+++ b/contrib/recode.pl
@@ -42,8 +42,10 @@ use constant MAX_STRING_LEN => 25;
# For certain tables, we can't automatically determine their Primary Key.
# So, we specify it here as a string.
use constant SPECIAL_KEYS => {
+ # bugs_activity since 4.4 has a unique primary key added
bugs_activity => 'bug_id,bug_when,fieldid',
profile_setting => 'user_id,setting_name',
+ # profiles_activity since 4.4 has a unique primary key added
profiles_activity => 'userid,profiles_when,fieldid',
setting_value => 'name,value',
# longdescs didn't used to have a PK, before 2.20.
diff --git a/contrib/reorg-tools/README b/contrib/reorg-tools/README
new file mode 100644
index 000000000..4e5d6eb4d
--- /dev/null
+++ b/contrib/reorg-tools/README
@@ -0,0 +1,9 @@
+Upstreaming attempt: https://bugzilla.mozilla.org/show_bug.cgi?id=616499
+
+Included in this directory is a group of tools we've used for moving components
+around in a Bugzilla 3.2 install on bugzilla.mozilla.org.
+
+They may require tweaking if you use them on your own install. Putting them
+here to make it easier to collaborate on them and keep them up-to-date.
+Hopefully Bugzilla upstream will be able to just do this from the web UI
+eventually.
diff --git a/contrib/reorg-tools/bmo-plan.txt b/contrib/reorg-tools/bmo-plan.txt
new file mode 100755
index 000000000..838ff0ab9
--- /dev/null
+++ b/contrib/reorg-tools/bmo-plan.txt
@@ -0,0 +1,82 @@
+==BMO Reorg Plan==
+
+Do the following things, mostly in order (but see "Timing" at the end):
+
+1) Create new classifications using GUI:
+ Graveyard (Description: "Old, retired products", sort key: <last>)
+
+2) Rename classifications using GUI:
+ Client Support to Other
+
+3) Move products between classification using GUI:
+ Grendel from Client Software to Graveyard
+ CCK from Unclassified to Graveyard
+ Derivatives from Unclassified to Graveyard
+ MozillaClassic from Unclassified to Graveyard
+ UI: "reclassify" link from the top-level classification list
+
+4) Create new products using GUI:
+ Core Graveyard in Graveyard
+ (desc: "Old, retired Core components", closed for entry, no charts)
+ MailNews Core in Components
+ (desc: "Mail and news components common to Thunderbird and SeaMonkey",
+ open for bug entry, create charts)
+
+5) Rename products using GUI, and fix queries using fixqueries.pl:
+mysql> update series_categories set name="SeaMonkey (2)" where id=32;
+ Mozilla Application Suite to SeaMonkey
+ Sumo to support.mozilla.com
+
+5.5) Rename versions and milestones in Toolkit to match Firefox before step 6
+
+6) Sync milestones, versions and groups between products using
+syncmsandversions.pl:
+ Core -> Core Graveyard (new)
+ Core -> MailNews Core (new)
+ Firefox -> Toolkit
+ Core -> SeaMonkey
+ mozilla.org -> Websites (only 1)
+
+6.5) Sync flag inclusions using syncflags.pl:
+ Core -> Core Graveyard (new)
+ Core -> MailNews Core (new)
+ Core -> SeaMonkey
+ mozilla.org -> Websites (only 1)
+
+6.7) Allow Firefox flags temporarily in Toolkit:
+ 250 | blocking-firefox3 | blocking1.9 - 387
+ 419 | blocking-firefox3.1 | blocking1.9.1 - 416
+ 63 | blocking0.8 | blocking1.6 - 69
+ 76 | blocking0.9 | blocking1.7 - 83
+ 36 | review | review - 4
+ 356 | wanted-firefox3 | wanted1.9 - 357
+ 418 | wanted-firefox3.1 | wanted1.9.1 - 417
+
+7) Move components using movecomponent.pl.
+ Any instruction beginning "moved from".
+ Can't fix the queries for this - oh well
+ <Very long list>
+
+8) Rename components using GUI, and fix queries using fixqueries.pl.
+ Any instruction beginning "renamed from" or "MailNews: prefix removed".
+ <Long list>
+
+9) Create new components using GUI.
+ Any instruction beginning "new".
+ <Long list>
+
+10) Move open bugs using GUI:
+ XP Miscellany to Core/General
+
+11) Merge components by moving bugs using GUI:
+ Any instruction beginning "merge in".
+ Merge all bugs, including closed. Delete empty component when done.
+ <long list of merges>
+
+12) Close Core Graveyard (and Grendel if necessary) to new bugs
+
+13) Rename Toolkit versions and milestones back (from 5.5)
+
+14) Execute flag mapping SQL using Reed's mapping to update bugs in Toolkit
+
+15) Disable above-listed flags in point 6.7 in Toolkit again
diff --git a/contrib/reorg-tools/fix_all_open_status_queries.pl b/contrib/reorg-tools/fix_all_open_status_queries.pl
new file mode 100644
index 000000000..b51ac21c2
--- /dev/null
+++ b/contrib/reorg-tools/fix_all_open_status_queries.pl
@@ -0,0 +1,140 @@
+#!/usr/bin/perl -w
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+use strict;
+
+use lib qw(. lib);
+
+use Bugzilla;
+use Bugzilla::Constants;
+use Bugzilla::Status;
+use Bugzilla::Util;
+
+sub usage() {
+ print <<USAGE;
+Usage: fix_all_open_status_queries.pl <new_open_status>
+
+E.g.: fix_all_open_status_queries.pl READY
+This will add a new open state to user queries which currently look for
+all open bugs by listing every open status in their query criteria.
+For users who only look for bug_status=__open__, they will get the new
+open status automatically.
+USAGE
+}
+
+sub do_namedqueries {
+ my ($new_status) = @_;
+ my $dbh = Bugzilla->dbh;
+ my $replace_count = 0;
+
+ my $query = $dbh->selectall_arrayref("SELECT id, query FROM namedqueries");
+
+ if ($query) {
+ $dbh->bz_start_transaction();
+
+ my $sth = $dbh->prepare("UPDATE namedqueries SET query = ? WHERE id = ?");
+
+ foreach my $row (@$query) {
+ my ($id, $old_query) = @$row;
+ my $new_query = all_open_states($new_status, $old_query);
+ if ($new_query) {
+ trick_taint($new_query);
+ $sth->execute($new_query, $id);
+ $replace_count++;
+ }
+ }
+
+ $dbh->bz_commit_transaction();
+ }
+
+ print "namedqueries: $replace_count replacements made.\n";
+}
+
+# series
+sub do_series {
+ my ($new_status) = @_;
+ my $dbh = Bugzilla->dbh;
+ my $replace_count = 0;
+
+ my $query = $dbh->selectall_arrayref("SELECT series_id, query FROM series");
+
+ if ($query) {
+ $dbh->bz_start_transaction();
+
+ my $sth = $dbh->prepare("UPDATE series SET query = ? WHERE series_id = ?");
+
+ foreach my $row (@$query) {
+ my ($series_id, $old_query) = @$row;
+ my $new_query = all_open_states($new_status, $old_query);
+ if ($new_query) {
+ trick_taint($new_query);
+ $sth->execute($new_query, $series_id);
+ $replace_count++;
+ }
+ }
+
+ $dbh->bz_commit_transaction();
+ }
+
+ print "series: $replace_count replacements made.\n";
+}
+
+sub all_open_states {
+ my ($new_status, $query) = @_;
+
+ my @open_states = Bugzilla::Status::BUG_STATE_OPEN();
+ my $cgi = Bugzilla::CGI->new($query);
+ my @query_states = $cgi->param('bug_status');
+
+ my ($removed, $added) = diff_arrays(\@query_states, \@open_states);
+
+ if (scalar @$added == 1 && $added->[0] eq $new_status) {
+ push(@query_states, $new_status);
+ $cgi->param('bug_status', @query_states);
+ return $cgi->canonicalise_query();
+ }
+
+ return '';
+}
+
+sub validate_status {
+ my ($status) = @_;
+ my $dbh = Bugzilla->dbh;
+ my $exists = $dbh->selectrow_array("SELECT 1 FROM bug_status
+ WHERE value = ?",
+ undef, $status);
+ return $exists ? 1 : 0;
+}
+
+#############################################################################
+# MAIN CODE
+#############################################################################
+# This is a pure command line script.
+Bugzilla->usage_mode(USAGE_MODE_CMDLINE);
+
+if (scalar @ARGV < 1) {
+ usage();
+ exit(1);
+}
+
+my ($new_status) = @ARGV;
+
+$new_status = uc($new_status);
+
+if (!validate_status($new_status)) {
+ print "Invalid status: $new_status\n\n";
+ usage();
+ exit(1);
+}
+
+print "Adding new status '$new_status'.\n\n";
+
+do_namedqueries($new_status);
+do_series($new_status);
+
+exit(0);
diff --git a/contrib/reorg-tools/fixgroupqueries.pl b/contrib/reorg-tools/fixgroupqueries.pl
new file mode 100755
index 000000000..1c75edb97
--- /dev/null
+++ b/contrib/reorg-tools/fixgroupqueries.pl
@@ -0,0 +1,119 @@
+#!/usr/bin/perl -w
+# -*- 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 Netscape Communications
+# Corporation. Portions created by Netscape are
+# Copyright (C) 1998 Netscape Communications Corporation. All
+# Rights Reserved.
+#
+# Contributor(s): Gervase Markham <gerv@gerv.net>
+
+use strict;
+
+use lib qw(. lib);
+
+use Bugzilla;
+use Bugzilla::Constants;
+use Bugzilla::Util;
+
+sub usage() {
+ print <<USAGE;
+Usage: fixgroupqueries.pl <oldvalue> <newvalue>
+
+E.g.: fixgroupqueries.pl w-security webtools-security
+will change all occurrences of "w-security" to "webtools-security" in the
+appropriate places in the namedqueries.
+
+Note that all parameters are case-sensitive.
+USAGE
+}
+
+sub do_namedqueries($$) {
+ my ($old, $new) = @_;
+ $old = url_quote($old);
+ $new = url_quote($new);
+
+ my $dbh = Bugzilla->dbh;
+
+ my $replace_count = 0;
+ my $query = $dbh->selectall_arrayref("SELECT id, query FROM namedqueries");
+ if ($query) {
+ my $sth = $dbh->prepare("UPDATE namedqueries SET query = ?
+ WHERE id = ?");
+
+ foreach my $row (@$query) {
+ my ($id, $query) = @$row;
+ if (($query =~ /field\d+-\d+-\d+=bug_group/) &&
+ ($query =~ /(?:^|&|;)value\d+-\d+-\d+=$old(?:;|&|$)/)) {
+ $query =~ s/((?:^|&|;)value\d+-\d+-\d+=)$old(;|&|$)/$1$new$2/;
+ $sth->execute($query, $id);
+ $replace_count++;
+ }
+ }
+ }
+
+ print "namedqueries: $replace_count replacements made.\n";
+}
+
+# series
+sub do_series($$) {
+ my ($old, $new) = @_;
+ $old = url_quote($old);
+ $new = url_quote($new);
+
+ my $dbh = Bugzilla->dbh;
+ #$dbh->bz_start_transaction();
+
+ my $replace_count = 0;
+ my $query = $dbh->selectall_arrayref("SELECT series_id, query
+ FROM series");
+ if ($query) {
+ my $sth = $dbh->prepare("UPDATE series SET query = ?
+ WHERE series_id = ?");
+ foreach my $row (@$query) {
+ my ($series_id, $query) = @$row;
+
+ if (($query =~ /field\d+-\d+-\d+=bug_group/) &&
+ ($query =~ /(?:^|&|;)value\d+-\d+-\d+=$old(?:;|&|$)/)) {
+ $query =~ s/((?:^|&|;)value\d+-\d+-\d+=)$old(;|&|$)/$1$new$2/;
+ $sth->execute($query, $series_id);
+ $replace_count++;
+ }
+ }
+ }
+
+ #$dbh->bz_commit_transaction();
+ print "series: $replace_count replacements made.\n";
+}
+
+#############################################################################
+# MAIN CODE
+#############################################################################
+# This is a pure command line script.
+Bugzilla->usage_mode(USAGE_MODE_CMDLINE);
+
+if (scalar @ARGV < 2) {
+ usage();
+ exit();
+}
+
+my ($old, $new) = @ARGV;
+
+print "Changing all instances of '$old' to '$new'.\n\n";
+
+#do_namedqueries($old, $new);
+do_series($old, $new);
+
+exit(0);
diff --git a/contrib/reorg-tools/fixqueries.pl b/contrib/reorg-tools/fixqueries.pl
new file mode 100755
index 000000000..4b862fd72
--- /dev/null
+++ b/contrib/reorg-tools/fixqueries.pl
@@ -0,0 +1,132 @@
+#!/usr/bin/perl -w
+# -*- 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 Netscape Communications
+# Corporation. Portions created by Netscape are
+# Copyright (C) 1998 Netscape Communications Corporation. All
+# Rights Reserved.
+#
+# Contributor(s): Gervase Markham <gerv@gerv.net>
+
+use strict;
+
+use lib qw(. lib);
+
+use Bugzilla;
+use Bugzilla::Constants;
+use Bugzilla::Util;
+
+sub usage() {
+ print <<USAGE;
+Usage: fixqueries.pl <parameter> <oldvalue> <newvalue>
+
+E.g.: fixqueries.pl product FoodReplicator SeaMonkey
+will change all occurrences of "FoodReplicator" to "Seamonkey" in the
+appropriate places in the namedqueries, series and series_categories tables.
+
+Note that all parameters are case-sensitive.
+USAGE
+}
+
+sub do_namedqueries($$$) {
+ my ($field, $old, $new) = @_;
+ $old = url_quote($old);
+ $new = url_quote($new);
+
+ my $dbh = Bugzilla->dbh;
+ #$dbh->bz_start_transaction();
+
+ my $replace_count = 0;
+ my $query = $dbh->selectall_arrayref("SELECT id, query FROM namedqueries");
+ if ($query) {
+ my $sth = $dbh->prepare("UPDATE namedqueries SET query = ?
+ WHERE id = ?");
+
+ foreach my $row (@$query) {
+ my ($id, $query) = @$row;
+ if ($query =~ /(?:^|&|;)$field=$old(?:&|$|;)/) {
+ $query =~ s/((?:^|&|;)$field=)$old(;|&|$)/$1$new$2/;
+ $sth->execute($query, $id);
+ $replace_count++;
+ }
+ }
+ }
+
+ #$dbh->bz_commit_transaction();
+ print "namedqueries: $replace_count replacements made.\n";
+}
+
+# series
+sub do_series($$$) {
+ my ($field, $old, $new) = @_;
+ $old = url_quote($old);
+ $new = url_quote($new);
+
+ my $dbh = Bugzilla->dbh;
+ #$dbh->bz_start_transaction();
+
+ my $replace_count = 0;
+ my $query = $dbh->selectall_arrayref("SELECT series_id, query
+ FROM series");
+ if ($query) {
+ my $sth = $dbh->prepare("UPDATE series SET query = ?
+ WHERE series_id = ?");
+ foreach my $row (@$query) {
+ my ($series_id, $query) = @$row;
+
+ if ($query =~ /(?:^|&|;)$field=$old(?:&|$|;)/) {
+ $query =~ s/((?:^|&|;)$field=)$old(;|&|$)/$1$new$2/;
+ $replace_count++;
+ }
+
+ $sth->execute($query, $series_id);
+ }
+ }
+
+ #$dbh->bz_commit_transaction();
+ print "series: $replace_count replacements made.\n";
+}
+
+# series_categories
+sub do_series_categories($$) {
+ my ($old, $new) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ $dbh->do("UPDATE series_categories SET name = ? WHERE name = ?",
+ undef,
+ ($new, $old));
+}
+
+#############################################################################
+# MAIN CODE
+#############################################################################
+# This is a pure command line script.
+Bugzilla->usage_mode(USAGE_MODE_CMDLINE);
+
+if (scalar @ARGV < 3) {
+ usage();
+ exit();
+}
+
+my ($field, $old, $new) = @ARGV;
+
+print "Changing all instances of '$old' to '$new'.\n\n";
+
+do_namedqueries($field, $old, $new);
+do_series($field, $old, $new);
+do_series_categories($old, $new);
+
+exit(0);
+
diff --git a/contrib/reorg-tools/migrate_crash_signatures.pl b/contrib/reorg-tools/migrate_crash_signatures.pl
new file mode 100755
index 000000000..b12446280
--- /dev/null
+++ b/contrib/reorg-tools/migrate_crash_signatures.pl
@@ -0,0 +1,126 @@
+#!/usr/bin/perl
+# -*- 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 Initial Developer of the Original Code is Mozilla Foundation.
+# Portions created by the Initial Developer are Copyright (C) 2011 the
+# Initial Developer. All Rights Reserved.
+#
+#===============================================================================
+#
+# FILE: migrate_crash_signatures.pl
+#
+# USAGE: ./migrate_crash_signatures.pl
+#
+# DESCRIPTION: Migrate current summary data on matched bugs to the
+# new cf_crash_signature custom fields.
+#
+# OPTIONS: No params, then performs dry-run without updating the database.
+# If a true value is passed as single argument, then the database
+# is updated.
+# REQUIREMENTS: None
+# BUGS: 577724
+# NOTES: None
+# AUTHOR: David Lawrence (dkl@mozilla.com),
+# COMPANY: Mozilla Corproation
+# VERSION: 1.0
+# CREATED: 05/31/2011 03:57:52 PM
+# REVISION: 1
+#===============================================================================
+
+use strict;
+use warnings;
+
+use lib qw(. lib);
+
+use Bugzilla;
+use Bugzilla::Constants;
+use Bugzilla::Util;
+
+use Data::Dumper;
+
+Bugzilla->usage_mode(USAGE_MODE_CMDLINE);
+
+my $UPDATE_DB = shift; # Pass true value as single argument to perform database update
+
+my $dbh = Bugzilla->dbh;
+
+# User to make changes as
+my $user_id = $dbh->selectrow_array(
+ "SELECT userid FROM profiles WHERE login_name='nobody\@mozilla.org'");
+$user_id or die "Can't find user ID for 'nobody\@mozilla.org'\n";
+
+my $field_id = $dbh->selectrow_array(
+ "SELECT id FROM fielddefs WHERE name = 'cf_crash_signature'");
+$field_id or die "Can't find field ID for 'cf_crash_signature' field\n";
+
+# Search criteria
+# a) crash or topcrash keyword,
+# b) not have [notacrash] in whiteboard,
+# c) have a properly formulated [@ ...]
+
+# crash and topcrash keyword ids
+my $crash_keyword_id = $dbh->selectrow_array(
+ "SELECT id FROM keyworddefs WHERE name = 'crash'");
+$crash_keyword_id or die "Can't find keyword id for 'crash'\n";
+
+my $topcrash_keyword_id = $dbh->selectrow_array(
+ "SELECT id FROM keyworddefs WHERE name = 'topcrash'");
+$topcrash_keyword_id or die "Can't find keyword id for 'topcrash'\n";
+
+# main search query
+my $bugs = $dbh->selectall_arrayref("
+ SELECT bugs.bug_id, bugs.short_desc
+ FROM bugs LEFT JOIN keywords ON bugs.bug_id = keywords.bug_id
+ WHERE (keywords.keywordid = ? OR keywords.keywordid = ?)
+ AND bugs.status_whiteboard NOT REGEXP '\\\\[notacrash\\\\]'
+ AND bugs.short_desc REGEXP '\\\\[@.+\\\\]'
+ AND (bugs.cf_crash_signature IS NULL OR bugs.cf_crash_signature = '')
+ ORDER BY bugs.bug_id",
+ {'Slice' => {}}, $crash_keyword_id, $topcrash_keyword_id);
+
+my $bug_count = scalar @$bugs;
+$bug_count or die "No bugs were found in matching search criteria.\n";
+
+print "Migrating $bug_count bugs to new crash signature field\n";
+
+$dbh->bz_start_transaction() if $UPDATE_DB;
+
+foreach my $bug (@$bugs) {
+ my $bug_id = $bug->{'bug_id'};
+ my $summary = $bug->{'short_desc'};
+
+ print "Updating bug $bug_id ...";
+
+ my @signatures;
+ while ($summary =~ /(\[\@(?:\[.*\]|[^\[])*\])/g) {
+ push(@signatures, $1);
+ }
+
+ if (@signatures && $UPDATE_DB) {
+ my $timestamp = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
+ $dbh->do("UPDATE bugs SET cf_crash_signature = ? WHERE bug_id = ?",
+ undef, join("\n", @signatures), $bug_id);
+ $dbh->do("INSERT INTO bugs_activity(bug_id, who, bug_when, fieldid, removed, added) " .
+ "VALUES (?, ?, ?, ?, '', ?)",
+ undef, $bug_id, $user_id, $timestamp, $field_id, join("\n", @signatures));
+ $dbh->do("UPDATE bugs SET delta_ts = ?, lastdiffed = ? WHERE bug_id = ?",
+ undef, $timestamp, $timestamp, $bug_id);
+ }
+ elsif (@signatures) {
+ print Dumper(\@signatures);
+ }
+
+ print "done.\n";
+}
+
+$dbh->bz_commit_transaction() if $UPDATE_DB;
diff --git a/contrib/reorg-tools/migrate_orange_bugs.pl b/contrib/reorg-tools/migrate_orange_bugs.pl
new file mode 100644
index 000000000..82913bc98
--- /dev/null
+++ b/contrib/reorg-tools/migrate_orange_bugs.pl
@@ -0,0 +1,151 @@
+#!/usr/bin/perl -wT
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+#===============================================================================
+#
+# FILE: migrate_orange_bugs.pl
+#
+# USAGE: ./migrate_orange_bugs.pl [--remove]
+#
+# DESCRIPTION: Add intermittent-keyword to bugs with [orange] stored in
+# whiteboard field. If --remove, then also remove the [orange]
+# value from whiteboard.
+#
+# OPTIONS: Without --doit, does a dry-run without updating the database.
+# If --doit is passed, then the database is updated.
+# --remove will remove [orange] from the whiteboard.
+# REQUIREMENTS: None
+# BUGS: 791758
+# NOTES: None
+# AUTHOR: David Lawrence (dkl@mozilla.com),
+# COMPANY: Mozilla Corproation
+# VERSION: 1.0
+# CREATED: 10/31/2012
+# REVISION: 1
+#===============================================================================
+
+use strict;
+
+use lib qw(. lib);
+
+use Bugzilla;
+use Bugzilla::Constants;
+use Bugzilla::Field;
+use Bugzilla::User;
+use Bugzilla::Keyword;
+
+use Getopt::Long;
+use Term::ANSIColor qw(colored);
+
+Bugzilla->usage_mode(USAGE_MODE_CMDLINE);
+
+my ($remove_whiteboard, $help, $doit);
+GetOptions("r|remove" => \$remove_whiteboard,
+ "h|help" => \$help, 'doit' => \$doit);
+
+sub usage {
+ my $error = shift || "";
+ print colored(['red'], $error) if $error;
+ print <<USAGE;
+Usage: migrate_orange_bugs.pl [--remove|-r] [--help|-h] [--doit]
+
+E.g.: migrate_orange_bugs.pl --remove --doit
+This script will add the intermittent-failure keyword to any bugs that
+contant [orange] in the status whiteboard. If the --remove option is
+given, then [orange] will be removed from the whiteboard as well.
+
+Pass --doit to make the database changes permanent.
+USAGE
+ exit(1);
+}
+
+# Exit if help was requested
+usage() if $help;
+
+# User to make changes as
+my $user_id = login_to_id('nobody@mozilla.org');
+$user_id or usage("Can't find user ID for 'nobody\@mozilla.org'\n");
+
+my $field_id = get_field_id('keywords');
+$field_id or usage("Can't find field ID for 'keywords' field\n");
+
+# intermittent-keyword id (assumes already created)
+my $keyword_obj = Bugzilla::Keyword->new({ name => 'intermittent-failure' });
+$keyword_obj or usage("Can't find keyword id for 'intermittent-failure'\n");
+my $keyword_id = $keyword_obj->id;
+
+my $dbh = Bugzilla->dbh;
+
+my $bugs = $dbh->selectall_arrayref("
+ SELECT DISTINCT bugs.bug_id, bugs.status_whiteboard
+ FROM bugs WHERE bugs.status_whiteboard LIKE '%[orange]%'
+ OR bugs.status_whiteboard LIKE '%[tb-orange]%'",
+ {'Slice' => {}});
+
+my $bug_count = scalar @$bugs;
+$bug_count or usage("No bugs were found in matching search criteria.\n");
+
+print colored(['green'], "Processing $bug_count [orange] bugs\n");
+
+$dbh->bz_start_transaction() if $doit;
+
+foreach my $bug (@$bugs) {
+ my $bug_id = $bug->{'bug_id'};
+ my $whiteboard = $bug->{'status_whiteboard'};
+
+ print "Checking bug $bug_id ... ";
+
+ my $timestamp = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
+
+ my $keyword_present = $dbh->selectrow_array("
+ SELECT bug_id FROM keywords WHERE bug_id = ? AND keywordid = ?",
+ undef, $bug_id, $keyword_id);
+
+ if (!$keyword_present) {
+ print "adding keyword ... ";
+
+ if ($doit) {
+ $dbh->do("INSERT INTO keywords (bug_id, keywordid) VALUES (?, ?)",
+ undef, $bug_id, $keyword_id);
+ $dbh->do("INSERT INTO bugs_activity(bug_id, who, bug_when, fieldid, removed, added) " .
+ "VALUES (?, ?, ?, ?, '', 'intermittent-failure')",
+ undef, $bug_id, $user_id, $timestamp, $field_id);
+ $dbh->do("UPDATE bugs SET delta_ts = ?, lastdiffed = ? WHERE bug_id = ?",
+ undef, $timestamp, $timestamp, $bug_id);
+ }
+ }
+
+ if ($remove_whiteboard) {
+ print "removing whiteboard ... ";
+
+ if ($doit) {
+ my $old_whiteboard = $whiteboard;
+ $whiteboard =~ s/\[(tb-)?orange\]//ig;
+
+ $dbh->do("UPDATE bugs SET status_whiteboard = ? WHERE bug_id = ?",
+ undef, $whiteboard, $bug_id);
+ $dbh->do("INSERT INTO bugs_activity(bug_id, who, bug_when, fieldid, removed, added) " .
+ "VALUES (?, ?, ?, ?, ?, ?)",
+ undef, $bug_id, $user_id, $timestamp, $field_id, $old_whiteboard, $whiteboard);
+ $dbh->do("UPDATE bugs SET delta_ts = ?, lastdiffed = ? WHERE bug_id = ?",
+ undef, $timestamp, $timestamp, $bug_id);
+ }
+ }
+
+ print "done.\n";
+}
+
+$dbh->bz_commit_transaction() if $doit;
+
+if ($doit) {
+ print colored(['green'], "DATABASE WAS UPDATED\n");
+}
+else {
+ print colored(['red'], "DATABASE WAS NOT UPDATED\n");
+}
+
+exit(0);
diff --git a/contrib/reorg-tools/move_flag_types.pl b/contrib/reorg-tools/move_flag_types.pl
new file mode 100755
index 000000000..a75b7f497
--- /dev/null
+++ b/contrib/reorg-tools/move_flag_types.pl
@@ -0,0 +1,168 @@
+#!/usr/bin/perl
+# -*- 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 Initial Developer of the Original Code is Mozilla Foundation
+# Portions created by the Initial Developer are Copyright (C) 2011 the
+# Initial Developer. All Rights Reserved.
+#
+#===============================================================================
+#
+# FILE: move_flag_types.pl
+#
+# USAGE: ./move_flag_types.pl
+#
+# DESCRIPTION: Move current set flag from one type_id to another
+# based on product and optionally component.
+#
+# OPTIONS: ---
+# REQUIREMENTS: ---
+# BUGS: ---
+# NOTES: ---
+# AUTHOR: David Lawrence (:dkl), dkl@mozilla.com
+# COMPANY: Mozilla Foundation
+# VERSION: 1.0
+# CREATED: 08/22/2011 05:18:06 PM
+# REVISION: ---
+#===============================================================================
+
+=head1 NAME
+
+move_flag_types.pl - Move currently set flags from one type id to another based
+on product and optionally component.
+
+=head1 SYNOPSIS
+
+This script will move bugs matching a specific product (and optionally a component)
+from one flag type id to another if the bug has the flag set to either +, -, or ?.
+
+./move_flag_types.pl --old-id 4 --new-id 720 --product Firefox --component Installer
+
+=head1 OPTIONS
+
+=over
+
+=item B<--help|-h|?>
+
+Print a brief help message and exits.
+
+=item B<--oldid|-o>
+
+Old flag type id. Use editflagtypes.cgi to determine the type id from the URL.
+
+=item B<--newid|-n>
+
+New flag type id. Use editflagtypes.cgi to determine the type id from the URL.
+
+=item B<--product|-p>
+
+The product that the bugs most be assigned to.
+
+=item B<--component|-c>
+
+Optional: The component of the given product that the bugs must be assigned to.
+
+=item B<--doit|-d>
+
+Without this argument, changes are not actually committed to the database.
+
+=back
+
+=cut
+
+use strict;
+use warnings;
+
+use lib '.';
+
+use Bugzilla;
+use Getopt::Long;
+use Pod::Usage;
+
+my %params;
+GetOptions(\%params, 'help|h|?', 'oldid|o=s', 'newid|n=s',
+ 'product|p=s', 'component|c:s', 'doit|d') or pod2usage(1);
+
+if ($params{'help'} || !$params{'oldid'}
+ || !$params{'newid'} || !$params{'product'}) {
+ pod2usage({ -message => "Missing required argument",
+ -exitval => 1 });
+}
+
+# Set defaults
+$params{'doit'} ||= 0;
+$params{'component'} ||= '';
+
+my $dbh = Bugzilla->dbh;
+
+# Get the flag names
+my $old_flag_name = $dbh->selectrow_array(
+ "SELECT name FROM flagtypes WHERE id = ?",
+ undef, $params{'oldid'});
+my $new_flag_name = $dbh->selectrow_array(
+ "SELECT name FROM flagtypes WHERE id = ?",
+ undef, $params{'newid'});
+
+# Find the product id
+my $product_id = $dbh->selectrow_array(
+ "SELECT id FROM products WHERE name = ?",
+ undef, $params{'product'});
+
+# Find the component id if not __ANY__
+my $component_id;
+if ($params{'component'}) {
+ $component_id = $dbh->selectrow_array(
+ "SELECT id FROM components WHERE name = ? AND product_id = ?",
+ undef, $params{'component'}, $product_id);
+}
+
+my @query_args = ($params{'oldid'});
+
+my $flag_query = "SELECT flags.id AS flag_id, flags.bug_id AS bug_id
+ FROM flags JOIN bugs ON flags.bug_id = bugs.bug_id
+ WHERE flags.type_id = ? ";
+
+if ($component_id) {
+ # No need to compare against product_id as component_id is already
+ # tied to a specific product
+ $flag_query .= "AND bugs.component_id = ?";
+ push(@query_args, $component_id);
+}
+else {
+ # All bugs for a product regardless of component
+ $flag_query .= "AND bugs.product_id = ?";
+ push(@query_args, $product_id);
+}
+
+my $flags = $dbh->selectall_arrayref($flag_query, undef, @query_args);
+
+if (@$flags) {
+ print "Moving '" . scalar @$flags . "' flags " .
+ "from $old_flag_name (" . $params{'oldid'} . ") " .
+ "to $new_flag_name (" . $params{'newid'} . ")...\n";
+
+ if (!$params{'doit'}) {
+ print "Pass the argument --doit or -d to permanently make changes to the database.\n";
+ }
+ else {
+ my $flag_update_sth = $dbh->prepare("UPDATE flags SET type_id = ? WHERE id = ?");
+
+ foreach my $flag (@$flags) {
+ my ($flag_id, $bug_id) = @$flag;
+ print "Bug: $bug_id Flag: $flag_id\n";
+ $flag_update_sth->execute($params{'newid'}, $flag_id);
+ }
+ }
+}
+else {
+ print "No flags to move\n";
+}
diff --git a/contrib/reorg-tools/movebugs.pl b/contrib/reorg-tools/movebugs.pl
new file mode 100755
index 000000000..33156dad7
--- /dev/null
+++ b/contrib/reorg-tools/movebugs.pl
@@ -0,0 +1,151 @@
+#!/usr/bin/perl -w
+use strict;
+
+use Cwd 'abs_path';
+use File::Basename;
+BEGIN {
+ my $root = abs_path(dirname(__FILE__) . '/../..');
+ chdir($root);
+}
+use lib qw(. lib);
+
+use Bugzilla;
+use Bugzilla::Constants;
+use Bugzilla::Util;
+
+Bugzilla->usage_mode(USAGE_MODE_CMDLINE);
+
+if (scalar @ARGV < 4) {
+ die <<USAGE;
+Usage: movebugs.pl <old-product> <old-component> <new-product> <new-component>
+
+Eg. movebugs.pl mozilla.org bmo bugzilla.mozilla.org admin
+Will move all bugs in the mozilla.org:bmo component to the
+bugzilla.mozilla.org:admin component.
+
+The new product must have matching versions and milestones from the old
+product.
+USAGE
+}
+
+my ($old_product, $old_component, $new_product, $new_component) = @ARGV;
+
+my $dbh = Bugzilla->dbh;
+
+my $old_product_id = $dbh->selectrow_array(
+ "SELECT id FROM products WHERE name=?",
+ undef, $old_product);
+$old_product_id
+ or die "Can't find product ID for '$old_product'.\n";
+
+my $old_component_id = $dbh->selectrow_array(
+ "SELECT id FROM components WHERE name=? AND product_id=?",
+ undef, $old_component, $old_product_id);
+$old_component_id
+ or die "Can't find component ID for '$old_component'.\n";
+
+my $new_product_id = $dbh->selectrow_array(
+ "SELECT id FROM products WHERE name=?",
+ undef, $new_product);
+$new_product_id
+ or die "Can't find product ID for '$new_product'.\n";
+
+my $new_component_id = $dbh->selectrow_array(
+ "SELECT id FROM components WHERE name=? AND product_id=?",
+ undef, $new_component, $new_product_id);
+$new_component_id
+ or die "Can't find component ID for '$new_component'.\n";
+
+my $product_field_id = $dbh->selectrow_array(
+ "SELECT id FROM fielddefs WHERE name = 'product'");
+$product_field_id
+ or die "Can't find field ID for 'product' field\n";
+my $component_field_id = $dbh->selectrow_array(
+ "SELECT id FROM fielddefs WHERE name = 'component'");
+$component_field_id
+ or die "Can't find field ID for 'component' field\n";
+
+my $user_id = $dbh->selectrow_array(
+ "SELECT userid FROM profiles WHERE login_name='nobody\@mozilla.org'");
+$user_id
+ or die "Can't find user ID for 'nobody\@mozilla.org'\n";
+
+$dbh->bz_start_transaction();
+
+# build list of bugs
+my $ra_ids = $dbh->selectcol_arrayref(
+ "SELECT bug_id FROM bugs WHERE product_id=? AND component_id=?",
+ undef, $old_product_id, $old_component_id);
+my $bug_count = scalar @$ra_ids;
+$bug_count
+ or die "No bugs were found in '$old_component'\n";
+my $where_sql = 'bug_id IN (' . join(',', @$ra_ids) . ')';
+
+# check versions
+my @missing_versions;
+my $ra_versions = $dbh->selectcol_arrayref(
+ "SELECT DISTINCT version FROM bugs WHERE $where_sql");
+foreach my $version (@$ra_versions) {
+ my $has_version = $dbh->selectrow_array(
+ "SELECT 1 FROM versions WHERE product_id=? AND value=?",
+ undef, $new_product_id, $version);
+ push @missing_versions, $version unless $has_version;
+}
+
+# check milestones
+my @missing_milestones;
+my $ra_milestones = $dbh->selectcol_arrayref(
+ "SELECT DISTINCT target_milestone FROM bugs WHERE $where_sql");
+foreach my $milestone (@$ra_milestones) {
+ my $has_milestone = $dbh->selectrow_array(
+ "SELECT 1 FROM milestones WHERE product_id=? AND value=?",
+ undef, $new_product_id, $milestone);
+ push @missing_milestones, $milestone unless $has_milestone;
+}
+
+my $missing_error = '';
+if (@missing_versions) {
+ $missing_error .= "'$new_product' is missing the following version(s):\n " .
+ join("\n ", @missing_versions) . "\n";
+}
+if (@missing_milestones) {
+ $missing_error .= "'$new_product' is missing the following milestone(s):\n " .
+ join("\n ", @missing_milestones) . "\n";
+}
+die $missing_error if $missing_error;
+
+# confirmation
+print <<EOF;
+About to move $bug_count bugs
+From '$old_product' : '$old_component'
+To '$new_product' : '$new_component'
+
+Press <Ctrl-C> to stop or <Enter> to continue...
+EOF
+getc();
+
+print "Moving $bug_count bugs from $old_product:$old_component to $new_product:$new_component\n";
+
+# update bugs
+$dbh->do(
+ "UPDATE bugs SET product_id=?, component_id=? WHERE $where_sql",
+ undef, $new_product_id, $new_component_id);
+
+# touch bugs
+$dbh->do("UPDATE bugs SET delta_ts=NOW() WHERE $where_sql");
+$dbh->do("UPDATE bugs SET lastdiffed=NOW() WHERE $where_sql");
+
+# update bugs_activity
+$dbh->do(
+ "INSERT INTO bugs_activity(bug_id, who, bug_when, fieldid, removed, added)
+ SELECT bug_id, ?, delta_ts, ?, ?, ? FROM bugs WHERE $where_sql",
+ undef,
+ $user_id, $product_field_id, $old_product, $new_product);
+$dbh->do(
+ "INSERT INTO bugs_activity(bug_id, who, bug_when, fieldid, removed, added)
+ SELECT bug_id, ?, delta_ts, ?, ?, ? FROM bugs WHERE $where_sql",
+ undef,
+ $user_id, $component_field_id, $old_component, $new_component);
+
+$dbh->bz_commit_transaction();
+
diff --git a/contrib/reorg-tools/movecomponent.pl b/contrib/reorg-tools/movecomponent.pl
new file mode 100755
index 000000000..8f8bc0abc
--- /dev/null
+++ b/contrib/reorg-tools/movecomponent.pl
@@ -0,0 +1,193 @@
+#!/usr/bin/perl -w
+# -*- 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 Netscape Communications
+# Corporation. Portions created by Netscape are
+# Copyright (C) 1998 Netscape Communications Corporation. All
+# Rights Reserved.
+#
+# Contributor(s): Gervase Markham <gerv@gerv.net>
+
+# See also https://bugzilla.mozilla.org/show_bug.cgi?id=119569
+#
+
+use strict;
+
+use Cwd 'abs_path';
+use File::Basename;
+BEGIN {
+ my $root = abs_path(dirname(__FILE__) . '/../..');
+ chdir($root);
+}
+use lib qw(. lib);
+
+use Bugzilla;
+use Bugzilla::Constants;
+use Bugzilla::Util;
+
+sub usage() {
+ print <<USAGE;
+Usage: movecomponent.pl <oldproduct> <newproduct> <component> <doit>
+
+E.g.: movecomponent.pl ReplicationEngine FoodReplicator SeaMonkey
+will move the component "SeaMonkey" from the product "ReplicationEngine"
+to the product "FoodReplicator".
+
+Important: You must make sure the milestones and versions of the bugs in the
+component are available in the new product. See syncmsandversions.pl.
+
+Pass in a true value for "doit" to make the database changes permament.
+USAGE
+
+ exit(1);
+}
+
+#############################################################################
+# MAIN CODE
+#############################################################################
+
+# This is a pure command line script.
+Bugzilla->usage_mode(USAGE_MODE_CMDLINE);
+
+if (scalar @ARGV < 3) {
+ usage();
+ exit();
+}
+
+my ($oldproduct, $newproduct, $component, $doit) = @ARGV;
+
+my $dbh = Bugzilla->dbh;
+
+$dbh->{'AutoCommit'} = 0 unless $doit; # Turn off autocommit by default
+
+# Find product IDs
+my $oldprodid = $dbh->selectrow_array("SELECT id FROM products WHERE name = ?",
+ undef, $oldproduct);
+if (!$oldprodid) {
+ print "Can't find product ID for '$oldproduct'.\n";
+ exit(1);
+}
+
+my $newprodid = $dbh->selectrow_array("SELECT id FROM products WHERE name = ?",
+ undef, $newproduct);
+if (!$newprodid) {
+ print "Can't find product ID for '$newproduct'.\n";
+ exit(1);
+}
+
+# Find component ID
+my $compid = $dbh->selectrow_array("SELECT id FROM components
+ WHERE name = ? AND product_id = ?",
+ undef, $component, $oldprodid);
+if (!$compid) {
+ print "Can't find component ID for '$component' in product " .
+ "'$oldproduct'.\n";
+ exit(1);
+}
+
+my $fieldid = $dbh->selectrow_array("SELECT id FROM fielddefs
+ WHERE name = 'product'");
+if (!$fieldid) {
+ print "Can't find field ID for 'product' field!\n";
+ exit(1);
+}
+
+# check versions
+my @missing_versions;
+my $ra_versions = $dbh->selectcol_arrayref(
+ "SELECT DISTINCT version FROM bugs WHERE component_id = ?",
+ undef, $compid);
+foreach my $version (@$ra_versions) {
+ my $has_version = $dbh->selectrow_array(
+ "SELECT 1 FROM versions WHERE product_id = ? AND value = ?",
+ undef, $newprodid, $version);
+ push @missing_versions, $version unless $has_version;
+}
+
+# check milestones
+my @missing_milestones;
+my $ra_milestones = $dbh->selectcol_arrayref(
+ "SELECT DISTINCT target_milestone FROM bugs WHERE component_id = ?",
+ undef, $compid);
+foreach my $milestone (@$ra_milestones) {
+ my $has_milestone = $dbh->selectrow_array(
+ "SELECT 1 FROM milestones WHERE product_id=? AND value=?",
+ undef, $newprodid, $milestone);
+ push @missing_milestones, $milestone unless $has_milestone;
+}
+
+my $missing_error = '';
+if (@missing_versions) {
+ $missing_error .= "'$newproduct' is missing the following version(s):\n " .
+ join("\n ", @missing_versions) . "\n";
+}
+if (@missing_milestones) {
+ $missing_error .= "'$newproduct' is missing the following milestone(s):\n " .
+ join("\n ", @missing_milestones) . "\n";
+}
+die $missing_error if $missing_error;
+
+# confirmation
+print <<EOF;
+About to move the component '$component'
+From '$oldproduct'
+To '$newproduct'
+
+Press <Ctrl-C> to stop or <Enter> to continue...
+EOF
+getc();
+
+print "Moving '$component' from '$oldproduct' to '$newproduct'...\n\n";
+$dbh->bz_start_transaction() if $doit;
+
+# Bugs table
+$dbh->do("UPDATE bugs SET product_id = ? WHERE component_id = ?",
+ undef,
+ ($newprodid, $compid));
+
+# Flags tables
+$dbh->do("UPDATE flaginclusions SET product_id = ? WHERE component_id = ?",
+ undef,
+ ($newprodid, $compid));
+
+$dbh->do("UPDATE flagexclusions SET product_id = ? WHERE component_id = ?",
+ undef,
+ ($newprodid, $compid));
+
+# Components
+$dbh->do("UPDATE components SET product_id = ? WHERE id = ?",
+ undef,
+ ($newprodid, $compid));
+
+# Mark bugs as touched
+$dbh->do("UPDATE bugs SET delta_ts = NOW()
+ WHERE component_id = ?", undef, $compid);
+$dbh->do("UPDATE bugs SET lastdiffed = NOW()
+ WHERE component_id = ?", undef, $compid);
+
+# Update bugs_activity
+my $userid = 1; # nobody@mozilla.org
+
+$dbh->do("INSERT INTO bugs_activity(bug_id, who, bug_when, fieldid, removed,
+ added)
+ SELECT bug_id, ?, delta_ts, ?, ?, ?
+ FROM bugs WHERE component_id = ?",
+ undef,
+ ($userid, $fieldid, $oldproduct, $newproduct, $compid));
+
+$dbh->bz_commit_transaction() if $doit;
+
+exit(0);
+
diff --git a/contrib/reorg-tools/reset_default_user.pl b/contrib/reorg-tools/reset_default_user.pl
new file mode 100644
index 000000000..42a7998de
--- /dev/null
+++ b/contrib/reorg-tools/reset_default_user.pl
@@ -0,0 +1,143 @@
+#!/usr/bin/perl -wT
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+use strict;
+
+use lib '.';
+
+use Bugzilla;
+use Bugzilla::Constants;
+use Bugzilla::User;
+use Bugzilla::Field;
+use Bugzilla::Util qw(trick_taint);
+
+use Getopt::Long;
+
+Bugzilla->usage_mode(USAGE_MODE_CMDLINE);
+
+my $dbh = Bugzilla->dbh;
+
+my $field_name = "";
+my $product = "";
+my $component = "";
+my $help = "";
+my %user_cache = ();
+
+my $result = GetOptions('field=s' => \$field_name,
+ 'product=s' => \$product,
+ 'component=s' => \$component,
+ 'help|h' => \$help);
+
+sub usage {
+ print <<USAGE;
+Usage: reset_default_user.pl --field <fieldname> --product <product> [--component <component>] [--help]
+
+This script will load all bugs matching the product, and optionally component,
+and reset the default user value back to the default value for the component.
+Valid field names are assigned_to and qa_contact.
+USAGE
+}
+
+if (!$product || $help
+ || ($field_name ne 'assigned_to' && $field_name ne 'qa_contact'))
+{
+ usage();
+ exit(1);
+}
+
+# We will need these for entering into bugs_activity
+my $who = Bugzilla::User->new({ name => 'nobody@mozilla.org' });
+my $field = Bugzilla::Field->new({ name => $field_name });
+
+trick_taint($product);
+my $product_id = $dbh->selectrow_array(
+ "SELECT id FROM products WHERE name = ?",
+ undef, $product);
+$product_id or die "Can't find product ID for '$product'.\n";
+
+my $component_id;
+my $default_user_id;
+if ($component) {
+ trick_taint($component);
+ my $colname = $field->name eq 'qa_contact'
+ ? 'initialqacontact'
+ : 'initialowner';
+ ($component_id, $default_user_id) = $dbh->selectrow_array(
+ "SELECT id, $colname FROM components " .
+ "WHERE name = ? AND product_id = ?",
+ undef, $component, $product_id);
+ $component_id or die "Can't find component ID for '$component'.\n";
+ $user_cache{$default_user_id} ||= Bugzilla::User->new($default_user_id);
+}
+
+# build list of bugs
+my $bugs_query = "SELECT bug_id, qa_contact, component_id " .
+ "FROM bugs WHERE product_id = ?";
+my @args = ($product_id);
+
+if ($component_id) {
+ $bugs_query .= " AND component_id = ? AND qa_contact != ?";
+ push(@args, $component_id, $default_user_id);
+}
+
+my $bugs = $dbh->selectall_arrayref($bugs_query, {Slice => {}}, @args);
+my $bug_count = scalar @$bugs;
+$bug_count
+ or die "No bugs were found.\n";
+
+# confirmation
+print <<EOF;
+About to reset $field_name for $bug_count bugs.
+
+Press <Ctrl-C> to stop or <Enter> to continue...
+EOF
+getc();
+
+$dbh->bz_start_transaction();
+
+foreach my $bug (@$bugs) {
+ my $bug_id = $bug->{bug_id};
+ my $old_user_id = $bug->{$field->name};
+ my $old_comp_id = $bug->{component_id};
+
+ # If only changing one component, we already have the default user id
+ my $new_user_id;
+ if ($default_user_id) {
+ $new_user_id = $default_user_id;
+ }
+ else {
+ my $colname = $field->name eq 'qa_contact'
+ ? 'initialqacontact'
+ : 'initialowner';
+ $new_user_id = $dbh->selectrow_array(
+ "SELECT $colname FROM components WHERE id = ?",
+ undef, $old_comp_id);
+ }
+
+ if ($old_user_id != $new_user_id) {
+ print "Resetting " . $field->name . " for bug $bug_id ...";
+
+ # Use the cached version if already exists
+ my $old_user = $user_cache{$old_user_id} ||= Bugzilla::User->new($old_user_id);
+ my $new_user = $user_cache{$new_user_id} ||= Bugzilla::User->new($new_user_id);
+
+ my $timestamp = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
+
+ $dbh->do("UPDATE bugs SET " . $field->name . " = ? WHERE bug_id = ?",
+ undef, $new_user_id, $bug_id);
+ $dbh->do("INSERT INTO bugs_activity(bug_id, who, bug_when, fieldid, removed, added) " .
+ "VALUES (?, ?, ?, ?, ?, ?)",
+ undef, $bug_id, $who->id, $timestamp, $field->id, $old_user->login, $new_user->login);
+ $dbh->do("UPDATE bugs SET delta_ts = ?, lastdiffed = ? WHERE bug_id = ?",
+ undef, $timestamp, $timestamp, $bug_id);
+
+ print "done.\n";
+ }
+}
+
+$dbh->bz_commit_transaction();
diff --git a/contrib/reorg-tools/syncflags.pl b/contrib/reorg-tools/syncflags.pl
new file mode 100755
index 000000000..6c5b8293a
--- /dev/null
+++ b/contrib/reorg-tools/syncflags.pl
@@ -0,0 +1,86 @@
+#!/usr/bin/perl -w
+# -*- 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 Netscape Communications
+# Corporation. Portions created by Netscape are
+# Copyright (C) 1998 Netscape Communications Corporation. All
+# Rights Reserved.
+#
+# Contributor(s): Gervase Markham <gerv@gerv.net>
+
+# See also https://bugzilla.mozilla.org/show_bug.cgi?id=119569
+
+use strict;
+
+use lib qw(. lib);
+
+use Bugzilla;
+use Bugzilla::Constants;
+
+sub usage() {
+ print <<USAGE;
+Usage: syncflags.pl <srcproduct> <tgtproduct>
+
+E.g.: syncflags.pl FoodReplicator SeaMonkey
+will copy any flag inclusions (only) for the product "FoodReplicator"
+so matching inclusions exist for the product "SeaMonkey". This script is
+normally used prior to moving components from srcproduct to tgtproduct.
+USAGE
+
+ exit(1);
+}
+
+#############################################################################
+# MAIN CODE
+#############################################################################
+
+# This is a pure command line script.
+Bugzilla->usage_mode(USAGE_MODE_CMDLINE);
+
+if (scalar @ARGV < 2) {
+ usage();
+ exit();
+}
+
+my ($srcproduct, $tgtproduct) = @ARGV;
+
+my $dbh = Bugzilla->dbh;
+
+# Find product IDs
+my $srcprodid = $dbh->selectrow_array("SELECT id FROM products WHERE name = ?",
+ undef, $srcproduct);
+if (!$srcprodid) {
+ print "Can't find product ID for '$srcproduct'.\n";
+ exit(1);
+}
+
+my $tgtprodid = $dbh->selectrow_array("SELECT id FROM products WHERE name = ?",
+ undef, $tgtproduct);
+if (!$tgtprodid) {
+ print "Can't find product ID for '$tgtproduct'.\n";
+ exit(1);
+}
+
+$dbh->do("INSERT INTO flaginclusions(component_id, type_id, product_id)
+ SELECT fi1.component_id, fi1.type_id, ? FROM flaginclusions fi1
+ LEFT JOIN flaginclusions fi2
+ ON fi1.type_id = fi2.type_id
+ AND fi2.product_id = ?
+ WHERE fi1.product_id = ?
+ AND fi2.type_id IS NULL",
+ undef,
+ $tgtprodid, $tgtprodid, $srcprodid);
+
+exit(0);
diff --git a/contrib/reorg-tools/syncmsandversions.pl b/contrib/reorg-tools/syncmsandversions.pl
new file mode 100755
index 000000000..b25b3348b
--- /dev/null
+++ b/contrib/reorg-tools/syncmsandversions.pl
@@ -0,0 +1,107 @@
+#!/usr/bin/perl -w
+# -*- 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 Netscape Communications
+# Corporation. Portions created by Netscape are
+# Copyright (C) 1998 Netscape Communications Corporation. All
+# Rights Reserved.
+#
+# Contributor(s): Gervase Markham <gerv@gerv.net>
+
+# See also https://bugzilla.mozilla.org/show_bug.cgi?id=119569
+
+use strict;
+
+use lib qw(. lib);
+
+use Bugzilla;
+use Bugzilla::Constants;
+
+sub usage() {
+ print <<USAGE;
+Usage: syncmsandversions.pl <srcproduct> <tgtproduct>
+
+E.g.: syncmsandversions.pl FoodReplicator SeaMonkey
+will copy any versions and milstones in the product "FoodReplicator"
+which do not exist in product "SeaMonkey" into it. This script is normally
+used prior to moving components from srcproduct to tgtproduct.
+USAGE
+
+ exit(1);
+}
+
+#############################################################################
+# MAIN CODE
+#############################################################################
+
+# This is a pure command line script.
+Bugzilla->usage_mode(USAGE_MODE_CMDLINE);
+
+if (scalar @ARGV < 2) {
+ usage();
+ exit();
+}
+
+my ($srcproduct, $tgtproduct) = @ARGV;
+
+my $dbh = Bugzilla->dbh;
+
+# Find product IDs
+my $srcprodid = $dbh->selectrow_array("SELECT id FROM products WHERE name = ?",
+ undef, $srcproduct);
+if (!$srcprodid) {
+ print "Can't find product ID for '$srcproduct'.\n";
+ exit(1);
+}
+
+my $tgtprodid = $dbh->selectrow_array("SELECT id FROM products WHERE name = ?",
+ undef, $tgtproduct);
+if (!$tgtprodid) {
+ print "Can't find product ID for '$tgtproduct'.\n";
+ exit(1);
+}
+
+#$dbh->bz_start_transaction();
+
+$dbh->do("INSERT INTO milestones(value, sortkey, product_id)
+ SELECT m1.value, m1.sortkey, ? FROM milestones m1
+ LEFT JOIN milestones m2 ON m1.value = m2.value AND
+ m2.product_id = ?
+ WHERE m1.product_id = ? AND m2.value IS NULL",
+ undef,
+ $tgtprodid, $tgtprodid, $srcprodid);
+
+$dbh->do("INSERT INTO versions(value, product_id)
+ SELECT v1.value, ? FROM versions v1
+ LEFT JOIN versions v2 ON v1.value = v2.value AND
+ v2.product_id = ?
+ WHERE v1.product_id = ? AND v2.value IS NULL",
+ undef,
+ $tgtprodid, $tgtprodid, $srcprodid);
+
+$dbh->do("INSERT INTO group_control_map (group_id, product_id, entry, membercontrol, othercontrol, canedit, editcomponents, editbugs, canconfirm)
+ SELECT g1.group_id, ?, g1.entry, g1.membercontrol, g1.othercontrol, g1.canedit, g1.editcomponents, g1.editbugs, g1.canconfirm
+ FROM group_control_map g1
+ LEFT JOIN group_control_map g2 ON g1.product_id = ? AND
+ g2.product_id = ? AND
+ g1.group_id = g2.group_id
+ WHERE g1.product_id = ? AND g2.group_id IS NULL",
+ undef,
+ $tgtprodid, $srcprodid, $tgtprodid, $srcprodid);
+
+#$dbh->bz_commit_transaction();
+
+exit(0);
+
diff --git a/contrib/sanitizeme.pl b/contrib/sanitizeme.pl
new file mode 100755
index 000000000..362700be0
--- /dev/null
+++ b/contrib/sanitizeme.pl
@@ -0,0 +1,176 @@
+#!/usr/bin/perl -wT
+# -*- 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
+# Corporation. Portions created by Mozilla are
+# Copyright (C) 2006 Mozilla Foundation. All Rights Reserved.
+#
+# Contributor(s): Myk Melez <myk@mozilla.org>
+# Alex Brugh <alex@cs.umn.edu>
+# Dave Miller <justdave@mozilla.com>
+# Byron Jones <glob@mozilla.com>
+
+use strict;
+
+use lib qw(.);
+
+use Bugzilla;
+use Bugzilla::Constants;
+use Bugzilla::Util;
+
+use Getopt::Long;
+
+my $dbh = Bugzilla->dbh;
+
+# This SQL is designed to sanitize a copy of a Bugzilla database so that it
+# doesn't contain any information that can't be viewed from a web browser by
+# a user who is not logged in.
+
+# Last validated against Bugzilla version 4.0
+
+my ($dry_run, $from_cron, $keep_attachments, $keep_groups,
+ $keep_passwords, $keep_insider, $trace) = (0, 0, 0, '', 0, 0, 0);
+my $keep_groups_sql = '';
+
+GetOptions(
+ "dry-run" => \$dry_run,
+ "from-cron" => \$from_cron,
+ "keep-attachments" => \$keep_attachments,
+ "keep-passwords" => \$keep_passwords,
+ "keep-insider" => \$keep_insider,
+ "keep-groups:s" => \$keep_groups,
+ "trace" => \$trace,
+) or exit;
+
+if ($keep_groups ne '') {
+ my @groups;
+ foreach my $group_id (split(/\s*,\s*/, $keep_groups)) {
+ my $group;
+ if ($group_id =~ /\D/) {
+ $group = Bugzilla::Group->new({ name => $group_id });
+ } else {
+ $group = Bugzilla::Group->new($group_id);
+ }
+ die "Invalid group '$group_id'\n" unless $group;
+ push @groups, $group->id;
+ }
+ $keep_groups_sql = "NOT IN (" . join(",", @groups) . ")";
+}
+
+$dbh->{TraceLevel} = 1 if $trace;
+
+if ($dry_run) {
+ print "** dry run : no changes to the database will be made **\n";
+ $dbh->bz_start_transaction();
+}
+eval {
+ delete_non_public_products();
+ delete_secure_bugs();
+ delete_insider_comments() unless $keep_insider;
+ delete_security_groups();
+ delete_sensitive_user_data();
+ delete_attachment_data() unless $keep_attachments;
+ print "All done!\n";
+ $dbh->bz_rollback_transaction() if $dry_run;
+};
+if ($@) {
+ $dbh->bz_rollback_transaction() if $dry_run;
+ die "$@" if $@;
+}
+
+sub delete_non_public_products {
+ # Delete all non-public products, and all data associated with them
+ my @products = Bugzilla::Product->get_all();
+ my $mandatory = CONTROLMAPMANDATORY;
+ foreach my $product (@products) {
+ # if there are any mandatory groups on the product, nuke it and
+ # everything associated with it (including the bugs)
+ Bugzilla->params->{'allowbugdeletion'} = 1; # override this in memory for now
+ my $mandatorygroups = $dbh->selectcol_arrayref("SELECT group_id FROM group_control_map WHERE product_id = ? AND (membercontrol = $mandatory)", undef, $product->id);
+ if (0 < scalar(@$mandatorygroups)) {
+ print "Deleting product '" . $product->name . "'...\n";
+ $product->remove_from_db();
+ }
+ }
+}
+
+sub delete_secure_bugs {
+ # Delete all data for bugs in security groups.
+ my $buglist = $dbh->selectall_arrayref(
+ $keep_groups
+ ? "SELECT DISTINCT bug_id FROM bug_group_map WHERE group_id $keep_groups_sql"
+ : "SELECT DISTINCT bug_id FROM bug_group_map"
+ );
+ $|=1; # disable buffering so the bug progress counter works
+ my $numbugs = scalar(@$buglist);
+ my $bugnum = 0;
+ print "Deleting $numbugs bugs in " . ($keep_groups ? 'non-' : '') . "security groups...\n";
+ foreach my $row (@$buglist) {
+ my $bug_id = $row->[0];
+ $bugnum++;
+ print "\r$bugnum/$numbugs" unless $from_cron;
+ my $bug = new Bugzilla::Bug($bug_id);
+ $bug->remove_from_db();
+ }
+ print "\rDone \n" unless $from_cron;
+}
+
+sub delete_insider_comments {
+ # Delete all 'insidergroup' comments and attachments
+ print "Deleting 'insidergroup' comments and attachments...\n";
+ $dbh->do("DELETE FROM longdescs WHERE isprivate = 1");
+ $dbh->do("DELETE attach_data FROM attachments JOIN attach_data ON attachments.attach_id = attach_data.id WHERE attachments.isprivate = 1");
+ $dbh->do("DELETE FROM attachments WHERE isprivate = 1");
+ $dbh->do("UPDATE bugs_fulltext SET comments = comments_noprivate");
+}
+
+sub delete_security_groups {
+ # Delete all security groups.
+ print "Deleting " . ($keep_groups ? 'non-' : '') . "security groups...\n";
+ $dbh->do("DELETE user_group_map FROM groups JOIN user_group_map ON groups.id = user_group_map.group_id WHERE groups.isbuggroup = 1");
+ $dbh->do("DELETE group_group_map FROM groups JOIN group_group_map ON (groups.id = group_group_map.member_id OR groups.id = group_group_map.grantor_id) WHERE groups.isbuggroup = 1");
+ $dbh->do("DELETE group_control_map FROM groups JOIN group_control_map ON groups.id = group_control_map.group_id WHERE groups.isbuggroup = 1");
+ $dbh->do("UPDATE flagtypes LEFT JOIN groups ON flagtypes.grant_group_id = groups.id SET grant_group_id = NULL WHERE groups.isbuggroup = 1");
+ $dbh->do("UPDATE flagtypes LEFT JOIN groups ON flagtypes.request_group_id = groups.id SET request_group_id = NULL WHERE groups.isbuggroup = 1");
+ if ($keep_groups) {
+ $dbh->do("DELETE FROM groups WHERE isbuggroup = 1 AND id $keep_groups_sql");
+ } else {
+ $dbh->do("DELETE FROM groups WHERE isbuggroup = 1");
+ }
+}
+
+sub delete_sensitive_user_data {
+ # Remove sensitive user account data.
+ print "Deleting sensitive user account data...\n";
+ $dbh->do("UPDATE profiles SET cryptpassword = 'deleted'") unless $keep_passwords;
+ $dbh->do("DELETE FROM profiles_activity");
+ $dbh->do("DELETE FROM profile_search");
+ $dbh->do("DELETE FROM namedqueries");
+ $dbh->do("DELETE FROM tokens");
+ $dbh->do("DELETE FROM logincookies");
+ $dbh->do("DELETE FROM login_failure");
+ $dbh->do("DELETE FROM ts_error");
+ $dbh->do("DELETE FROM ts_exitstatus");
+ $dbh->do("DELETE FROM ts_funcmap");
+ $dbh->do("DELETE FROM ts_job");
+ $dbh->do("DELETE FROM ts_note");
+}
+
+sub delete_attachment_data {
+ # Delete unnecessary attachment data.
+ print "Removing attachment data to preserve disk space...\n";
+ $dbh->do("UPDATE attach_data SET thedata = ''");
+}
+
diff --git a/contrib/verify-user.pl b/contrib/verify-user.pl
new file mode 100755
index 000000000..d12cd745f
--- /dev/null
+++ b/contrib/verify-user.pl
@@ -0,0 +1,129 @@
+#!/usr/bin/perl -wT
+# -*- 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 Netscape Communications
+# Corporation. Portions created by Netscape are
+# Copyright (C) 1998 Netscape Communications Corporation. All
+# Rights Reserved.
+#
+# Contributor(s): Myk Melez <myk@mozilla.org>
+# Dave Miller <justdave@bugzilla.org>
+
+# See if a user account has ever done anything
+
+# ./verify-user.pl foo@baz.com
+
+use strict;
+
+use lib qw(.);
+
+use Bugzilla;
+use Bugzilla::Util;
+use Bugzilla::DB;
+use Bugzilla::Constants;
+
+# Make sure accounts were specified on the command line and exist.
+my $user = $ARGV[0] || die "You must specify an user.\n";
+my $dbh = Bugzilla->dbh;
+my $sth;
+
+#$sth = $dbh->prepare("SELECT name, count(*) as qty from bugs, products where reporter=198524 and product_id=products.id group by name order by qty desc");
+#$sth->execute();
+#my $results = $sth->fetchall_arrayref();
+#use Data::Dumper;
+#print Data::Dumper::Dumper($results);
+#exit;
+
+trick_taint($user);
+if ($user =~ /^\d+$/) { # user ID passed instead of email
+ $sth = $dbh->prepare('SELECT login_name FROM profiles WHERE userid = ?');
+ $sth->execute($user);
+ ($user) = $sth->fetchrow_array || die "The user with ID $ARGV[0] does not exist.\n";
+ print "User $ARGV[0]'s login name is $user.\n";
+}
+$sth = $dbh->prepare("SELECT userid FROM profiles WHERE login_name = ?");
+$sth->execute($user);
+my ($user_id) = $sth->fetchrow_array || die "The user $user does not exist.\n";
+
+print "${user}'s ID is $user_id.\n";
+
+$sth = $dbh->prepare("SELECT DISTINCT ipaddr FROM logincookies WHERE userid = ?");
+$sth->execute($user_id);
+my $iplist = $sth->fetchall_arrayref;
+if (@$iplist > 0) {
+ print "This user has recently connected from the following IP addresses:\n";
+ foreach my $ip (@$iplist) {
+ print $$ip[0] . "\n";
+ }
+}
+
+
+# A list of tables and columns to be checked.
+my $columns = {
+ attachments => ['submitter_id'] ,
+ bugs => ['assigned_to', 'reporter', 'qa_contact'] ,
+ bugs_activity => ['who'] ,
+ cc => ['who'] ,
+ components => ['initialowner', 'initialqacontact'] ,
+ flags => ['setter_id', 'requestee_id'] ,
+ logincookies => ['userid'] ,
+ longdescs => ['who'] ,
+ namedqueries => ['userid'] ,
+ profiles_activity => ['userid', 'who'] ,
+ quips => ['userid'] ,
+ series => ['creator'] ,
+ tokens => ['userid'] ,
+ user_group_map => ['user_id'] ,
+ votes => ['who'] ,
+ watch => ['watcher', 'watched'] ,
+
+};
+
+my $fields = 0;
+# Check records for user.
+foreach my $table (keys(%$columns)) {
+ foreach my $column (@{$columns->{$table}}) {
+ $sth = $dbh->prepare("SELECT COUNT(*) FROM $table WHERE $column = ?");
+ if ($table eq 'user_group_map') {
+ $sth = $dbh->prepare("SELECT COUNT(*) FROM $table WHERE $column = ? AND grant_type = " . GRANT_DIRECT);
+ }
+ $sth->execute($user_id);
+ my ($val) = $sth->fetchrow_array;
+ $fields++ if $val;
+ print "$table.$column: $val\n" if $val;
+ }
+}
+
+print "The user is mentioned in $fields fields.\n";
+
+if ($::ARGV[1] && $::ARGV[1] eq '-r') {
+ if ($fields == 0) {
+ $sth = $dbh->prepare("SELECT login_name FROM profiles WHERE login_name = ?");
+ my $count = 0;
+ print "Finding an unused recycle ID";
+ do {
+ $count++;
+ $sth->execute(sprintf("reuseme%03d\@bugzilla.org", $count));
+ print ".";
+ } while (my ($match) = $sth->fetchrow_array());
+ printf "\nUsing reuseme%03d\@bugzilla.org.\n", $count;
+ $dbh->do("DELETE FROM user_group_map WHERE user_id=?",undef,$user_id);
+ $dbh->do("UPDATE profiles SET realname='', cryptpassword='randomgarbage' WHERE userid=?",undef,$user_id);
+ $dbh->do("UPDATE profiles SET login_name=? WHERE userid=?",undef,sprintf("reuseme%03d\@bugzilla.org",$count),$user_id);
+ }
+ else {
+ print "Account has been used, so not recycling.\n";
+ }
+}
diff --git a/describecomponents.cgi b/describecomponents.cgi
index ee1361284..ed1f2388c 100755
--- a/describecomponents.cgi
+++ b/describecomponents.cgi
@@ -41,7 +41,9 @@ print $cgi->header();
# This script does nothing but displaying mostly static data.
Bugzilla->switch_to_shadow_db;
-my $product_name = trim($cgi->param('product') || '');
+my $product_name = trim($cgi->param('product') || '');
+my $component_mark = trim($cgi->param('component') || '');
+
my $product = new Bugzilla::Product({'name' => $product_name});
unless ($product && $user->can_access_product($product->name)) {
@@ -82,7 +84,8 @@ unless ($product && $user->can_access_product($product->name)) {
# End Data/Security Validation
######################################################################
-$vars->{'product'} = $product;
+$vars->{'product'} = $product;
+$vars->{'component_mark'} = $component_mark;
$template->process("reports/components.html.tmpl", $vars)
|| ThrowTemplateError($template->error());
diff --git a/describekeywords.cgi b/describekeywords.cgi
index 9796b77d5..b8ed9bb48 100755
--- a/describekeywords.cgi
+++ b/describekeywords.cgi
@@ -38,7 +38,17 @@ my $vars = {};
# Run queries against the shadow DB.
Bugzilla->switch_to_shadow_db;
-$vars->{'keywords'} = Bugzilla::Keyword->get_all_with_bug_count();
+# Hide bug counts for security keywords from users who aren't a member of the
+# security group
+my $can_see_security = Bugzilla->user->in_group('security-group');
+my $keywords = Bugzilla::Keyword->get_all_with_bug_count();
+foreach my $keyword (@$keywords) {
+ $keyword->{'bug_count'} = 0
+ if $keyword->name =~ /^(?:sec|csec|wsec|opsec)-/
+ && !$can_see_security;
+}
+
+$vars->{'keywords'} = $keywords;
$vars->{'caneditkeywords'} = Bugzilla->user->in_group("editkeywords");
print Bugzilla->cgi->header();
diff --git a/editusers.cgi b/editusers.cgi
index 4182f6875..bd643e893 100755
--- a/editusers.cgi
+++ b/editusers.cgi
@@ -658,8 +658,17 @@ if ($action eq 'search') {
}
###########################################################################
-} elsif ($action eq 'activity') {
+} elsif ($action eq 'activity' || $action eq 'admin_activity') {
my $otherUser = check_user($otherUserID, $otherUserLogin);
+ my $activity_who = "profiles_activity.who";
+ my $activity_userid = "profiles_activity.userid";
+
+ if ($action eq 'admin_activity') {
+ $editusers || ThrowUserError("auth_failure", { group => "editusers",
+ action => "admin_activity",
+ object => "users" });
+ ($activity_userid, $activity_who) = ($activity_who, $activity_userid);
+ }
$vars->{'profile_changes'} = $dbh->selectall_arrayref(
"SELECT profiles.login_name AS who, " .
@@ -668,14 +677,15 @@ if ($action eq 'search') {
profiles_activity.oldvalue AS removed,
profiles_activity.newvalue AS added
FROM profiles_activity
- INNER JOIN profiles ON profiles_activity.who = profiles.userid
+ INNER JOIN profiles ON $activity_who = profiles.userid
INNER JOIN fielddefs ON fielddefs.id = profiles_activity.fieldid
- WHERE profiles_activity.userid = ?
+ WHERE $activity_userid = ?
ORDER BY profiles_activity.profiles_when",
{'Slice' => {}},
$otherUser->id);
$vars->{'otheruser'} = $otherUser;
+ $vars->{'action'} = $action;
$template->process("account/profile-activity.html.tmpl", $vars)
|| ThrowTemplateError($template->error());
diff --git a/enter_bug.cgi b/enter_bug.cgi
index 5b684a965..2ee23897b 100755
--- a/enter_bug.cgi
+++ b/enter_bug.cgi
@@ -51,6 +51,7 @@ use Bugzilla::Keyword;
use Bugzilla::Token;
use Bugzilla::Field;
use Bugzilla::Status;
+use Bugzilla::UserAgent;
my $user = Bugzilla->login(LOGIN_REQUIRED);
@@ -62,9 +63,21 @@ my $dbh = Bugzilla->dbh;
my $template = Bugzilla->template;
my $vars = {};
+# BMO add a hook for the guided extension
+Bugzilla::Hook::process('enter_bug_start', { vars => $vars });
+
# All pages point to the same part of the documentation.
$vars->{'doc_section'} = 'bugreports.html';
+if (!$vars->{'disable_guided'}) {
+ # Purpose: force guided format for newbies
+ $cgi->param(-name=>'format', -value=>'guided')
+ if !$cgi->param('format') && !$user->in_group('canconfirm');
+
+ $cgi->delete('format')
+ if ($cgi->param('format') && ($cgi->param('format') eq "__default__"));
+}
+
my $product_name = trim($cgi->param('product') || '');
# Will contain the product object the bug is created in.
my $product;
@@ -74,8 +87,14 @@ if ($product_name eq '') {
my @enterable_products = @{$user->get_enterable_products};
ThrowUserError('no_products') unless scalar(@enterable_products);
- my $classification = Bugzilla->params->{'useclassification'} ?
- scalar($cgi->param('classification')) : '__all';
+ # MOZILLA CUSTOMIZATION
+ # skip the classification selection page
+ my $classification;
+ if (Bugzilla->params->{'useclassification'}) {
+ $classification = scalar($cgi->param('classification')) || '__all';
+ } else {
+ $classification = '__all';
+ }
# Unless a real classification name is given, we sort products
# by classification.
@@ -158,6 +177,9 @@ if ($product_name eq '') {
# to enter a bug against this product.
$product = $user->can_enter_product($product || $product_name, THROW_ERROR);
+# Preloading certain attributes such as components/versions/milestones/flags
+Bugzilla::Product::preload([ $product ], 1, { is_active => 1 });
+
##############################################################################
# Useful Subroutines
##############################################################################
@@ -166,198 +188,6 @@ sub formvalue {
return Bugzilla->cgi->param($name) || $default || "";
}
-# 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.
-# The field should be named after its DB table.
-# Returns undef if none of the platforms match.
-sub pick_valid_field_value (@) {
- my ($field, @values) = @_;
- my $dbh = Bugzilla->dbh;
-
- foreach my $value (@values) {
- return $value if $dbh->selectrow_array(
- "SELECT 1 FROM $field WHERE value = ?", undef, $value);
- }
- return undef;
-}
-
-sub pickplatform {
- return formvalue("rep_platform") if formvalue("rep_platform");
-
- my @platform;
-
- if (Bugzilla->params->{'defaultplatform'}) {
- @platform = Bugzilla->params->{'defaultplatform'};
- } else {
- # If @platform is a list, this function will return the first
- # item in the list that is a valid platform choice. If
- # no choice is valid, we return "Other".
- for ($ENV{'HTTP_USER_AGENT'}) {
- #PowerPC
- /\(.*PowerPC.*\)/i && do {push @platform, ("PowerPC", "Macintosh");};
- #AMD64, Intel x86_64
- /\(.*amd64.*\)/ && do {push @platform, ("AMD64", "x86_64", "PC");};
- /\(.*x86_64.*\)/ && do {push @platform, ("AMD64", "x86_64", "PC");};
- #Intel Itanium
- /\(.*IA64.*\)/ && do {push @platform, "IA64";};
- #Intel x86
- /\(.*Intel.*\)/ && do {push @platform, ("IA32", "x86", "PC");};
- /\(.*[ix0-9]86.*\)/ && do {push @platform, ("IA32", "x86", "PC");};
- #Versions of Windows that only run on Intel x86
- /\(.*Win(?:dows |)[39M].*\)/ && do {push @platform, ("IA32", "x86", "PC");};
- /\(.*Win(?:dows |)16.*\)/ && do {push @platform, ("IA32", "x86", "PC");};
- #Sparc
- /\(.*sparc.*\)/ && do {push @platform, ("Sparc", "Sun");};
- /\(.*sun4.*\)/ && do {push @platform, ("Sparc", "Sun");};
- #Alpha
- /\(.*AXP.*\)/i && do {push @platform, ("Alpha", "DEC");};
- /\(.*[ _]Alpha.\D/i && do {push @platform, ("Alpha", "DEC");};
- /\(.*[ _]Alpha\)/i && do {push @platform, ("Alpha", "DEC");};
- #MIPS
- /\(.*IRIX.*\)/i && do {push @platform, ("MIPS", "SGI");};
- /\(.*MIPS.*\)/i && do {push @platform, ("MIPS", "SGI");};
- #68k
- /\(.*68K.*\)/ && do {push @platform, ("68k", "Macintosh");};
- /\(.*680[x0]0.*\)/ && do {push @platform, ("68k", "Macintosh");};
- #HP
- /\(.*9000.*\)/ && do {push @platform, ("PA-RISC", "HP");};
- #ARM
- /\(.*ARM.*\)/ && do {push @platform, ("ARM", "PocketPC");};
- #PocketPC intentionally before PowerPC
- /\(.*Windows CE.*PPC.*\)/ && do {push @platform, ("ARM", "PocketPC");};
- #PowerPC
- /\(.*PPC.*\)/ && do {push @platform, ("PowerPC", "Macintosh");};
- /\(.*AIX.*\)/ && do {push @platform, ("PowerPC", "Macintosh");};
- #Stereotypical and broken
- /\(.*Windows CE.*\)/ && do {push @platform, ("ARM", "PocketPC");};
- /\(.*Macintosh.*\)/ && do {push @platform, ("68k", "Macintosh");};
- /\(.*Mac OS [89].*\)/ && do {push @platform, ("68k", "Macintosh");};
- /\(.*Win64.*\)/ && do {push @platform, "IA64";};
- /\(Win.*\)/ && do {push @platform, ("IA32", "x86", "PC");};
- /\(.*Win(?:dows[ -])NT.*\)/ && do {push @platform, ("IA32", "x86", "PC");};
- /\(.*OSF.*\)/ && do {push @platform, ("Alpha", "DEC");};
- /\(.*HP-?UX.*\)/i && do {push @platform, ("PA-RISC", "HP");};
- /\(.*IRIX.*\)/i && do {push @platform, ("MIPS", "SGI");};
- /\(.*(SunOS|Solaris).*\)/ && do {push @platform, ("Sparc", "Sun");};
- #Braindead old browsers who didn't follow convention:
- /Amiga/ && do {push @platform, ("68k", "Macintosh");};
- /WinMosaic/ && do {push @platform, ("IA32", "x86", "PC");};
- }
- }
-
- return pick_valid_field_value('rep_platform', @platform) || "Other";
-}
-
-sub pickos {
- if (formvalue('op_sys') ne "") {
- return formvalue('op_sys');
- }
-
- my @os = ();
-
- if (Bugzilla->params->{'defaultopsys'}) {
- @os = Bugzilla->params->{'defaultopsys'};
- } else {
- # This function will return the first
- # item in @os that is a valid platform choice. If
- # no choice is valid, we return "Other".
- for ($ENV{'HTTP_USER_AGENT'}) {
- /\(.*IRIX.*\)/ && do {push @os, "IRIX";};
- /\(.*OSF.*\)/ && do {push @os, "OSF/1";};
- /\(.*Linux.*\)/ && do {push @os, "Linux";};
- /\(.*Solaris.*\)/ && do {push @os, "Solaris";};
- /\(.*SunOS.*\)/ && do {
- /\(.*SunOS 5.11.*\)/ && do {push @os, ("OpenSolaris", "Opensolaris", "Solaris 11");};
- /\(.*SunOS 5.10.*\)/ && do {push @os, "Solaris 10";};
- /\(.*SunOS 5.9.*\)/ && do {push @os, "Solaris 9";};
- /\(.*SunOS 5.8.*\)/ && do {push @os, "Solaris 8";};
- /\(.*SunOS 5.7.*\)/ && do {push @os, "Solaris 7";};
- /\(.*SunOS 5.6.*\)/ && do {push @os, "Solaris 6";};
- /\(.*SunOS 5.5.*\)/ && do {push @os, "Solaris 5";};
- /\(.*SunOS 5.*\)/ && do {push @os, "Solaris";};
- /\(.*SunOS.*sun4u.*\)/ && do {push @os, "Solaris";};
- /\(.*SunOS.*i86pc.*\)/ && do {push @os, "Solaris";};
- /\(.*SunOS.*\)/ && do {push @os, "SunOS";};
- };
- /\(.*HP-?UX.*\)/ && do {push @os, "HP-UX";};
- /\(.*BSD.*\)/ && do {
- /\(.*BSD\/(?:OS|386).*\)/ && do {push @os, "BSDI";};
- /\(.*FreeBSD.*\)/ && do {push @os, "FreeBSD";};
- /\(.*OpenBSD.*\)/ && do {push @os, "OpenBSD";};
- /\(.*NetBSD.*\)/ && do {push @os, "NetBSD";};
- };
- /\(.*BeOS.*\)/ && do {push @os, "BeOS";};
- /\(.*AIX.*\)/ && do {push @os, "AIX";};
- /\(.*OS\/2.*\)/ && do {push @os, "OS/2";};
- /\(.*QNX.*\)/ && do {push @os, "Neutrino";};
- /\(.*VMS.*\)/ && do {push @os, "OpenVMS";};
- /\(.*Win.*\)/ && do {
- /\(.*Windows XP.*\)/ && do {push @os, "Windows XP";};
- /\(.*Windows NT 6\.2.*\)/ && do {push @os, "Windows 8";};
- /\(.*Windows NT 6\.1.*\)/ && do {push @os, "Windows 7";};
- /\(.*Windows NT 6\.0.*\)/ && do {push @os, "Windows Vista";};
- /\(.*Windows NT 5\.2.*\)/ && do {push @os, "Windows Server 2003";};
- /\(.*Windows NT 5\.1.*\)/ && do {push @os, "Windows XP";};
- /\(.*Windows 2000.*\)/ && do {push @os, "Windows 2000";};
- /\(.*Windows NT 5.*\)/ && do {push @os, "Windows 2000";};
- /\(.*Win.*9[8x].*4\.9.*\)/ && do {push @os, "Windows ME";};
- /\(.*Win(?:dows |)M[Ee].*\)/ && do {push @os, "Windows ME";};
- /\(.*Win(?:dows |)98.*\)/ && do {push @os, "Windows 98";};
- /\(.*Win(?:dows |)95.*\)/ && do {push @os, "Windows 95";};
- /\(.*Win(?:dows |)16.*\)/ && do {push @os, "Windows 3.1";};
- /\(.*Win(?:dows[ -]|)NT.*\)/ && do {push @os, "Windows NT";};
- /\(.*Windows.*NT.*\)/ && do {push @os, "Windows NT";};
- };
- /\(.*Mac OS X.*\)/ && do {
- /\(.*Mac OS X (?:|Mach-O |\()10.8.*\)/ && do {push @os, "Mac OS X 10.8";};
- /\(.*Mac OS X (?:|Mach-O |\()10.7.*\)/ && do {push @os, "Mac OS X 10.7";};
- /\(.*Mac OS X (?:|Mach-O |\()10.6.*\)/ && do {push @os, "Mac OS X 10.6";};
- /\(.*Mac OS X (?:|Mach-O |\()10.5.*\)/ && do {push @os, "Mac OS X 10.5";};
- /\(.*Mac OS X (?:|Mach-O |\()10.4.*\)/ && do {push @os, "Mac OS X 10.4";};
- /\(.*Mac OS X (?:|Mach-O |\()10.3.*\)/ && do {push @os, "Mac OS X 10.3";};
- /\(.*Mac OS X (?:|Mach-O |\()10.2.*\)/ && do {push @os, "Mac OS X 10.2";};
- /\(.*Mac OS X (?:|Mach-O |\()10.1.*\)/ && do {push @os, "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.
- /\(.*Intel.*Mac OS X.*\)/ && do {push @os, "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
- /\(.*Mac OS X.*\)/ && do {push @os, ("Mac OS X 10.3", "Mac OS X 10.0", "Mac OS X");};
- };
- /\(.*32bit.*\)/ && do {push @os, "Windows 95";};
- /\(.*16bit.*\)/ && do {push @os, "Windows 3.1";};
- /\(.*Mac OS \d.*\)/ && do {
- /\(.*Mac OS 9.*\)/ && do {push @os, ("Mac System 9.x", "Mac System 9.0");};
- /\(.*Mac OS 8\.6.*\)/ && do {push @os, ("Mac System 8.6", "Mac System 8.5");};
- /\(.*Mac OS 8\.5.*\)/ && do {push @os, "Mac System 8.5";};
- /\(.*Mac OS 8\.1.*\)/ && do {push @os, ("Mac System 8.1", "Mac System 8.0");};
- /\(.*Mac OS 8\.0.*\)/ && do {push @os, "Mac System 8.0";};
- /\(.*Mac OS 8[^.].*\)/ && do {push @os, "Mac System 8.0";};
- /\(.*Mac OS 8.*\)/ && do {push @os, "Mac System 8.6";};
- };
- /\(.*Darwin.*\)/ && do {push @os, ("Mac OS X 10.0", "Mac OS X");};
- # Silly
- /\(.*Mac.*\)/ && do {
- /\(.*Mac.*PowerPC.*\)/ && do {push @os, "Mac System 9.x";};
- /\(.*Mac.*PPC.*\)/ && do {push @os, "Mac System 9.x";};
- /\(.*Mac.*68k.*\)/ && do {push @os, "Mac System 8.0";};
- };
- # Evil
- /Amiga/i && do {push @os, "Other";};
- /WinMosaic/ && do {push @os, "Windows 95";};
- /\(.*PowerPC.*\)/ && do {push @os, "Mac System 9.x";};
- /\(.*PPC.*\)/ && do {push @os, "Mac System 9.x";};
- /\(.*68K.*\)/ && do {push @os, "Mac System 8.0";};
- }
- }
-
- push(@os, "Windows") if grep(/^Windows /, @os);
- push(@os, "Mac OS") if grep(/^Mac /, @os);
-
- return pick_valid_field_value('op_sys', @os) || "Other";
-}
##############################################################################
# End of subroutines
##############################################################################
@@ -420,19 +250,20 @@ $default{'product'} = $product->name;
if ($cloned_bug_id) {
- $default{'component_'} = $cloned_bug->component;
- $default{'priority'} = $cloned_bug->priority;
- $default{'bug_severity'} = $cloned_bug->bug_severity;
- $default{'rep_platform'} = $cloned_bug->rep_platform;
- $default{'op_sys'} = $cloned_bug->op_sys;
-
- $vars->{'short_desc'} = $cloned_bug->short_desc;
- $vars->{'bug_file_loc'} = $cloned_bug->bug_file_loc;
- $vars->{'keywords'} = $cloned_bug->keywords;
- $vars->{'dependson'} = join (", ", $cloned_bug_id, @{$cloned_bug->dependson});
- $vars->{'blocked'} = join (", ", @{$cloned_bug->blocked});
- $vars->{'deadline'} = $cloned_bug->deadline;
- $vars->{'estimated_time'} = $cloned_bug->estimated_time;
+ $default{'component_'} = $cloned_bug->component;
+ $default{'priority'} = $cloned_bug->priority;
+ $default{'bug_severity'} = $cloned_bug->bug_severity;
+ $default{'rep_platform'} = $cloned_bug->rep_platform;
+ $default{'op_sys'} = $cloned_bug->op_sys;
+
+ $vars->{'short_desc'} = $cloned_bug->short_desc;
+ $vars->{'bug_file_loc'} = $cloned_bug->bug_file_loc;
+ $vars->{'keywords'} = $cloned_bug->keywords;
+ $vars->{'dependson'} = join (", ", $cloned_bug_id, @{$cloned_bug->dependson});
+ $vars->{'blocked'} = join (", ", @{$cloned_bug->blocked});
+ $vars->{'deadline'} = $cloned_bug->deadline;
+ $vars->{'estimated_time'} = $cloned_bug->estimated_time;
+ $vars->{'status_whiteboard'} = $cloned_bug->status_whiteboard;
if (defined $cloned_bug->cc) {
$vars->{'cc'} = join (", ", @{$cloned_bug->cc});
@@ -468,12 +299,13 @@ if ($cloned_bug_id) {
} # end of cloned bug entry form
else {
-
$default{'component_'} = formvalue('component');
$default{'priority'} = formvalue('priority', Bugzilla->params->{'defaultpriority'});
$default{'bug_severity'} = formvalue('bug_severity', Bugzilla->params->{'defaultseverity'});
- $default{'rep_platform'} = pickplatform();
- $default{'op_sys'} = pickos();
+ $default{'rep_platform'} = formvalue('rep_platform',
+ Bugzilla->params->{'defaultplatform'} || detect_platform());
+ $default{'op_sys'} = formvalue('op_sys',
+ Bugzilla->params->{'defaultopsys'} || detect_op_sys());
$vars->{'alias'} = formvalue('alias');
$vars->{'short_desc'} = formvalue('short_desc');
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 <gerv@gerv.net>
+
+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..a276a110e
--- /dev/null
+++ b/extensions/BMO/Extension.pm
@@ -0,0 +1,1014 @@
+# -*- 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 <gerv@gerv.net>
+# David Lawrence <dkl@mozilla.com>
+# Byron Jones <glob@mozilla.com>
+
+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 Bugzilla::Util;
+
+use Scalar::Util qw(blessed);
+use Date::Parse;
+use DateTime;
+use Encode qw(find_encoding);
+
+use Bugzilla::Extension::BMO::Constants;
+use Bugzilla::Extension::BMO::FakeBug;
+use Bugzilla::Extension::BMO::Data qw($cf_visible_in_products
+ $cf_flags
+ $cf_project_flags
+ $cf_disabled_flags
+ %group_change_notification
+ $blocking_trusted_setters
+ $blocking_trusted_requesters
+ $status_trusted_wanters
+ $status_trusted_setters
+ $other_setters
+ %always_fileable_group
+ %group_auto_cc
+ %product_sec_groups);
+use Bugzilla::Extension::BMO::Reports qw(user_activity_report
+ triage_reports
+ group_admins_report
+ email_queue_report
+ release_tracking_report
+ group_membership_report);
+
+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;
+ $vars->{'cf_is_project_flag'} = \&cf_is_project_flag;
+ $vars->{'cf_flag_disabled'} = \&cf_flag_disabled;
+
+ 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'} || {});
+ }
+
+ if ($file =~ /^attachment\/diff-header\./) {
+ my $attachid = $vars->{attachid} ? $vars->{attachid} : $vars->{newid};
+ $vars->{attachment} = Bugzilla::Attachment->new($attachid) if $attachid;
+ }
+}
+
+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') {
+ # Recently global/field-descs.none.tmpl and bug/field-help.none.tmpl
+ # were changed for better performance and are now only loaded once.
+ # I have not found an easy way to allow our hook template to check if
+ # it is called from pages/fields.html.tmpl. So we set a value in request_cache
+ # that our hook template can see.
+ Bugzilla->request_cache->{'bmo_fields_page'} = 1;
+ }
+ elsif ($page eq 'group_admins.html') {
+ group_admins_report($vars);
+ }
+ elsif ($page eq 'group_membership.html' or $page eq 'group_membership.txt') {
+ group_membership_report($page, $vars);
+ }
+ elsif ($page eq 'email_queue.html') {
+ email_queue_report($vars);
+ }
+ elsif ($page eq 'release_tracking_report.html') {
+ release_tracking_report($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 active_custom_fields {
+ my ($self, $args) = @_;
+ my $fields = $args->{'fields'};
+ my $params = $args->{'params'};
+ my $product = $params->{'product'};
+ my $component = $params->{'component'};
+
+ return if !$product;
+
+ my $product_name = blessed $product ? $product->name : $product;
+ my $component_name = blessed $component ? $component->name : $component;
+
+ my @tmp_fields;
+ foreach my $field (@$$fields) {
+ next if cf_hidden_in_product($field->name, $product_name, $component_name, $params->{'type'});
+ push(@tmp_fields, $field);
+ }
+ $$fields = \@tmp_fields;
+}
+
+sub cf_is_project_flag {
+ my ($field_name) = @_;
+ foreach my $flag_re (@$cf_project_flags) {
+ return 1 if $field_name =~ $flag_re;
+ }
+ return 0;
+}
+
+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 component name everywhere else.
+ my $component_list = [];
+ if ($component_name) {
+ $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 (ref($component) eq 'Regexp') {
+ if (grep($_ =~ $component, @$component_list)) {
+ $found_component = 1;
+ last;
+ }
+ } else {
+ 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;
+}
+
+sub cf_flag_disabled {
+ my ($field_name, $bug) = @_;
+ return 0 unless grep { $field_name eq $_ } @$cf_disabled_flags;
+ my $value = $bug->{$field_name};
+ return $value eq '---' || $value eq '';
+}
+
+# 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 $old = $ref->{old};
+ my $new = $ref->{new};
+ my $fieldname = $ref->{field_name};
+
+ 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 (exists $group_change_notification{$group}) {
+ foreach my $login (@{ $group_change_notification{$group} }) {
+ my $id = login_to_id($login);
+ $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 (
+ ($field eq 'bug_status' && $old_value eq 'VERIFIED')
+ || ($field eq 'dup_id' && $bug->status->name eq 'VERIFIED')
+ || ($field eq 'resolution' && $bug->status->name eq 'VERIFIED')
+ ) {
+ # You need at least editbugs to reopen a resolved/verified bug
+ if (!$user->in_group('editbugs', $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{<a href="https://crash-stats.mozilla.com/report/index/$match">bp-$match</a>};
+}
+
+sub _link_cve {
+ my $args = shift;
+ my $match = html_quote($args->{matches}->[0]);
+
+ return qq{<a href="http://cve.mitre.org/cgi-bin/cvename.cgi?name=$match">$match</a>};
+}
+
+sub _link_svn {
+ my $args = shift;
+ my $match = html_quote($args->{matches}->[0]);
+
+ return qq{<a href="http://viewvc.svn.mozilla.org/vc?view=rev&amp;revision=$match">r$match</a>};
+}
+
+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{<a href="https://hg.mozilla.org/$repo/rev/$id">$text</a>};
+}
+
+sub _link_bzr {
+ my $args = shift;
+ my $preamble = html_quote($args->{matches}->[0]);
+ my $url = html_quote($args->{matches}->[1]);
+ my $text = html_quote($args->{matches}->[2]);
+ my $id = html_quote($args->{matches}->[3]);
+
+ $url =~ s/\s+$//;
+ $url =~ s/\/$//;
+
+ return qq{$preamble<a href="http://$url/revision/$id">$text</a>};
+}
+
+sub bug_format_comment {
+ my ($self, $args) = @_;
+ my $regexes = $args->{'regexes'};
+
+ # Only match if not already in an URL using the negative lookbehind (?<!\/)
+ push (@$regexes, {
+ match => qr/(?<!\/)\b(?:UUID\s+|bp\-)([a-f0-9]{8}\-[a-f0-9]{4}\-[a-f0-9]{4}\-
+ [a-f0-9]{4}\-[a-f0-9]{12})\b/x,
+ replace => \&_link_uuid
+ });
+
+ 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
+ });
+
+ push (@$regexes, {
+ match => qr/\b(Committing\sto:\sbzr\+ssh:\/\/
+ (?:[^\@]+\@)?(bzr\.mozilla\.org[^\n]+)\n.*?\nCommitted\s)
+ (revision\s(\d+))/sx,
+ replace => \&_link_bzr
+ });
+
+ # 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'};
+
+ return unless $group_names;
+ $group_names = ref $group_names
+ ? $group_names
+ : [ map { trim($_) } split(',', $group_names) ];
+
+ foreach my $name (@$group_names) {
+ if (exists $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";
+ }
+ }
+ }
+ }
+}
+
+# Automatically CC users to bugs based on group & product
+sub bug_end_of_create {
+ my ($self, $args) = @_;
+ my $bug = $args->{'bug'};
+
+ foreach my $group_name (keys %group_auto_cc) {
+ if ($bug->in_group(Bugzilla::Group->new({ name => $group_name }))) {
+ my $ra_logins = exists $group_auto_cc{$group_name}->{$bug->product}
+ ? $group_auto_cc{$group_name}->{$bug->product}
+ : $group_auto_cc{$group_name}->{'_default'};
+ foreach my $login (@$ra_logins) {
+ $bug->add_cc($login);
+ }
+ }
+ }
+}
+
+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 _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 tp-bugs01-master01:';
+ push @message, " GRANT SELECT ON `bugs`.`$name` TO 'metrics'\@'10.8.70.20_';";
+ push @message, " GRANT SELECT ($name) ON `bugs`.`bugs` TO 'metrics'\@'10.8.70.20_';";
+ push @message, " GRANT SELECT ON `bugs`.`$name` TO 'metrics'\@'10.8.70.21_';";
+ push @message, " GRANT SELECT ($name) ON `bugs`.`bugs` TO 'metrics'\@'10.8.70.21_';";
+ push @message, '';
+ MessageToMTA(join("\n", @message));
+}
+
+sub webservice {
+ my ($self, $args) = @_;
+
+ my $dispatch = $args->{dispatch};
+ $dispatch->{BMO} = "Bugzilla::Extension::BMO::WebService";
+}
+
+our $search_content_matches;
+BEGIN {
+ $search_content_matches = \&Bugzilla::Search::_content_matches;
+}
+
+sub search_operator_field_override {
+ my ($self, $args) = @_;
+ my $search = $args->{'search'};
+ my $operators = $args->{'operators'};
+
+ my $cgi = Bugzilla->cgi;
+ my @comments = $cgi->param('comments');
+ my $exclude_comments = scalar(@comments) && !grep { $_ eq '1' } @comments;
+
+ if ($cgi->param('query_format')
+ && $cgi->param('query_format') eq 'specific'
+ && $exclude_comments
+ ) {
+ # use the non-comment operator
+ $operators->{'content'}->{matches} = \&_short_desc_matches;
+ $operators->{'content'}->{notmatches} = \&_short_desc_matches;
+
+ } else {
+ # restore default content operator
+ $operators->{'content'}->{matches} = $search_content_matches;
+ $operators->{'content'}->{notmatches} = $search_content_matches;
+ }
+}
+
+sub _short_desc_matches {
+ # copy of Bugzilla::Search::_content_matches with comment searching removed
+
+ my ($self, $args) = @_;
+ my ($chart_id, $joins, $fields, $operator, $value) =
+ @$args{qw(chart_id joins fields operator value)};
+ my $dbh = Bugzilla->dbh;
+
+ # Add the fulltext table to the query so we can search on it.
+ my $table = "bugs_fulltext_$chart_id";
+ push(@$joins, { table => 'bugs_fulltext', as => $table });
+
+ # Create search terms to add to the SELECT and WHERE clauses.
+ my ($term, $rterm) =
+ $dbh->sql_fulltext_search("$table.short_desc", $value, 2);
+ $rterm = $term if !$rterm;
+
+ # The term to use in the WHERE clause.
+ if ($operator =~ /not/i) {
+ $term = "NOT($term)";
+ }
+ $args->{term} = $term;
+
+ my $current = $self->COLUMNS->{'relevance'}->{name};
+ $current = $current ? "$current + " : '';
+ # For NOT searches, we just add 0 to the relevance.
+ my $select_term = $operator =~ /not/ ? 0 : "($current$rterm)";
+ $self->COLUMNS->{'relevance'}->{name} = $select_term;
+}
+
+sub mailer_before_send {
+ my ($self, $args) = @_;
+ my $email = $args->{email};
+
+ # Add X-Bugzilla-Tracking header
+ if ($email->header('X-Bugzilla-ID')) {
+ my $bug_id = $email->header('X-Bugzilla-ID');
+
+ # return if we cannot successfully load the bug object
+ my $bug = new Bugzilla::Bug($bug_id);
+ return if !$bug;
+
+ # The BMO hook in active_custom_fields will filter
+ # the fields for us based on product and component
+ my @fields = Bugzilla->active_custom_fields({
+ product => $bug->product_obj,
+ component => $bug->component_obj,
+ type => 2,
+ });
+
+ my @set_values = ();
+ foreach my $field (@fields) {
+ my $field_name = $field->name;
+ next if cf_flag_disabled($field_name, $bug);
+ next if !$bug->$field_name || $bug->$field_name eq '---';
+ push(@set_values, $field->description . ":" . $bug->$field_name);
+ }
+
+ if (@set_values) {
+ $email->header_set('X-Bugzilla-Tracking' => join(' ', @set_values));
+ }
+ }
+
+ # attachments disabled, see bug 714488
+ return;
+
+ # If email is a request for a review, add the attachment itself
+ # to the email as an attachment. Attachment must be content type
+ # text/plain and below a certain size. Otherwise the email already
+ # contain a link to the attachment.
+ if ($email
+ && $email->header('X-Bugzilla-Type') eq 'request'
+ && ($email->header('X-Bugzilla-Flag-Requestee')
+ && $email->header('X-Bugzilla-Flag-Requestee') eq $email->header('to')))
+ {
+ my $body = $email->body;
+
+ if (my ($attach_id) = $body =~ /Attachment\s+(\d+)\s*:/) {
+ my $attachment = Bugzilla::Attachment->new($attach_id);
+ if ($attachment
+ && $attachment->ispatch
+ && $attachment->contenttype eq 'text/plain'
+ && $attachment->linecount
+ && $attachment->linecount < REQUEST_MAX_ATTACH_LINES)
+ {
+ # Don't send a charset header with attachments, as they might
+ # not be UTF-8, unless we can properly detect it.
+ my $charset;
+ if (Bugzilla->feature('detect_charset')) {
+ my $encoding = detect_encoding($attachment->data);
+ if ($encoding) {
+ $charset = find_encoding($encoding)->mime_name;
+ }
+ }
+
+ my $attachment_part = Email::MIME->create(
+ attributes => {
+ content_type => $attachment->contenttype,
+ filename => $attachment->filename,
+ disposition => "attachment",
+ },
+ body => $attachment->data,
+ );
+ $attachment_part->charset_set($charset) if $charset;
+
+ $email->parts_add([ $attachment_part ]);
+ }
+ }
+ }
+}
+
+sub post_bug_after_creation {
+ my ($self, $args) = @_;
+ my $vars = $args->{vars};
+ my $bug = $vars->{bug};
+
+ if (Bugzilla->input_params->{format}
+ && Bugzilla->input_params->{format} eq 'employee-incident'
+ && $bug->component eq 'Server Operations: Desktop Issues')
+ {
+ my $error_mode_cache = Bugzilla->error_mode;
+ Bugzilla->error_mode(ERROR_MODE_DIE);
+
+ my $template = Bugzilla->template;
+ my $cgi = Bugzilla->cgi;
+
+ my ($investigate_bug, $ssh_key_bug);
+ my $old_user = Bugzilla->user;
+ eval {
+ Bugzilla->set_user(Bugzilla::User->new({ name => 'nobody@mozilla.org' }));
+ my $new_user = Bugzilla->user;
+
+ # HACK: User needs to be in the editbugs and primary bug's group to allow
+ # setting of dependencies.
+ $new_user->{'groups'} = [ Bugzilla::Group->new({ name => 'editbugs' }),
+ Bugzilla::Group->new({ name => 'infra' }),
+ Bugzilla::Group->new({ name => 'infrasec' }) ];
+
+ my $recipients = { changer => $new_user };
+ $vars->{original_reporter} = $old_user;
+
+ my $comment;
+ $cgi->param('display_action', '');
+ $template->process('bug/create/comment-employee-incident.txt.tmpl', $vars, \$comment)
+ || ThrowTemplateError($template->error());
+
+ $investigate_bug = Bugzilla::Bug->create({
+ short_desc => 'Investigate Lost Device',
+ product => 'mozilla.org',
+ component => 'Security Assurance: Incident',
+ status_whiteboard => '[infrasec:incident]',
+ bug_severity => 'critical',
+ cc => [ 'mcoates@mozilla.com', 'jstevensen@mozilla.com' ],
+ groups => [ 'infrasec' ],
+ comment => $comment,
+ op_sys => 'All',
+ rep_platform => 'All',
+ version => 'other',
+ dependson => $bug->bug_id,
+ });
+ $bug->set_all({ blocked => { add => [ $investigate_bug->bug_id ] }});
+ Bugzilla::BugMail::Send($investigate_bug->id, $recipients);
+
+ Bugzilla->set_user($old_user);
+ $vars->{original_reporter} = '';
+ $comment = '';
+ $cgi->param('display_action', 'ssh');
+ $template->process('bug/create/comment-employee-incident.txt.tmpl', $vars, \$comment)
+ || ThrowTemplateError($template->error());
+
+ $ssh_key_bug = Bugzilla::Bug->create({
+ short_desc => 'Disable/Regenerate SSH Key',
+ product => $bug->product,
+ component => $bug->component,
+ bug_severity => 'critical',
+ cc => $bug->cc,
+ groups => [ map { $_->{name} } @{ $bug->groups } ],
+ comment => $comment,
+ op_sys => 'All',
+ rep_platform => 'All',
+ version => 'other',
+ dependson => $bug->bug_id,
+ });
+ $bug->set_all({ blocked => { add => [ $ssh_key_bug->bug_id ] }});
+ Bugzilla::BugMail::Send($ssh_key_bug->id, $recipients);
+ };
+ my $error = $@;
+
+ Bugzilla->set_user($old_user);
+ Bugzilla->error_mode($error_mode_cache);
+
+ if ($error || !$investigate_bug || !$ssh_key_bug) {
+ warn "Failed to create additional employee-incident bug: $error" if $error;
+ $vars->{'message'} = 'employee_incident_creation_failed';
+ }
+ }
+}
+
+sub buglist_columns {
+ my ($self, $args) = @_;
+ my $columns = $args->{columns};
+ $columns->{'cc_count'} = {
+ name => '(SELECT COUNT(*) FROM cc WHERE cc.bug_id = bugs.bug_id)',
+ title => 'CC Count',
+ };
+}
+
+__PACKAGE__->NAME;
diff --git a/extensions/BMO/lib/Constants.pm b/extensions/BMO/lib/Constants.pm
new file mode 100644
index 000000000..23eaae9cb
--- /dev/null
+++ b/extensions/BMO/lib/Constants.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 BMO Bugzilla Extension.
+#
+# The Initial Developer of the Original Code is the Mozilla Foundation.
+# Portions created by the Initial Developer are Copyright (C) 2007
+# the Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+# David Lawrence <dkl@mozilla.com>
+
+package Bugzilla::Extension::BMO::Constants;
+use strict;
+use base qw(Exporter);
+our @EXPORT = qw(
+ REQUEST_MAX_ATTACH_LINES
+);
+
+# Maximum attachment size in lines that will be sent with a
+# requested attachment flag notification.
+use constant REQUEST_MAX_ATTACH_LINES => 1000;
+
+1;
diff --git a/extensions/BMO/lib/Data.pm b/extensions/BMO/lib/Data.pm
new file mode 100644
index 000000000..b2b05222f
--- /dev/null
+++ b/extensions/BMO/lib/Data.pm
@@ -0,0 +1,410 @@
+# -*- 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 <gerv@gerv.net>
+# Reed Loden <reed@reedloden.com>
+
+package Bugzilla::Extension::BMO::Data;
+use strict;
+
+use base qw(Exporter);
+use Tie::IxHash;
+
+our @EXPORT_OK = qw($cf_visible_in_products
+ $cf_flags $cf_project_flags
+ $cf_disabled_flags
+ %group_change_notification
+ $blocking_trusted_setters
+ $blocking_trusted_requesters
+ $status_trusted_wanters
+ $status_trusted_setters
+ $other_setters
+ %always_fileable_group
+ %group_auto_cc
+ %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",
+ qw/^cf_blocking_kilimanjaro|cf_blocking_basecamp/ => {
+ "Boot2Gecko" => [],
+ "Core" => [],
+ "Fennec" => [],
+ "Firefox for Android" => [],
+ "Firefox" => [],
+ "Firefox for Metro" => [],
+ "Marketplace" => [],
+ "mozilla.org" => [],
+ "Mozilla Services" => [],
+ "NSPR" => [],
+ "NSS" => [],
+ "Socorro" => [],
+ "Testing" => [],
+ "Thunderbird" => [],
+ "Toolkit" => [],
+ "Tracking" => [],
+ "Web Apps" => [],
+ },
+ qr/^cf_blocking_fennec/ => {
+ "addons.mozilla.org" => [],
+ "AUS" => [],
+ "Core" => [],
+ "Fennec" => [],
+ "Firefox for Android" => [],
+ "Marketing" => ["General"],
+ "mozilla.org" => ["Release Engineering", qr/^Release Engineering: /],
+ "Mozilla Localizations" => [],
+ "Mozilla Services" => [],
+ "NSPR" => [],
+ "support.mozilla.org" => [],
+ "Toolkit" => [],
+ "Tech Evangelism" => [],
+ "Testing" => ["General"],
+ },
+ 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" => [],
+ "Boot2Gecko" => [],
+ "Core Graveyard" => [],
+ "Core" => [],
+ "Directory" => [],
+ "Fennec" => [],
+ "Firefox for Android" => [],
+ "Firefox" => [],
+ "Firefox for Metro" => [],
+ "MailNews Core" => [],
+ "mozilla.org" => ["Release Engineering", qr/^Release Engineering: /],
+ "Mozilla QA" => ["Mozmill Tests"],
+ "Mozilla Localizations" => [],
+ "Mozilla Services" => [],
+ "NSPR" => [],
+ "NSS" => [],
+ "Other Applications" => [],
+ "SeaMonkey" => [],
+ "Socorro" => [],
+ "support.mozilla.org" => [],
+ "Tech Evangelism" => [],
+ "Testing" => [],
+ "Toolkit" => [],
+ "Websites" => ["getpersonas.com"],
+ "Webtools" => [],
+ "Plugins" => [],
+ },
+ qr/^cf_colo_site$/ => {
+ "mozilla.org" => [
+ "Server Operations",
+ "Server Operations: DCOps",
+ "Server Operations: Projects",
+ "Server Operations: RelEng",
+ "Server Operations: Security",
+ ],
+ },
+ qw/^cf_office$/ => {
+ "mozilla.org" => ["Server Operations: Desktop Issues"],
+ },
+ qr/^cf_crash_signature$/ => {
+ "addons.mozilla.org" => [],
+ "Add-on SDK" => [],
+ "Calendar" => [],
+ "Camino" => [],
+ "Composer" => [],
+ "Fennec" => [],
+ "Firefox for Android" => [],
+ "Firefox" => [],
+ "Firefox for Metro" => [],
+ "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" => [],
+ },
+ qw/^cf_due_date$/ => {
+ "Mozilla Reps" => [],
+ "mozilla.org" => ["Security Assurance: Review Request"],
+ },
+ qw/^cf_locale$/ => {
+ "www.mozilla.org" => [],
+ },
+);
+
+# Which custom fields are acting as flags (ie. custom flags)
+our $cf_flags = [
+ qr/^cf_(?:blocking|tracking|status)_/,
+];
+
+our $cf_project_flags = [
+ 'cf_blocking_kilimanjaro',
+ 'cf_blocking_basecamp',
+];
+
+# List of disabled fields.
+# Temp kludge until custom fields can be disabled correctly upstream.
+# Disabled fields are hidden unless they have a value set
+our $cf_disabled_flags = [
+ 'cf_blocking_20',
+ 'cf_status_20',
+ 'cf_tracking_firefox5',
+ 'cf_status_firefox5',
+ 'cf_blocking_thunderbird32',
+ 'cf_status_thunderbird32',
+ 'cf_blocking_thunderbird30',
+ 'cf_status_thunderbird30',
+ 'cf_blocking_seamonkey21',
+ 'cf_status_seamonkey21',
+ 'cf_tracking_seamonkey22',
+ 'cf_status_seamonkey22',
+ 'cf_tracking_firefox6',
+ 'cf_status_firefox6',
+ 'cf_tracking_thunderbird6',
+ 'cf_status_thunderbird6',
+ 'cf_tracking_seamonkey23',
+ 'cf_status_seamonkey23',
+ 'cf_tracking_firefox7',
+ 'cf_status_firefox7',
+ 'cf_tracking_thunderbird7',
+ 'cf_status_thunderbird7',
+ 'cf_tracking_seamonkey24',
+ 'cf_status_seamonkey24',
+ 'cf_tracking_firefox8',
+ 'cf_status_firefox8',
+ 'cf_tracking_thunderbird8',
+ 'cf_status_thunderbird8',
+ 'cf_tracking_seamonkey25',
+ 'cf_status_seamonkey25',
+ 'cf_blocking_191',
+ 'cf_status_191',
+ 'cf_blocking_thunderbird33',
+ 'cf_status_thunderbird33',
+ 'cf_tracking_firefox9',
+ 'cf_status_firefox9',
+ 'cf_tracking_thunderbird9',
+ 'cf_status_thunderbird9',
+ 'cf_tracking_seamonkey26',
+ 'cf_status_seamonkey26',
+ 'cf_tracking_firefox10',
+ 'cf_status_firefox10',
+ 'cf_tracking_thunderbird10',
+ 'cf_status_thunderbird10',
+ 'cf_tracking_seamonkey27',
+ 'cf_status_seamonkey27',
+ 'cf_tracking_firefox11',
+ 'cf_status_firefox11',
+ 'cf_tracking_thunderbird11',
+ 'cf_status_thunderbird11',
+ 'cf_tracking_seamonkey28',
+ 'cf_status_seamonkey28',
+ 'cf_tracking_firefox12',
+ 'cf_status_firefox12',
+ 'cf_tracking_thunderbird12',
+ 'cf_status_thunderbird12',
+ 'cf_tracking_seamonkey29',
+ 'cf_status_seamonkey29',
+ 'cf_blocking_192',
+ 'cf_status_192',
+ 'cf_blocking_fennec10',
+ 'cf_tracking_firefox13',
+ 'cf_status_firefox13',
+ 'cf_tracking_thunderbird13',
+ 'cf_status_thunderbird13',
+ 'cf_tracking_seamonkey210',
+ 'cf_status_seamonkey210',
+ 'cf_tracking_firefox14',
+ 'cf_status_firefox14',
+ 'cf_tracking_thunderbird14',
+ 'cf_status_thunderbird14',
+ 'cf_tracking_seamonkey211',
+ 'cf_status_seamonkey211',
+ 'cf_tracking_firefox15',
+ 'cf_status_firefox15',
+ 'cf_tracking_thunderbird15',
+ 'cf_status_thunderbird15',
+ 'cf_tracking_seamonkey212',
+ 'cf_status_seamonkey212',
+];
+
+# Who to CC on particular bugmails when certain groups are added or removed.
+our %group_change_notification = (
+ 'addons-security' => ['amo-editors@mozilla.org'],
+ 'bugzilla-security' => ['security@bugzilla.org'],
+ 'client-services-security' => ['amo-admins@mozilla.org', 'web-security@mozilla.org'],
+ 'core-security' => ['security@mozilla.org'],
+ 'mozilla-services-security' => ['web-security@mozilla.org'],
+ 'tamarin-security' => ['tamarinsecurity@adobe.com'],
+ 'websites-security' => ['web-security@mozilla.org'],
+ 'webtools-security' => ['web-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',
+ qr/^cf_blocking_kilimanjaro/ => 'kilimanjaro-drivers',
+ qr/^cf_blocking_basecamp/ => 'kilimanjaro-drivers',
+ '_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 = (
+ 'addons-security' => 1,
+ 'bugzilla-security' => 1,
+ 'client-services-security' => 1,
+ 'consulting' => 1,
+ 'core-security' => 1,
+ 'finance' => 1,
+ 'infra' => 1,
+ 'infrasec' => 1,
+ 'l20n-security' => 1,
+ 'marketing-private' => 1,
+ 'mozilla-confidential' => 1,
+ 'mozilla-corporation-confidential' => 1,
+ 'mozilla-foundation-confidential' => 1,
+ 'mozilla-messaging-confidential' => 1,
+ 'partner-confidential' => 1,
+ 'payments-confidential' => 1,
+ 'tamarin-security' => 1,
+ 'websites-security' => 1,
+ 'webtools-security' => 1,
+ 'winqual-data' => 1,
+);
+
+# Mapping of products to their security bits
+our %product_sec_groups = (
+ "addons.mozilla.org" => 'client-services-security',
+ "AUS" => 'client-services-security',
+ "Bugzilla" => 'bugzilla-security',
+ "bugzilla.mozilla.org" => 'bugzilla-security',
+ "Community Tools" => 'websites-security',
+ "Finance" => 'finance',
+ "Input" => 'websites-security',
+ "L20n" => 'l20n-security',
+ "Legal" => 'legal',
+ "Marketing" => 'marketing-private',
+ "Marketplace" => 'client-services-security',
+ "Mozilla Corporation" => 'mozilla-corporation-confidential',
+ "Mozilla Developer Network" => 'websites-security',
+ "Mozilla Grants" => 'grants',
+ "Mozilla Messaging" => 'mozilla-messaging-confidential',
+ "Mozilla Metrics" => 'metrics-private',
+ "mozilla.org" => 'mozilla-confidential',
+ "Mozilla PR" => 'pr-private',
+ "Mozilla QA" => 'mozilla-corporation-confidential',
+ "Mozilla Reps" => 'mozilla-reps',
+ "Mozilla Services" => 'mozilla-services-security',
+ "mozillaignite" => 'websites-security',
+ "Popcorn" => 'websites-security',
+ "Privacy" => 'privacy',
+ "quality.mozilla.org" => 'websites-security',
+ "Skywriter" => 'websites-security',
+ "Socorro" => 'client-services-security',
+ "support.mozilla.org" => 'websites-security',
+ "support.mozillamessaging.com" => 'websites-security',
+ "Talkback" => 'talkback-private',
+ "Tamarin" => 'tamarin-security',
+ "Testopia" => 'bugzilla-security',
+ "Web Apps" => 'client-services-security',
+ "webmaker.org" => 'websites-security',
+ "Thimble" => 'websites-security',
+ "Websites" => 'websites-security',
+ "Websites Graveyard" => 'websites-security',
+ "Webtools" => 'webtools-security',
+ "www.mozilla.org" => 'websites-security',
+ "_default" => 'core-security'
+);
+
+# Automatically CC users to bugs filed into configured groups and products
+our %group_auto_cc = (
+ 'partner-confidential' => {
+ '_default' => ['mbest@mozilla.com'],
+ },
+);
+
+# Default security groups for products should always been fileable
+map { $always_fileable_group{$_} = 1 } values %product_sec_groups;
+
+1;
diff --git a/extensions/BMO/lib/FakeBug.pm b/extensions/BMO/lib/FakeBug.pm
new file mode 100644
index 000000000..6127cb560
--- /dev/null
+++ b/extensions/BMO/lib/FakeBug.pm
@@ -0,0 +1,42 @@
+package Bugzilla::Extension::BMO::FakeBug;
+
+# 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 strict;
+
+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..b660f6075
--- /dev/null
+++ b/extensions/BMO/lib/Reports.pm
@@ -0,0 +1,1078 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::BMO::Reports;
+use strict;
+
+use Bugzilla::Extension::BMO::Data qw($cf_disabled_flags);
+
+use Bugzilla::Constants;
+use Bugzilla::Error;
+use Bugzilla::Field;
+use Bugzilla::User;
+use Bugzilla::Util qw(trim detaint_natural trick_taint correct_urlbase);
+
+use Date::Parse;
+use DateTime;
+use JSON qw(-convert_blessed_universally);
+use List::MoreUtils qw(uniq);
+
+use base qw(Exporter);
+
+our @EXPORT_OK = qw(user_activity_report
+ triage_reports
+ group_admins_report
+ email_queue_report
+ release_tracking_report
+ group_membership_report);
+
+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'} || '');
+ my $action = $input->{'action'} || '';
+
+ # fix non-breaking hyphens
+ $from =~ s/\N{U+2011}/-/g;
+ $to =~ s/\N{U+2011}/-/g;
+
+ if ($from eq '') {
+ my $dt = DateTime->now()->subtract('weeks' => 8);
+ $from = $dt->ymd('-');
+ }
+ if ($to eq '') {
+ my $dt = DateTime->now();
+ $to = $dt->ymd('-');
+ }
+
+ if ($action eq 'run') {
+ if ($input->{'who'} eq '') {
+ ThrowUserError('user_activity_missing_username');
+ }
+ Bugzilla::User::match_field({ 'who' => {'type' => 'multi'} });
+
+ my $from_dt = _string_to_datetime($from);
+ $from = $from_dt->ymd();
+
+ my $to_dt = _string_to_datetime($to);
+ $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 $order = ($input->{'sort'} && $input->{'sort'} eq 'bug')
+ ? 'bug_id, bug_when' : 'bug_when';
+
+ 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.description' AS name,
+ attachments.bug_id,
+ attachments.attach_id,
+ ".$dbh->sql_date_format('attachments.creation_ts', '%Y.%m.%d %H:%i:%s')." AS ts,
+ '(new attachment)' 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 $order ";
+
+ my $list = $dbh->selectall_arrayref($query, undef, @params);
+
+ if ($input->{debug}) {
+ while (my $param = shift @params) {
+ $query =~ s/\?/$dbh->quote($param)/e;
+ }
+ $vars->{debug_sql} = $query;
+ }
+
+ my @operations;
+ my $operation = {};
+ my $changes = [];
+ my $incomplete_data = 0;
+ my %bug_ids;
+
+ 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;
+ }
+
+ # Start a new changeset if required (depends on the sort order)
+ my $is_new_changeset;
+ if ($order eq 'bug_when') {
+ $is_new_changeset =
+ $operation->{'who'} &&
+ (
+ $who ne $operation->{'who'}
+ || $when ne $operation->{'when'}
+ || $bugid != $operation->{'bug'}
+ );
+ } else {
+ $is_new_changeset =
+ $operation->{'bug'} &&
+ $bugid != $operation->{'bug'};
+ }
+ if ($is_new_changeset) {
+ $operation->{'changes'} = $changes;
+ push (@operations, $operation);
+ $operation = {};
+ $changes = [];
+ }
+
+ $bug_ids{$bugid} = 1;
+
+ $operation->{'bug'} = $bugid;
+ $operation->{'who'} = $who;
+ $operation->{'when'} = $when;
+
+ $change{'fieldname'} = $fieldname;
+ $change{'attachid'} = $attachid;
+ $change{'removed'} = $removed;
+ $change{'added'} = $added;
+ $change{'when'} = $when;
+
+ if ($comment_id) {
+ $change{'comment'} = Bugzilla::Comment->new($comment_id);
+ next if $change{'comment'}->count == 0;
+ }
+
+ if ($attachid) {
+ $change{'attach'} = Bugzilla::Attachment->new($attachid);
+ }
+
+ push (@$changes, \%change);
+ }
+ }
+
+ if ($operation->{'who'}) {
+ $operation->{'changes'} = $changes;
+ push (@operations, $operation);
+ }
+
+ $vars->{'incomplete_data'} = $incomplete_data;
+ $vars->{'operations'} = \@operations;
+
+ my @bug_ids = sort { $a <=> $b } keys %bug_ids;
+ $vars->{'bug_ids'} = \@bug_ids;
+ }
+
+ $vars->{'action'} = $action;
+ $vars->{'who'} = join(',', @who);
+ $vars->{'who_count'} = scalar @who;
+ $vars->{'from'} = $from;
+ $vars->{'to'} = $to;
+ $vars->{'sort'} = $input->{'sort'};
+}
+
+sub _string_to_datetime {
+ my $input = shift;
+ my $time = _parse_date($input)
+ or ThrowUserError('report_invalid_date', { date => $input });
+ return _time_to_datetime($time);
+}
+
+sub _time_to_datetime {
+ my $time = shift;
+ return DateTime->from_epoch(epoch => $time)
+ ->set_time_zone('local')
+ ->truncate(to => 'day');
+}
+
+sub _parse_date {
+ my ($str) = @_;
+ if ($str =~ /^(-|\+)?(\d+)([hHdDwWmMyY])$/) {
+ # relative date
+ my ($sign, $amount, $unit, $date) = ($1, $2, lc $3, time);
+ my ($sec, $min, $hour, $mday, $month, $year, $wday) = localtime($date);
+ $amount = -$amount if $sign && $sign eq '+';
+ if ($unit eq 'w') {
+ # convert weeks to days
+ $amount = 7*$amount + $wday;
+ $unit = 'd';
+ }
+ if ($unit eq 'd') {
+ $date -= $sec + 60*$min + 3600*$hour + 24*3600*$amount;
+ return $date;
+ }
+ elsif ($unit eq 'y') {
+ return str2time(sprintf("%4d-01-01 00:00:00", $year+1900-$amount));
+ }
+ elsif ($unit eq 'm') {
+ $month -= $amount;
+ while ($month<0) { $year--; $month += 12; }
+ return str2time(sprintf("%4d-%02d-01 00:00:00", $year+1900, $month+1));
+ }
+ elsif ($unit eq 'h') {
+ # Special case 0h for 'beginning of this hour'
+ if ($amount == 0) {
+ $date -= $sec + 60*$min;
+ } else {
+ $date -= 3600*$amount;
+ }
+ return $date;
+ }
+ return undef;
+ }
+ return str2time($str);
+}
+
+sub triage_reports {
+ my ($vars, $filter) = @_;
+ my $dbh = Bugzilla->dbh;
+ my $input = Bugzilla->input_params;
+ my $user = Bugzilla->user;
+
+ if (exists $input->{'action'} && $input->{'action'} eq 'run' && $input->{'product'}) {
+
+ # load product and components from input
+
+ my $product = Bugzilla::Product->new({ name => $input->{'product'} })
+ || ThrowUserError('invalid_object', { object => 'Product', value => $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 })
+ || ThrowUserError('invalid_object', { object => 'Component', value => $component_name });
+ 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_last = $input->{'filter_last'};
+ my $filter_last_period = $input->{'last'};
+
+ if (!$filter_commenter || $filter_last) {
+ $filter_commenter = '1';
+ $filter_commenter_on = 'reporter';
+ }
+
+ 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'} })
+ || ThrowUserError('invalid_object', { object => 'User', value => $input->{'commenter_is'} });
+ $filter_commenter_id = $user ? $user->id : 0;
+ }
+
+ 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;
+ }
+ }
+
+ # form sql queries
+
+ my $now = (time);
+ 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;
+}
+
+sub group_admins_report {
+ my ($vars) = @_;
+ my $dbh = Bugzilla->dbh;
+ my $user = Bugzilla->user;
+
+ $user->in_group('editusers')
+ || ThrowUserError('auth_failure', { group => 'editusers',
+ action => 'run',
+ object => 'group_admins' });
+
+ my $query = "
+ SELECT groups.name, " .
+ $dbh->sql_group_concat('profiles.login_name', "','", 1) . "
+ FROM groups
+ LEFT JOIN user_group_map
+ ON user_group_map.group_id = groups.id
+ AND user_group_map.isbless = 1
+ AND user_group_map.grant_type = 0
+ LEFT JOIN profiles
+ ON user_group_map.user_id = profiles.userid
+ WHERE groups.isbuggroup = 1
+ GROUP BY groups.name";
+
+ my @groups;
+ foreach my $group (@{ $dbh->selectall_arrayref($query) }) {
+ my @admins;
+ if ($group->[1]) {
+ foreach my $admin (split(/,/, $group->[1])) {
+ push(@admins, Bugzilla::User->new({ name => $admin }));
+ }
+ }
+ push(@groups, { name => $group->[0], admins => \@admins });
+ }
+
+ $vars->{'groups'} = \@groups;
+}
+
+sub group_membership_report {
+ my ($page, $vars) = @_;
+ my $dbh = Bugzilla->dbh;
+ my $user = Bugzilla->user;
+ my $cgi = Bugzilla->cgi;
+
+ ($user->in_group('editusers') || $user->in_group('infrasec'))
+ || ThrowUserError('auth_failure', { group => 'editusers',
+ action => 'run',
+ object => 'group_admins' });
+
+ my $who = $cgi->param('who');
+ if (!defined($who) || $who eq '') {
+ if ($page eq 'group_membership.txt') {
+ print $cgi->redirect("page.cgi?id=group_membership.html&output=txt");
+ exit;
+ }
+ $vars->{'output'} = $cgi->param('output');
+ return;
+ }
+
+ Bugzilla::User::match_field({ 'who' => {'type' => 'multi'} });
+ $who = Bugzilla->input_params->{'who'};
+ $who = ref($who) ? $who : [ $who ];
+
+ my @users;
+ foreach my $login (@$who) {
+ my $u = Bugzilla::User->new(login_to_id($login, 1));
+
+ # this is lifted from $user->groups()
+ # we need to show which groups are direct and which are inherited
+
+ my $groups_to_check = $dbh->selectcol_arrayref(
+ q{SELECT DISTINCT group_id
+ FROM user_group_map
+ WHERE user_id = ? AND isbless = 0}, undef, $u->id);
+
+ my $rows = $dbh->selectall_arrayref(
+ "SELECT DISTINCT grantor_id, member_id
+ FROM group_group_map
+ WHERE grant_type = " . GROUP_MEMBERSHIP);
+
+ my %group_membership;
+ foreach my $row (@$rows) {
+ my ($grantor_id, $member_id) = @$row;
+ push (@{ $group_membership{$member_id} }, $grantor_id);
+ }
+
+ my %checked_groups;
+ my %direct_groups;
+ my %indirect_groups;
+ my %groups;
+
+ foreach my $member_id (@$groups_to_check) {
+ $direct_groups{$member_id} = 1;
+ }
+
+ while (scalar(@$groups_to_check) > 0) {
+ my $member_id = shift @$groups_to_check;
+ if (!$checked_groups{$member_id}) {
+ $checked_groups{$member_id} = 1;
+ my $members = $group_membership{$member_id};
+ my @new_to_check = grep(!$checked_groups{$_}, @$members);
+ push(@$groups_to_check, @new_to_check);
+ foreach my $id (@new_to_check) {
+ $indirect_groups{$id} = $member_id;
+ }
+ $groups{$member_id} = 1;
+ }
+ }
+
+ my @groups;
+ my $ra_groups = Bugzilla::Group->new_from_list([keys %groups]);
+ foreach my $group (@$ra_groups) {
+ my $via;
+ if ($direct_groups{$group->id}) {
+ $via = '';
+ } else {
+ foreach my $g (@$ra_groups) {
+ if ($g->id == $indirect_groups{$group->id}) {
+ $via = $g->name;
+ last;
+ }
+ }
+ }
+ push @groups, {
+ name => $group->name,
+ desc => $group->description,
+ via => $via,
+ };
+ }
+
+ push @users, {
+ user => $u,
+ groups => \@groups,
+ };
+ }
+
+ $vars->{'who'} = $who;
+ $vars->{'users'} = \@users;
+}
+
+sub email_queue_report {
+ my ($vars, $filter) = @_;
+ my $dbh = Bugzilla->dbh;
+ my $user = Bugzilla->user;
+
+ $user->in_group('admin') || $user->in_group('infra')
+ || ThrowUserError('auth_failure', { group => 'admin',
+ action => 'run',
+ object => 'email_queue' });
+
+ my $query = "
+ SELECT j.jobid,
+ j.insert_time,
+ j.run_after AS run_time,
+ COUNT(e.jobid) AS error_count,
+ MAX(e.error_time) AS error_time,
+ e.message AS error_message
+ FROM ts_job j
+ LEFT JOIN ts_error e ON e.jobid = j.jobid
+ GROUP BY j.jobid
+ ORDER BY j.run_after";
+
+ $vars->{'jobs'} = $dbh->selectall_arrayref($query, { Slice => {} });
+ $vars->{'now'} = (time);
+}
+
+sub release_tracking_report {
+ my ($vars) = @_;
+ my $dbh = Bugzilla->dbh;
+ my $input = Bugzilla->input_params;
+ my $user = Bugzilla->user;
+
+ my @flag_names = qw(
+ approval-mozilla-release
+ approval-mozilla-beta
+ approval-mozilla-aurora
+ approval-mozilla-central
+ approval-comm-release
+ approval-comm-beta
+ approval-comm-aurora
+ approval-calendar-release
+ approval-calendar-beta
+ approval-calendar-aurora
+ approval-mozilla-esr10
+ );
+
+ my @flags_json;
+ my @fields_json;
+ my @products_json;
+
+ #
+ # tracking flags
+ #
+
+ my $all_products = $user->get_selectable_products;
+ my @usable_products;
+
+ # build list of flags and their matching products
+
+ my @invalid_flag_names;
+ foreach my $flag_name (@flag_names) {
+ # grab all matching flag_types
+ my @flag_types = @{Bugzilla::FlagType::match({ name => $flag_name, is_active => 1 })};
+
+ # remove invalid flags
+ if (!@flag_types) {
+ push @invalid_flag_names, $flag_name;
+ next;
+ }
+
+ # we need a list of products, based on inclusions/exclusions
+ my @products;
+ my %flag_types;
+ foreach my $flag_type (@flag_types) {
+ $flag_types{$flag_type->name} = $flag_type->id;
+ my $has_all = 0;
+ my @exclusion_ids;
+ my @inclusion_ids;
+ foreach my $flag_type (@flag_types) {
+ if (scalar keys %{$flag_type->inclusions}) {
+ my $inclusions = $flag_type->inclusions;
+ foreach my $key (keys %$inclusions) {
+ push @inclusion_ids, ($inclusions->{$key} =~ /^(\d+)/);
+ }
+ } elsif (scalar keys %{$flag_type->exclusions}) {
+ my $exclusions = $flag_type->exclusions;
+ foreach my $key (keys %$exclusions) {
+ push @exclusion_ids, ($exclusions->{$key} =~ /^(\d+)/);
+ }
+ } else {
+ $has_all = 1;
+ last;
+ }
+ }
+
+ if ($has_all) {
+ push @products, @$all_products;
+ } elsif (scalar @exclusion_ids) {
+ push @products, @$all_products;
+ foreach my $exclude_id (uniq @exclusion_ids) {
+ @products = grep { $_->id != $exclude_id } @products;
+ }
+ } else {
+ foreach my $include_id (uniq @inclusion_ids) {
+ push @products, grep { $_->id == $include_id } @$all_products;
+ }
+ }
+ }
+ @products = uniq @products;
+ push @usable_products, @products;
+ my @product_ids = map { $_->id } sort { lc($a->name) cmp lc($b->name) } @products;
+
+ push @flags_json, {
+ name => $flag_name,
+ id => $flag_types{$flag_name} || 0,
+ products => \@product_ids,
+ fields => [],
+ };
+ }
+ foreach my $flag_name (@invalid_flag_names) {
+ @flag_names = grep { $_ ne $flag_name } @flag_names;
+ }
+ @usable_products = uniq @usable_products;
+
+ # build a list of tracking flags for each product
+ # also build the list of all fields
+
+ my @unlink_products;
+ foreach my $product (@usable_products) {
+ my @fields =
+ grep { _is_active_status_field($_->name) }
+ Bugzilla->active_custom_fields({ product => $product });
+ my @field_ids = map { $_->id } @fields;
+ if (!scalar @fields) {
+ push @unlink_products, $product;
+ next;
+ }
+
+ # product
+ push @products_json, {
+ name => $product->name,
+ id => $product->id,
+ fields => \@field_ids,
+ };
+
+ # add fields to flags
+ foreach my $rh (@flags_json) {
+ if (grep { $_ eq $product->id } @{$rh->{products}}) {
+ push @{$rh->{fields}}, @field_ids;
+ }
+ }
+
+ # add fields to fields_json
+ foreach my $field (@fields) {
+ my $existing = 0;
+ foreach my $rh (@fields_json) {
+ if ($rh->{id} == $field->id) {
+ $existing = 1;
+ last;
+ }
+ }
+ if (!$existing) {
+ push @fields_json, {
+ name => $field->name,
+ id => $field->id,
+ };
+ }
+ }
+ }
+ foreach my $rh (@flags_json) {
+ my @fields = uniq @{$rh->{fields}};
+ $rh->{fields} = \@fields;
+ }
+
+ # remove products which aren't linked with status fields
+
+ foreach my $rh (@flags_json) {
+ my @product_ids;
+ foreach my $id (@{$rh->{products}}) {
+ unless (grep { $_->id == $id } @unlink_products) {
+ push @product_ids, $id;
+ }
+ $rh->{products} = \@product_ids;
+ }
+ }
+
+ #
+ # rapid release dates
+ #
+
+ my @ranges;
+ my $start_date = _string_to_datetime('2011-08-16');
+ my $end_date = $start_date->clone->add(weeks => 6)->add(days => -1);
+ my $now_date = _time_to_datetime((time));
+
+ while ($start_date <= $now_date) {
+ unshift @ranges, {
+ value => sprintf("%s-%s", $start_date->ymd(''), $end_date->ymd('')),
+ label => sprintf("%s and %s", $start_date->ymd('-'), $end_date->ymd('-')),
+ };
+
+ $start_date = $end_date->clone;;
+ $start_date->add(days => 1);
+ $end_date->add(weeks => 6);
+ }
+ push @ranges, {
+ value => '*',
+ label => 'Anytime',
+ };
+
+ #
+ # run report
+ #
+
+ if ($input->{q} && !$input->{edit}) {
+ my $q = _parse_query($input->{q});
+
+ my @where;
+ my @params;
+ my $query = "
+ SELECT DISTINCT b.bug_id
+ FROM bugs b
+ INNER JOIN flags f ON f.bug_id = b.bug_id ";
+
+ if ($q->{start_date}) {
+ $query .= "INNER JOIN bugs_activity a ON a.bug_id = b.bug_id ";
+ }
+
+ $query .= "WHERE ";
+
+ if ($q->{start_date}) {
+ push @where, "(a.fieldid = ?)";
+ push @params, $q->{field_id};
+
+ push @where, "(a.bug_when >= ?)";
+ push @params, $q->{start_date} . ' 00:00:00';
+ push @where, "(a.bug_when < ?)";
+ push @params, $q->{end_date} . ' 00:00:00';
+
+ push @where, "(a.added LIKE ?)";
+ push @params, '%' . $q->{flag_name} . $q->{flag_status} . '%';
+ }
+
+ push @where, "(f.type_id IN (SELECT id FROM flagtypes WHERE name = ?))";
+ push @params, $q->{flag_name};
+
+ push @where, "(f.status = ?)";
+ push @params, $q->{flag_status};
+
+ if ($q->{product_id}) {
+ push @where, "(b.product_id = ?)";
+ push @params, $q->{product_id};
+ }
+
+ if (scalar @{$q->{fields}}) {
+ my @fields;
+ foreach my $field (@{$q->{fields}}) {
+ push @fields,
+ "(" .
+ ($field->{value} eq '+' ? '' : '!') .
+ "(b.".$field->{name}." IN ('fixed','verified'))" .
+ ") ";
+ }
+ my $join = uc $q->{join};
+ push @where, '(' . join(" $join ", @fields) . ')';
+ }
+
+ $query .= join("\nAND ", @where);
+
+ if ($input->{debug}) {
+ print "Content-Type: text/plain\n\n";
+ $query =~ s/\?/\000/g;
+ foreach my $param (@params) {
+ $query =~ s/\000/$param/;
+ }
+ print "$query\n";
+ exit;
+ }
+
+ my $bugs = $dbh->selectcol_arrayref($query, undef, @params);
+ push @$bugs, 0 unless @$bugs;
+
+ my $urlbase = correct_urlbase();
+ my $cgi = Bugzilla->cgi;
+ print $cgi->redirect(
+ -url => "${urlbase}buglist.cgi?bug_id=" . join(',', @$bugs)
+ );
+ exit;
+ }
+
+ #
+ # set template vars
+ #
+
+ my $json = JSON->new();
+ if (0) {
+ # debugging
+ $json->shrink(0);
+ $json->canonical(1);
+ $vars->{flags_json} = $json->pretty->encode(\@flags_json);
+ $vars->{products_json} = $json->pretty->encode(\@products_json);
+ $vars->{fields_json} = $json->pretty->encode(\@fields_json);
+ } else {
+ $json->shrink(1);
+ $vars->{flags_json} = $json->encode(\@flags_json);
+ $vars->{products_json} = $json->encode(\@products_json);
+ $vars->{fields_json} = $json->encode(\@fields_json);
+ }
+
+ $vars->{flag_names} = \@flag_names;
+ $vars->{ranges} = \@ranges;
+ $vars->{default_query} = $input->{q};
+ foreach my $field (qw(product flags range)) {
+ $vars->{$field} = $input->{$field};
+ }
+}
+
+sub _parse_query {
+ my $q = shift;
+ my @query = split(/:/, $q);
+ my $query;
+
+ # field_id for flag changes
+ $query->{field_id} = get_field_id('flagtypes.name');
+
+ # flag_name
+ my $flag_name = shift @query;
+ @{Bugzilla::FlagType::match({ name => $flag_name, is_active => 1 })}
+ or ThrowUserError('report_invalid_parameter', { name => 'flag_name' });
+ trick_taint($flag_name);
+ $query->{flag_name} = $flag_name;
+
+ # flag_status
+ my $flag_status = shift @query;
+ $flag_status =~ /^([\?\-\+])$/
+ or ThrowUserError('report_invalid_parameter', { name => 'flag_status' });
+ $query->{flag_status} = $1;
+
+ # date_range -> from_ymd to_ymd
+ my $date_range = shift @query;
+ if ($date_range ne '*') {
+ $date_range =~ /^(\d\d\d\d)(\d\d)(\d\d)-(\d\d\d\d)(\d\d)(\d\d)$/
+ or ThrowUserError('report_invalid_parameter', { name => 'date_range' });
+ $query->{start_date} = "$1-$2-$3";
+ $query->{end_date} = "$4-$5-$6";
+ }
+
+ # product_id
+ my $product_id = shift @query;
+ $product_id =~ /^(\d+)$/
+ or ThrowUserError('report_invalid_parameter', { name => 'product_id' });
+ $query->{product_id} = $1;
+
+ # join
+ my $join = shift @query;
+ $join =~ /^(and|or)$/
+ or ThrowUserError('report_invalid_parameter', { name => 'join' });
+ $query->{join} = $1;
+
+ # fields
+ my @fields;
+ foreach my $field (@query) {
+ $field =~ /^(\d+)([\-\+])$/
+ or ThrowUserError('report_invalid_parameter', { name => 'fields' });
+ my ($id, $value) = ($1, $2);
+ my $field_obj = Bugzilla::Field->new($id)
+ or ThrowUserError('report_invalid_parameter', { name => 'field_id' });
+ push @fields, { id => $id, value => $value, name => $field_obj->name };
+ }
+ $query->{fields} = \@fields;
+
+ return $query;
+}
+
+sub _is_active_status_field {
+ my ($field_name) = @_;
+ if ($field_name =~ /^cf_status/) {
+ return !grep { $field_name eq $_ } @$cf_disabled_flags
+ }
+ return 0;
+}
+
+1;
diff --git a/extensions/BMO/lib/WebService.pm b/extensions/BMO/lib/WebService.pm
new file mode 100644
index 000000000..cd3b9a92c
--- /dev/null
+++ b/extensions/BMO/lib/WebService.pm
@@ -0,0 +1,274 @@
+# -*- 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 <dkl@mozilla.com>
+
+package Bugzilla::Extension::BMO::WebService;
+
+use strict;
+use warnings;
+
+use base qw(Bugzilla::WebService);
+
+use Bugzilla::Constants;
+use Bugzilla::Error;
+use Bugzilla::Util qw(detaint_natural trick_taint);
+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;
+}
+
+sub prod_comp_search {
+ my ($self, $params) = @_;
+ my $user = Bugzilla->user;
+ my $dbh = Bugzilla->switch_to_shadow_db();
+
+ my $search = $params->{'search'};
+ $search || ThrowCodeError('param_required',
+ { function => 'Bug.prod_comp_search', param => 'search' });
+
+ my $limit = detaint_natural($params->{'limit'})
+ ? $dbh->sql_limit($params->{'limit'})
+ : '';
+
+ # We do this in the DB directly as we want it to be fast and
+ # not have the overhead of loading full product objects
+
+ # All products which the user has "Entry" access to.
+ my $enterable_ids = $dbh->selectcol_arrayref(
+ 'SELECT products.id FROM products
+ LEFT JOIN group_control_map
+ ON group_control_map.product_id = products.id
+ AND group_control_map.entry != 0
+ AND group_id NOT IN (' . $user->groups_as_string . ')
+ WHERE group_id IS NULL
+ AND products.isactive = 1');
+
+ if (scalar @$enterable_ids) {
+ # And all of these products must have at least one component
+ # and one version.
+ $enterable_ids = $dbh->selectcol_arrayref(
+ 'SELECT DISTINCT products.id FROM products
+ WHERE ' . $dbh->sql_in('products.id', $enterable_ids) .
+ ' AND products.id IN (SELECT DISTINCT components.product_id
+ FROM components
+ WHERE components.isactive = 1)
+ AND products.id IN (SELECT DISTINCT versions.product_id
+ FROM versions
+ WHERE versions.isactive = 1)');
+ }
+
+ return { products => [] } if !scalar @$enterable_ids;
+
+ my @list;
+ foreach my $word (split(/[\s,]+/, $search)) {
+ if ($word ne "") {
+ my $sql_word = $dbh->quote($word);
+ trick_taint($sql_word);
+ # XXX CONCAT_WS is MySQL specific
+ my $field = "CONCAT_WS(' ', products.name, components.name, components.description)";
+ push(@list, $dbh->sql_iposition($sql_word, $field) . " > 0");
+ }
+ }
+
+ my $products = $dbh->selectall_arrayref("
+ SELECT products.name AS product,
+ components.name AS component
+ FROM products
+ INNER JOIN components ON products.id = components.product_id
+ WHERE (" . join(" AND ", @list) . ")
+ AND products.id IN (" . join(",", @$enterable_ids) . ")
+ ORDER BY products.name $limit",
+ { Slice => {} });
+
+ return { products => $products };
+}
+
+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<Bugzilla::WebService> for a description of how parameters are passed,
+and what B<STABLE>, B<UNSTABLE>, and B<EXPERIMENTAL> mean.
+
+=head2 getBugsConfirmer
+
+B<UNSTABLE>
+
+=over
+
+=item B<Description>
+
+This method returns public bug ids that a given user has confirmed (changed from
+C<UNCONFIRMED> to C<NEW>).
+
+=item B<Params>
+
+You pass a field called C<names> that is a list of Bugzilla login names to find bugs for.
+
+=over
+
+=item C<names> (array) - An array of strings representing Bugzilla login names.
+
+=back
+
+=item B<Returns>
+
+=over
+
+A hash of Bugzilla login names. Each name points to an array of bug ids that the user has confirmed.
+
+=back
+
+=item B<Errors>
+
+=over
+
+=back
+
+=item B<History>
+
+=over
+
+=item Added in BMO Bugzilla B<4.0>.
+
+=back
+
+=back
+
+=head2 getBugsVerifier
+
+B<UNSTABLE>
+
+=over
+
+=item B<Description>
+
+This method returns public bug ids that a given user has verified (changed from
+C<RESOLVED> to C<VERIFIED>).
+
+=item B<Params>
+
+You pass a field called C<names> that is a list of Bugzilla login names to find bugs for.
+
+=over
+
+=item C<names> (array) - An array of strings representing Bugzilla login names.
+
+=back
+
+=item B<Returns>
+
+=over
+
+A hash of Bugzilla login names. Each name points to an array of bug ids that the user has verified.
+
+=back
+
+=item B<Errors>
+
+=over
+
+=back
+
+=item B<History>
+
+=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..8bd4a9812
--- /dev/null
+++ b/extensions/BMO/template/en/default/account/create.html.tmpl
@@ -0,0 +1,184 @@
+[%# 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 <gerv@gerv.net>
+ # Byron Jones <glob@mozilla.com>
+ #%]
+
+[%# 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' ]
+%]
+
+<script type="text/javascript">
+function onSubmit() {
+ var email = document.getElementById('login').value;
+ if (email == '') {
+ alert('You must enter your email address.');
+ return false;
+ }
+ var isValid =
+ email.match(/@/)
+ && email.match(/@.+\./)
+ && !email.match(/\.$/)
+ && !email.match(/[\\()&<>,'"\[\]]/)
+ ;
+ if (!isValid) {
+ alert(
+ "The e-mail address doesn't pass our syntax checking for a legal " +
+ "email address.\n\nA legal address must contain exactly one '@', and " +
+ "at least one '.' after the @.\n\nIt must also not contain any of " +
+ "these special characters: \ ( ) & < > , ; : \" [ ], or any whitespace."
+ );
+ return false;
+ }
+ return true;
+}
+</script>
+
+<table border="0" id="create-account">
+<tr>
+
+<td width="50%" id="create-account-left" valign="top">
+
+ <h2 class="column-header">I need help using a Mozilla Product</h2>
+
+ <table border="0" id="product-list">
+ [% 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/projects/"
+ 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."
+ %]
+ </table>
+
+</td>
+
+<td width="50%" id="create-account-right" valign="top">
+
+ <h2 class="column-header">I want to help</h2>
+
+ <div id="right-blurb">
+ <p>
+ Great! There are three things to know and do:
+ </p>
+ <ol>
+ <li>
+ Please consider reading our
+ <a href="https://developer.mozilla.org/en/Bug_writing_guidelines" target="_blank">[% terms.bug %] writing guidelines</a>.
+ </li>
+ <li>
+ [% 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
+ <a href="http://email.about.com/od/freeemailreviews/tp/free_email.htm" target="_blank">alternative email address</a>
+ for this reason.
+ </li>
+ <li>
+ 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.
+ </li>
+ </ol>
+ </div>
+
+ <h2 class="column-header">Create an account</h2>
+
+ <form method="post" action="createaccount.cgi" onsubmit="return onSubmit()">
+ <table id="create-account-form">
+ <tr>
+ <td class="label">Email Address:</td>
+ <td>
+ <input size="35" id="login" name="login" placeholder="you@example.com">[% Param('emailsuffix') FILTER html %]</td>
+ <td>
+ <input type="hidden" id="token" name="token" value="[% issue_hash_token(['create_account']) FILTER html %]">
+ <input type="submit" value="Create Account">
+ </td>
+ </tr>
+ </table>
+ </form>
+
+ [% Hook.process('additional_methods') %]
+
+</td>
+
+</tr>
+</table>
+
+<p id="bmo-admin">
+ If you think there's something wrong with [% terms.Bugzilla %], you can
+ <a href="mailto:bugzilla-admin@mozilla.org">send an email to the admins</a>, but
+ remember, they can't file [% terms.bugs %] for you, or solve tech support problems.
+</p>
+
+[% PROCESS global/footer.html.tmpl %]
+
+[% BLOCK product %]
+ <tr>
+ <td valign="top">
+ <a href="[% url FILTER none %]"><img
+ src="extensions/BMO/web/producticons/[% icon FILTER uri %].png"
+ border="0" width="64" height="64"></a>
+ </td>
+ <td valign="top">
+ <h2><a href="[% url FILTER none %]">[% name FILTER html %]</a></h2>
+ <div>[% desc FILTER html %]</div>
+ </td>
+ </tr>
+[% END %]
+
diff --git a/extensions/BMO/template/en/default/bug/create/comment-brownbag.txt.tmpl b/extensions/BMO/template/en/default/bug/create/comment-brownbag.txt.tmpl
new file mode 100644
index 000000000..d9aa35f17
--- /dev/null
+++ b/extensions/BMO/template/en/default/bug/create/comment-brownbag.txt.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 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):
+ # David Lawrence <dkl@mozilla.com>
+ #%]
+[% USE Bugzilla %]
+[% cgi = Bugzilla.cgi %]
+
+Topic: [% cgi.param('topic') %]
+Presenter: [% cgi.param('presenter') %]
+Date: [% cgi.param('date') %]
+Time: [% cgi.param('time_hour') %]:[% cgi.param('time_minute') %][% cgi.param('ampm') +%] [%+ cgi.param('time_zone') %]
+Duration: [% IF cgi.param('duration') == 'Other' %][% cgi.param('duration_other') %][% ELSE %][% cgi.param('duration') %][% END %]
+Audience: [% cgi.param('audience') %]
+Air Mozilla: [% IF cgi.param('airmozilla') %]Yes[% ELSE %]No[% END %]
+Dial-in: [% IF cgi.param('dialin') %]Yes[% ELSE %]No[% END %]
+Archive: [% IF cgi.param('archive') %]Yes[% ELSE %]No[% END %]
+Member of IT to help with A/V: [% IF cgi.param('ithelp') %]Yes[% ELSE %]No[% END %]
+Description:
+[% cgi.param('description') %]
diff --git a/extensions/BMO/template/en/default/bug/create/comment-employee-incident.txt.tmpl b/extensions/BMO/template/en/default/bug/create/comment-employee-incident.txt.tmpl
new file mode 100644
index 000000000..1b0902d64
--- /dev/null
+++ b/extensions/BMO/template/en/default/bug/create/comment-employee-incident.txt.tmpl
@@ -0,0 +1,57 @@
+[%# 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):
+ # David Lawrence <dkl@mozilla.com>
+ #%]
+[% USE Bugzilla %]
+[% cgi = Bugzilla.cgi %]
+
+[% IF cgi.param('incident_type') == 'stolen' %]
+[% IF original_reporter -%]
+Reporter: [% original_reporter.identity FILTER none %]
+[%- END -%]
+
+ [% IF cgi.param('display_action') %]
+ [% IF cgi.param('display_action') == 'ldap' %]
+Action needed: Please immediately reset the LDAP password for this user.
+ [% ELSIF cgi.param('display_action') == 'ssh' %]
+Action needed: Please immediately disable the SSH key for this user.
+ [% END %]
+
+The user reported that their mobile or laptop device has been lost or stolen.
+This ticket was automatically generated from the employee incident reporting
+form. An additional ticket has been filed (see blocker bugs) for InfraSec to
+review the impact of this lost device.
+ [% END %]
+
+Type of device: [% cgi.param('device') %]
+Was the device encrypted?: [% cgi.param('encrypted') %]
+Any user data on the device?: [% cgi.param('userdata') %]
+ [% IF cgi.param('userdata') == 'Yes' %]
+Sensitive data on the device:
+[%+ cgi.param('sensitivedata') %]
+ [% END %]
+Browser configured to remember passwords?: [% cgi.param('rememberpasswords') %]
+ [% IF cgi.param('rememberpasswords') == 'Yes' %]
+Critical sites:
+[%+ cgi.param('criticalsites') %]
+ [% END %]
+[% END %]
+[% IF cgi.param('comment') %]
+Extra Notes:
+[%+ cgi.param('comment') %]
+[% END %]
diff --git a/extensions/BMO/template/en/default/bug/create/comment-finance.txt.tmpl b/extensions/BMO/template/en/default/bug/create/comment-finance.txt.tmpl
new file mode 100644
index 000000000..f0427b4c5
--- /dev/null
+++ b/extensions/BMO/template/en/default/bug/create/comment-finance.txt.tmpl
@@ -0,0 +1,35 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% USE Bugzilla %]
+[% cgi = Bugzilla.cgi %]
+
+Request Type: [% cgi.param('component') %]
+Summary: [% cgi.param('short_desc') %]
+Priority to your Team: [% cgi.param('team_priority') %]
+Timeframe for Signature: [% cgi.param('signature_time') %]
+
+Name of Other Party:
+[%+ cgi.param('other_party') %]
+
+Business Objective:
+[%+ cgi.param('business_obj') %]
+
+What is this purchase?:
+[%+ cgi.param('what_purchase') %]
+
+Why is this purchase needed?:
+[%+ cgi.param('why_purchase') %]
+
+What is the risk if this is not purchased?:
+[%+ cgi.param('risk_purchase') %]
+
+What is the alternative?:
+[%+ cgi.param('alternative_purchase') %]
+
+Total Cost: [% cgi.param('total_cost') %]
diff --git a/extensions/BMO/template/en/default/bug/create/comment-legal.txt.tmpl b/extensions/BMO/template/en/default/bug/create/comment-legal.txt.tmpl
new file mode 100644
index 000000000..eb00a88d9
--- /dev/null
+++ b/extensions/BMO/template/en/default/bug/create/comment-legal.txt.tmpl
@@ -0,0 +1,39 @@
+[%# 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):
+ # David Lawrence <dkl@mozilla.com>
+ #%]
+[% USE Bugzilla %]
+[% cgi = Bugzilla.cgi %]
+
+Priority for your Team:
+[%+ cgi.param('teampriority') %]
+
+Timeframe for Completion:
+[%+ cgi.param('timeframe') %]
+
+Goal:
+[%+ cgi.param('goal') %]
+
+Business Objective:
+[%+ cgi.param('busobj') %]
+
+Other Party:
+[%+ cgi.param('otherparty') %]
+
+Description:
+[%+ cgi.param("comment") %]
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 <gerv@gerv.net>
+ #%]
+[%# 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 <gerv@gerv.net>
+ #%]
+[%# 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-privacy-data.txt.tmpl b/extensions/BMO/template/en/default/bug/create/comment-privacy-data.txt.tmpl
new file mode 100644
index 000000000..279d59b6b
--- /dev/null
+++ b/extensions/BMO/template/en/default/bug/create/comment-privacy-data.txt.tmpl
@@ -0,0 +1,30 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% USE Bugzilla %]
+[% cgi = Bugzilla.cgi %]
+
+Where does this data come from:
+
+[%+ cgi.param('source') %]
+
+What people and things does this data describe, and what fields does it contain:
+
+[%+ cgi.param('data_desc') %]
+
+What parts of this data do you want to release:
+
+[%+ cgi.param('release') %]
+
+Why are we releasing this data, and what do we hope people will do with it:
+
+[%+ cgi.param('why') %]
+
+Is there a particular time by which you would like to release this data:
+
+[%+ cgi.param('when') %]
diff --git a/extensions/BMO/template/en/default/bug/create/comment-recoverykey.txt.tmpl b/extensions/BMO/template/en/default/bug/create/comment-recoverykey.txt.tmpl
new file mode 100644
index 000000000..9a38af7cc
--- /dev/null
+++ b/extensions/BMO/template/en/default/bug/create/comment-recoverykey.txt.tmpl
@@ -0,0 +1,28 @@
+[%# 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):
+ # David Lawrence <dkl@mozilla.com>
+ #%]
+[% USE Bugzilla %]
+[% cgi = Bugzilla.cgi %]
+
+Recovery Key: [% cgi.param('recoverykey') %]
+Asset Tag Number: [% cgi.param('assettag') %]
+
+[% IF cgi.param('comment') %]
+[%+ cgi.param('comment') %]
+[% END %]
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 <gerv@gerv.net>
+ #%]
+[%# 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..a73ae73cb
--- /dev/null
+++ b/extensions/BMO/template/en/default/bug/create/create-brownbag.html.tmpl
@@ -0,0 +1,331 @@
+[%# 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 <reed@mozilla.com>
+ # David Tran <dtran@mozilla.com>
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+
+[% PROCESS global/header.html.tmpl
+ title = "Event Request"
+ style = ".yui-skin-sam .yui-calcontainer { z-index: 1; }"
+ style_urls = [ 'skins/standard/enter_bug.css' ]
+ javascript_urls = [ 'js/attachment.js', 'js/field.js', 'js/util.js' ]
+ yui = [ 'autocomplete', 'calendar' ]
+%]
+
+[% USE Bugzilla %]
+
+<script type="text/javascript">
+function trySubmit() {
+ var timeZone = document.getElementById('time_zone').value;
+ if (!timeZone) {
+ alert('You must select an appropriate time zone');
+ return false;
+ }
+ var topic = document.getElementById('topic').value;
+ var date = document.getElementById('date').value;
+ var time = document.getElementById('time_hour').value + ':' +
+ document.getElementById('time_minute').value + ' ' + timeZone;
+ var location = document.getElementById('location').value;
+ var shortdesc = 'Event - (' + date + ' ' + time + ') - ' + location + ' - ' + topic;
+ document.getElementById('short_desc').value = shortdesc;
+
+ var public_yes = document.getElementById('public_yes').checked;
+ var public_no = document.getElementById('public_no').checked;
+ if (!public_yes && !public_no) {
+ alert('You must select whether the event is public or not');
+ return false;
+ }
+ if (public_no) {
+ var brownbagRequestForm = document.getElementById('brownbagRequestForm');
+ var groups = document.createElement('input');
+ groups.type = 'hidden';
+ groups.name = 'groups';
+ groups.value = 'mozilla-corporation-confidential';
+ brownbagRequestForm.appendChild(groups);
+ }
+
+ return true;
+}
+</script>
+
+<p>
+ <strong>Event Request:</strong> Please use this form to schedule an event
+ in any of the Mozilla Common Spaces.</b>
+</p>
+
+<p>Process:</p>
+
+<ol>
+ <li>Complete and submit request below.</li>
+ <li>Your request will be reviewed and assigned to the appropriate person in IT.</li>
+</ol>
+
+<form method="post" action="post_bug.cgi" id="brownbagRequestForm" class="enter_bug_form"
+ enctype="multipart/form-data" onSubmit="return trySubmit();">
+ <input type="hidden" name="format" value="brownbag">
+ <input type="hidden" name="product" value="Air Mozilla">
+ <input type="hidden" name="component" value="Events">
+ <input type="hidden" name="rep_platform" value="All">
+ <input type="hidden" name="op_sys" value="Other">
+ <input type="hidden" name="priority" value="--">
+ <input type="hidden" name="version" value="unspecified">
+ <input type="hidden" name="bug_severity" id="bug_severity" value="normal">
+ <input type="hidden" name="comment" id="comment" value="">
+ <input type="hidden" name="short_desc" id="short_desc" value="">
+ <input type="hidden" name="token" value="[% token FILTER html %]">
+
+[% FOREACH type = product.flag_types.bug %]
+ [% NEXT IF type.name != 'pr-review' %]
+ <input type="hidden" id="flag_type-[% type.id FILTER html %]"
+ name="flag_type-[% type.id FILTER html %]" value="?">
+ <input type="hidden" id="flag_type-[% type.id FILTER html %]"
+ name="requestee_type-[% type.id FILTER html %]"
+ value="PRreview@mozilla.com">
+[% END %]
+
+<table>
+
+<tr>
+ <th class="field_label">
+ <label for="presenter">Presenter:</label>
+ </th>
+ <td>
+ <input type="text" name="presenter" id="presenter" value="" size="60" />
+ </td>
+</tr>
+
+<tr>
+ <th class="field_label">
+ <label for="topic">Topic:</label>
+ </th>
+ <td>
+ <input type="text" name="topic" id="topic" value="" size="60" />
+ </td>
+</tr>
+
+<tr>
+ <th class="field_label">
+ <label for="date">Date:</label>
+ </th>
+ <td>
+ <input type="text" name="date" size="10" id="date"
+ align="right" value="" maxlength="10"
+ onchange="updateCalendarFromField(this)">
+ <button type="button" class="calendar_button" id="button_calendar_date"
+ onclick="showCalendar('date')"><span>Calendar</span>
+ </button>
+ <div id="con_calendar_date"></div>
+ </td>
+</tr>
+
+<tr>
+ <th class="field_label">
+ <label for="time_hour">Start Time (24 hr clock):</label>
+ </th>
+ <td>
+ <select name="time_hour" id="time_hour">
+ <option value="0">0</option>
+ <option value="1">1</option>
+ <option value="2">2</option>
+ <option value="3">3</option>
+ <option value="4">4</option>
+ <option value="5">5</option>
+ <option value="6">6</option>
+ <option value="7">7</option>
+ <option value="8">8</option>
+ <option value="9">9</option>
+ <option value="10">10</option>
+ <option value="11">11</option>
+ <option value="12" selected>12</option>
+ <option value="13">13</option>
+ <option value="14">14</option>
+ <option value="15">15</option>
+ <option value="16">16</option>
+ <option value="17">17</option>
+ <option value="18">18</option>
+ <option value="19">19</option>
+ <option value="20">20</option>
+ <option value="21">21</option>
+ <option value="22">22</option>
+ <option value="23">23</option>
+ </select>:
+ <select name="time_minute" id="time_minute">
+ <option value="00" selected>00</option>
+ <option value="15">15</option>
+ <option value="30">30</option>
+ <option value="45">45</option>
+ </select>
+ <select name="time_zone" id="time_zone">
+ <option value="" selected>Select Time Zone</option>
+ <option value="UTC-14">UTC-14</option>
+ <option value="UTC-13">UTC-13</option>
+ <option value="UTC-12">UTC-12</option>
+ <option value="UTC-11">UTC-11</option>
+ <option value="UTC-10">UTC-10</option>
+ <option value="UTC-9">UTC-9</option>
+ <option value="UTC-8">UTC-8</option>
+ <option value="UTC-7">UTC-7</option>
+ <option value="UTC-6">UTC-6</option>
+ <option value="UTC-5">UTC-5</option>
+ <option value="UTC-4">UTC-4</option>
+ <option value="UTC-3">UTC-3</option>
+ <option value="UTC-2">UTC-2</option>
+ <option value="UTC-1">UTC-1</option>
+ <option value="UTC+0">UTC+0</option>
+ <option value="UTC+1">UTC+1</option>
+ <option value="UTC+2">UTC+2</option>
+ <option value="UTC+3">UTC+3</option>
+ <option value="UTC+4">UTC+4</option>
+ <option value="UTC+5">UTC+5</option>
+ <option value="UTC+6">UTC+6</option>
+ <option value="UTC+7">UTC+7</option>
+ <option value="UTC+8">UTC+8</option>
+ <option value="UTC+9">UTC+9</option>
+ <option value="UTC+10">UTC+10</option>
+ <option value="UTC+11">UTC+11</option>
+ <option value="UTC+12">UTC+12</option>
+ <option value="UTC+13">UTC+13</option>
+ <option value="UTC+14">UTC+14</option>
+ </select>
+ </td>
+</tr>
+
+<tr>
+ <th class="field_label">
+ <label for="duration_one_hour_radio">Duration:</label>
+ </th>
+ <td>
+ <input type="radio" name="duration" id="duration_one_hour_radio" value="1 Hour" checked="checked">
+ <label for="duration_one_hour_radio">1 Hour</label><br>
+ <input type="radio" name="duration" id="duration_one_day_radio" value="1 Day">
+ <label for="duration_one_day_radio">1 Day</label><br>
+ <input type="radio" name="duration" id="duration_other_radio" value="Other"
+ onclick="YAHOO.util.Dom.get('duration_other').focus();">
+ <label for="duration_other_radio">Other</label>
+ <input type="text" name="duration_other" id="duration_other" value="">
+ </td>
+</tr>
+
+<tr>
+ <th class="field_label">
+ <label for="location">Originating Location:</label>
+ </th>
+ <td>
+ <input type="text" name="location" id="location"
+ value="[% default.location || 'Ten Forward' FILTER html %]" size="60" />
+ </td>
+</tr>
+
+<tr>
+ <th class="field_label">
+ <label for="large_screen_loc_mtv_radio">Show on large screens in<br>these Mozilla Spaces Commons:</label>
+ </th>
+ <td>
+ <input type="checkbox" name="large_screen_loc" id="large_screen_loc_mtv_checkbox" value="MTV" checked="checked">
+ <label for="large_screen_loc_mtv_checkbox">MTV</label><br>
+ <input type="checkbox" name="large_screen_loc" id="large_screen_loc_sfo_checkbox" value="SFO">
+ <label for="large_screen_loc_sfo_checkbox">SFO</label><br>
+ <input type="checkbox" name="large_screen_loc" id="large_screen_loc_tor_checkbox" value="TOR">
+ <label for="large_screen_loc_tor_checkbox">TOR</label><br>
+ <input type="checkbox" name="large_screen_loc" id="large_screen_loc_lon_checkbox" value="LON">
+ <label for="large_screen_loc_lon_checkbox">LON</label><br>
+ </td>
+</tr>
+
+<tr>
+ <th class="field_label">This event may be<br>viewed by the public:</th>
+ <td>
+ If <strong>No</strong> is chosen, this request will only be visible internally as well
+ as the reporter and anyone designated in the CC field.<br>
+ <input type="radio" name="public" id="public_yes" value="Yes">
+ <label for="public_yes">Yes</label><br>
+ <input type="radio" name="public" id="public_no" value="No">
+ <label for="public_no">No</label>
+ </td>
+</tr>
+
+<tr>
+ <th class="field_label">
+ <label for="airmozilla">Air Mozilla Broadcasting?</label>
+ </th>
+ <td align="left"><input type="checkbox" name="airmozilla" id="airmozilla"></td>
+</tr>
+
+<tr>
+ <th class="field_label">
+ <label for="ithelp">Need IT to help run A/V?</label>
+ </th>
+ <td align="left"><input type="checkbox" name="ithelp" id="ithelp" value="yes" checked></td>
+</tr>
+
+<tr>
+ <th class="field_label">
+ <label for="cc">CC&nbsp;(optional):</label>
+ </th>
+ <td colspan="3">
+ [% INCLUDE global/userselect.html.tmpl
+ id => "cc"
+ name => "cc"
+ value => cc
+ size => 60
+ multiple => 5
+ %]
+ </td>
+</tr>
+
+<tr>
+ <th class="field_label">
+ <label for="description">Description</label>:
+ </th>
+ <td>
+ Please describe the event the way you would in a program guide listing<br>
+ <textarea id="description" name="description" rows="10" cols="80"></textarea>
+ </td>
+</tr>
+
+<tr>
+ <th class="field_label">
+ <label for="special_requirements">Special Requirements:</label>
+ </th>
+ <td>
+ <textarea id="special_requirements" name="special_requirements" rows="10" cols="80"></textarea>
+ </td>
+</tr>
+
+<tr>
+ <td></td>
+ <td>
+ <input type="submit" id="commit" value="Submit Request">
+ </td>
+</tr>
+</table>
+
+</form>
+
+<p>
+ Thanks for contacting us.
+ You will be notified by email of any progress made in resolving your request.
+</p>
+
+<script type="text/javascript">
+ createCalendar('date');
+</script>
+
+[% PROCESS global/footer.html.tmpl %]
diff --git a/extensions/BMO/template/en/default/bug/create/create-employee-incident.html.tmpl b/extensions/BMO/template/en/default/bug/create/create-employee-incident.html.tmpl
new file mode 100644
index 000000000..2bbacdb12
--- /dev/null
+++ b/extensions/BMO/template/en/default/bug/create/create-employee-incident.html.tmpl
@@ -0,0 +1,288 @@
+[%# 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):
+ # David Lawrence <dkl@mozilla.com>
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+
+[% PROCESS global/header.html.tmpl
+ title = "Mozilla Corporation/Foundation Employee Incident"
+%]
+
+[% USE Bugzilla %]
+
+<script type="text/javascript">
+ var type_desc = new Array();
+ type_desc['safety'] = "If this is an emergency please immediately call your local police or emergency number.";
+ type_desc['stolen'] = "Please report a lost Mozilla laptop or any mobile device that was used to access<br> " +
+ "Mozilla email or contained passwords for Mozilla servers, devices, applications, etc.";
+
+ function validateAndSubmit() {
+ var alert_text = '';
+ var typeSelect = YAHOO.util.Dom.get('incident_type');
+ var typeValue = typeSelect.options[typeSelect.selectedIndex].value;
+
+ if (typeValue != 'stolen' && !isFilledOut('short_desc')) {
+ alert_text += "Please enter a summary.\n";
+ }
+
+ var select = YAHOO.util.Dom.get('incident_type');
+ var selectValue = select.options[select.selectedIndex].value;
+ if (selectValue == 'stolen') {
+ if (!isFilledOut('device')) {
+ alert_text += "Please provide the type of device.\n";
+ }
+ if (!isFilledOut('encrypted')) {
+ alert_text += "Please answer whether the device was encrypted.\n";
+ }
+ if (!isFilledOut('userdata')) {
+ alert_text += "Please answer whether the device had user data.\n";
+ }
+ if (!isFilledOut('rememberpasswords')) {
+ alert_text += "Please answer whether the browser was configured to remember passwords.\n";
+ }
+ }
+
+ if (alert_text) {
+ alert(alert_text);
+ return false;
+ }
+
+ // Hard code summary if stolen type was chosen
+ if (typeValue == 'stolen') {
+ document.getElementById('short_desc').value = '[Lost Device] Change LDAP Password for [% user.name FILTER js %]';
+ }
+
+ return true;
+ }
+
+ function setType (select) {
+ var selectValue = select.options[select.selectedIndex].value;
+
+ // Set the current description displayed.
+ document.getElementById('type_desc').innerHTML = type_desc[selectValue];
+
+ // Display/hide some additional fields based on type selected
+ if (selectValue == 'stolen') {
+ YAHOO.util.Dom.removeClass('stolen', 'bz_default_hidden');
+ YAHOO.util.Dom.addClass('safety', 'bz_default_hidden');
+ }
+ else {
+ YAHOO.util.Dom.removeClass('safety', 'bz_default_hidden');
+ YAHOO.util.Dom.addClass('stolen', 'bz_default_hidden');
+ }
+
+ // Alter the product/component/group based on type selected
+ if (selectValue == 'stolen') {
+ document.getElementById('product').value = 'mozilla.org';
+ document.getElementById('component').value = 'Server Operations: Desktop Issues';
+ document.getElementById('groups').value = 'infra';
+ document.getElementById('cc').value = 'mcoates@mozilla.com, jstevensen@mozilla.com, afowler@mozilla.com';
+ document.getElementById('bug_severity').value = 'critical';
+ document.getElementById('display_action').value = 'ldap';
+ }
+ else {
+ document.getElementById('product').value = 'Mozilla Corporation';
+ document.getElementById('component').value = 'Facilities Management';
+ document.getElementById('groups').value = 'hr';
+ document.getElementById('cc').value = 'dcohen@mozilla.com, mcoates@mozilla.com, jill@mozilla.com';
+ document.getElementById('bug_severity').value = 'normal';
+ document.getElementById('display_action').value = '';
+ }
+ }
+
+ function toggleEnabled (source, value, target) {
+ var sourceElement = YAHOO.util.Dom.get(source);
+ var targetElement = YAHOO.util.Dom.get(target);
+ if (sourceElement[sourceElement.selectedIndex].value == value) {
+ targetElement.disabled = false;
+ targetElement.focus();
+ }
+ else {
+ targetElement.disabled = true;
+ }
+ }
+
+ function isFilledOut(elem_id) {
+ var str = document.getElementById(elem_id).value;
+ return str.length > 0 && str != "noneselected";
+ }
+
+ YAHOO.util.Event.onDOMReady(function () {
+ setType(document.getElementById('incident_type'));
+ toggleEnabled('userdata', 'Yes', 'sensitivedata');
+ toggleEnabled('rememberpasswords', 'Yes', 'criticalsites');
+ });
+</script>
+
+<p><strong>Please use this form for employee incidents only!</strong></p>
+<p>If you have a [% terms.bug %] to file, go <a href="enter_bug.cgi">here</a>.</p>
+<p><span style="color: red;">*</span> Required Fields</p>
+<form method="post" action="post_bug.cgi" id="incidentForm" enctype="multipart/form-data"
+ onSubmit="return validateAndSubmit();">
+ <input type="hidden" id="product" name="product" value="">
+ <input type="hidden" id="component" name="component" value="">
+ <input type="hidden" id="rep_platform" name="rep_platform" value="All">
+ <input type="hidden" id="op_sys" name="op_sys" value="All">
+ <input type="hidden" id="priority" name="priority" value="--">
+ <input type="hidden" id="version" name="version" value="other">
+ <input type="hidden" id="cc" name="cc" value="">
+ <input type="hidden" id="groups" name="groups" value="">
+ <input type="hidden" id="format" name="format" value="employee-incident">
+ <input type="hidden" id="bug_severity" name="bug_severity" value="">
+ <input type="hidden" id="display_action" name="display_action" value="">
+ <input type="hidden" id="token" name="token" value="[% token FILTER html %]">
+
+ <table>
+ <tr>
+ <td align="right" valign="top"><strong>Incident Type:</strong></td>
+ <td>
+ <select id="incident_type" name="incident_type" onchange="setType(this);">
+ <option value="safety" selected>Report a Safety Concern</option>
+ <option value="stolen">My laptop or phone was lost/stolen</option>
+ </select>
+ <div id="type_desc" style="color:red;"></div>
+ </td>
+ </tr>
+ <tbody id="safety" class="bz_default_hidden">
+ <tr class="safety">
+ <td align="right">
+ <strong><span style="color: red;">*</span> Summary:</strong>
+ </td>
+ <td>
+ <input name="short_desc" id="short_desc" size="60"
+ value="[% short_desc FILTER html %]">
+ </td>
+ </tr>
+ </tbody>
+ <tbody id="stolen" class="bz_default_hidden">
+ <tr>
+ <td align="right" valign="top"><strong>Stolen Details:</strong></td>
+ <td>
+ <table>
+ <tr>
+ <td>
+ <label for="device">
+ <strong><span style="color: red;">*</span></strong>
+ Type of device lost:
+ </label>
+ </td>
+ <td>
+ <select name="device" id="device">
+ <option value="">---</option>
+ <option value="Mobile Phone">Mobile Phone</option>
+ <option value="Tablet">Tablet</option>
+ <option value="Laptop">Laptop</option>
+ <option value="WorkStation">WorkStation</option>
+ <option value="Portable Storage Device">Portable Storage Device</option>
+ <option value="Other">Other (describe in 'Extra Notes')</option>
+ </select>
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <label for="encrypted">
+ <strong><span style="color: red;">*</span></strong>
+ To your knowledge, was your device encrypted?
+ </label>
+ </td>
+ <td>
+ <select name="encrypted" id="encrypted">
+ <option value="">---</option>
+ <option value="No">No</option>
+ <option value="Yes">Yes</option>
+ </select>
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <label for="userdata">
+ <strong><span style="color: red;">*</span></strong>
+ Did you have any user data on your device?
+ </label>
+ </td>
+ <td>
+ <select name="userdata" id="userdata"
+ onchange="toggleEnabled('userdata', 'Yes', 'sensitivedata');">
+ <option value="">---</option>
+ <option value="No">No</option>
+ <option value="Yes">Yes</option>
+ </select>
+ </td>
+ </tr>
+ </table>
+ </td>
+ </tr>
+ <tr>
+ <td>&nbsp;</td>
+ <td>If yes, what sensitive data was stored on your device?</td>
+ </tr>
+ <tr>
+ <td>&nbsp;</td>
+ <td>
+ <textarea name="sensitivedata" id="sensitivedata" rows="10" cols="80"></textarea>
+ </td>
+ </tr>
+ <tr>
+ <td>&nbsp;</td>
+ <td>
+ <label for="rememberpasswords">
+ <strong><span style="color: red;">*</span></strong>
+ Was your browser configured to remember passwords
+ (<a href="http://support.mozilla.com/en-US/kb/make-firefox-remember-usernames-and-passwords">more info</a>)?
+ </label>
+ <select name="rememberpasswords" id="rememberpasswords"
+ onchange="toggleEnabled('rememberpasswords', 'Yes', 'criticalsites');">
+ <option value="">---</option>
+ <option value="No">No</option>
+ <option value="Yes">Yes</option>
+ </select>
+ </td>
+ </tr>
+ <tr>
+ <td>&nbsp;</td>
+ <td>If yes, which critical sites were included?</td>
+ </tr>
+ <tr>
+ <td>&nbsp;</td>
+ <td>
+ <textarea name="criticalsites" id="criticalsites" rows="10" cols="80"></textarea>
+ </td>
+ </tr>
+ </tbody>
+ <tr>
+ <td align="right" valign="top"><strong>Extra Notes:</strong></td>
+ <td>
+ <textarea name="comment" rows="10" cols="80">
+ [% comment FILTER html %]</textarea>
+ </td>
+ </tr>
+ <tr>
+ <td>&nbsp;</td>
+ <td>
+ <input type="submit" id="commit" value="Submit Request">
+ </td>
+ </tr>
+ </table>
+</form>
+
+<p>
+ Thanks for contacting us. You will be notified by email of any progress made in resolving your request.
+</p>
+
+[% PROCESS global/footer.html.tmpl %]
diff --git a/extensions/BMO/template/en/default/bug/create/create-finance.html.tmpl b/extensions/BMO/template/en/default/bug/create/create-finance.html.tmpl
new file mode 100644
index 000000000..fa8dc5f5b
--- /dev/null
+++ b/extensions/BMO/template/en/default/bug/create/create-finance.html.tmpl
@@ -0,0 +1,257 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+
+[% inline_style = BLOCK %]
+ #bug_form input[type=text], #bug_form input[type=file], #cc_autocomplete, #bug_form textarea {
+ width: 100%;
+ }
+[% END %]
+
+[% inline_js = BLOCK %]
+ var compdesc = new Array();
+ [% FOREACH comp = product.components %]
+ compdesc['[% comp.name FILTER js %]'] = '[% comp.description FILTER js %]';
+ [% END %]
+ function showCompDesc(component) {
+ var value = component.value;
+ document.getElementById('comp_description').innerHTML = compdesc[value];
+ }
+
+ function onSubmit() {
+ var alert_text = '';
+ if (!isFilledOut('component'))
+ alert_text += "Please select a value for request type.\n";
+ if (!isFilledOut('short_desc'))
+ alert_text += "Please enter a value for the summary.\n";
+ if (!isFilledOut('team_priority'))
+ alert_text += "Please select a value for team priority.\n";
+ if (!isFilledOut('signature_time'))
+ alert_text += "Please enter a value for signture timeframe.\n";
+ if (!isFilledOut('other_party'))
+ alert_text += "Please enter a value for the name of other party.\n";
+ if (!isFilledOut('business_obj'))
+ alert_text += "Please enter a value for business objective.\n";
+ if (!isFilledOut('what_purchase'))
+ alert_text += "Please enter a value for what you are purchasing.\n";
+ if (!isFilledOut('why_purchase'))
+ alert_text += "Please enter a value for why the purchase is needed.\n";
+ if (!isFilledOut('risk_purchase'))
+ alert_text += "Please enter a value for the risk if not purchased.\n";
+ if (!isFilledOut('alternative_purchase'))
+ alert_text += "Please enter a value for the purchase alternative.\n";
+ if (!isFilledOut('total_cost'))
+ alert_text += "Please enter a value for total cost.\n";
+ if (!isFilledOut('attachment'))
+ alert_text += "Please enter an attachment.\n";
+
+ if (alert_text != '') {
+ alert(alert_text);
+ return false;
+ }
+
+ return true;
+ }
+[% END %]
+
+[% PROCESS global/header.html.tmpl
+ title = "Finance"
+ style = inline_style
+ style_urls = [ 'skins/standard/enter_bug.css' ]
+ javascript = inline_js
+ javascript_urls = [ 'extensions/BMO/web/js/form_validate.js',
+ 'js/attachment.js', 'js/field.js', 'js/util.js' ]
+ onload = "showCompDesc(document.getElementById('component'));"
+%]
+
+<h2>Finance</h2>
+
+<p>All fields are mandatory</p>
+
+<form method="post" action="post_bug.cgi" id="bug_form" class="enter_bug_form"
+ enctype="multipart/form-data" onsubmit="return onSubmit();">
+<input type="hidden" name="format" value="finance">
+<input type="hidden" name="product" value="Finance">
+<input type="hidden" name="rep_platform" value="All">
+<input type="hidden" name="op_sys" value="Other">
+<input type="hidden" name="priority" value="--">
+<input type="hidden" name="version" value="unspecified">
+<input type="hidden" name="bug_severity" id="bug_severity" value="normal">
+<input type="hidden" name="comment" id="comment" value="">
+<input type="hidden" name="groups" id="groups" value="finance">
+<input type="hidden" name="token" value="[% token FILTER html %]">
+
+<table>
+
+<tr>
+ <th>
+ <label for="component">Request Type:</label>
+ </th>
+ <td>
+ <select name="component" id="component" onchange="showCompDesc(this);">
+ [%- FOREACH c = product.components %]
+ [% NEXT IF NOT c.is_active %]
+ <option value="[% c.name FILTER html %]"
+ id="v[% c.id FILTER html %]_component"
+ [% IF c.name == default.component_ %]
+ selected="selected"
+ [% END %]>
+ [% c.name FILTER html -%]
+ </option>
+ [%- END %]
+ </select
+ </td>
+</tr>
+
+<tr>
+ <td></td>
+ <td id="comp_description" align="left" style="color: green; padding-left: 1em"></td>
+</tr>
+
+<tr>
+ <th>
+ <label for="short_desc">Description:</label>
+ </th>
+ <td>
+ <i>Short description of what is being asked to sign</i><br>
+ <input name="short_desc" id="short_desc" size="60"
+ value="[% short_desc FILTER html %]">
+ </td>
+</tr>
+
+<tr>
+ <th>
+ <label for="team_priority">Priority to your Team:</label>
+ </th>
+ <td>
+ <select id="team_priority" name="team_priority">
+ <option value="Low">Low</option>
+ <option value="Medium">Medium</option>
+ <option value="High">High</option>
+ </select>
+ </td>
+</tr>
+
+<tr>
+ <th>
+ <label for="signature_time">Timeframe for Signature:</label>
+ </th>
+ <td>
+ <select id="signature_time" name="signature_time">
+ <option value="24 hours">Within 24 hours</option>
+ <option value="2 days">2 days</option>
+ <option value="A week">A week</option>
+ <option value="2 - 4 weeks" selected>2 -4 weeks</option>
+ </select>
+ </td>
+</tr>
+
+<tr>
+ <th>
+ <label for="other_party">Name of Other Party:</label>
+ </th>
+ <td>
+ <i>Include full legal entity name and any other relevant contact information</i><br>
+ <textarea id="other_party" name="other_party"
+ rows="5" cols="40"></textarea>
+ </td>
+<tr>
+
+<tr>
+ <th>
+ <label for="business_obj">Business Objective:</label>
+ </th>
+ <td>
+ <i>
+ Which Initiative or Overall goal this purchase is for. i.e. B2G, Data Center, Network, etc.</i><br>
+ <textarea id="business_obj" name="business_obj" rows="5" cols="40"></textarea>
+ </td>
+<tr>
+
+<tr>
+ <th>
+ <label for="what_purchase">If this is a purchase order,<br>what are we purchasing?</label>
+ </th>
+ <td>
+ <i>
+ Describe your request, what items are we purchasing, including number of
+ units if available.<br>Also provide context and background. Enter No if not
+ a purchase order.</i><br>
+ <textarea name="what_purchase" id="what_purchase" rows="5" cols="40"></textarea>
+ </td>
+</tr>
+
+<tr>
+ <th>
+ <label for="why_purchase">Why is this purchase needed?</label>
+ </th>
+ <td>
+ <i>
+ Why do we need this? What is the work around if this is not approved?</i><br>
+ <textarea name="why_purchase" id="why_purchase" rows="5" cols="40"></textarea>
+ </td>
+</tr>
+
+<tr>
+ <th>
+ <label for="risk_purchase">What is the risk if<br>this is not purchased?</label>
+ </th>
+ <td>
+ <i>
+ What will happen if this is not purchased?</i><br>
+ <textarea name="risk_purchase" id="risk_purchase" rows="5" cols="40"></textarea>
+ </td>
+</tr>
+
+<tr>
+ <th>
+ <label for="alternative_purchase">What is the alternative?</label>
+ </th>
+ <td>
+ <i>
+ How did the team come to this recommendation? Did we get other bids, if so, how many?</i><br>
+ <textarea name="alternative_purchase" id="alternative_purchase" rows="5" cols="40"></textarea>
+ </td>
+</tr>
+
+<tr>
+ <th>
+ <label for="total_cost">Total Cost</label>
+ </th>
+ <td>
+ <input type="text" name="total_cost" id="total_cost" value="" size="60">
+ </td>
+</tr>
+
+<tr>
+ <th>
+ <label for="attachment">Attachment:</label>
+ </th>
+ <td>
+ <i>Upload document that needs to be signed. If this is a Purchase Request form,<br>
+ also upload any supporting document such as draft SOW, quote, order form, etc.</i>
+ <div>
+ <input type="file" id="attachment" name="data" size="50">
+ <input type="hidden" name="contenttypemethod" value="autodetect">
+ <input type="hidden" name="description" value="Finance Document">
+ </div>
+ </td>
+</tr>
+
+<tr>
+ <td>&nbsp;</td>
+ <td>
+ <input type="submit" id="commit" value="Submit Request">
+ </td>
+</tr>
+</table>
+
+</form>
+
+[% 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..0db96e893
--- /dev/null
+++ b/extensions/BMO/template/en/default/bug/create/create-itrequest.html.tmpl
@@ -0,0 +1,230 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+
+[% inline_javascript = BLOCK %]
+ function setsevdesc(theSelect) {
+ var theValue = theSelect.options[theSelect.selectedIndex].value;
+ if (theValue == 'blocker') {
+ document.getElementById('blockerdesc').style.display = 'block';
+ document.getElementById('critdesc').style.display = 'none';
+ } else if (theValue == 'critical') {
+ document.getElementById('blockerdesc').style.display = 'none';
+ document.getElementById('critdesc').style.display = 'block';
+ } else {
+ document.getElementById('blockerdesc').style.display = 'none';
+ document.getElementById('critdesc').style.display = 'none';
+ }
+ }
+
+ var compdesc = new Array();
+ [% FOREACH comp IN product.components %]
+ compdesc['[% comp.name FILTER js %]'] = '[% comp.description FILTER js %]';
+ [% END %]
+ compdesc['invalid'] = '';
+
+ var serviceNowText = 'Use <a href="https://mozilla.service-now.com/">Service Now</a> to:<br>' +
+ 'Request an LDAP/E-mail/etc. account<br>' +
+ 'Desktop/Laptop/Printer/Phone/Tablet/License problem/order/request';
+
+ function setcompdesc(theRadio) {
+ if (theRadio.id == 'componentmvd') {
+ [%# helpdesk issue/request %]
+ document.getElementById('main_form').style.display = 'none';
+ document.getElementById('service_now_form').style.display = '';
+ document.getElementById('compdescription').innerHTML = serviceNowText;
+ } else {
+ document.getElementById('main_form').style.display = '';
+ document.getElementById('service_now_form').style.display = 'none';
+ var theValue = theRadio.value;
+ var compDescText = compdesc[theValue];
+ if (theRadio.id == 'componentso') {
+ compDescText = compDescText + '<br><br>' + serviceNowText;
+ }
+ document.getElementById('compdescription').innerHTML = compDescText;
+ }
+ }
+
+ function on_submit() {
+ if (document.getElementById('componentmvd').checked) {
+ [%# redirect desktop issues to service-now #%]
+ document.location.href = 'https://mozilla.service-now.com/';
+ return false;
+ }
+ return true;
+ }
+
+ YAHOO.util.Event.onDOMReady(function() {
+ var comps = document.getElementsByName('component');
+ for (var i = 0, l = comps.length; i < l; i++) {
+ if (comps[i].checked) {
+ setcompdesc(comps[i]);
+ break;
+ }
+ }
+ });
+[% END %]
+
+[% PROCESS global/header.html.tmpl
+ title = "Mozilla Corporation/Foundation IT Requests"
+ javascript = inline_javascript
+ javascript_urls = [ 'js/field.js' ]
+ yui = [ 'autocomplete' ]
+%]
+
+[% USE Bugzilla %]
+
+<p><strong>Please use this form for IT requests only!</strong></p>
+<p>If you have a [% terms.bug %] to file, go <a href="enter_bug.cgi">here</a>.</p>
+
+<form method="post" action="post_bug.cgi" id="itRequestForm" enctype="multipart/form-data"
+ onsubmit="return on_submit()">
+ <input type="hidden" name="product" value="mozilla.org">
+ <input type="hidden" name="rep_platform" value="All">
+ <input type="hidden" name="op_sys" value="Other">
+ <input type="hidden" name="priority" value="--">
+ <input type="hidden" name="version" value="other">
+ <input type="hidden" name="token" value="[% token FILTER html %]">
+ <table>
+ <tr>
+
+ <td align="right">
+ <strong>Urgency:</strong>
+ </td>
+
+ <td>
+ <select id="bug_severity" name="bug_severity" onchange="setsevdesc(this)">
+ <option value="blocker">All work for IT stops until this is done</option>
+ <option value="critical">IT should work on it soon as possible (urgent)</option>
+ <option value="major">IT should get to it within 24 hours</option>
+ <option value="normal">IT should get to it within the next week</option>
+ <option value="minor" selected="selected">No rush, but hopefully IT can get to it soon</option>
+ <option value="trivial">Whenever IT can get around to it</option>
+ <option value="enhancement">This is just an idea, filing it so we don't forget</option>
+ </select>
+ </td>
+ <td>
+ <div id="blockerdesc" style="color:red;display:none">This will page the on-call sysadmin if not handled within 30 minutes.</div>
+ <div id="critdesc" style="color:red;display:none">This will page the on-call sysadmin if not handled within 8 hours.</div>
+ </td>
+
+ </tr>
+ <tr>
+ <td align="right"><strong>Request Type:</strong></td>
+ <td style="white-space: nowrap;">
+ <input type="radio" name="component" id="componentmvd" onclick="setcompdesc(this)" value="Server Operations: Desktop Issues">
+ <label for="componentmvd">Desktop issue/request</label><br>
+ <input type="radio" name="component" id="componenttbm" onclick="setcompdesc(this)" value="Server Operations: RelEng">
+ <label for="componenttbm">Report a problem with a tinderbox machine</label><br>
+ <input type="radio" name="component" id="componentwcp" onclick="setcompdesc(this)" value="Server Operations: Web Operations">
+ <label for="componentwcp">Report a problem with a Mozilla website, or to request a change or push</label><br>
+ <input type="radio" name="component" id="componentacl" onclick="setcompdesc(this)" value="Server Operations: ACL Request">
+ <label for="componentacl">Request a firewall change</label><br>
+ <input type="radio" name="component" id="componentso" onclick="setcompdesc(this)" value="Server Operations">
+ <label for="componentso">Any other issue</label><br>
+ Mailing list requests should be filed <a href="[% ulrbase FILTER none %]enter_bug.cgi?product=mozilla.org&amp;format=mozlist">here</a> instead.
+ </td>
+ <td id="compdescription" align="left" style="color: green; padding-left: 1em">
+ </td>
+ </tr>
+
+ <tbody id="main_form">
+
+ <tr>
+ <td align="right"><strong>Summary:</strong></td>
+ <td colspan="3">
+ <input name="short_desc" size="60" value="[% short_desc FILTER html %]">
+ </td>
+ </tr>
+
+ <tr>
+ <td align="right"><strong>CC&nbsp;(optional):</strong></td>
+ <td colspan="3">
+ [% INCLUDE global/userselect.html.tmpl
+ id => "cc"
+ name => "cc"
+ value => cc
+ size => 60
+ multiple => 5
+ %]
+ </td>
+ </tr>
+
+ <tr><td align="right" valign="top"><strong>Description:</strong></td>
+ <td colspan="3">
+ <textarea name="comment" rows="10" cols="80">
+ [% comment FILTER html %]</textarea>
+ <br>
+ </td>
+ </tr>
+
+ <tr>
+ <td align="right"><strong>URL&nbsp;(optional):</strong></td>
+ <td colspan="3">
+ <input name="bug_file_loc" size="60"
+ value="[% bug_file_loc FILTER html %]">
+ </td>
+ </tr>
+
+ <tr><td colspan="4">&nbsp;</td></tr>
+
+ <tr>
+ <td colspan="4">
+ <strong>Attachment&nbsp;(optional):</strong>
+ </td>
+ </tr>
+
+ <tr>
+ <td align="right">File:</td>
+ <td colspan="3">
+ <em>Enter the path to the file on your computer.</em><br>
+ <input type="file" id="data" name="data" size="50">
+ <input type="hidden" name="contenttypemethod" value="autodetect" />
+ </td>
+ </tr>
+
+ <tr>
+ <td align="right">Description:</td>
+ <td colspan="3">
+ <em>Describe the attachment briefly.</em><br>
+ <input type="text" id="description" name="description" size="60" maxlength="200">
+ </td>
+ </tr>
+
+ <tr>
+ <td>&nbsp;</td>
+ <td>
+ <br>
+ <!-- infra -->
+ <input type="checkbox" name="groups" id="groups" value="infra" checked="checked">
+ <label for="groups"><strong>This is an internal issue which should not be publicly visible.</strong></label><br>
+ (please uncheck this box if it isn't)<br>
+ <br>
+ <input type="submit" id="commit" value="Submit Request"><br>
+ <br>
+ Thanks for contacting us. You will be notified by email of any progress made in resolving your request.
+ </td>
+ </tr>
+
+ </tbody>
+
+ <tbody id="service_now_form" style="display:none">
+ <tr>
+ <td>&nbsp;</td>
+ <td>
+ <br>
+ <input type="submit" value="Go to Service Now">
+ </td>
+ </tr>
+ </tbody>
+ </table>
+</form>
+
+
+[% 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..fdb92c11b
--- /dev/null
+++ b/extensions/BMO/template/en/default/bug/create/create-legal.html.tmpl
@@ -0,0 +1,226 @@
+[%# 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 <mark@mozilla.com>
+ # Reed Loden <reed@mozilla.com>
+ #%]
+
+[% 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")
+ OR user.in_group("mozilla-foundation-confidential") %]
+
+<div style='text-align: center; width: 98%; font-size: 2em; font-weight: bold; margin: 10px;'>MoLegal</div>
+
+<p><strong>Welcome to MoLegal.</strong> For legal help please fill in the form below completely.</p>
+
+<p>Legal [% terms.bugs %] are only visible to the reporter, members of the legal team, and those on the
+CC list. This is necessary to maintain attorney-client privilege. Please do not add non-
+employees to the cc list.</p>
+
+<p><strong>All Submissions, And Information Provided In Response To This Request,
+Are Confidential And Subject To The Attorney-Client Privilege And Work Product Doctrine.</strong></p>
+
+<p>If you are requesting legal review of a new product or service, a new feature of an existing product
+ or service, or any type of contract, please go
+ <a href="[% urlbase FILTER none %]enter_bug.cgi?product=mozilla.org&format=moz-project-review">here</a>
+ to kick-off review of your project. If you are requesting another type of legal action, e.g patent analysis,
+ trademark misuse investigation, HR issue, or standards work, please use this form.</p>
+
+<form method="post" action="post_bug.cgi" id="legalRequestForm" enctype="multipart/form-data">
+ <input type="hidden" name="product" value="Legal">
+ <input type="hidden" name="rep_platform" value="All">
+ <input type="hidden" name="op_sys" value="Other">
+ <input type="hidden" name="version" value="unspecified">
+ <input type="hidden" name="priority" value="--">
+ <input type="hidden" name="bug_severity" value="normal">
+ <input type="hidden" name="format" value="legal">
+ <input type="hidden" name="token" value="[% token FILTER html %]">
+ [% IF user.in_group('canconfirm') %]
+ <input type="hidden" name="bug_status" value="NEW">
+ [% END %]
+
+<table>
+
+<tr>
+ <td align="right" width="170px"><strong>Request Type:</strong></td>
+ <td>
+ <select name="component">
+ [%- FOREACH c = product.components %]
+ [% NEXT IF NOT c.is_active %]
+ <option value="[% c.name FILTER html %]"
+ [% " selected=\"selected\"" IF c.name == "General" %]>
+ [% c.name FILTER html -%]
+ </option>
+ [%- END %]
+ </select>
+ </td>
+</tr>
+
+<tr>
+ <td align="right" valign="top">
+ <strong>Goal:</strong>
+ </td>
+ <td colspan="3">
+ <em>Identify the company goal this request maps to.</em><br>
+ <input name="goal" id="goal" size="60" value="[% goal FILTER html %]">
+ </td>
+</tr>
+
+<tr>
+ <td align="right">
+ <strong>Priority to your Team:</strong>
+ </td>
+ <td>
+ <select id="teampriority" name="teampriority">
+ <option value="High">High</option>
+ <option value="Medium">Medium</option>
+ <option value="Low" selected="selected">Low</option>
+ </select>
+ </td>
+</tr>
+
+<tr>
+ <td align="right">
+ <strong>Timeframe for Completion:</strong>
+ </td>
+ <td>
+ <select id="timeframe" name="timeframe">
+ <option value="2 days">2 days</option>
+ <option value="a week">a week</option>
+ <option value="2-4 weeks">2-4 weeks</option>
+ <option value="this will take a while, but please get started soon">
+ this will take a while, but please get started soon</option>
+ <option value="no rush" selected="selected">no rush</option>
+ </select>
+ </td>
+</tr>
+
+<tr>
+ <td align="right" valign="top">
+ <strong>Summary:</strong>
+ </td>
+ <td colspan="3">
+ <em>Include the name of the vendor, partner, product, or other identifier.</em><br>
+ <input name="short_desc" size="60" value="[% short_desc FILTER html %]">
+ </td>
+</tr>
+
+<tr>
+ <td align="right">
+ <strong>CC&nbsp;(optional):</strong>
+ </td>
+ <td colspan="3">
+ [% INCLUDE global/userselect.html.tmpl
+ id => "cc"
+ name => "cc"
+ value => cc
+ size => 60
+ multiple => 5
+ %]
+ </td>
+</tr>
+
+<tr>
+ <td align="right" valign="top">
+ <strong>Name of Other Party:</strong>
+ </td>
+ <td>
+ <em>If applicable, include full legal entity name, address, and any other relevant contact information.</em><br>
+ <textarea id="otherparty" name="otherparty" rows="3" cols="80"></textarea>
+ </td>
+</tr>
+
+<tr>
+ <td align="right">
+ <strong>Business Objective:</strong>
+ </td>
+ <td>
+ <input type="text" name="busobj" id="busobj" value="" size="60" />
+ </td>
+</tr>
+
+<tr>
+ <td align="right" valign="top">
+ <strong>Description:</strong>
+ </td>
+ <td colspan="3">
+ <em>Describe your question, what you want and/or provide any relevant deal terms, restrictions,<br>
+ or provisions that are applicable. Also provide context and background.</em><br>
+ <textarea id="comment" name="comment" rows="10" cols="80">
+ [% comment FILTER html %]</textarea>
+ </td>
+</tr>
+
+<tr>
+ <td align="right"><strong>URL&nbsp;(optional):</strong></td>
+ <td colspan="3">
+ <input name="bug_file_loc" size="60"
+ value="[% bug_file_loc FILTER html %]">
+ </td>
+</tr>
+
+<tr>
+ <td></td>
+ <td colspan=2><strong>Attachment (this is optional)</strong></td>
+</tr>
+
+<tr>
+ <td align="right" valign="top">
+ <strong><label for="data">File:</label></strong>
+ </td>
+ <td>
+ <em>Enter the path to the file on your computer.</em><br>
+ <input type="file" id="data" name="data" size="50">
+ <input type="hidden" name="contenttypemethod" value="autodetect" />
+ </td>
+</tr>
+
+<tr>
+ <td align="right" valign="top">
+ <strong><label for="description">Description:</label></strong>
+ </td>
+ <td>
+ <em>Describe the attachment briefly.</em><br>
+ <input type="text" id="description" name="description" size="60" maxlength="200">
+ </td>
+</tr>
+
+</table>
+
+<br>
+
+ <input type="submit" id="commit" value="Submit Request">
+</form>
+
+<p>Thanks for contacting us. You will be notified by email of any progress made in resolving your request.</p>
+
+[% ELSE %]
+
+<p>Sorry, you do not have access to this page.</p>
+
+[% 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..ea60f6c19
--- /dev/null
+++ b/extensions/BMO/template/en/default/bug/create/create-mktgevent.html.tmpl
@@ -0,0 +1,251 @@
+[%# 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 <reed@mozilla.com>
+ # David Tran <dtran@mozilla.com>
+ #%]
+
+[% 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' ]
+%]
+
+<div style='text-align: center; width: 98%; font-size: 2em; font-weight: bold; margin: 10px;'>Event Request Form</div>
+
+<p><strong>Event Request:</strong> Please use this form to file a request for an event. <b>This form is not a request for swag.</b></p>
+
+<p>Process:</p>
+
+<ol>
+ <li>Complete and submit request below.</li>
+ <li>Your request will be reviewed by the appropriate person in the Engagement team.</li>
+</ol>
+
+<p>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.</p>
+
+<script language="javascript" type="text/javascript">
+function trySubmit() {
+
+ var date = document.getElementById('date').value;
+ var eventname = document.getElementById('eventname').value;
+ var shortdesc = 'Event Request (' + date + ') - ' + eventname;
+ document.getElementById('short_desc').value = shortdesc;
+ if(!isChecked('doing-other')) document.getElementById('doing-other-what').value = '';
+ return true;
+}
+
+function validateAndSubmit() {
+
+ var alert_text = '';
+ if(!isFilledOut('firstname')) alert_text += "Please enter your first name\n";
+ if(!isFilledOut('lastname')) alert_text += "Please enter your last name\n";
+ if(!isValidEmail(document.getElementById('email').value)) alert_text += "Please enter a valid email address\n";
+ if(!isFilledOut('eventname')) alert_text += "Please enter an event name.\n";
+ if(!isFilledOut('website')) alert_text += "Please enter an event website.\n";
+ if(!isFilledOut('goals')) alert_text += "Please enter the Description and Objectives.\n";
+ if(!isFilledOut('date')) alert_text += "Please enter an event date.\n";
+ if(!isFilledOut('goals')) alert_text += "Please enter the event goals.\n";
+ if(!isFilledOut('successmeasure')) alert_text += "Please enter how you will measure the success of the event.\n";
+
+ if(!isChecked('doing-booth') &&
+ !isChecked('doing-speaking') &&
+ !isChecked('doing-outreach') &&
+ !isChecked('doing-other')
+ ) alert_text += "Please indicate what you'll be doing at the event.\n";
+ if(isChecked('doing-other') && !isFilledOut('doing-other-what')) alert_text += "Please describe what 'other' thing you'll be doing at the event.\n";
+
+ //Everything required is filled out..try to submit the form!
+ if(alert_text == '') {
+ return trySubmit();
+ }
+
+ //alert text, stay here on the pagee
+ alert(alert_text);
+ return false;
+}
+
+</script>
+
+<form method="post" action="post_bug.cgi" id="swagRequestForm" enctype="multipart/form-data"
+ onSubmit="return validateAndSubmit();">
+
+ <input type="hidden" name="format" value="mktgevent">
+ <input type="hidden" name="product" value="Marketing">
+ <input type="hidden" name="component" value="Event Requests">
+ <input type="hidden" name="rep_platform" value="All">
+ <input type="hidden" name="op_sys" value="Other">
+ <input type="hidden" name="priority" value="--">
+ <input type="hidden" name="version" value="unspecified">
+ <input type="hidden" name="bug_severity" id="bug_severity" value="normal">
+ <input type="hidden" name="short_desc" id="short_desc" value="">
+ <input type="hidden" name="groups" value="mozilla-corporation-confidential">
+ <input type="hidden" name="token" value="[% token FILTER html %]">
+
+<table>
+
+<tr>
+ <td align="right"><strong>First Name: <span style="color: red;">*</span></strong></td>
+ <td align="left">
+ <input type="text" name="firstname" id="firstname" value="" size="20" maxlength="20" />
+ </td>
+</tr>
+<tr>
+ <td align="right"><strong>Last Name: <span style="color: red;">*</span></strong></td>
+ <td align="left">
+ <input type="text" name="lastname" id="lastname" value="" size="20" maxlength="20"/>
+ </td>
+</tr>
+
+<tr>
+ <td align="right"><strong>Email Address: <span style="color: red;">*</span></strong></td>
+ <td>
+ <input type="text" name="email" id="email" value="" size="50" maxlength="50"/>
+ </td>
+</tr>
+
+<tr>
+ <td align="right"><strong>CC&nbsp;(optional):</strong></td>
+ <td colspan="3">
+ [% INCLUDE global/userselect.html.tmpl
+ id => "cc"
+ name => "cc"
+ value => cc
+ size => 50
+ multiple => 5
+ %]
+ </td>
+</tr>
+
+<tr>
+ <td><!-- spacer -->&nbsp;</td>
+</tr>
+
+<tr>
+ <td><!-- spacer -->&nbsp;</td>
+</tr>
+
+<tr>
+ <td align="right"><strong>Event Name: <span style="color: red;">*</span></strong></td>
+ <td>
+ <input type="text" name="eventname" id="eventname" value="" size="50" maxlength="50"/>
+ </td>
+</tr>
+
+<tr>
+ <td align="right"><strong>Event Date: <span style="color: red;">*</span></strong></td>
+ <td>
+ <input type="text" id="date" name="date" size="10"
+ onchange="updateCalendarFromField(this)">
+ <button type="button" class="calendar_button" id="button_calendar_date"
+ onclick="showCalendar('date')"><span>Calendar</span>
+ </button>
+ <div id="con_calendar_date"></div>
+ </td>
+</tr>
+
+<tr>
+ <td align="right"><strong>Event Location: <span style="color: red;">*</span></strong><br /><small>(City, Country; or City, State)</small></td>
+ <td>
+ <input type="text" name="location" id="location" value="" size="50" maxlength="80" />
+ </td>
+</tr>
+
+<tr>
+ <td align="right"><strong>Event Website: <span style="color: red;">*</span></strong></td>
+ <td>
+ <input type="text" name="website" id="website" value="" size="50" maxlength="120" />
+ </td>
+</tr>
+
+<tr>
+ <td align="right"><strong>Event Description and Key Objectives: <span style="color: red;">*</span></strong></td>
+ <td>
+ <textarea id="goals" name="goals" rows="5" cols="50"></textarea>
+ </td>
+</tr>
+
+<tr>
+ <td align="right"><strong>What will you be doing at the event? <span style="color: red;">*</span></strong></td>
+ <td>
+ <input type="checkbox" id="doing-booth" name="doing" value="Booth or Table"><label for="doing-booth">Booth/Table</label><br />
+ <input type="checkbox" id="doing-speaking" name="doing" value="Speaking Opportunity"><label for="doing-speaking">Speaking Opportunity</label><br />
+ <input type="checkbox" id="doing-outreach" name="doing" value="PR or Media Outreach"><label for="doing-outreach">PR or Media Outreach</label><br />
+ <input type="checkbox" id="doing-other" name="doing" value="Other"><label for="doing-other">Other, please explain:</label>
+ <input type="text" name="doing-other-what" id="doing-other-what" value="" size="50" maxlength="120"><br />
+ </td>
+</tr>
+
+<tr>
+ <td><!-- spacer -->&nbsp;</td>
+</tr>
+
+<tr>
+ <td align="right"><strong>How many attendees will be at the event?</strong></td>
+ <td>
+ <select name="attendees" id="attendees">
+ <option value="--Please Select--" selected>--Please Select--</option>
+ <option value="1-99">1-99</option>
+ <option value="100-499">100-499</option>
+ <option value="500-999">500-999</option>
+ <option value="1000+">1000+</option>
+ </select>
+ </td>
+</tr>
+
+<tr>
+ <td align="right"><strong>Targeted Audience:</strong></td>
+ <td>
+ <select name="audience" id="audience">
+ <option value="--Please Select--" selected>--Please Select--</option>
+ <option value="Contributors">Contributors (Developers, Education, Security, Designers, Localization, Support & Marketing)</option>
+ <option value="Potential New Users">Potential New Users</option>
+ <option value="User Community">User Community (Recent adopters, fans, Mozilla product evangelizers)</option>
+ </select>
+ </td>
+</tr>
+
+<tr>
+ <td align="right"><strong>How will you measure the<br />success of your event? <span style="color: red;">*</span></strong></td>
+ <td>
+ <textarea id="successmeasure" name="successmeasure" rows="5" cols="50"></textarea>
+ </td>
+</tr>
+
+
+ </table>
+ <br>
+ <input type="submit" id="commit" value="Submit Request">
+
+<p>
+ <strong><span style="color: red;">*</span></strong> - Required field<br />
+ Thanks for contacting us.
+
+</p>
+
+<script type="text/javascript">
+ createCalendar('date');
+</script>
+
+[% PROCESS global/footer.html.tmpl %]
diff --git a/extensions/BMO/template/en/default/bug/create/create-mozlist.html.tmpl b/extensions/BMO/template/en/default/bug/create/create-mozlist.html.tmpl
new file mode 100644
index 000000000..bccca2509
--- /dev/null
+++ b/extensions/BMO/template/en/default/bug/create/create-mozlist.html.tmpl
@@ -0,0 +1,321 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+
+[% PROCESS global/header.html.tmpl
+ title = "Mozilla Discussion Forum / Mailing List Requests"
+ javascript_urls = [ 'extensions/BMO/web/js/form_validate.js',
+ 'js/field.js' ]
+ yui = [ 'autocomplete' ]
+%]
+
+<script type="text/javascript">
+<!--
+ function toggleGroup (theRadio) {
+ var radioValue = theRadio.value;
+ var groupDiv = YAHOO.util.Dom.get('groups');
+ var group = YAHOO.util.Dom.get('group_35');
+ if (radioValue == 'lists.mozilla.org') {
+ YAHOO.util.Dom.setStyle('listNameTR', 'display', 'table-row');
+ YAHOO.util.Dom.setStyle('listNameDiscussion', 'display', 'inline');
+ YAHOO.util.Dom.setStyle('listNameOther', 'display', 'none');
+ YAHOO.util.Dom.setStyle('listAdminTR', 'display', 'table-row');
+ YAHOO.util.Dom.setStyle('listShortDescTR', 'display', 'table-row');
+ YAHOO.util.Dom.setStyle('listShortDescMailman', 'display', 'inline');
+ YAHOO.util.Dom.setStyle('listShortDescZimbra', 'display', 'none');
+ YAHOO.util.Dom.setStyle('listLongDescTR', 'display', 'table-row');
+ YAHOO.util.Dom.setStyle('listCommentTR', 'display', 'table-row');
+ YAHOO.util.Dom.setStyle('CCTR', 'display', 'table-row');
+ YAHOO.util.Dom.setStyle('URLTR', 'display', 'table-row');
+ YAHOO.util.Dom.setStyle('serviceNow', 'display', 'none');
+ YAHOO.util.Dom.setStyle('groups', 'display', 'none');
+ group.disabled = true;
+ YAHOO.util.Dom.setStyle('submitDiv', 'display', 'block');
+ } else if (radioValue == 'mozilla.org') {
+ YAHOO.util.Dom.setStyle('listNameTR', 'display', 'table-row');
+ YAHOO.util.Dom.setStyle('listNameDiscussion', 'display', 'none');
+ YAHOO.util.Dom.setStyle('listNameOther', 'display', 'inline');
+ YAHOO.util.Dom.setStyle('listAdminTR', 'display', 'table-row');
+ YAHOO.util.Dom.setStyle('listShortDescTR', 'display', 'table-row');
+ YAHOO.util.Dom.setStyle('listShortDescMailman', 'display', 'inline');
+ YAHOO.util.Dom.setStyle('listShortDescZimbra', 'display', 'none');
+ YAHOO.util.Dom.setStyle('listLongDescTR', 'display', 'table-row');
+ YAHOO.util.Dom.setStyle('listCommentTR', 'display', 'table-row');
+ YAHOO.util.Dom.setStyle('CCTR', 'display', 'table-row');
+ YAHOO.util.Dom.setStyle('URLTR', 'display', 'table-row');
+ YAHOO.util.Dom.setStyle('serviceNow', 'display', 'none');
+ YAHOO.util.Dom.setStyle('groups', 'display', 'table-row');
+ group.disabled = false;
+ YAHOO.util.Dom.setStyle('submitDiv', 'display', 'block');
+ } else {
+ YAHOO.util.Dom.setStyle('listNameTR', 'display', 'none');
+ YAHOO.util.Dom.setStyle('listNameDiscussion', 'display', 'none');
+ YAHOO.util.Dom.setStyle('listNameOther', 'display', 'none');
+ YAHOO.util.Dom.setStyle('listAdminTR', 'display', 'none');
+ YAHOO.util.Dom.setStyle('listShortDescTR', 'display', 'none');
+ YAHOO.util.Dom.setStyle('listShortDescMailman', 'display', 'none');
+ YAHOO.util.Dom.setStyle('listShortDescZimbra', 'display', 'none');
+ YAHOO.util.Dom.setStyle('listLongDescTR', 'display', 'none');
+ YAHOO.util.Dom.setStyle('listCommentTR', 'display', 'none');
+ YAHOO.util.Dom.setStyle('CCTR', 'display', 'none');
+ YAHOO.util.Dom.setStyle('URLTR', 'display', 'none');
+ YAHOO.util.Dom.setStyle('serviceNow', 'display', 'table-row');
+ YAHOO.util.Dom.setStyle('groups', 'display', 'none');
+ group.disabled = false;
+ YAHOO.util.Dom.setStyle('submitDiv', 'display', 'none');
+ }
+ }
+
+ function trySubmit() {
+ var listName = document.getElementById('listName').value;
+ var listAdmin = document.getElementById('listAdmin').value;
+ var listTypeRadio = document.getElementsByName('listType');
+ var listType = "";
+
+ for (var i = 0; i < listTypeRadio.length; i++) {
+ if (listTypeRadio[i].checked) {
+ listType = listTypeRadio[i].value;
+ }
+ }
+
+ var alert_text = "";
+ var short_desc = "";
+
+ if (listType) {
+ if (listType == "lists.mozilla.org") {
+ document.getElementById('component').value = "Discussion Forums";
+ short_desc = "Discussion Forum: " + listName;
+ } else if (listType == "mozilla.com" ) {
+ document.location.href = 'https://mozilla.service-now.com/';
+ return false;
+ } else {
+ document.getElementById('component').value = "Server Operations";
+ short_desc = "[Mailman List Request] " + listName + "@" + listType;
+ }
+ } else {
+ alert_text += "Please select a list type\n";
+ }
+
+ if (!isFilledOut('listName')) {
+ alert_text += "Please enter the list name\n";
+ }
+
+ if ((!isValidEmail(listAdmin)) && (listType != 'mozilla.com')) {
+ alert_text += "Please enter a valid email address for the list administrator\n";
+ }
+
+ if (alert_text) {
+ alert(alert_text);
+ return false;
+ }
+
+ document.getElementById('short_desc').value = short_desc;
+
+ return true;
+ }
+
+ YAHOO.util.Event.onDOMReady(function() {
+ var elements = document.getElementsByName('listType');
+ for (var i = 0, l = elements.length; i < l; i++) {
+ if (elements[i].checked) {
+ toggleGroup(elements[i]);
+ break;
+ }
+ }
+ });
+// -->
+</script>
+
+<form method="post" action="post_bug.cgi" id="mozListRequestForm"
+ enctype="multipart/form-data" onSubmit="return trySubmit();">
+ <input type="hidden" id="format" name="format" value="mozlist">
+ <input type="hidden" id="product" name="product" value="mozilla.org">
+ <input type="hidden" id="rep_platform" name="rep_platform" value="All">
+ <input type="hidden" id="op_sys" name="op_sys" value="Other">
+ <input type="hidden" id="priority" name="priority" value="--">
+ <input type="hidden" id="version" name="version" value="other">
+ <input type="hidden" id="short_desc" name="short_desc" value="">
+ <input type="hidden" id="component" name="component" value="">
+ <input type="hidden" id="bug_severity" name="bug_severity" value="normal">
+ <input type="hidden" id="token" name="token" value="[% token FILTER html %]">
+
+ <table>
+ <tr id="listTypeTR" style="display: table-row;">
+ <td align="right" width="15%"><strong>List Type:</strong></td>
+ <td>
+ <dl>
+ <dt>
+ <input type="radio" name="listType" id="lists_mozilla_org"
+ onclick="toggleGroup(this);" value="lists.mozilla.org">
+ <label for="lists_mozilla_org">Standard Discussion Forum</label>
+ </dt>
+ <dd>
+ <label for="lists_mozilla_org">
+ This option gives you a Mozilla <a
+ href="https://www.mozilla.org/about/forums/">Discussion Forum</a>.
+ These are the normal mechanism for public discussion in the Mozilla
+ project. They are made up of a mailing list on
+ <b>lists.mozilla.org</b>, a newsgroup on <b>news.mozilla.org</b> and
+ a <b>Google Group</b> (which maintains the list archives), all linked
+ together. Users can add and remove themselves. If you aren't sure,
+ pick this one.
+ </label>
+ </dd>
+ <dt>
+ <input type="radio" name="listType" id="mozilla_org"
+ onclick="toggleGroup(this);" value="mozilla.org">
+ <label for="mozilla_org">Mailing List Only</label>
+ </dt>
+ <dd>
+ <label for="mozilla_org">
+ This option gives you a mailing list without the other access mechanisms. The
+ list can be private, although Mozilla is an "open by default" organization so
+ you need a good reason to have a private list. The archives are maintained by
+ Mailman, our mailing list software. Subscription can be open or
+ moderator-controlled. This type of list is normally hosted on the <b>mozilla.org</b> domain.
+ </label>
+ </dd>
+ <dt>
+ <input type="radio" name="listType" id="mozilla_com"
+ onclick="toggleGroup(this)" value="mozilla.com">
+ <label for="mozilla_com">Distribution List</label>
+ </dt>
+ <dd>
+ <label for="mozilla_com">
+ This option gives you a distribution list - basically, a mail exploder - on
+ <b>mozilla.com</b>. (This option is only appropriate for things which relate
+ specifically to Mozilla Corporation, such as confidential partner projects
+ or internal department lists.) Send email to the address, and it gets remailed
+ out to everyone on the list. There are no archives, and the "subscriber" list
+ is controlled by the IT team.
+ </label>
+ </dd>
+ </dl>
+ <hr>
+ </td>
+ </tr>
+ <tr id="listNameTR" style="display: none;">
+ <td align="right" valign="top"><strong>List Name:</strong></td>
+ <td>
+ <input name="listName" id="listName" size="60" value="[% listName FILTER html %]"><br>
+ <span id="listNameDiscussion" style="display: none;">The desired name for the newsgroup. Should start with 'mozilla.' and fit somewhere in the hierarchy described <a href="https://www.mozilla.org/about/forums/">here</a>.</span>
+ <span id="listNameOther" style="display: none;">Only enter the part before the @, the domain name is chosen based on the list type.</span>
+ <hr>
+ </td>
+ </tr>
+ <tr id="listAdminTR" style="display: none;">
+ <td align="right" valign="top" width="15%"><strong>List Administrator:</strong></td>
+ <td>
+ [% INCLUDE global/userselect.html.tmpl
+ id => "listAdmin"
+ name => "listAdmin"
+ value => ""
+ size => 60
+ multiple => 5
+ %]
+ <br>
+ <b>Note:</b>The list administrator is also initially considered to be the list moderator
+ and will be responsible for moderation tasks unless delegated to someone else. For
+ convenience, [% terms.Bugzilla %] user accounts will autocomplete. The administrator is not required
+ to have a [% terms.Bugzilla %] account, and you can enter an address that doesn't autocomplete if
+ necessary.<hr />
+ </td>
+ </tr>
+ <tr id="listShortDescTR" style="display: none;">
+ <td align="right" valign="top"><strong>Short one-line description:</strong></td>
+ <td>
+ <input name="listShortDesc" id="listShortDesc" size="60" value="[% listShortDesc FILTER html %]"><br />
+ <span id="listShortDescMailman" style="display: none;">This will be shown to users on the index of lists on the server.</span>
+ <span id="listShortDescZimbra" style="display: none;">This will be shown as the "real name" in the Zimbra address book.</span>
+ <hr />
+ </td>
+ </tr>
+ <tr id="listLongDescTR" style="display: none;">
+ <td align="right" valign="top"><strong>Long description:</strong></td>
+ <td colspan="3">
+ [% INCLUDE global/textarea.html.tmpl
+ name = 'listLongDesc'
+ id = 'listLongDesc'
+ minrows = 10
+ maxrows = 25
+ cols = constants.COMMENT_COLS
+ defaultcontent = listLongDesc
+ %]
+ <br>This will be shown at the top of the list's listinfo page.
+ <hr>
+ </td>
+ </tr>
+ <tr id="listCommentTR" style="display: none;">
+ <td align="right" valign="top"><strong>Additional comments:</strong></td>
+ <td colspan="3">
+ Justification for the list, special instructions, etc.<br>
+ [% INCLUDE global/textarea.html.tmpl
+ name = 'comment'
+ id = 'comment'
+ minrows = 10
+ maxrows = 25
+ cols = constants.COMMENT_COLS
+ defaultcontent = comment
+ %]
+ <hr>
+ </td>
+ </tr>
+ <tr id="CCTR" style="display: none;">
+ <td align="right"><strong>CC&nbsp;(optional):</strong></td>
+ <td>
+ [% INCLUDE global/userselect.html.tmpl
+ id => "cc"
+ name => "cc"
+ value => cc
+ size => 60
+ multiple => 5
+ %]
+ </td>
+ </tr>
+ <tr id="URLTR" style="display: none;">
+ <td align="right"><strong>URL&nbsp;(optional):</strong></td>
+ <td colspan="3">
+ <input name="bug_file_loc" size="60"
+ value="[% bug_file_loc FILTER html %]">
+ </td>
+ </tr>
+ <tr id="serviceNow" style="display: none">
+ <td>&nbsp;</td>
+ <td>
+ <p>
+ Please use <b>Service Now</b> to request a distribution list.
+ </p>
+ <input type="submit" value="Go to Service Now">
+ </td>
+ </tr>
+ <tr>
+ <td>&nbsp;</td>
+ <td>
+ <br>
+ <div id="groups" style="display:none;">
+ <input type="checkbox" name="groups" id="group_35" value="infra" disabled="true">
+ <label for="group_35"><strong>This is an internal issue which should not be publicly visible.</strong></label>
+ <br><br>
+ </div>
+
+ <div id="submitDiv" style="display: none;">
+ <input type="submit" id="commit" value="Submit Request">
+ <p>
+ Thanks for contacting us. You will be notified by email of any progress made
+ in resolving your request.
+ </p>
+ </div>
+ </td>
+ </tr>
+ </table>
+ <br>
+
+</form>
+
+[% PROCESS global/footer.html.tmpl %]
diff --git a/extensions/BMO/template/en/default/bug/create/create-mozpr.html.tmpl b/extensions/BMO/template/en/default/bug/create/create-mozpr.html.tmpl
new file mode 100644
index 000000000..a272e0b41
--- /dev/null
+++ b/extensions/BMO/template/en/default/bug/create/create-mozpr.html.tmpl
@@ -0,0 +1,654 @@
+[%# 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 <gerv@gerv.net>
+ # Ville Skyttä <ville.skytta@iki.fi>
+ # Shane H. W. Travis <travis@sedsystems.ca>
+ # Marc Schumann <wurblzap@gmail.com>
+ # Akamai Technologies <bugzilla-dev@akamai.com>
+ # Max Kanat-Alexander <mkanat@bugzilla.org>
+ # Frédéric Buclin <LpSolit@gmail.com>
+ #%]
+
+[% PROCESS "global/field-descs.none.tmpl" %]
+
+[% title = BLOCK %]Create a PR Request[% END %]
+
+[% PROCESS global/header.html.tmpl
+ title = title
+ style_urls = [ 'skins/standard/attachment.css' ]
+ javascript_urls = [ "js/attachment.js", "js/util.js",
+ "js/field.js", "js/TUI.js" ]
+ onload = 'set_assign_to();'
+ yui = [ 'autocomplete' ]
+%]
+
+<script type="text/javascript">
+<!--
+
+var initialowners = new Array([% product.components.size %]);
+var last_initialowner;
+var initialccs = new Array([% product.components.size %]);
+var components = new Array([% product.components.size %]);
+var comp_desc = new Array([% product.components.size %]);
+var flags = new Array([% product.components.size %]);
+[% IF Param("useqacontact") %]
+ var initialqacontacts = new Array([% product.components.size %]);
+ var last_initialqacontact;
+[% END %]
+[% count = 0 %]
+[%- FOREACH c = product.components %]
+ [% NEXT IF NOT c.is_active %]
+ components[[% count %]] = "[% c.name FILTER js %]";
+ comp_desc[[% count %]] = "[% c.description FILTER html_light FILTER js %]";
+ initialowners[[% count %]] = "[% c.default_assignee.login FILTER js %]";
+ [% flag_list = [] %]
+ [% FOREACH f = c.flag_types(is_active=>1).bug %]
+ [% flag_list.push(f.id) %]
+ [% END %]
+ [% FOREACH f = c.flag_types(is_active=>1).attachment %]
+ [% flag_list.push(f.id) %]
+ [% END %]
+ flags[[% count %]] = [[% flag_list.join(",") FILTER js %]];
+ [% IF Param("useqacontact") %]
+ initialqacontacts[[% count %]] = "[% c.default_qa_contact.login FILTER js %]";
+ [% END %]
+
+ [% SET initial_cc_list = [] %]
+ [% FOREACH cc_user = c.initial_cc %]
+ [% initial_cc_list.push(cc_user.login) %]
+ [% END %]
+ initialccs[[% count %]] = "[% initial_cc_list.join(', ') FILTER js %]";
+
+ [% count = count + 1 %]
+[%- END %]
+
+function set_assign_to() {
+ // Based on the selected component, fill the "Assign To:" field
+ // with the default component owner, and the "QA Contact:" field
+ // with the default QA Contact. It also selectively enables flags.
+ var form = document.Create;
+ var assigned_to = form.assigned_to.value;
+
+[% IF Param("useqacontact") %]
+ var qa_contact = form.qa_contact.value;
+[% END %]
+
+ var index = -1;
+ if (form.component.type == 'select-one') {
+ index = form.component.selectedIndex;
+ } else if (form.component.type == 'hidden') {
+ // Assume there is only one component in the list
+ index = 0;
+ }
+ if (index != -1) {
+ var owner = initialowners[index];
+ var component = components[index];
+ if (assigned_to == last_initialowner
+ || assigned_to == owner
+ || assigned_to == '') {
+ form.assigned_to.value = owner;
+ last_initialowner = owner;
+ }
+
+ document.getElementById('initial_cc').innerHTML = initialccs[index];
+ document.getElementById('comp_desc').innerHTML = comp_desc[index];
+
+ [% IF Param("useqacontact") %]
+ var contact = initialqacontacts[index];
+ if (qa_contact == last_initialqacontact
+ || qa_contact == contact
+ || qa_contact == '') {
+ form.qa_contact.value = contact;
+ last_initialqacontact = contact;
+ }
+ [% END %]
+
+ // First, we disable all flags. Then we re-enable those
+ // which are available for the selected component.
+ var inputElements = document.getElementsByTagName("select");
+ var inputElement, flagField;
+ for ( var i=0 ; i<inputElements.length ; i++ ) {
+ inputElement = inputElements.item(i);
+ if (inputElement.name.search(/^flag_type-(\d+)$/) != -1) {
+ var id = inputElement.name.replace(/^flag_type-(\d+)$/, "$1");
+ inputElement.disabled = true;
+ // Also disable the requestee field, if it exists.
+ inputElement = document.getElementById("requestee_type-" + id);
+ if (inputElement) inputElement.disabled = true;
+ }
+ }
+ // Now enable flags available for the selected component.
+ for (var i = 0; i < flags[index].length; i++) {
+ flagField = document.getElementById("flag_type-" + flags[index][i]);
+ // Do not enable flags the user cannot set nor request.
+ if (flagField && flagField.options.length > 1) {
+ flagField.disabled = false;
+ // Re-enabling the requestee field depends on the status
+ // of the flag.
+ toggleRequesteeField(flagField, 1);
+ }
+ }
+ }
+}
+
+function fix_component() {
+ var form = document.Create;
+ var location = form.location.options[form.location.selectedIndex].value;
+ var fakecomp = form.fakecomp.options[form.fakecomp.selectedIndex].value;
+ var newcomp = location + " - " + fakecomp;
+ form.component.value = newcomp;
+ set_assign_to();
+}
+
+function handleWantsAttachment(wants_attachment) {
+ if (wants_attachment) {
+ document.getElementById('attachment_false').style.display = 'none';
+ document.getElementById('attachment_true').style.display = 'block';
+ }
+ else {
+ document.getElementById('attachment_false').style.display = 'block';
+ document.getElementById('attachment_true').style.display = 'none';
+ clearAttachmentFields();
+ }
+}
+
+
+TUI_alternates['expert_fields'] = 'Show Advanced Fields';
+// Hide the Advanced Fields by default, unless the user has a cookie
+// that specifies otherwise.
+TUI_hide_default('expert_fields');
+
+-->
+</script>
+
+[% IF user.in_group("mozilla-confidential") %]
+
+[% USE Bugzilla %]
+[% SET select_fields = {} %]
+[% FOREACH field = Bugzilla.get_fields(
+ { type => constants.FIELD_TYPE_SINGLE_SELECT, custom => 0 })
+%]
+ [% select_fields.${field.name} = field %]
+[% END %]
+
+<form name="Create" id="Create" method="post" action="post_bug.cgi"
+ enctype="multipart/form-data">
+<input type="hidden" name="product" value="[% product.name FILTER html %]">
+<input type="hidden" name="token" value="[% token FILTER html %]">
+
+<table cellspacing="4" cellpadding="2" border="0" style="background: url(extensions/BMO/web/images/presshat.png) top right no-repeat">
+<tbody>
+ <tr>
+ <td colspan="2">
+ <a id="expert_fields_controller" class="controller bz_default_hidden"
+ href="javascript:TUI_toggle_class('expert_fields')">Hide
+ Advanced Fields</a>
+ [%# Show the link if the browser supports JS %]
+ <script type="text/javascript">
+ YAHOO.util.Dom.removeClass('expert_fields_controller',
+ 'bz_default_hidden');
+ </script>
+ </td>
+ <td colspan="2">
+ (<span class="required_star">*</span> =
+ <span class="required_explanation">Required Field</span>)
+ </td>
+ </tr>
+
+ <tr>
+ <th>Product:</th>
+ <td width="10%">[% product.name FILTER html %]</td>
+
+ <th>Reporter:</th>
+ <td width="100%">[% user.login FILTER html %]</td>
+ </tr>
+
+ [%# We can't use the select block in these two cases for various reasons. %]
+[% matches = default.component_.matches('^(.*) - (.*)$') %]
+[% default.location = matches.0 %]
+[% default.fakecomp = matches.1 %]
+[% IF default.location == '' %]
+ [% default.location = 'US' %]
+[% END %]
+[% locations = [] %]
+[% fakecomps = [] %]
+[% FOREACH c = product.components %]
+ [% matches = c.name.match('^(.*) - (.*)$') %]
+ [% locations.push(matches.0) %]
+ [% fakecomps.push(matches.1) %]
+[% END %]
+[% locations = locations.unique %]
+[% fakecomps = fakecomps.unique %]
+ <tr>
+ <th class="required">
+ Location:
+ </th>
+ <td>
+
+ <select name="location" onchange="fix_component();" size="7">
+ [% FOREACH l = locations %]
+ <option value="[% l FILTER html %]" [% " selected=\"selected\"" IF l == default.location %]>
+ [% l FILTER html %]
+ </option>
+ [% END %]
+ </select>
+ <select name="component" onchange="set_assign_to();" size="7"
+ aria-required="true" class="required" style="display: none;">
+ [%# Build the lists of assignees and QA contacts if "usemenuforusers" is enabled. %]
+ [% IF Param("usemenuforusers") %]
+ [% assignees_list = user.get_userlist.clone %]
+ [% qa_contacts_list = user.get_userlist.clone %]
+ [% END %]
+
+ [%- FOREACH c = product.components %]
+ [% NEXT IF NOT c.is_active %]
+ <option value="[% c.name FILTER html %]"
+ [% " selected=\"selected\"" IF c.name == default.component_ %]>
+ [% c.name FILTER html -%]
+ </option>
+ [% IF Param("usemenuforusers") %]
+ [% INCLUDE build_userlist default_user = c.default_assignee,
+ userlist = assignees_list %]
+ [% INCLUDE build_userlist default_user = c.default_qa_contact,
+ userlist = qa_contacts_list %]
+ [% END %]
+ [%- END %]
+ </select>
+ </td>
+
+ </tr>
+ <tr>
+ <th>
+ Request type:
+ </th>
+ <td>
+
+ <select name="fakecomp" onchange="fix_component();" size="7">
+ [% FOREACH f = fakecomps %]
+ <option value="[% f FILTER html %]" [% " selected=\"selected\"" IF f == default.fakecomp %]>
+ [% f FILTER html %]
+ </option>
+ [% END %]
+ </select>
+ </td>
+ <td colspan="2">
+ [%# Enclose the fieldset in a nested table so that its width changes based
+ # on the length on the component description. %]
+ <table>
+ <tr>
+ <td>
+ <fieldset>
+ <legend>Request Description</legend>
+ <div id="comp_desc" class="comment">Select a request type to read its description.</div>
+ </fieldset>
+ </td>
+ </tr>
+ </table>
+ <input type="hidden" name="bug_severity" value="[% default.bug_severity FILTER html %]">
+ <input type="hidden" name="rep_platform" value="[% default.rep_platform FILTER html %]">
+ <input type="hidden" name="op_sys" value="[% default.op_sys FILTER html %]">
+ <input type="hidden" name="version" value="unspecified">
+ </td>
+ </tr>
+</tbody>
+
+<tbody class="expert_fields">
+ <tr>
+ <td colspan="4">&nbsp;</td>
+ </tr>
+
+ <tr>
+[% IF bug_status.size <= 1 %]
+ <input type="hidden" name="bug_status"
+ value="[% default.bug_status FILTER html %]">
+ <th>Initial State:</th>
+ <td>[% display_value("bug_status", default.bug_status) FILTER html %]</td>
+[% ELSE %]
+ [% INCLUDE bug/field.html.tmpl
+ bug = default, field = bug_fields.bug_status,
+ editable = (bug_status.size > 1), value = default.bug_status
+ override_legal_values = bug_status %]
+[% END %]
+
+ <td>&nbsp;</td>
+ [%# Calculate the number of rows we can use for flags %]
+ [% num_rows = 6 + (Param("useqacontact") ? 1 : 0) +
+ (user.is_timetracker ? 3 : 0) +
+ (Param("usebugaliases") ? 1 : 0)
+ %]
+
+ <td rowspan="[% num_rows FILTER html %]">
+ [% IF product.flag_types(is_active=>1).bug.size > 0 %]
+ [% display_flag_headers = 0 %]
+ [% any_flags_requesteeble = 0 %]
+
+ [% FOREACH flag_type = product.flag_types(is_active=>1).bug %]
+ [% display_flag_headers = 1 %]
+ [% SET any_flags_requesteeble = 1 IF flag_type.is_requestable && flag_type.is_requesteeble %]
+ [% END %]
+
+ [% IF display_flag_headers %]
+ [% PROCESS "flag/list.html.tmpl" flag_types = product.flag_types(is_active=>1).bug
+ any_flags_requesteeble = any_flags_requesteeble
+ flag_table_id = "bug_flags"
+ %]
+ [% END %]
+ [% END %]
+ </td>
+ </tr>
+
+ <tr>
+ <th><a href="page.cgi?id=fields.html#assigned_to">Assign To</a>:</th>
+ <td colspan="2">
+ [% INCLUDE global/userselect.html.tmpl
+ id => "assigned_to"
+ name => "assigned_to"
+ value => assigned_to
+ disabled => assigned_to_disabled
+ size => 30
+ emptyok => 1
+ custom_userlist => assignees_list
+ %]
+ <noscript>(Leave blank to assign to component's default assignee)</noscript>
+ </td>
+ </tr>
+
+[% IF Param("useqacontact") %]
+ <tr>
+ <th>QA Contact:</th>
+ <td colspan="2">
+ [% INCLUDE global/userselect.html.tmpl
+ id => "qa_contact"
+ name => "qa_contact"
+ value => qa_contact
+ disabled => qa_contact_disabled
+ size => 30
+ emptyok => 1
+ custom_userlist => qa_contacts_list
+ %]
+ <noscript>(Leave blank to assign to default qa contact)</noscript>
+ </td>
+ </tr>
+[% END %]
+
+ <tr>
+ <th>CC:</th>
+ <td colspan="2">
+ [% INCLUDE global/userselect.html.tmpl
+ id => "cc"
+ name => "cc"
+ value => cc
+ disabled => cc_disabled
+ size => 30
+ multiple => 5
+ %]
+ </td>
+ </tr>
+
+ <tr>
+ <th>Default CC:</th>
+ <td colspan="2">
+ <div id="initial_cc">
+ </div>
+ </td>
+ </tr>
+
+ <tr>
+ <td colspan="3">&nbsp;</td>
+ </tr>
+
+[% IF user.is_timetracker %]
+ <tr>
+ <th>Estimated Hours:</th>
+ <td colspan="2">
+ <input name="estimated_time" size="6" maxlength="6" value="[% estimated_time FILTER html %]">
+ </td>
+ </tr>
+ <tr>
+ <th>Deadline:</th>
+ <td colspan="2">
+ <input name="deadline" size="10" maxlength="10" value="[% deadline FILTER html %]">
+ <small>(YYYY-MM-DD)</small>
+ </td>
+ </tr>
+
+ <tr>
+ <td colspan="3">&nbsp;</td>
+ </tr>
+[% END %]
+
+[% IF Param("usebugaliases") %]
+ <tr>
+ <th>Alias:</th>
+ <td colspan="2">
+ <input name="alias" size="20" value="[% alias FILTER html %]">
+ </td>
+ </tr>
+[% END %]
+
+ <tr>
+ <th>URL:</th>
+ <td colspan="2">
+ <input name="bug_file_loc" size="40"
+ value="[% bug_file_loc FILTER html %]">
+ </td>
+ </tr>
+</tbody>
+
+<tbody>
+
+ <tr>
+ <th class="required">Summary:</th>
+ <td colspan="3">
+ <input name="short_desc" size="70" value="[% short_desc FILTER html %]"
+ maxlength="255" spellcheck="true" aria-required="true"
+ class="required">
+ </td>
+ </tr>
+
+ <tr>
+ <th>Description:</th>
+ <td colspan="3">
+ [% defaultcontent = BLOCK %]
+ [% IF cloned_bug_id %]
++++ This [% terms.bug %] was initially created as a clone of [% terms.Bug %] #[% cloned_bug_id FILTER html %] +++
+
+
+ [% END %]
+ [%-# We are within a BLOCK. The comment will be correctly HTML-escaped
+ # by global/textarea.html.tmpl. So we must not escape the comment here. %]
+ [% comment FILTER none %]
+ [%- END %]
+ [% INCLUDE global/textarea.html.tmpl
+ name = 'comment'
+ id = 'comment'
+ minrows = 10
+ maxrows = 25
+ cols = constants.COMMENT_COLS
+ defaultcontent = defaultcontent
+ %]
+ <br>
+ </td>
+ </tr>
+
+ [% IF user.is_insider %]
+ <tr class="expert_fields">
+ <th>&nbsp;</th>
+ <td colspan="3">
+ &nbsp;&nbsp;
+ <input type="checkbox" id="commentprivacy" name="commentprivacy"
+ [% " checked=\"checked\"" IF commentprivacy %]>
+ <label for="commentprivacy">
+ Make description private (visible only to members of the
+ <strong>[% Param('insidergroup') FILTER html %]</strong> group)
+ </label>
+ </td>
+ </tr>
+ [% END %]
+
+ <tr>
+ <th>Attachment:</th>
+ <td colspan="3">
+ <script type="text/javascript">
+ <!--
+ document.write( '<div id="attachment_false">'
+ + '<input type="button" value="Add an attachment" '
+ + 'onClick="handleWantsAttachment(true)"> '
+ + '<em style="display: none">This button has no '
+ + 'functionality for you because your browser does '
+ + 'not support CSS or does not use it.<\/em>'
+ + '<\/div>'
+ + '<div id="attachment_true" style="display: none">'
+ + '<input type="button" '
+ + 'value="Don\'t add an attachment " '
+ + 'onClick="handleWantsAttachment(false)">');
+ //-->
+ </script>
+ <fieldset>
+ <legend>Add an attachment</legend>
+ <table class="attachment_entry">
+ [% PROCESS attachment/createformcontents.html.tmpl
+ flag_types = product.flag_types(is_active=>1).attachment
+ any_flags_requesteeble = 1
+ flag_table_id ="attachment_flags" %]
+ </table>
+ </fieldset>
+ <script type="text/javascript">
+ <!--
+ document.write('<\/div>');
+ //-->
+ </script>
+ </td>
+ </tr>
+</tbody>
+
+<tbody class="expert_fields">
+ [% IF user.in_group('editbugs', product.id) %]
+ [% IF use_keywords %]
+ <tr>
+ [% INCLUDE bug/field.html.tmpl
+ bug = default, field = bug_fields.keywords, editable = 1,
+ value = keywords, desc_url = "describekeywords.cgi",
+ value_span = 3 %]
+ </tr>
+ [% END %]
+
+ <tr>
+ <th>Status Whiteboard:</th>
+ <td colspan="3">
+ <input id="status_whiteboard" name="status_whiteboard" size="70"
+ value="[% status_whiteboard FILTER html %]">
+ </td>
+ </tr>
+ <tr>
+ <th>Depends on:</th>
+ <td colspan="3">
+ <input name="dependson" accesskey="d" value="[% dependson FILTER html %]">
+ </td>
+ </tr>
+ <tr>
+ <th>Blocks:</th>
+ <td colspan="3">
+ <input name="blocked" accesskey="b" value="[% blocked FILTER html %]">
+ </td>
+ </tr>
+ [% END %]
+</tbody>
+
+<tbody class="expert_fields">
+ [% IF product.groups_available.size %]
+ <tr>
+ <th>&nbsp;</th>
+ <td colspan="3">
+ <br>
+ <strong>
+ Only users in all of the selected groups can view this
+ [%+ terms.bug %]:
+ </strong>
+ <br>
+ <font size="-1">
+ (Leave all boxes unchecked to make this a public [% terms.bug %].)
+ </font>
+ <br>
+ <br>
+
+ <!-- Checkboxes -->
+ <input type="hidden" name="defined_groups" value="1">
+ [% FOREACH group = product.groups_available %]
+ <input type="checkbox" id="group_[% group.id FILTER html %]"
+ name="groups" value="[% group.name FILTER html %]"
+ [% ' checked="checked"' IF default.groups.contains(group.name)
+ OR group.is_default %]>
+ <label for="group_[% group.id FILTER html %]">
+ [%- group.description FILTER html_light %]</label><br>
+ [% END %]
+ </td>
+ </tr>
+ [% END %]
+</tbody>
+
+<tbody>
+ [%# Form controls for entering additional data about the bug being created. %]
+ [% Hook.process("form") %]
+
+ <tr>
+ <th>&nbsp;</th>
+ <td colspan="3">
+ <input type="submit" id="commit" value="Submit [% terms.Bug %]"
+ onclick="if (this.form.short_desc.value == '')
+ { alert('Please enter a summary sentence for this [% terms.bug %].');
+ return false; } return true;">
+ &nbsp;&nbsp;&nbsp;&nbsp;
+ <input type="submit" name="maketemplate" id="maketemplate"
+ value="Remember values as bookmarkable template"
+ class="expert_fields">
+ </td>
+ </tr>
+</tbody>
+ </table>
+ <input type="hidden" name="form_name" value="enter_bug">
+</form>
+
+[%# Links or content with more information about the bug being created. %]
+[% Hook.process("end") %]
+
+[% ELSE %]
+
+<p>Sorry, you do not have access to this page.</p>
+
+[% END %]
+
+[% PROCESS global/footer.html.tmpl %]
+
+[% BLOCK build_userlist %]
+ [% user_found = 0 %]
+ [% default_login = default_user.login %]
+ [% RETURN UNLESS default_login %]
+
+ [% FOREACH user = userlist %]
+ [% IF user.login == default_login %]
+ [% user_found = 1 %]
+ [% LAST %]
+ [% END %]
+ [% END %]
+
+ [% userlist.push({login => default_login,
+ identity => default_user.identity,
+ visible => 1})
+ UNLESS user_found %]
+[% END %]
diff --git a/extensions/BMO/template/en/default/bug/create/create-poweredby.html.tmpl b/extensions/BMO/template/en/default/bug/create/create-poweredby.html.tmpl
new file mode 100644
index 000000000..e231cd9d5
--- /dev/null
+++ b/extensions/BMO/template/en/default/bug/create/create-poweredby.html.tmpl
@@ -0,0 +1,87 @@
+[%# 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 <gerv@gerv.net>
+ # Ville Skytt <ville.skytta@iki.fi>
+ # John Hoogstrate <hoogstrate@zeelandnet.nl>
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+
+[% PROCESS global/header.html.tmpl
+ title = "Powered by Mozilla Logo Requests"
+%]
+
+[% USE Bugzilla %]
+
+<p>If you are interested in using the <a href="http://www.mozilla.org/poweredby">Powered by Mozilla logo</a>,
+please provide some information about your application or product.</p>
+
+<p><strong>Please use this form for Powered by Mozilla logo requests only.</strong></p>
+
+<form method="post" action="post_bug.cgi" id="tmRequestForm">
+
+ <input type="hidden" name="product" value="Marketing">
+ <input type="hidden" name="component" value="Trademark Permissions">
+ <input type="hidden" name="bug_severity" value="enhancement">
+ <input type="hidden" name="rep_platform" value="All">
+ <input type="hidden" name="priority" value="--">
+ <input type="hidden" name="op_sys" value="Other">
+ <input type="hidden" name="version" value="unspecified">
+ <input type="hidden" name="assigned_to" value="dboswell@mozilla.com">
+ <input type="hidden" name="cc" value="liz@mozilla.com">
+ <input type="hidden" name="groups" value="marketing-private">
+ <input type="hidden" name="token" value="[% token FILTER html %]">
+
+ <table>
+ <tr>
+ <td align="right"><strong>Application or Product Name:</strong></td>
+ <td colspan="3">
+ <input name="short_desc" size="60" value="Powered by Mozilla request for: [% short_desc FILTER html %]">
+ </td>
+ </tr>
+
+ <tr>
+ <td align="right"><strong>URL&nbsp;(optional):</strong></td>
+ <td colspan="3">
+ <input name="bug_file_loc" size="60"
+ value="[% bug_file_loc FILTER html %]">
+ </td>
+ </tr>
+
+ <tr><td align="right" valign="top"><strong>Comments&nbsp;(optional):</strong></td>
+ <td colspan="3">
+ <textarea name="comment" rows="10" cols="80">
+ [% comment FILTER html %]</textarea>
+ <br>
+ </td>
+ </tr>
+
+ </table>
+
+ <br>
+
+ <input type="submit" id="commit" value="Submit Request">
+</form>
+
+<p>Thanks for contacting us.
+ You will be notified by email of any progress made in resolving your
+ request.
+</p>
+
+[% PROCESS global/footer.html.tmpl %]
diff --git a/extensions/BMO/template/en/default/bug/create/create-presentation.html.tmpl b/extensions/BMO/template/en/default/bug/create/create-presentation.html.tmpl
new file mode 100644
index 000000000..fd8d3c655
--- /dev/null
+++ b/extensions/BMO/template/en/default/bug/create/create-presentation.html.tmpl
@@ -0,0 +1,219 @@
+[%# 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 <reed@mozilla.com>
+ # David Tran <dtran@mozilla.com>
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+
+[% PROCESS global/header.html.tmpl
+ title = "Mozilla Corporation Mountain View Presentation Request"
+ javascript_urls = [ 'js/field.js', 'js/util.js' ]
+ style = ".yui-skin-sam .yui-calcontainer { z-index: 1; }"
+ yui = [ 'autocomplete', 'calendar' ]
+%]
+
+<div style='text-align: center; width: 98%; font-size: 2em; font-weight: bold; margin: 10px;'>Mountain View Presentation Request</div>
+
+<p><strong>Mountain View Presentation Request:</strong> Please use this form if you plan on hosting a presentation so that IT will be able to properly provide support. </p>
+
+<p>Process:</p>
+
+<ol><li>Complete and submit request below.</li>
+ <li>Your request will be reviewed and assigned to the appropriate person in IT.</li>
+</ol>
+
+<p>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.</p>
+
+<script type="text/javascript">
+function trySubmit() {
+ var out = 'Topic: the_topic\r\nPresenter: the_presenter\r\nDate: the_date\r\nTime: the_time\r\nAudience: the_audience\r\nAir Mozilla: air_mozilla\r\nDial-in: dial_in\r\nArchive: to_archive\r\nMember of IT to help with A/V: it_help\r\nDescription: the_description';
+
+ var topic = document.getElementById('topic').value;
+ var presenter = document.getElementById('presenter').value;
+ var date = document.getElementById('date').value;
+ var time = document.getElementById('time_hour').value + ':' + document.getElementById('time_minute').value + document.getElementById('ampm').value;
+ var shortdesc = 'Mountain View Presentation Request - ' + topic + ' (' + date + ' ' + time + ')';
+ var airmozilla = document.getElementById('airmozilla').checked? 'yes' : 'no';
+ var dialin = document.getElementById('dialin').checked? 'yes' : 'no';
+ var archive = document.getElementById('archive').checked? 'yes' : 'no';
+ var ithelp = document.getElementById('ithelp').checked? 'yes' : 'no';
+
+ out = out.replace( /the_topic/, topic );
+ out = out.replace( /the_presenter/, presenter );
+ out = out.replace( /the_date/, date);
+ out = out.replace( /the_time/, time);
+ out = out.replace( /the_audience/, document.getElementById('audience').value );
+ out = out.replace( /air_mozilla/, airmozilla);
+ out = out.replace( /dial_in/, dialin);
+ out = out.replace( /the_description/, document.getElementById('description').value );
+ out = out.replace( /to_archive/, archive);
+ out = out.replace( /it_help/, ithelp);
+
+ document.getElementById('comment').value = out;
+ document.getElementById('short_desc').value = shortdesc;
+
+ return true;
+}
+
+</script>
+
+<form method="post" action="post_bug.cgi" id="presentationRequestForm" enctype="multipart/form-data"
+ onSubmit="return trySubmit();">
+
+ <input type="hidden" name="product" value="mozilla.org">
+ <input type="hidden" name="component" value="Server Operations: Desktop Issues">
+ <input type="hidden" name="rep_platform" value="All">
+ <input type="hidden" name="op_sys" value="Other">
+ <input type="hidden" name="priority" value="--">
+ <input type="hidden" name="version" value="other">
+ <input type="hidden" name="bug_severity" id="bug_severity" value="normal">
+ <input type="hidden" name="comment" id="comment" value="">
+ <input type="hidden" name="short_desc" id="short_desc" value="">
+ <input type="hidden" name="groups" value="mozilla-corporation-confidential">
+ <input type="hidden" name="token" value="[% token FILTER html %]">
+
+<table>
+
+<tr>
+ <td align="right"><strong>Presenter:</strong></td>
+ <td>
+ <input type="text" name="presenter" id="presenter" value="" size="60" />
+ </td>
+
+</tr>
+
+<tr>
+ <td align="right"><strong>Topic:</strong></td>
+ <td>
+ <input type="text" name="topic" id="topic" value="" size="60" />
+ </td>
+</tr>
+
+<tr>
+ <td align="right"><strong>Date:</strong></td>
+ <td>
+ <input type="text" id="date" name="date" size="10"
+ onchange="updateCalendarFromField(this)">
+ <button type="button" class="calendar_button" id="button_calendar_date"
+ onclick="showCalendar('date')"><span>Calendar</span>
+ </button>
+ <div id="con_calendar_date"></div>
+ </td>
+</tr>
+
+<tr>
+ <td align="right"><strong>Start Time:</strong></td>
+ <td>
+ <select name="time_hour" id="time_hour">
+ <option value="12" selected>12</option>
+ <option value="1">1</option>
+ <option value="2">2</option>
+ <option value="3">3</option>
+ <option value="4">4</option>
+ <option value="5">5</option>
+ <option value="6">6</option>
+ <option value="7">7</option>
+ <option value="8">8</option>
+ <option value="9">9</option>
+ <option value="10">10</option>
+ <option value="11">11</option>
+ </select>:<select name="time_minute" id="time_minute">
+ <option value="00" selected>00</option>
+ <option value="15">15</option>
+ <option value="30">30</option>
+ <option value="45">45</option>
+ </select>
+ <select name="ampm" id="ampm">
+ <option value="AM" selected>AM</option>
+ <option value="PM">PM</option>
+ </select>
+ </td>
+</tr>
+
+<tr>
+ <td align="right"><strong>Intended Audience:</strong></td>
+ <td>
+ <select name="audience" id="audience">
+ <option value="Public" selected>Open to Public</option>
+ <option value="Employees Only">Employees Only</option>
+ <option value="Interns">Interns</option>
+ </select>
+ </td>
+</tr>
+
+<tr>
+ <td align="right"><strong>Air Mozilla Broadcasting?</strong></td>
+ <td align="left"><input type="checkbox" name="airmozilla" id="airmozilla"></td>
+</tr>
+
+<tr>
+ <td align="right"><strong>Dial In?</strong></td>
+ <td align="left"><input type="checkbox" name="dialin" id="dialin"></td>
+</tr>
+
+<tr>
+<td align="right"><strong>Archive this?</strong></td>
+<td align="left"><input type="checkbox" name="archive" id="archive" value="yes"></td>
+</tr>
+
+
+<tr>
+<td align="right"><strong>Need IT to help run A/V?</strong></td>
+<td align="left"><input type="checkbox" name="ithelp" id="ithelp" value="yes" checked></td>
+</tr>
+
+<tr>
+ <td align="right"><strong>CC&nbsp;(optional):</strong></td>
+ <td colspan="3">
+ [% INCLUDE global/userselect.html.tmpl
+ id => "cc"
+ name => "cc"
+ value => cc
+ size => 60
+ multiple => 5
+ %]
+ </td>
+</tr>
+
+<tr>
+ <th><label for="description">Description</label>:</th>
+ <td>
+ <em>Please briefly describe the presentation and any specific needs you might have.</em><br>
+
+ <textarea id="description" name="description" rows="10" cols="80"></textarea>
+ </td>
+</tr>
+
+ </table>
+
+ <br>
+ <input type="submit" id="commit" value="Submit Request">
+</form>
+
+<p>Thanks for contacting us.
+ You will be notified by email of any progress made in resolving your request.
+
+</p>
+
+<script type="text/javascript">
+ createCalendar('date');
+</script>
+
+[% PROCESS global/footer.html.tmpl %]
diff --git a/extensions/BMO/template/en/default/bug/create/create-privacy-data.html.tmpl b/extensions/BMO/template/en/default/bug/create/create-privacy-data.html.tmpl
new file mode 100644
index 000000000..fbf3bed55
--- /dev/null
+++ b/extensions/BMO/template/en/default/bug/create/create-privacy-data.html.tmpl
@@ -0,0 +1,219 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+
+[% inline_style = BLOCK %]
+ #bug_form input[type=text], #bug_form input[type=file], #cc_autocomplete, #bug_form textarea {
+ width: 100%;
+ }
+[% END %]
+
+[% inline_js = BLOCK %]
+ function onSubmit() {
+ var error = '';
+ if (!isFilledOut('short_desc')) error += 'Please enter a summary.\n';
+ if (!isFilledOut('attachment')) error += 'Please attach the data set/representative sample.\n';
+ if (!isFilledOut('source')) error += 'Please enter the data source.\n';
+ if (!isFilledOut('data_desc')) error += 'Please enter the data description.\n';
+ if (!isFilledOut('release')) error += 'Please enter the parts of data you want released.\n';
+ if (!isFilledOut('why')) error += 'Please enter why you want to release this data.\n';
+ if (!isFilledOut('when')) error += 'Please enter when you would like to release this data.\n';
+
+ if (error) {
+ alert(error);
+ return false;
+ }
+
+ return true;
+ }
+[% END %]
+
+[% PROCESS global/header.html.tmpl
+ title = "Privacy - Data Release Proposal"
+ style = inline_style
+ style_urls = [ 'skins/standard/enter_bug.css' ]
+ javascript = inline_js
+ javascript_urls = [ 'extensions/BMO/web/js/form_validate.js',
+ 'js/attachment.js', 'js/field.js', 'js/util.js' ]
+ yui = [ 'autocomplete' ]
+%]
+
+<h2>Privacy - Data Release Proposal</h2>
+
+<p>
+ Before filling out this form, please look at the
+ <a href="https://wiki.mozilla.org/Privacy/How_To/Deidentify" target="_blank">guide</a>
+ for releasing info about people.
+</p>
+
+<p>
+ All fields except for CC are required.
+</p>
+
+<form method="post" action="post_bug.cgi" id="bug_form" class="enter_bug_form"
+ enctype="multipart/form-data" onSubmit="return onSubmit()">
+<input type="hidden" name="format" value="privacy-data">
+<input type="hidden" name="product" value="Privacy">
+<input type="hidden" name="component" value="Data Release Proposal">
+<input type="hidden" name="rep_platform" value="All">
+<input type="hidden" name="op_sys" value="Other">
+<input type="hidden" name="priority" value="--">
+<input type="hidden" name="version" value="unspecified">
+<input type="hidden" name="bug_severity" id="bug_severity" value="normal">
+<input type="hidden" name="comment" id="comment" value="">
+<input type="hidden" name="groups" id="groups" value="privacy">
+<input type="hidden" name="token" value="[% token FILTER html %]">
+
+<table>
+
+<tr>
+ <th>
+ <label for="short_desc">Summary:</label>
+ </th>
+ <td>
+ <input type="text" name="short_desc" id="short_desc" value="" size="60">
+ </td>
+</tr>
+
+<tr>
+ <th>
+ <label for="cc">CC:</label>
+ </th>
+ <td>
+ [% INCLUDE global/userselect.html.tmpl
+ id => "cc"
+ name => "cc"
+ value => cc
+ size => 60
+ multiple => 5
+ %]
+ </td>
+ <td>
+ <i>&nbsp;Optional</i>
+ </td>
+</tr>
+
+<tr>
+ <th>
+ <label for="attachment">Data Set:</label>
+ </th>
+ <td>
+ <i>Please attach the data set, or a representative sample.</i>
+ <div>
+ <input type="file" id="attachment" name="data" size="50">
+ <input type="hidden" name="contenttypemethod" value="autodetect">
+ <input type="hidden" name="description" value="Data Set">
+ </div>
+ </td>
+</tr>
+
+<tr>
+ <th>
+ <label for="source">Source:</label>
+ </th>
+ <td>
+ <i>Where does this data come from?</i>
+ <div>
+ <textarea name="source" id="source" rows="5" cols="40"></textarea>
+ </div>
+ </td>
+</tr>
+
+<tr>
+ <th>
+ <label for="data_desc">Data Description:</label>
+ </th>
+ <td>
+ <i>What people and things does this data describe, and what fields does it contain?</i>
+ <div>
+ <textarea name="data_desc" id="data_desc" rows="5" cols="40"></textarea>
+ </div>
+ </td>
+</tr>
+
+<tr>
+ <th>
+ <label for="release">Release:</label>
+ </th>
+ <td>
+ <i>What parts of this data do you want to release?</i>
+ <div>
+ <textarea name="release" id="release" rows="5" cols="40"></textarea>
+ </div>
+ </td>
+</tr>
+
+<tr>
+ <th>
+ <label for="why">Why:</label>
+ </th>
+ <td>
+ <i>Why are we releasing this data, and what do we hope people will do with it?</i>
+ <div>
+ <textarea name="why" id="why" rows="5" cols="40"></textarea>
+ </div>
+ </td>
+</tr>
+
+<tr>
+ <th>
+ <label for="when">Release Time:</label>
+ </th>
+ <td>
+ <i>Is there a particular time by which you would like to release this data?</i>
+ <div>
+ <input type="text" name="when" id="when" value="" size="60">
+ </div>
+ </td>
+</tr>
+
+<tr>
+ <td colspan="2">
+ Expect to discover that you've missed a few of things, so plan for a couple weeks to get them corrected.
+ </td>
+</tr>
+
+<tr>
+ <td>&nbsp;</td>
+ <td>
+ <input type="submit" id="commit" value="Submit Request">
+ </td>
+</tr>
+</table>
+
+</form>
+
+<script type="text/javascript">
+function trySubmit() {
+ var topic = document.getElementById('topic').value;
+ var date = document.getElementById('date').value;
+ var time = document.getElementById('time_hour').value + ':' +
+ document.getElementById('time_minute').value +
+ document.getElementById('ampm').value + " " +
+ document.getElementById('time_zone').value;
+ var location = document.getElementById('location').value;
+ var shortdesc = 'Event - (' + date + ' ' + time + ') - ' + location + ' - ' + topic;
+ document.getElementById('short_desc').value = shortdesc;
+
+ // If intended audience is employees only, add mozilla-corporation-confidential group
+ var audience = document.getElementById('audience').value;
+ if (audience == 'Employees Only') {
+ var brownbagRequestForm = document.getElementById('brownbagRequestForm');
+ var groups = document.createElement('input');
+ groups.type = 'hidden';
+ groups.name = 'groups';
+ groups.value = 'mozilla-corporation-confidential';
+ brownbagRequestForm.appendChild(groups);
+ }
+
+ return true;
+}
+</script>
+
+[% PROCESS global/footer.html.tmpl %]
diff --git a/extensions/BMO/template/en/default/bug/create/create-recoverykey.html.tmpl b/extensions/BMO/template/en/default/bug/create/create-recoverykey.html.tmpl
new file mode 100644
index 000000000..a75959abb
--- /dev/null
+++ b/extensions/BMO/template/en/default/bug/create/create-recoverykey.html.tmpl
@@ -0,0 +1,70 @@
+[%# 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):
+ # David Lawrence <dkl@mozilla.com>
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+
+[% PROCESS global/header.html.tmpl
+ title = "Mozilla Corporation/Foundation Encryption Recovery Key"
+%]
+
+<p>Please complete the following information as you are encrypting your laptop.</p>
+
+<ul>
+ <li>The Recovery Key will be displayed during the encryption process
+ (<a href="https://mana.mozilla.org/wiki/display/INFRASEC/Desktop+Security#DesktopSecurity-DiskencryptionFileVault">more info</a>)
+ </li>
+ <li>The asset tag number is located on a sticker typically on the bottom of the device.</li>
+</ul>
+
+<form method="post" action="post_bug.cgi" id="recoveryKeyForm" enctype="multipart/form-data">
+ <input type="hidden" name="product" value="mozilla.org">
+ <input type="hidden" name="component" value="Server Operations: Desktop Issues">
+ <input type="hidden" name="rep_platform" value="All">
+ <input type="hidden" name="op_sys" value="All">
+ <input type="hidden" name="priority" value="--">
+ <input type="hidden" name="version" value="other">
+ <input type="hidden" name="bug_severity" value="normal">
+ <input type="hidden" name="groups" value="mozilla-corporation-confidential">
+ <input type="hidden" name="groups" value="infra">
+ <input type="hidden" name="token" value="[% token FILTER html %]">
+ <input type="hidden" name="cc" value="tfairfield@mozilla.com, ghuerta@mozilla.com">
+ <input type="hidden" name="short_desc" value="Encryption Recovery Key for [% user.name || user.login FILTER html %]">
+ <input type="hidden" name="format" value="recoverykey">
+ <table>
+ <tr>
+ <td align="right"><strong>Recovery Key:</strong></td>
+ <td>
+ <input name="recoverykey" size="60" value="[% recoverykey FILTER html %]">
+ </td>
+ </tr>
+ <tr>
+ <td align="right"><strong>Asset Tag Number:</strong></td>
+ <td>
+ <input name="assettag" size="60" value="[% assettag FILTER html %]">
+ </td>
+ </tr>
+ <tr>
+ <td></td>
+ <td><input type="submit" id="commit" value="Submit"></td>
+ </tr>
+ </table>
+</form>
+
+[% PROCESS global/footer.html.tmpl %]
diff --git a/extensions/BMO/template/en/default/bug/create/create-swag.html.tmpl b/extensions/BMO/template/en/default/bug/create/create-swag.html.tmpl
new file mode 100644
index 000000000..58eb39d5f
--- /dev/null
+++ b/extensions/BMO/template/en/default/bug/create/create-swag.html.tmpl
@@ -0,0 +1,222 @@
+[%# 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 <reed@mozilla.com>
+ # David Tran <dtran@mozilla.com>
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+
+[% PROCESS global/header.html.tmpl
+ title = "Swag Request Form"
+ javascript_urls = [ 'extensions/BMO/web/js/swag.js',
+ 'extensions/BMO/web/js/form_validate.js',
+ 'js/field.js' ]
+ yui = [ 'autocomplete' ]
+%]
+
+<div style='text-align: center; width: 98%; font-size: 2em; font-weight: bold; margin: 10px;'>Swag Request Form</div>
+
+<p><strong>Swag Request:</strong> Please use this form to file a request for swag. </p>
+
+<ol>
+ <li>You first need submit a <a href="/enter_bug.cgi?product=Marketing&amp;format=mktgevent">Event Request Form</a>. You'll be asked for the [% terms.bug %] number below.</li>
+ <li>Complete and submit request below.</li>
+ <li>Your request will be reviewed by the appropriate person in the Engagement team.</li>
+ <li>Your swag request will be reviewed and if approved shipped to you from
+ one of our two fulfillment houses. <i>Please note that swag is expensive and
+ products change over time - we are happy to send you a small quantity of swag
+ to use at your event!</i></li>
+</ol>
+
+<p>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.</p>
+
+<script language="javascript" type="text/javascript">
+function trySubmit() {
+
+ var firstname = document.getElementById('firstname').value;
+ var lastname = document.getElementById('lastname').value;
+ var requester = firstname + ' ' + lastname;
+ var shortdesc = 'Swag Request - ' + requester;
+ document.getElementById('short_desc').value = shortdesc;
+
+ // the following fields we don't let the user mess with because they're
+ // calculated, but they need to be submitted, and disabled fields don't submit
+ document.getElementById('Totalswag').disabled = false;
+ document.getElementById('mens_total').disabled = false;
+ document.getElementById('womens_total').disabled = false;
+
+ return true;
+}
+
+function validateAndSubmit() {
+
+ var alert_text = '';
+ if(!isFilledOut('firstname')) alert_text += "Please enter your first name\n";
+ if(!isFilledOut('lastname')) alert_text += "Please enter your last name\n";
+ if(!isFilledOut('dependson')) alert_text += "Please enter the [% terms.bug %] number for your Event Request Form\n";
+ if(!isValidEmail(document.getElementById('email').value)) alert_text += "Please enter a valid email address\n";
+
+ //Everything required is filled out..try to submit the form!
+ if(alert_text == '') {
+ return trySubmit();
+ }
+
+ //alert text, stay here on the pagee
+ alert(alert_text);
+ return false;
+}
+
+</script>
+
+<form method="post" action="post_bug.cgi" id="swagRequestForm" enctype="multipart/form-data"
+ onSubmit="return validateAndSubmit();">
+
+ <input type="hidden" name="format" value="swag">
+ <input type="hidden" name="product" value="Marketing">
+ <input type="hidden" name="component" value="Swag Requests">
+ <input type="hidden" name="rep_platform" value="All">
+ <input type="hidden" name="op_sys" value="Other">
+ <input type="hidden" name="priority" value="--">
+ <input type="hidden" name="version" value="unspecified">
+ <input type="hidden" name="bug_severity" id="bug_severity" value="normal">
+ <input type="hidden" name="short_desc" id="short_desc" value="">
+ <input type="hidden" name="groups" value="mozilla-corporation-confidential">
+ <input type="hidden" name="token" value="[% token FILTER html %]">
+
+<table>
+
+<tr>
+ <td align="right"><strong>First Name: <span style="color: red;">*</span></strong></td>
+ <td align="left">
+ <input type="text" name="firstname" id="firstname" value="" size="20" maxlength="20" />
+ </td>
+</tr>
+<tr>
+ <td align="right"><strong>Last Name: <span style="color: red;">*</span></strong></td>
+ <td align="left">
+ <input type="text" name="lastname" id="lastname" value="" size="20" maxlength="20"/>
+ </td>
+</tr>
+
+<tr>
+ <td align="right"><strong>Email Address: <span style="color: red;">*</span></strong></td>
+ <td>
+ <input type="text" name="email" id="email" value="" size="50" maxlength="50"/>
+ </td>
+</tr>
+
+<tr>
+ <td align="right"><strong>CC:</strong></td>
+ <td colspan="3">
+ [% INCLUDE global/userselect.html.tmpl
+ id => "cc"
+ name => "cc"
+ value => cc
+ size => 50
+ multiple => 5
+ %]
+ </td>
+</tr>
+
+<tr>
+ <td><!-- spacer -->&nbsp;</td>
+</tr>
+
+<tr>
+ <td><!-- spacer -->&nbsp;</td>
+</tr>
+
+<tr>
+ <td align="right"><strong>[% terms.Bug %] number assigned to previously- &nbsp;&nbsp;<br>submitted <a href="/enter_bug.cgi?product=Marketing&amp;format=mktgevent">Event Request Form</a>: <span style="color: red;">*</span></strong></td>
+ <td colspan="3"><input name="dependson" id="dependson"></td>
+</tr>
+
+<tr>
+ <td align="right"><strong>Specific swag needed?</strong></td>
+ <td>
+ <textarea id="additional" name="additional" rows="5" cols="50"></textarea>
+ </td>
+</tr>
+
+<tr>
+ <td align="right"><br><br><strong>Ship to:</strong></td>
+ <td colspan="3"></td>
+</tr>
+<tr>
+ <td align="right"><strong>First name:</strong></td>
+ <td colspan="3"><input name="shiptofirstname" id="shiptofirstname"></td>
+</tr>
+<tr>
+ <td align="right"><strong>Last name:</strong></td>
+ <td colspan="3"><input name="shiptolastname" id="shiptolastname"></td>
+</tr>
+<tr>
+ <td align="right"><strong>Address</strong></td>
+ <td colspan="3"><input name="shiptoaddress" id="shiptoaddress" size="60"></td>
+</tr>
+<tr>
+ <td align="right"><strong>Address 2</strong></td>
+ <td colspan="3"><input name="shiptoaddress2" id="shiptoaddress2" size="60"></td>
+</tr>
+<tr>
+ <td align="right"><strong>City</strong></td>
+ <td colspan="3"><input name="shiptocity" id="shiptocity"></td>
+</tr>
+<tr>
+ <td align="right"><strong>State</strong></td>
+ <td colspan="3"><input name="shiptostate" id="shiptostate"></td>
+</tr>
+<tr>
+ <td align="right"><strong>Country</strong></td>
+ <td colspan="3"><input name="shiptocountry" id="shiptocountry"></td>
+</tr>
+<tr>
+ <td align="right"><strong>Postal Code</strong></td>
+ <td colspan="3"><input name="shiptopcode" id="shiptopcode"></td>
+</tr>
+<tr>
+ <td align="right"><strong>Telephone</strong></td>
+ <td colspan="3"><input name="shiptophone" id="shiptophone"></td>
+</tr>
+<tr>
+ <td align="right"><strong>Personal ID/RUT</strong><br><small>(if your country requires this)</small></td>
+ <td colspan="3"><input name="shiptoidrut" id="shiptoidrut"></td>
+</tr>
+
+<tr><td colspan="4"><br><br></td></tr>
+
+<tr>
+ <td align="right"><label for="comment"><strong>Any additional comments?</strong></label></td>
+ <td>
+ <textarea id="comment" name="comment" rows="5" cols="50"></textarea>
+ </td>
+</tr>
+
+ </table>
+ <br>
+ <input type="submit" id="commit" value="Submit Request">
+
+<p>
+ <strong><span style="color: red;">*</span></strong> - Required field<br />
+ Thanks for contacting us.
+ You will be notified by email of any progress made in resolving your request.
+</p>
+
+[% PROCESS global/footer.html.tmpl %]
diff --git a/extensions/BMO/template/en/default/bug/create/create-trademark.html.tmpl b/extensions/BMO/template/en/default/bug/create/create-trademark.html.tmpl
new file mode 100644
index 000000000..977ad00d4
--- /dev/null
+++ b/extensions/BMO/template/en/default/bug/create/create-trademark.html.tmpl
@@ -0,0 +1,87 @@
+[%# 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 <gerv@gerv.net>
+ # Ville Skyttä <ville.skytta@iki.fi>
+ # John Hoogstrate <hoogstrate@zeelandnet.nl>
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+
+[% PROCESS global/header.html.tmpl
+ title = "Trademark Usage Requests"
+%]
+
+[% USE Bugzilla %]
+
+<p>
+ If, after reading
+ <a href="http://www.mozilla.org/foundation/trademarks/">the trademark policy
+ documents</a>, you know you need permission to use a certain trademark, this
+ is the place to be.
+</p>
+
+<p><strong>Please use this form for trademark requests only!</strong></p>
+
+<form method="post" action="post_bug.cgi" id="tmRequestForm">
+
+ <input type="hidden" name="product" value="Marketing">
+ <input type="hidden" name="component" value="Trademark Permissions">
+ <input type="hidden" name="bug_severity" value="enhancement">
+ <input type="hidden" name="rep_platform" value="All">
+ <input type="hidden" name="priority" value="P3">
+ <input type="hidden" name="op_sys" value="Other">
+ <input type="hidden" name="version" value="unspecified">
+ <input type="hidden" name="groups" value="marketing-private">
+ <input type="hidden" name="token" value="[% token FILTER html %]">
+
+ <table>
+ <tr>
+ <td align="right"><strong>Summary:</strong></td>
+ <td colspan="3">
+ <input name="short_desc" size="60" value="[% short_desc FILTER html %]">
+ </td>
+ </tr>
+
+ <tr><td align="right" valign="top"><strong>Description:</strong></td>
+ <td colspan="3">
+ <textarea name="comment" rows="10" cols="80">
+ [% comment FILTER html %]</textarea>
+ <br>
+ </td>
+ </tr>
+ <tr>
+ <td align="right"><strong>URL&nbsp;(optional):</strong></td>
+ <td colspan="3">
+ <input name="bug_file_loc" size="60"
+ value="[% bug_file_loc FILTER html %]">
+ </td>
+ </tr>
+ </table>
+
+ <br>
+
+ <input type="submit" id="commit" value="Submit Request">
+</form>
+
+<p>Thanks for contacting us.
+ You will be notified by email of any progress made in resolving your
+ request.
+</p>
+
+[% PROCESS global/footer.html.tmpl %]
diff --git a/extensions/BMO/template/en/default/bug/create/create-winqual.html.tmpl b/extensions/BMO/template/en/default/bug/create/create-winqual.html.tmpl
new file mode 100644
index 000000000..d14cca810
--- /dev/null
+++ b/extensions/BMO/template/en/default/bug/create/create-winqual.html.tmpl
@@ -0,0 +1,800 @@
+[%# 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 <gerv@gerv.net>
+ # Ville Skyttä <ville.skytta@iki.fi>
+ # Shane H. W. Travis <travis@sedsystems.ca>
+ # Marc Schumann <wurblzap@gmail.com>
+ # Akamai Technologies <bugzilla-dev@akamai.com>
+ # Max Kanat-Alexander <mkanat@bugzilla.org>
+ # Frédéric Buclin <LpSolit@gmail.com>
+ #%]
+
+[% PROCESS "global/field-descs.none.tmpl" %]
+
+[% title = BLOCK %]Enter [% terms.Bug %]: [% product.name FILTER html %][% END %]
+
+[% PROCESS global/header.html.tmpl
+ title = title
+ yui = [ 'autocomplete', 'calendar', 'datatable', 'button' ]
+ style_urls = [ 'skins/standard/attachment.css',
+ 'skins/standard/enter_bug.css',
+ 'skins/custom/create_bug.css' ]
+ javascript_urls = [ "js/attachment.js", "js/util.js",
+ "js/field.js", "js/TUI.js", "js/bug.js",
+ "js/create_bug.js" ]
+ onload = "init();"
+%]
+
+<script type="text/javascript">
+<!--
+
+function init() {
+ set_assign_to();
+ hideElementById('attachment_true');
+ showElementById('attachment_false');
+ showElementById('btn_no_attachment');
+ initCrashSignatureField();
+ init_take_handler('[% user.login FILTER js %]');
+}
+
+function initCrashSignatureField() {
+ var el = document.getElementById('cf_crash_signature');
+ if (!el) return;
+ [% IF cf_crash_signature.length %]
+ YAHOO.util.Dom.addClass('cf_crash_signature_container', 'bz_default_hidden');
+ [% ELSE %]
+ hideEditableField('cf_crash_signature_container','cf_crash_signature_input',
+ 'cf_crash_signature_action', 'cf_crash_signature');
+ [% END %]
+}
+
+var initialowners = new Array();
+var last_initialowner;
+var initialccs = new Array();
+var components = new Array();
+var comp_desc = new Array();
+var flags = new Array();
+[% IF Param("useqacontact") %]
+ var initialqacontacts = new Array([% product.components.size %]);
+ var last_initialqacontact;
+[% END %]
+[% count = 0 %]
+[%- FOREACH c = product.components %]
+ [% NEXT IF NOT c.is_active %]
+ [% NEXT IF c.name != 'WinQual Reports' %]
+ components[[% count %]] = "[% c.name FILTER js %]";
+ comp_desc[[% count %]] = "[% c.description FILTER html_light FILTER js %]";
+ initialowners[[% count %]] = "[% c.default_assignee.login FILTER js %]";
+ [% flag_list = [] %]
+ [% FOREACH f = c.flag_types(is_active=>1).bug %]
+ [% flag_list.push(f.id) %]
+ [% END %]
+ [% FOREACH f = c.flag_types(is_active=>1).attachment %]
+ [% flag_list.push(f.id) %]
+ [% END %]
+ flags[[% count %]] = [[% flag_list.join(",") FILTER js %]];
+ [% IF Param("useqacontact") %]
+ initialqacontacts[[% count %]] = "[% c.default_qa_contact.login FILTER js %]";
+ [% END %]
+
+ [% SET initial_cc_list = [] %]
+ [% FOREACH cc_user = c.initial_cc %]
+ [% initial_cc_list.push(cc_user.login) %]
+ [% END %]
+ initialccs[[% count %]] = "[% initial_cc_list.join(', ') FILTER js %]";
+
+ [% count = count + 1 %]
+[%- END %]
+
+function set_assign_to() {
+ // Based on the selected component, fill the "Assign To:" field
+ // with the default component owner, and the "QA Contact:" field
+ // with the default QA Contact. It also selectively enables flags.
+ var form = document.Create;
+ var assigned_to = form.assigned_to.value;
+
+[% IF Param("useqacontact") %]
+ var qa_contact = form.qa_contact.value;
+[% END %]
+
+ var index = -1;
+ if (form.component.type == 'select-one') {
+ index = form.component.selectedIndex;
+ } else if (form.component.type == 'hidden') {
+ // Assume there is only one component in the list
+ index = 0;
+ }
+ if (index != -1) {
+ var owner = initialowners[index];
+ var component = components[index];
+ if (assigned_to == last_initialowner
+ || assigned_to == owner
+ || assigned_to == '') {
+ form.assigned_to.value = owner;
+ last_initialowner = owner;
+ }
+
+ document.getElementById('initial_cc').innerHTML = initialccs[index];
+ document.getElementById('comp_desc').innerHTML = comp_desc[index];
+
+ if (initialccs[index]) {
+ showElementById('initial_cc_label');
+ showElementById('initial_cc');
+ } else {
+ hideElementById('initial_cc_label');
+ hideElementById('initial_cc');
+ }
+
+ [% IF Param("useqacontact") %]
+ var contact = initialqacontacts[index];
+ if (qa_contact == last_initialqacontact
+ || qa_contact == contact
+ || qa_contact == '') {
+ form.qa_contact.value = contact;
+ last_initialqacontact = contact;
+ }
+ [% END %]
+
+ // First, we disable all flags. Then we re-enable those
+ // which are available for the selected component.
+ var inputElements = document.getElementsByTagName("select");
+ var inputElement, flagField;
+ for ( var i=0 ; i<inputElements.length ; i++ ) {
+ inputElement = inputElements.item(i);
+ if (inputElement.name.search(/^flag_type-(\d+)$/) != -1) {
+ var id = inputElement.name.replace(/^flag_type-(\d+)$/, "$1");
+ inputElement.disabled = true;
+ // Also hide the requestee field, if it exists.
+ inputElement = document.getElementById("requestee_type-" + id);
+ if (inputElement)
+ YAHOO.util.Dom.addClass(inputElement.parentNode, 'bz_default_hidden');
+ }
+ }
+ // Now enable flags available for the selected component.
+ for (var i = 0; i < flags[index].length; i++) {
+ flagField = document.getElementById("flag_type-" + flags[index][i]);
+ // Do not enable flags the user cannot set nor request.
+ if (flagField && flagField.options.length > 1) {
+ flagField.disabled = false;
+ // Re-enabling the requestee field depends on the status
+ // of the flag.
+ toggleRequesteeField(flagField, 1);
+ }
+ }
+ }
+}
+
+var status_comment_required = new Array();
+[% FOREACH status = bug_status %]
+ status_comment_required['[% status.name FILTER js %]'] =
+ [% status.comment_required_on_change_from() ? 'true' : 'false' %]
+[% END %]
+
+TUI_alternates['expert_fields'] = 'Show Advanced Fields';
+// Hide the Advanced Fields by default, unless the user has a cookie
+// that specifies otherwise.
+TUI_hide_default('expert_fields');
+
+-->
+</script>
+
+<form name="Create" id="Create" method="post" action="post_bug.cgi"
+ class="enter_bug_form" enctype="multipart/form-data"
+ onsubmit="return validateEnterBug(this)">
+ <input type="hidden" name="product" value="Firefox">
+ <input type="hidden" name="component" value="WinQual Reports">
+ <input type="hidden" name="token" value="[% token FILTER html %]">
+ <input type="hidden" name="groups" value="winqual-data">
+
+<table>
+<tbody>
+ <tr>
+ <td colspan="4">
+ [%# Migration note: The following file corresponds to the old Param
+ # 'entryheaderhtml'
+ #%]
+ [% PROCESS 'bug/create/user-message.html.tmpl' %]
+ </td>
+ </tr>
+
+ <tr>
+ <td colspan="2">
+ <input type="button" id="expert_fields_controller"
+ value="Hide Advanced Fields" onClick="toggleAdvancedFields()">
+ [%# Show the link if the browser supports JS %]
+ <script type="text/javascript">
+ YAHOO.util.Dom.removeClass('expert_fields_controller',
+ 'bz_default_hidden');
+ </script>
+ </td>
+ <td colspan="2">
+ (<span class="required_star">*</span> =
+ <span class="required_explanation">Required Field</span>)
+ </td>
+ </tr>
+
+ <tr>
+ [% INCLUDE bug/field.html.tmpl
+ bug = default, field = bug_fields.product, editable = 0,
+ value = product.name %]
+ [% INCLUDE bug/field.html.tmpl
+ bug = default, field = bug_fields.reporter, editable = 0,
+ value = user.login %]
+ </tr>
+
+ [%# We can't use the select block in these two cases for various reasons. %]
+ <tr>
+ [% component_desc_url = BLOCK -%]
+ describecomponents.cgi?product=[% product.name FILTER uri %]
+ [% END %]
+ [% INCLUDE "bug/field-label.html.tmpl"
+ field = bug_fields.component editable = 1
+ desc_url = component_desc_url
+ %]
+ <td id="field_container_component">
+ [% INCLUDE bug/field.html.tmpl
+ bug = default, field = bug_fields.component, editable = 0,
+ value = "WinQual Reports", no_tds = 1 %]
+ <script type="text/javascript">
+ <!--
+ [%+ INCLUDE "bug/field-events.js.tmpl"
+ field = bug_fields.component %]
+ YAHOO.util.Event.onDOMReady(set_assign_to);
+ //-->
+ </script>
+ </td>
+
+ <td colspan="2" id="comp_desc_container">
+ [%# Enclose the fieldset in a nested table so that its width changes based
+ # on the length on the component description. %]
+ <table>
+ <tr>
+ <td>
+ <fieldset>
+ <legend>Component Description</legend>
+ <div id="comp_desc" class="comment"></div>
+ </fieldset>
+ </td>
+ </tr>
+ </table>
+ </td>
+ </tr>
+
+ <tr>
+ [% INCLUDE "bug/field-label.html.tmpl"
+ field = bug_fields.version editable = 1 rowspan = 3
+ %]
+ <td rowspan="3">
+ <select name="version" id="version" size="5">
+ [%- FOREACH v = version %]
+ [% NEXT IF NOT v.is_active %]
+ <option value="[% v.name FILTER html %]"
+ [% ' selected="selected"' IF v.name == default.version %]>[% v.name FILTER html -%]
+ </option>
+ [%- END %]
+ </select>
+ </td>
+
+ [% INCLUDE bug/field.html.tmpl
+ bug = default, field = bug_fields.bug_severity, editable = 1,
+ value = default.bug_severity %]
+ </tr>
+
+ <tr>
+ [% INCLUDE bug/field.html.tmpl
+ bug = default, field = bug_fields.rep_platform, editable = 1,
+ value = default.rep_platform %]
+ </tr>
+
+ <tr>
+ [% INCLUDE bug/field.html.tmpl
+ bug = default, field = bug_fields.op_sys, editable = 1,
+ value = default.op_sys %]
+ </tr>
+ [% IF !Param('defaultplatform') || !Param('defaultopsys') %]
+ <tr>
+ <th colspan="3">&nbsp;</th>
+ <td id="os_guess_note" class="comment">
+ <div>We've made a guess at your
+ [% IF Param('defaultplatform') %]
+ operating system. Please check it
+ [% ELSIF Param('defaultopsys') %]
+ platform. Please check it
+ [% ELSE %]
+ operating system and platform. Please check them
+ [% END %]
+ and make any corrections if necessary.</div>
+ </td>
+ </tr>
+ [% END %]
+</tbody>
+
+<tbody class="expert_fields">
+ <tr>
+ [% IF Param('usetargetmilestone') && Param('letsubmitterchoosemilestone') %]
+ [% INCLUDE select field = bug_fields.target_milestone %]
+ [% ELSE %]
+ <td colspan="2">&nbsp;</td>
+ [% END %]
+
+ [% IF Param('letsubmitterchoosepriority') %]
+ [% INCLUDE bug/field.html.tmpl
+ bug = default, field = bug_fields.priority, editable = 1,
+ value = default.priority %]
+ [% ELSE %]
+ <td colspan="2">&nbsp;</td>
+ [% END %]
+ </tr>
+</tbody>
+
+<tbody class="expert_fields">
+ <tr>
+ <td colspan="4">&nbsp;</td>
+ </tr>
+
+ <tr>
+ [% INCLUDE bug/field.html.tmpl
+ bug = default, field = bug_fields.bug_status,
+ editable = (bug_status.size > 1), value = default.bug_status
+ override_legal_values = bug_status %]
+ </tr>
+
+ <tr>
+ [% INCLUDE "bug/field-label.html.tmpl"
+ field = bug_fields.assigned_to editable = 1
+ %]
+ <td>
+ [% INCLUDE global/userselect.html.tmpl
+ id => "assigned_to"
+ name => "assigned_to"
+ value => assigned_to
+ disabled => assigned_to_disabled
+ size => 30
+ emptyok => 1
+ custom_userlist => assignees_list
+ %]
+ [% UNLESS assigned_to_disabled %]
+ <span id="take_bug">
+ &nbsp;(<a title="Assign to yourself" href="#"
+ onclick="return take_bug('[% user.login FILTER js %]')">take</a>)
+ </span>
+ [% END %]
+ <noscript>(Leave blank to assign to component's default assignee)</noscript>
+ </td>
+
+[% IF Param("useqacontact") %]
+ [% INCLUDE "bug/field-label.html.tmpl"
+ field = bug_fields.qa_contact editable = 1
+ %]
+ <td>
+ [% INCLUDE global/userselect.html.tmpl
+ id => "qa_contact"
+ name => "qa_contact"
+ value => qa_contact
+ disabled => qa_contact_disabled
+ size => 30
+ emptyok => 1
+ custom_userlist => qa_contacts_list
+ %]
+ <noscript>(Leave blank to assign to default qa contact)</noscript>
+ </td>
+ </tr>
+[% END %]
+
+ <tr>
+ [% INCLUDE "bug/field-label.html.tmpl"
+ field = bug_fields.cc editable = 1
+ %]
+ <td>
+ [% INCLUDE global/userselect.html.tmpl
+ id => "cc"
+ name => "cc"
+ value => cc
+ disabled => cc_disabled
+ size => 30
+ multiple => 5
+ %]
+ </td>
+ <th>
+ <span id="initial_cc_label" class="bz_default_hidden">
+ Default [% field_descs.cc FILTER html %]:
+ </span>
+ </th>
+ <td>
+ <span id="initial_cc"></span>
+ </td>
+ </tr>
+
+ <tr>
+ <td colspan="3">&nbsp;</td>
+ </tr>
+
+[% IF Param("usebugaliases") %]
+ <tr>
+ [% INCLUDE "bug/field-label.html.tmpl"
+ field = bug_fields.alias editable = 1
+ %]
+ <td colspan="2">
+ <input name="alias" size="20" value="[% alias FILTER html %]">
+ </td>
+ </tr>
+[% END %]
+</tbody>
+
+<tbody>
+ <tr>
+ [% INCLUDE "bug/field-label.html.tmpl"
+ field = bug_fields.short_desc editable = 1
+ %]
+ <td colspan="3" class="field_value">
+ <input name="short_desc" size="70" value="[% short_desc FILTER html %]"
+ maxlength="255" spellcheck="true" aria-required="true"
+ class="required text_input" id="short_desc">
+ </td>
+ </tr>
+
+ [% IF feature_enabled('jsonrpc') AND !cloned_bug_id %]
+ <tr id="possible_duplicates_container" class="bz_default_hidden">
+ <th>Possible<br>Duplicates:</th>
+ <td colspan="3">
+ <div id="possible_duplicates"></div>
+ <script type="text/javascript">
+ var dt_columns = [
+ { key: "id", label: "[% field_descs.bug_id FILTER js %]",
+ formatter: YAHOO.bugzilla.dupTable.formatBugLink },
+ { key: "summary",
+ label: "[% field_descs.short_desc FILTER js %]",
+ formatter: "text" },
+ { key: "status",
+ label: "[% field_descs.bug_status FILTER js %]",
+ formatter: YAHOO.bugzilla.dupTable.formatStatus },
+ { key: "update_token", label: '',
+ formatter: YAHOO.bugzilla.dupTable.formatCcButton }
+ ];
+ YAHOO.bugzilla.dupTable.addCcMessage = "Add Me to the CC List";
+ YAHOO.bugzilla.dupTable.init({
+ container: 'possible_duplicates',
+ columns: dt_columns,
+ product_name: '[% product.name FILTER js %]',
+ summary_field: 'short_desc',
+ options: {
+ MSG_LOADING: 'Searching for possible duplicates...',
+ MSG_EMPTY: 'No possible duplicates found.',
+ SUMMARY: 'Possible Duplicates'
+ }
+ });
+ </script>
+ </td>
+ </tr>
+ [% END %]
+
+ <tr>
+ <th>Description:</th>
+ <td colspan="3">
+
+ [% defaultcontent = BLOCK %]
+ [% IF cloned_bug_id %]
++++ This [% terms.bug %] was initially created as a clone of [% terms.Bug %] #[% cloned_bug_id FILTER html %] +++
+
+
+ [% END %]
+ [%-# We are within a BLOCK. The comment will be correctly HTML-escaped
+ # by global/textarea.html.tmpl. So we must not escape the comment here. %]
+ [% comment FILTER none %]
+ [%- END %]
+ [% INCLUDE global/textarea.html.tmpl
+ name = 'comment'
+ id = 'comment'
+ minrows = 10
+ maxrows = 25
+ cols = constants.COMMENT_COLS
+ defaultcontent = defaultcontent
+ %]
+ <br>
+ </td>
+ </tr>
+
+<tbody class="expert_fields">
+ <tr>
+ [% INCLUDE "bug/field-label.html.tmpl"
+ field = bug_fields.bug_file_loc editable = 1
+ %]
+ <td colspan="3" class="field_value">
+ <input name="bug_file_loc" id="bug_file_loc" class="text_input"
+ size="40" value="[% bug_file_loc FILTER html %]">
+ </td>
+ </tr>
+</tbody>
+
+<tbody>
+ [% IF Param("maxattachmentsize") %]
+ <tr>
+ <th>Attachment:</th>
+ <td colspan="3">
+ <div id="attachment_false" class="bz_default_hidden">
+ <input type="button" value="Add an attachment" onClick="handleWantsAttachment(true)">
+ </div>
+
+ <div id="attachment_true">
+ <input type="button" id="btn_no_attachment" value="Don't add an attachment"
+ class="bz_default_hidden" onClick="handleWantsAttachment(false)">
+ <fieldset>
+ <legend>Add an attachment</legend>
+ <table class="attachment_entry">
+ [% PROCESS attachment/createformcontents.html.tmpl
+ flag_types = product.flag_types(is_active=>1).attachment
+ any_flags_requesteeble = 1
+ flag_table_id ="attachment_flags" %]
+ </table>
+
+ [% IF user.is_insider %]
+ <input type="checkbox" id="comment_is_private" name="comment_is_private"
+ [% ' checked="checked"' IF comment_is_private %]
+ onClick="updateCommentTagControl(this, 'comment')">
+ <label for="comment_is_private">
+ Make this attachment and [% terms.bug %] description private (visible only
+ to members of the <strong>[% Param('insidergroup') FILTER html %]</strong> group)
+ </label>
+ [% END %]
+ </fieldset>
+ </div>
+ </td>
+ </tr>
+ [% END %]
+</tbody>
+
+<tbody class="expert_fields">
+ [% IF user.in_group('editbugs', product.id) %]
+ <tr>
+ [% INCLUDE "bug/field-label.html.tmpl"
+ field = bug_fields.dependson editable = 1
+ %]
+ <td>
+ <input name="dependson" accesskey="d" value="[% dependson FILTER html %]" size="30">
+ </td>
+ [% INCLUDE "bug/field-label.html.tmpl"
+ field = bug_fields.blocked editable = 1
+ %]
+ <td>
+ <input name="blocked" accesskey="b" value="[% blocked FILTER html %]" size="30">
+ </td>
+ </tr>
+
+ [% IF use_keywords %]
+ <tr>
+ [% INCLUDE bug/field.html.tmpl
+ bug = default, field = bug_fields.keywords, editable = 1,
+ value = keywords, desc_url = "describekeywords.cgi",
+ value_span = 3
+ %]
+ </tr>
+ [% END %]
+
+ <tr>
+ <th>Status Whiteboard:</th>
+ <td colspan="3" class="field_value">
+ <input id="status_whiteboard" name="status_whiteboard" size="70"
+ value="[% status_whiteboard FILTER html %]" class="text_input">
+ </td>
+ </tr>
+ [% END %]
+
+ [% IF user.is_timetracker %]
+ <tr>
+ [% INCLUDE "bug/field-label.html.tmpl"
+ field = bug_fields.estimated_time editable = 1
+ %]
+ <td>
+ <input name="estimated_time" size="6" maxlength="6" value="[% estimated_time FILTER html %]">
+ </td>
+ [% INCLUDE bug/field.html.tmpl
+ bug = default, field = bug_fields.deadline, value = deadline, editable = 1
+ %]
+ </tr>
+ [% END %]
+</tbody>
+
+<tbody>
+[%# non-tracking flags custom fields %]
+[% FOREACH field = Bugzilla.active_custom_fields %]
+ [% NEXT UNLESS field.enter_bug %]
+ [% NEXT IF cf_hidden_in_product(field.name, product.name, component.name, 1) %]
+ [%# crash-signature gets custom handling %]
+ [% NEXT IF field.name == 'cf_crash_signature' %]
+
+ [% SET value = ${field.name}.defined ? ${field.name} : "" %]
+ <tr [% 'class="expert_fields"' IF !field.is_mandatory %]>
+ [% INCLUDE bug/field.html.tmpl
+ bug = default, field = field, value = value, editable = 1,
+ value_span = 3 %]
+ </tr>
+[% END %]
+</tbody>
+
+[%# crash-signature handling %]
+[% UNLESS cf_hidden_in_product('cf_crash_signature', product.name, component.name, 1) %]
+<tbody class="expert_fields">
+ <tr>
+ <th id="field_label_cf_crash_signature" class="field_label">
+ <label for="cf_crash_signature"> Crash Signature: </label>
+ </th>
+ <td colspan="3">
+ <span id="cf_crash_signature_container">
+ <span id="cf_crash_signature_nonedit_display"><i>None</i></span>
+ (<a id="cf_crash_signature_action" href="#">edit</a>)
+ </span>
+ <span id="cf_crash_signature_input">
+ <textarea id="cf_crash_signature" name="cf_crash_signature" rows="4" cols="60"
+ >[% cf_crash_signature FILTER html %]</textarea>
+ </span>
+ </td>
+ </tr>
+</tbody>
+[% END %]
+
+[% display_bug_flags = 0 %]
+[% FOREACH field = Bugzilla.active_custom_fields %]
+ [% NEXT UNLESS field.enter_bug %]
+ [% NEXT IF cf_hidden_in_product(field.name, product.name, component.name, 2) %]
+ [% display_bug_flags = 1 %]
+ [% LAST %]
+[% END %]
+
+[% display_flags = 0 %]
+[% any_flags_requesteeble = 0 %]
+[% FOREACH flag_type = product.flag_types(is_active=>1).bug %]
+ [% display_flags = 1 %]
+ [% SET any_flags_requesteeble = 1 IF flag_type.is_requestable && flag_type.is_requesteeble %]
+ [% LAST IF display_flags && any_flags_requesteeable %]
+[% END %]
+
+[% IF display_bug_flags || display_flags %]
+ <tbody class="expert_fields">
+ <tr>
+ <th>Flags:</th>
+ <td colspan="3">
+ <div id="bug_flags_false" class="bz_default_hidden">
+ <input type="button" value="Set [% terms.bug FILTER html %] flags" onClick="handleWantsBugFlags(true)">
+ </div>
+
+ <div id="bug_flags_true">
+ <input type="button" id="btn_no_bug_flags" value="Don't set [% terms.bug %] flags"
+ class="bz_default_hidden" onClick="handleWantsBugFlags(false)">
+
+ <fieldset>
+ <legend>Set [% terms.bug %] flags</legend>
+
+ <table cellpadding="0" cellspacing="0">
+ <tr>
+ [% IF display_bug_flags %]
+ <td>
+ <table id="bug_tracking_flags">
+ <tr>
+ <th colspan="2" style="text-align:left">Tracking Flags:</th>
+ </tr>
+ <tr>
+ [% FOREACH field = Bugzilla.active_custom_fields %]
+ [% NEXT UNLESS field.enter_bug %]
+ [% NEXT IF cf_hidden_in_product(field.name, product.name, component.name, 2) %]
+
+ [% SET value = ${field.name}.defined ? ${field.name} : "" %]
+ <tr>
+ [% INCLUDE bug/field.html.tmpl
+ bug = default, field = field, value = value, editable = 1,
+ value_span = 3 %]
+ </tr>
+ [% END %]
+ </tr>
+ </table>
+ </td>
+ [% END %]
+ [% IF display_flags %]
+ <td>
+ [% PROCESS "flag/list.html.tmpl" flag_types = product.flag_types(is_active=>1).bug
+ any_flags_requesteeble = any_flags_requesteeble
+ flag_table_id = "bug_flags"
+ %]
+ </td>
+ [% END %]
+ </tr>
+ </table>
+ </fieldset>
+ </div>
+ </td>
+ </tr>
+ </tbody>
+[% END %]
+
+<tbody>
+ [%# Form controls for entering additional data about the bug being created. %]
+ [% Hook.process("form") %]
+
+ <tr>
+ <th>&nbsp;</th>
+ <td colspan="3">
+ <input type="submit" id="commit" value="Submit [% terms.Bug %]">
+ &nbsp;&nbsp;&nbsp;&nbsp;
+ <input type="submit" name="maketemplate" id="maketemplate"
+ value="Remember values as bookmarkable template"
+ onclick="bz_no_validate_enter_bug=true" class="expert_fields">
+ </td>
+ </tr>
+</tbody>
+ [%# "status whiteboard" and "qa contact" are the longest labels
+ # add them here to avoid shifting the page when toggling advanced fields %]
+ <tr>
+ <th class="hidden_text">Status Whiteboard:</th>
+ <td>&nbsp;</td>
+ <th class="hidden_text">QA Contact:</th>
+ </tr>
+ </table>
+ <input type="hidden" name="form_name" value="enter_bug">
+</form>
+
+[%# Links or content with more information about the bug being created. %]
+[% Hook.process("end") %]
+
+<div id="guided">
+ <a id="guided_img" href="enter_bug.cgi?format=guided&amp;product=[% product.name FILTER uri %]"><img
+ src="extensions/BMO/web/images/guided.png" width="16" height="16" border="0" align="absmiddle"></a>
+ <a id="guided_link" href="enter_bug.cgi?format=guided&amp;product=[% product.name FILTER uri %]"
+ >Switch to the [% terms.Bugzilla %] Helper</a>
+</div>
+
+[% PROCESS global/footer.html.tmpl %]
+
+[%############################################################################%]
+[%# Block for SELECT fields #%]
+[%############################################################################%]
+
+[% BLOCK select %]
+
+ [% INCLUDE "bug/field-label.html.tmpl"
+ field = field editable = 1
+ %]
+ <td>
+ <select name="[% field.name FILTER html %]"
+ id="[% field.name FILTER html %]">
+ [%- FOREACH x = ${field.name} %]
+ [% NEXT IF NOT x.is_active %]
+ <option value="[% x.name FILTER html %]"
+ [% " selected=\"selected\"" IF x.name == default.${field.name} %]>
+ [% display_value(field.name, x.name) FILTER html %]
+ </option>
+ [% END %]
+ </select>
+ </td>
+[% END %]
+
+[% BLOCK build_userlist %]
+ [% user_found = 0 %]
+ [% default_login = default_user.login %]
+ [% RETURN UNLESS default_login %]
+
+ [% FOREACH user = userlist %]
+ [% IF user.login == default_login %]
+ [% user_found = 1 %]
+ [% LAST %]
+ [% END %]
+ [% END %]
+
+ [% userlist.push({login => default_login,
+ identity => default_user.identity,
+ visible => 1})
+ UNLESS user_found %]
+[% END %]
diff --git a/extensions/BMO/template/en/default/bug/create/created-mozreps.html.tmpl b/extensions/BMO/template/en/default/bug/create/created-mozreps.html.tmpl
new file mode 100644
index 000000000..e9a480090
--- /dev/null
+++ b/extensions/BMO/template/en/default/bug/create/created-mozreps.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 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 <glob@mozilla.com>
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+
+[% PROCESS global/header.html.tmpl
+ title = "Mozilla Reps - Application Form"
+
+%]
+
+<h1>Thank you!</h1>
+
+<p>
+Thank you for submitting your Mozilla Reps Application Form. A Mozilla Rep
+mentor will contact you shortly at your bugzilla email address.
+</p>
+
+<p style="font-size: x-small">
+Reference: <a href="show_bug.cgi?id=[% id FILTER uri %]">#[% id FILTER html %]</a>
+</p>
+
+[% PROCESS global/footer.html.tmpl %]
diff --git a/extensions/BMO/template/en/default/bug/create/user-message.html.tmpl b/extensions/BMO/template/en/default/bug/create/user-message.html.tmpl
new file mode 100644
index 000000000..ccf008a38
--- /dev/null
+++ b/extensions/BMO/template/en/default/bug/create/user-message.html.tmpl
@@ -0,0 +1,37 @@
+<!-- 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): Matthew Tuck <matty@chariot.net.au>
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+
+<p>
+ [% UNLESS cloned_bug_id %]
+ Consider using the
+ <a href="enter_bug.cgi?product=[% product.name FILTER html %]&amp;format=guided"
+ ><img src="extensions/BMO/web/images/guided.png" width="16" height="16" align="absmiddle" border="0">
+ [%+ terms.Bugzilla %] Helper</a> instead of this form.
+ [% END +%]
+ Before reporting a [% terms.bug %], make sure you've read our
+ <a href="http://www.mozilla.org/quality/bug-writing-guidelines.html">
+ [% terms.bug %] writing guidelines</a> and double checked that your [% terms.bug %] hasn't already
+ been reported. Consult our list of <a href="https://bugzilla.mozilla.org/duplicates.cgi">
+ most frequently reported [% terms.bugs %]</a> and <a href="https://bugzilla.mozilla.org/query.cgi">
+ search through descriptions</a> of previously reported [% terms.bugs %].
+</p>
diff --git a/extensions/BMO/template/en/default/email/bugmail-header.txt.tmpl b/extensions/BMO/template/en/default/email/bugmail-header.txt.tmpl
new file mode 100644
index 000000000..cc254dee2
--- /dev/null
+++ b/extensions/BMO/template/en/default/email/bugmail-header.txt.tmpl
@@ -0,0 +1,39 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% PROCESS "global/field-descs.none.tmpl" %]
+[% PROCESS "global/reason-descs.none.tmpl" %]
+[% isnew = bug.lastdiffed ? 0 : 1 %]
+[% show_new = isnew
+ && (to_user.settings.bugmail_new_prefix.value == 'on') %]
+
+From: [% Param('mailfrom') %]
+To: [% to_user.email %]
+Subject: [[% terms.Bug %] [%+ bug.id %]] [% 'New: ' IF show_new %][%+ bug.short_desc %]
+Date: [% date %]
+X-Bugzilla-Reason: [% reasonsheader %]
+X-Bugzilla-Type: [% isnew ? 'new' : 'changed' %]
+X-Bugzilla-Watch-Reason: [% reasonswatchheader %]
+[% IF Param('useclassification') %]
+X-Bugzilla-Classification: [% bug.classification %]
+[% END %]
+X-Bugzilla-ID: [% bug.id %]
+X-Bugzilla-Product: [% bug.product %]
+X-Bugzilla-Component: [% bug.component %]
+X-Bugzilla-Keywords: [% bug.keywords %]
+X-Bugzilla-Severity: [% bug.bug_severity %]
+X-Bugzilla-Who: [% changer.login %]
+X-Bugzilla-Status: [% bug.bug_status %]
+X-Bugzilla-Resolution: [% bug.resolution %]
+X-Bugzilla-Priority: [% bug.priority %]
+X-Bugzilla-Assigned-To: [% bug.assigned_to.login %]
+X-Bugzilla-Target-Milestone: [% bug.target_milestone %]
+X-Bugzilla-OS: [% bug.op_sys %]
+X-Bugzilla-Changed-Fields: [% changedfields.join(" ") %]
+X-Bugzilla-Changed-Field-Names: [% changedfieldnames.join(" ") %]
+[%+ threadingmarker %]
diff --git a/extensions/BMO/template/en/default/email/bugmail.html.tmpl b/extensions/BMO/template/en/default/email/bugmail.html.tmpl
new file mode 100644
index 000000000..9fbefa02b
--- /dev/null
+++ b/extensions/BMO/template/en/default/email/bugmail.html.tmpl
@@ -0,0 +1,218 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% PROCESS "global/field-descs.none.tmpl" %]
+[% PROCESS "global/reason-descs.none.tmpl" %]
+
+[% isnew = bug.lastdiffed ? 0 : 1 %]
+<html>
+<head>
+ <base href="[% urlbase FILTER html %]">
+ <style>
+ body {
+ font-family: sans-serif;
+ color: #444444;
+ }
+ hr {
+ border: 1px dashed #969696;
+ }
+ .diffs .head td {
+ border-bottom: 1px solid #969696;
+ }
+ .diffs .c1, .diffs .c2 {
+ border-right: 1px solid #969696;
+ }
+ .new .c1 {
+ border-right: 1px solid #969696;
+ }
+ #noreply, #reason, #tracking {
+ font-size: 90%;
+ color: #666666;
+ }
+ </style>
+</head>
+<body>
+
+ [% IF !to_user.in_group('editbugs') %]
+ <div id="noreply">
+ Do not reply to this email. You can add comments to this [% terms.bug %] at
+ [%# using the bug_link filter here causes a weird template error %]
+ <a href="[% urlbase FILTER html %]show_bug.cgi?id=[% bug.id FILTER none %]">
+ [% urlbase FILTER html %]show_bug.cgi?id=[% bug.id FILTER none %]</a>
+ </div>
+ <br>
+ [% END %]
+
+ [% IF isnew %]
+ [% PROCESS generate_new %]
+ [% ELSE %]
+ [% PROCESS generate_diffs %]
+ [% END %]
+
+ [% IF new_comments.size %]
+ <div id="comments">
+ [% FOREACH comment = new_comments.reverse %]
+ <div>
+ [% IF comment.count %]
+ <b>
+ [% "Comment # ${comment.count}"
+ FILTER bug_link(bug, { comment_num => comment.count, full_url => 1 }) FILTER none %]
+ on [% "$terms.Bug $bug.id" FILTER bug_link(bug, { full_url => 1 }) FILTER none %]
+ from [% INCLUDE global/user.html.tmpl who = comment.author %]
+ at [% comment.creation_ts FILTER time %]
+ </b>
+ [% END %]
+ <pre>[% comment.body_full({ wrap => 1 }) FILTER quoteUrls(bug, comment) %]</pre>
+ </div>
+ [% END %]
+ </div>
+ <br>
+ [% END %]
+
+ [% IF referenced_bugs.size %]
+ <div id="referenced">
+ <hr>
+ <b>Referenced [% terms.Bugs %]:</b>
+ <ul>
+ [% FOREACH ref = referenced_bugs %]
+ <li>
+ [<a href="[% urlbase FILTER html %]show_bug.cgi?id=[% ref.id FILTER none %]">
+ [% terms.Bug %]&nbsp;[% ref.id FILTER none %]</a>] [% ref.short_desc FILTER html %]
+ </li>
+ [% END %]
+ </ul>
+ </div>
+ <br>
+ [% END %]
+
+[% USE Bugzilla %]
+[% tracking_flags = [] %]
+[% FOREACH field = Bugzilla.active_custom_fields(product => bug.product_obj, component => bug.component_obj, type => 2) %]
+ [% NEXT IF cf_flag_disabled(field.name, bug) %]
+ [% NEXT IF bug.${field.name} == "---" %]
+ [% tracking_flags.push(field) %]
+[% END %]
+[% IF tracking_flags.size %]
+ <div id="tracking">
+ <hr>
+ <b>Tracking Flags:</b>
+ <ul>
+ [% FOREACH field = tracking_flags %]
+ <li>[% field.description FILTER html %]:[% bug.${field.name} FILTER html %]</li>
+ [% END %]
+ </ul>
+ </div>
+[% END %]
+
+ <div id="reason">
+ <hr>
+ <b>You are receiving this mail because:</b>
+ <ul>
+ [% FOREACH reason = reasons %]
+ [% IF reason_descs.$reason %]
+ <li>[% reason_descs.$reason FILTER html %]</li>
+ [% END %]
+ [% END %]
+ [% FOREACH reason = reasons_watch %]
+ [% IF watch_reason_descs.$reason %]
+ <li>[% watch_reason_descs.$reason FILTER html %]</li>
+ [% END %]
+ [% END %]
+ </ul>
+ </div>
+
+</body>
+</html>
+
+[% BLOCK generate_new %]
+ <div class="new">
+ <table border="0" cellspacing="0" cellpadding="3">
+ [% FOREACH change = diffs %]
+ [% PROCESS "email/bugmail-common.txt.tmpl" %]
+ <tr>
+ <td class="c1" nowrap><b>[% field_label FILTER html %]</b></td>
+ <td class="c2">
+ [% IF change.field_name == "bug_id" %]
+ [% new_value FILTER bug_link(bug, full_url => 1) FILTER none %]
+ [% ELSE %]
+ [% new_value FILTER html %]
+ [% END %]
+ </td>
+ </tr>
+ [% END %]
+ </table>
+ </div>
+ <br>
+[% END %]
+
+[% BLOCK generate_diffs %]
+ [% SET in_table = 0 %]
+ [% last_changer = 0 %]
+ [% FOREACH change = diffs %]
+ [% PROCESS "email/bugmail-common.txt.tmpl" %]
+ [% IF changer.id != last_changer %]
+ [% last_changer = changer.id %]
+ [% IF in_table == 1 %]
+ </table>
+ </div>
+ <br>
+ [% SET in_table = 0 %]
+ [% END %]
+
+ <b>
+ [% IF change.blocker %]
+ [% "${terms.Bug} ${bug.id}" FILTER bug_link(bug, full_url => 1) FILTER none %]
+ depends on
+ <a href="[% urlbase FILTER html %]show_bug.cgi?id=[% change.blocker.id FILTER none %]">
+ [% terms.Bug %]&nbsp;[% change.blocker.id FILTER none %]</a>,
+ which changed state.<br>
+ [% ELSE %]
+ [% INCLUDE global/user.html.tmpl who = change.who %] changed
+ [%+ "${terms.Bug} ${bug.id}" FILTER bug_link(bug, full_url => 1) FILTER none %]
+ at [% change.bug_when FILTER time %]</b>:<br>
+ [% END %]
+ </b>
+
+ [% IF in_table == 0 %]
+ <br>
+ <div class="diffs">
+ <table border="0" cellspacing="0" cellpadding="5">
+ [% SET in_table = 1 %]
+ [% END %]
+ <tr class="head">
+ <td class="c1"><b>What</b></td>
+ <td class="c2"><b>Removed</b></td>
+ <td class="c3"><b>Added</b></td>
+ </tr>
+ [% END %]
+
+ <tr>
+ <td class="c1" nowrap>[% field_label FILTER html %]</td>
+ <td class="c2">
+ [% IF old_value %]
+ [% old_value FILTER html %]
+ [% ELSE %]
+ &nbsp;
+ [% END %]
+ </td>
+ <td>
+ [% IF new_value %]
+ [% new_value FILTER html %]
+ [% ELSE %]
+ &nbsp;
+ [% END %]
+ </td>
+ </tr>
+ [% END %]
+ [% IF in_table %]
+ </table>
+ </div>
+ <br>
+ [% END %]
+[% END %]
+
diff --git a/extensions/BMO/template/en/default/email/bugmail.txt.tmpl b/extensions/BMO/template/en/default/email/bugmail.txt.tmpl
new file mode 100644
index 000000000..c82827c61
--- /dev/null
+++ b/extensions/BMO/template/en/default/email/bugmail.txt.tmpl
@@ -0,0 +1,90 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% PROCESS "global/field-descs.none.tmpl" %]
+[% PROCESS "global/reason-descs.none.tmpl" %]
+
+[% isnew = bug.lastdiffed ? 0 : 1 %]
+
+[% IF !to_user.in_group('editbugs') %]
+Do not reply to this email. You can add comments to this [% terms.bug %] at
+[% END %]
+[%+ PROCESS generate_diffs -%]
+
+[% FOREACH comment = new_comments %]
+
+[%- IF comment.count %]
+--- Comment #[% comment.count %] from [% comment.author.identity %] [%+ comment.creation_ts FILTER time(undef, to_user.timezone) %] ---
+[% END %]
+[%+ comment.body_full({ is_bugmail => 1, wrap => 1 }) %]
+[% END %]
+[% IF referenced_bugs.size %]
+
+Referenced [% terms.Bugs %]:
+
+[% FOREACH ref = referenced_bugs %]
+[%+ urlbase %]show_bug.cgi?id=[% ref.id %]
+[%+ "[" _ terms.Bug _ " " _ ref.id _ "] " _ ref.short_desc FILTER wrap_comment(76) %]
+[% END %]
+[% END %]
+
+-- [%# Protect the trailing space of the signature marker %]
+Configure [% terms.bug %]mail: [% urlbase %]userprefs.cgi?tab=email
+
+[% USE Bugzilla %]
+[% tracking_flags = [] %]
+[% FOREACH field = Bugzilla.active_custom_fields(product => bug.product_obj, component => bug.component_obj, type => 2) %]
+ [% NEXT IF cf_flag_disabled(field.name, bug) %]
+ [% NEXT IF bug.${field.name} == "---" %]
+ [% tracking_flags.push(field) %]
+[% END %]
+[% IF tracking_flags.size %]
+------- Tracking Flags: -------
+[% FOREACH field = tracking_flags %]
+[%+ field.description %]:[% bug.${field.name} %]
+[% END %]
+[% END %]
+
+------- You are receiving this mail because: -------
+[% SET reason_lines = [] %]
+[% FOREACH reason = reasons %]
+ [% reason_lines.push(reason_descs.$reason) IF reason_descs.$reason %]
+[% END %]
+[% FOREACH reason = reasons_watch %]
+ [% reason_lines.push(watch_reason_descs.$reason)
+ IF watch_reason_descs.$reason %]
+[% END %]
+[%+ reason_lines.join("\n") %]
+
+[% BLOCK generate_diffs %]
+ [% urlbase %]show_bug.cgi?id=[% bug.id %]
+
+[%+ last_changer = 0 %]
+ [% FOREACH change = diffs %]
+ [% IF !isnew && changer.id != last_changer %]
+ [% last_changer = changer.id %]
+ [% IF change.blocker %]
+ [% terms.Bug %] [%+ bug.id %] depends on [% terms.bug %] [%+ change.blocker.id %], which changed state.
+
+[%+ terms.Bug %] [%+ change.blocker.id %] Summary: [% change.blocker.short_desc %]
+[%+ urlbase %]show_bug.cgi?id=[% change.blocker.id %]
+ [% ELSE %]
+ [%~ changer.identity %] changed:
+ [% END %]
+
+ What |Removed |Added
+----------------------------------------------------------------------------
+[%+ END %][%# End of IF. This indentation is intentional! ~%]
+ [% PROCESS "email/bugmail-common.txt.tmpl"%]
+ [%~ IF isnew %]
+ [% format_columns(2, field_label _ ":", new_value) -%]
+ [% ELSE %]
+ [% format_columns(3, field_label, old_value, new_value) -%]
+ [% END %]
+ [% 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..b9cd02cfc
--- /dev/null
+++ b/extensions/BMO/template/en/default/global/choose-product.html.tmpl
@@ -0,0 +1,210 @@
+[%# 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 <gerv@gerv.net>
+ #%]
+
+[%# 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 %]
+
+[% style_urls = [ "extensions/BMO/web/styles/choose_product.css" ] %]
+
+[% IF target == "enter_bug.cgi" %]
+ [% title = "Enter $terms.Bug" %]
+ [% h2 = "Which product is affected by the problem you would like to report?" %]
+[% ELSIF target == "describecomponents.cgi" %]
+ [% title = "Browse" %]
+ [% h2 = "Which product would you like to have described?" %]
+[% END %]
+
+[% yui = [ 'autocomplete' ] %]
+[% javascript_urls = [ "js/field.js", "js/create_bug.js",
+ "extensions/BMO/web/js/prod_comp_search.js" ] %]
+[% onload = "YAHOO.util.Dom.get('prod_comp_search').focus();" %]
+[% style_urls.push("extensions/BMO/web/styles/prod_comp_search.css") %]
+
+[% DEFAULT title = "Choose a Product" %]
+[% PROCESS global/header.html.tmpl %]
+
+<div id="choose_product">
+
+<hr>
+<p>
+ Looking for technical support or help getting your site to work with Mozilla?
+ <a href="http://www.mozilla.org/support/">Visit the mozilla.org support page</a>
+ before filing [% terms.bugs %].
+</p>
+<hr>
+
+<h2>[% h2 FILTER html %]</h2>
+
+[% PROCESS "global/prod-comp-search.html.tmpl" %]
+
+<h2>or choose from the following selections</h2>
+
+[% USE Bugzilla %]
+[% cgi = Bugzilla.cgi %]
+[% SET classification = cgi.param('classification') %]
+[% IF NOT ((cgi.param("full")) OR (user.settings.product_chooser.value == 'full_product_chooser')) %]
+
+<table align="center" border="0" width="600" cellpadding="5" cellspacing="0">
+[% 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="Firefox for Android"
+ icon="firefox.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"
+%]
+<tr>
+ <td><a href="[% target FILTER uri %]?full=1
+ [%- IF cloned_bug_id %]&amp;cloned_bug_id=[% cloned_bug_id FILTER uri %][% END -%]
+ [%- IF classification %]&amp;classification=[% classification FILTER uri %][% END -%]
+ [%- IF format %]&amp;format=[% format FILTER uri %][% END %]">
+ <img src="extensions/BMO/web/producticons/other.png" height="64" width="64" border="0"></a></td>
+ <td><h2 align="left" style="margin-bottom: 0px;"><a href="[% target FILTER uri %]?full=1
+ [%- IF cloned_bug_id %]&amp;cloned_bug_id=[% cloned_bug_id FILTER uri %][% END -%]
+ [%- IF classification %]&amp;classification=[% classification FILTER uri %][% END -%]
+ [%- IF format %]&amp;format=[% format FILTER uri %][% END %]">
+ Other Products</a></h2>
+ <p style="margin-top: 0px;">Other Mozilla products which aren't listed here</p>
+ </td>
+</tr>
+</table>
+[% ELSE %]
+
+<table>
+
+[% FOREACH c = classifications %]
+ [% IF c.object %]
+ <tr>
+ <td align="right"><h2>[% c.object.name FILTER html %]</h2></td>
+ <td><strong>[%+ c.object.description FILTER html_light %]</strong></td>
+ </tr>
+ [% END %]
+
+ [% FOREACH p = c.products %]
+ [% class = "" %]
+ [% has_entry_groups = 0 %]
+ [% FOREACH gid = p.group_controls.keys %]
+ [% IF p.group_controls.$gid.entry %]
+ [% has_entry_groups = 1 %]
+ [% class = class _ " group_$gid" %]
+ [% END %]
+ [% END %]
+ <tr class="[% "group_secure" IF has_entry_groups +%] [% class FILTER html %]"
+ [%- IF has_entry_groups %] title="This product requires one or more
+ group memberships in order to enter [% terms.bugs %] in it. You have them, but be
+ aware not everyone else does."[% END %]>
+ <th align="right" valign="top">
+ [% IF p.name == "Mozilla PR" AND target == "enter_bug.cgi" AND NOT format AND NOT cgi.param("debug") %]
+ <a href="[% target FILTER uri %]?product=[% p.name FILTER uri -%]
+ [%- IF cloned_bug_id %]&amp;cloned_bug_id=[% cloned_bug_id FILTER uri %][% END %]&amp;format=mozpr">
+ [% p.name FILTER html FILTER no_break %]</a>:&nbsp;
+ [% ELSE %]
+ <a href="[% target FILTER uri %]?product=[% p.name FILTER uri -%]
+ [%- IF cloned_bug_id %]&amp;cloned_bug_id=[% cloned_bug_id FILTER uri %][% END -%]
+ [%- IF format %]&amp;format=[% format FILTER uri %][% END %]">
+ [% p.name FILTER html FILTER no_break %]</a>:&nbsp;
+ [% END %]
+ </th>
+ <td valign="top">[% p.description FILTER html_light %]</td>
+ </tr>
+ [% END %]
+[% END %]
+
+</table>
+
+<br>
+[% IF target == "enter_bug.cgi" AND user.settings.product_chooser.value != 'full_product_chooser' %]
+<p>You can choose to get this screen by default when you click "New [% terms.Bug %]"
+by changing your <a href="userprefs.cgi?tab=settings">preferences</a>.</p>
+[% END %]
+[% END %]
+<br>
+
+</div>
+
+[% PROCESS global/footer.html.tmpl %]
+
+[%###########################################################################%]
+[%# Block for "easy" product sections #%]
+[%###########################################################################%]
+
+[% BLOCK easyproduct %]
+ [% FOREACH c = classifications %]
+ [% FOREACH p = c.products %]
+ [% IF p.name == name %]
+ <tr>
+ <td><a href="[% target FILTER uri %]?product=[% p.name FILTER uri %]
+ [%- IF cloned_bug_id %]&amp;cloned_bug_id=[% cloned_bug_id FILTER uri %][% END -%]
+ [%- IF format %]&amp;format=[% format FILTER uri %][% END %]">
+ <img src="extensions/BMO/web/producticons/[% icon FILTER uri %]" height="64" width="64" border="0"></a></td>
+ <td><h2 align="left" style="margin-bottom: 0px"><a href="[% target FILTER uri %]?product=[% p.name FILTER uri %]
+ [%- IF cloned_bug_id %]&amp;cloned_bug_id=[% cloned_bug_id FILTER uri %][% END -%]
+ [%- IF format %]&amp;format=[% format FILTER uri %][% END %]">
+ [% p.name FILTER html FILTER no_break %]</a>:</h2>
+ [% IF p.description %]
+ <p style="margin-top: 0px;">[% p.description FILTER html_light %]</p>
+ [% END %]
+ </td>
+ </tr>
+ [% LAST %]
+ [% END %]
+ [% END %]
+ [% END %]
+[% END %]
diff --git a/extensions/BMO/template/en/default/global/prod-comp-search.html.tmpl b/extensions/BMO/template/en/default/global/prod-comp-search.html.tmpl
new file mode 100644
index 000000000..2f1d67bec
--- /dev/null
+++ b/extensions/BMO/template/en/default/global/prod-comp-search.html.tmpl
@@ -0,0 +1,43 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+<div id="prod_comp_search_main">
+ <div id="prod_comp_search_autocomplete">
+ <div id="prod_comp_search_label">
+ Type to find product and component by name or description:
+ <img id="prod_comp_throbber" src="extensions/BMO/web/images/throbber.gif"
+ class="hidden" width="16" height="11">
+ </div>
+ <input id="prod_comp_search" type="text" size="60">
+ <div id="prod_comp_search_autocomplete_container"></div>
+ </div>
+</div>
+<script type="text/javascript">
+ if(typeof(YAHOO.bugzilla.prodCompSearch) !== 'undefined'
+ && YAHOO.bugzilla.prodCompSearch != null)
+ {
+ YAHOO.bugzilla.prodCompSearch.init(
+ "prod_comp_search",
+ "prod_comp_search_autocomplete_container",
+ "[% format FILTER js %]",
+ "[% cloned_bug_id FILTER js %]");
+ [% IF target == "describecomponents.cgi" %]
+ YAHOO.bugzilla.prodCompSearch.autoComplete.itemSelectEvent.subscribe(function (e, args) {
+ var oData = args[2];
+ var url = "describecomponents.cgi?product=" + encodeURIComponent(oData[0]) +
+ "&component=" + encodeURIComponent(oData[1]) +
+ "#" + encodeURIComponent(oData[1]);
+ var format = YAHOO.bugzilla.prodCompSearch.format;
+ if (format) {
+ url += "&format=" + encodeURIComponent(format);
+ }
+ window.location.href = url;
+ });
+ [% END %]
+ }
+</script>
diff --git a/extensions/BMO/template/en/default/hook/attachment/createformcontents-mimetypes.html.tmpl b/extensions/BMO/template/en/default/hook/attachment/createformcontents-mimetypes.html.tmpl
new file mode 100644
index 000000000..3dc727b87
--- /dev/null
+++ b/extensions/BMO/template/en/default/hook/attachment/createformcontents-mimetypes.html.tmpl
@@ -0,0 +1,2 @@
+[% mimetypes.push({type => "image/svg+xml", desc => "SVG image"}) %]
+[% mimetypes.push({type => "application/vnd.mozilla.xul+xml", desc => "XUL"}) %] \ No newline at end of file
diff --git a/extensions/BMO/template/en/default/hook/attachment/createformcontents-patch_notes.html.tmpl b/extensions/BMO/template/en/default/hook/attachment/createformcontents-patch_notes.html.tmpl
new file mode 100644
index 000000000..ea80fdc5e
--- /dev/null
+++ b/extensions/BMO/template/en/default/hook/attachment/createformcontents-patch_notes.html.tmpl
@@ -0,0 +1 @@
+<em>You can <a href="http://developer.mozilla.org/en/docs/Getting_your_patch_in_the_tree">read about the patch submission and approval process</a>.</em><br>
diff --git a/extensions/BMO/template/en/default/hook/bug/comments-a_comment-end.html.tmpl b/extensions/BMO/template/en/default/hook/bug/comments-a_comment-end.html.tmpl
new file mode 100644
index 000000000..caf7acca7
--- /dev/null
+++ b/extensions/BMO/template/en/default/hook/bug/comments-a_comment-end.html.tmpl
@@ -0,0 +1,19 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF user.id && comment.author.login_name == 'tbplbot@gmail.com' %]
+ [% has_tbpl_comment = 1 %]
+ <script>
+ var id = [% count FILTER none %];
+ tbpl_comment_ids.push(id);
+ collapse_comment(
+ document.getElementById('comment_link_' + id),
+ document.getElementById('comment_text_' + id)
+ );
+ </script>
+[% END %]
diff --git a/extensions/BMO/template/en/default/hook/bug/comments-aftercomments.html.tmpl b/extensions/BMO/template/en/default/hook/bug/comments-aftercomments.html.tmpl
new file mode 100644
index 000000000..d8dc5bba0
--- /dev/null
+++ b/extensions/BMO/template/en/default/hook/bug/comments-aftercomments.html.tmpl
@@ -0,0 +1,42 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF has_tbpl_comment %]
+ [% expand_caption = 'Expand TinderboxPushlog Comments' %]
+ [% collapse_caption = 'Collapse TinderboxPushlog Comments' %]
+ <script>
+ YAHOO.util.Event.onDOMReady(function () {
+ var ul = document.getElementsByClassName('bz_collapse_expand_comments');
+ if (ul.length == 0)
+ return;
+ var li = document.createElement('li');
+ var a = document.createElement('a');
+ Dom.setAttribute(a, 'href', 'javascript:void(0)');
+ Dom.setAttribute(a, 'id', 'toggle_tbplbot_comments');
+ a.innerHTML = '[% expand_caption FILTER js %]';
+ YAHOO.util.Event.on(a, 'click', function() {
+ var do_expand = a.innerHTML == '[% expand_caption FILTER js %]';
+ for (var i = 0, n = tbpl_comment_ids.length; i < n; i++) {
+ var id = tbpl_comment_ids[i];
+ var link = document.getElementById('comment_link_' + id);
+ var text = document.getElementById('comment_text_' + id);
+ if (do_expand) {
+ expand_comment(link, text);
+ } else {
+ collapse_comment(link, text);
+ }
+ }
+ a.innerHTML = do_expand
+ ? '[% collapse_caption FILTER js %]'
+ : '[% expand_caption FILTER js %]';
+ });
+ li.appendChild(a);
+ ul[0].appendChild(li);
+ });
+ </script>
+[% END %]
diff --git a/extensions/BMO/template/en/default/hook/bug/comments-comment_banner.html.tmpl b/extensions/BMO/template/en/default/hook/bug/comments-comment_banner.html.tmpl
new file mode 100644
index 000000000..2ae367456
--- /dev/null
+++ b/extensions/BMO/template/en/default/hook/bug/comments-comment_banner.html.tmpl
@@ -0,0 +1,13 @@
+[%# *** Disclaimer for Legal bugs *** %]
+[% IF bug.product == "Legal" %]
+ <div id="legal_disclaimer">
+ The material and information contained herein is Confidential and
+ subject to Attorney-Client Privilege and Work Product Doctrine.
+ </div>
+[% END %]
+
+[%# Needed for collapsing TinderboxPushlog comments %]
+[% has_tbpl_comment = 0 %]
+<script>
+ var tbpl_comment_ids = new Array();
+</script>
diff --git a/extensions/BMO/template/en/default/hook/bug/comments-end.html.tmpl b/extensions/BMO/template/en/default/hook/bug/comments-end.html.tmpl
new file mode 100644
index 000000000..3bf18a515
--- /dev/null
+++ b/extensions/BMO/template/en/default/hook/bug/comments-end.html.tmpl
@@ -0,0 +1,20 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF user.id && comment.author.login_name == 'tbplbot@gmail.com' %]
+ [% has_tbpl_comment = 1 %]
+ <script>
+ var id = [% count FILTER none %];
+ tbpl_comment_ids.push(id);
+ YAHOO.util.Dom.addClass(comment, 'collapsed');
+ collapse_comment(
+ document.getElementById('comment_link_' + id),
+ document.getElementById('comment_text_' + id)
+ );
+ </script>
+[% END %]
diff --git a/extensions/BMO/template/en/default/hook/bug/create/create-form.html.tmpl b/extensions/BMO/template/en/default/hook/bug/create/create-form.html.tmpl
new file mode 100644
index 000000000..0a3b75262
--- /dev/null
+++ b/extensions/BMO/template/en/default/hook/bug/create/create-form.html.tmpl
@@ -0,0 +1,32 @@
+ <tr>
+ <th>Security:</th>
+ <td colspan="3">
+ [% sec_group = sec_groups.${product.name} || sec_groups._default %]
+ [% PROCESS group_checkbox
+ name = sec_group
+ desc = "Many users could be harmed by this security problem: " _
+ "it should be kept hidden from the public until it is resolved."
+ %]
+ [% IF user.in_group('partner-confidential-visible') %]
+ [% PROCESS group_checkbox
+ name = 'partner-confidential'
+ desc = "Restrict the visiblity of this " _ terms.bug _ " to " _
+ "the assignee, QA contact, and CC list only."
+ %]
+ [% END %]
+ <br>
+ </td>
+ </tr>
+
+[% BLOCK group_checkbox %]
+ <input type="checkbox" name="groups"
+ value="[% name FILTER none %]" id="group_[% name FILTER html %]"
+ [% FOREACH g = group %]
+ [% IF g.name == name %]
+ [% ' checked="checked"' IF g.checked %]
+ [% LAST %]
+ [% END %]
+ [% END %]
+ >
+ <label for="group_[% name FILTER html %]">[% desc FILTER html %]</label><br>
+[% END %]
diff --git a/extensions/BMO/template/en/default/hook/bug/create/create-guided-form.html.tmpl b/extensions/BMO/template/en/default/hook/bug/create/create-guided-form.html.tmpl
new file mode 100644
index 000000000..a0fff4175
--- /dev/null
+++ b/extensions/BMO/template/en/default/hook/bug/create/create-guided-form.html.tmpl
@@ -0,0 +1,22 @@
+ <tr bgcolor="[% tablecolour FILTER html %]">
+ <td valign="middle" align="right">
+ <b>Security</b>
+ </td>
+ <td valign="top">
+ <p>
+ [% sec_group = sec_groups.${product.name} || sec_groups._default %]
+
+ <input type="checkbox" name="groups"
+ id="groups" value="[% sec_group FILTER none %]"
+ [% FOREACH g = group %]
+ [% IF g.name == sec_group %]
+ [% " checked=\"checked\"" IF g.checked %]
+ [% END %]
+ [% END %]
+ >
+ <label for="groups">
+ Many users could be harmed by this security problem: it should be kept
+ hidden from the public until it is resolved.</label>
+ </p>
+ </td>
+ </tr>
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..de97706b0
--- /dev/null
+++ b/extensions/BMO/template/en/default/hook/bug/edit-after_custom_fields.html.tmpl
@@ -0,0 +1,114 @@
+[%# ***** 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 <glob@mozilla.com>
+ #
+ # ***** END LICENSE BLOCK *****
+ #%]
+
+[% tracking_flags = [] %]
+[% project_flags = [] %]
+[% FOREACH field = Bugzilla.active_custom_fields(product=>bug.product_obj,component=>bug.component_obj,type=>2) %]
+ [% NEXT IF NOT user.id AND bug.${field.name} == "---" %]
+ [% NEXT IF cf_flag_disabled(field.name, bug) %]
+ [% IF cf_is_project_flag(field.name) %]
+ [% project_flags.push(field) %]
+ [% ELSE %]
+ [% tracking_flags.push(field) %]
+ [% END %]
+[% END %]
+
+[% IF project_flags.size %]
+ <tr>
+ <th class="field_label">
+ <label>Project Flags:</label>
+ </td>
+ <td>
+ <table id="project-flags">
+ [% FOREACH field = project_flags %]
+ [% NEXT IF NOT user.id AND field.value == "---" %]
+ <tr id="row_[% field.name FILTER js %]">
+ <td>&nbsp;</td>
+ <td>
+ <label for="[% field.name FILTER html %]">
+ [% field_descs.${field.name} FILTER html %]:
+ </label>
+ </td>
+ <td>
+ [% PROCESS bug/field.html.tmpl value = bug.${field.name}
+ editable = user.id
+ no_tds = 1 %]
+ [% IF user.id %]
+ <span id="ro_[% field.name FILTER html %]" class="bz_hidden">
+ [% bug.${field.name} FILTER html %]
+ </span>
+ [% END %]
+ </td>
+ </tr>
+ [% END %]
+ </table>
+ </td>
+ </tr>
+[% END %]
+
+[% IF tracking_flags.size %]
+ <tr>
+ <th class="field_label">
+ <label>Tracking Flags:</label>
+ </td>
+ <td>
+ [% IF user.id %]
+ <span id="edit_tracking_fields_action">
+ (<a onclick="bmo_show_tracking_flags()" href="javascript:void(0)">edit</a>)
+ </span>
+ [% END %]
+ <table id="custom-flags">
+ [% FOREACH field = tracking_flags %]
+ [% NEXT IF NOT user.id AND field.value == "---" %]
+ <tr id="row_[% field.name FILTER js %]">
+ <td>&nbsp;</td>
+ <td>
+ <label for="[% field.name FILTER html %]">
+ [% field_descs.${field.name} FILTER html %]:
+ </label>
+ </td>
+ <td>
+ [% PROCESS bug/field.html.tmpl value = bug.${field.name}
+ editable = user.id
+ no_tds = 1 %]
+ [% IF user.id %]
+ <span id="ro_[% field.name FILTER html %]" class="bz_hidden">
+ [% bug.${field.name} FILTER html %]
+ </span>
+ [% END %]
+ </td>
+ </tr>
+ [% END %]
+ </table>
+ </td>
+ </tr>
+ <script type="text/javascript">
+ var bmo_custom_flags = new Array([% tracking_flags.size FILTER none %]);
+ [% FOREACH field = tracking_flags %]
+ bmo_custom_flags['[% field.name FILTER js %]'] = '[% bug.${field.name} FILTER js %]';
+ [% END %]
+ bmo_hide_tracking_flags();
+ </script>
+[% 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..70132d7e0
--- /dev/null
+++ b/extensions/BMO/template/en/default/hook/bug/field-help-end.none.tmpl
@@ -0,0 +1,114 @@
+[%# 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 <dkl@mozilla.com>
+ #%]
+
+[% USE Bugzilla %]
+[% IF Bugzilla.request_cache.bmo_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 %]
+
+ [%
+ vars.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."
+
+ vars.help_html.bug_severity =
+ "This field describes the impact of ${terms.abug}.
+ <table>
+ <tr>
+ <th>$filtered_severity_blocker</th>
+ <td>Blocks development and/or testing work</td>
+ </tr>
+ <tr>
+ <th>$filtered_severity_critical</th>
+ <td>crashes, loss of data, severe memory leak</td>
+ </tr>
+ <tr>
+ <th>$filtered_severity_major</th>
+ <td>major loss of function</td>
+ </tr>
+ <tr>
+ <th>$filtered_severity_normal</th>
+ <td>regular issue, some loss of functionality under specific circumstances</td>
+ </tr>
+ <tr>
+ <th>$filtered_severity_minor</th>
+ <td>minor loss of function, or other problem where easy
+ workaround is present</td>
+ </tr>
+ <tr>
+ <th>$filtered_severity_trivial</th>
+ <td>cosmetic problem like misspelled words or misaligned
+ text</td>
+ </tr>
+ <tr>
+ <th>$filtered_severity_enhancement</th>
+ <td>Request for enhancement</td>
+ </table>"
+
+ vars.help_html.rep_platform =
+ "This is the hardware platform against which the $terms.bug was reported.
+ Legal platforms include:
+ <ul>
+ <li>$filtered_platform_all (happens on all platforms; cross-platform ${terms.bug})</li>
+ <li>$filtered_platform_x86_64</li>
+ <li>$filtered_platform_arm</li>
+ </ul>
+ <b>Note:</b> When searching, selecting the option
+ <em>$filtered_platform_all</em> 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 <em>$filtered_platform_all</em>.",
+
+ vars.help_html.op_sys =
+ "This is the operating system against which the $terms.bug was
+ reported. Legal operating systems include:
+ <ul>
+ <li>$filtered_opsys_all (happens on all operating systems; cross-platform ${terms.bug})</li>
+ <li>$filtered_opsys_windows</li>
+ <li>$filtered_opsys_mac</li>
+ <li>$filtered_opsys_linux</li>
+ </ul>
+ Sometimes the operating system implies the platform, but not
+ always. For example, Linux can run on x86_64, ARM, and others.",
+
+ vars.help_html.assigned_to =
+ "This is the person in charge of resolving the ${terms.bug}. Every time
+ this field changes, the status changes to
+ <b>$filtered_status_new</b> to make it
+ easy to see which new $terms.bugs have appeared on a person's list.</p>",
+ %]
+[% END %]
diff --git a/extensions/BMO/template/en/default/hook/bug/process/header-title.html.tmpl b/extensions/BMO/template/en/default/hook/bug/process/header-title.html.tmpl
new file mode 100644
index 000000000..a99b4f9f6
--- /dev/null
+++ b/extensions/BMO/template/en/default/hook/bug/process/header-title.html.tmpl
@@ -0,0 +1,9 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% title = title.replace('^' _ terms.Bug _ ' ', '') %]
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..e903b811d
--- /dev/null
+++ b/extensions/BMO/template/en/default/hook/bug/show-header-end.html.tmpl
@@ -0,0 +1,14 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% style_urls.push('extensions/BMO/web/styles/edit_bug.css') %]
+[% javascript_urls.push('extensions/BMO/web/js/edit_bug.js') %]
+[% title = "$bug.bug_id &ndash; $filtered_desc" %]
+[% javascript = javascript _
+ "document.title = document.title.replace(/^" _ terms.Bug _ " /, '');"
+%]
diff --git a/extensions/BMO/template/en/default/hook/global/field-descs-end.none.tmpl b/extensions/BMO/template/en/default/hook/global/field-descs-end.none.tmpl
new file mode 100644
index 000000000..2c8bb7494
--- /dev/null
+++ b/extensions/BMO/template/en/default/hook/global/field-descs-end.none.tmpl
@@ -0,0 +1,11 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF in_template_var %]
+ [% vars.field_descs.cc_count = "CC Count" %]
+[% END %]
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 @@
+<a href="https://www.mozilla.org/about/policies/privacy-policy.html">Privacy Policy</a>
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..e94b60bb4
--- /dev/null
+++ b/extensions/BMO/template/en/default/hook/global/header-additional_header.html.tmpl
@@ -0,0 +1,73 @@
+[%#
+ # 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 <reed@reedloden.com>
+ #%]
+
+<link rel="shortcut icon" href="extensions/BMO/web/images/favicon.ico">
+[% IF bug %]
+<link id="shorturl" rev="canonical" href="https://bugzil.la/[% bug.bug_id FILTER uri %]">
+[% END %]
+
+<style type="text/css">
+body {
+ background: url("extensions/BMO/web/images/background.png") repeat-x;
+}
+</style>
+
+[%# *** 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) %]
+ <link rel="Up" href="buglist.cgi?regetlastlist=
+ [%- my_search.id FILTER uri %]">
+ <link rel="First" href="show_bug.cgi?id=
+ [%- last_bug_list.first FILTER uri %]&amp;list_id=
+ [%- my_search.id FILTER uri %]">
+ <link rel="Last" href="show_bug.cgi?id=
+ [%- last_bug_list.last FILTER uri %]&amp;list_id=
+ [%- my_search.id FILTER uri %]">
+ [% IF this_bug_idx > 0 %]
+ [% prev_bug = this_bug_idx - 1 %]
+ <link rel="Prev" href="show_bug.cgi?id=
+ [%- last_bug_list.$prev_bug FILTER uri %]&amp;list_id=
+ [%- my_search.id FILTER uri %]">
+ [% END %]
+ [% IF this_bug_idx + 1 < last_bug_list.size %]
+ [% next_bug = this_bug_idx + 1 %]
+ <link rel="Next" href="show_bug.cgi?id=
+ [%- last_bug_list.$next_bug FILTER uri %]&amp;list_id=
+ [%- my_search.id FILTER uri %]">
+ [% END %]
+ [% END %]
+[% END %]
+
+[% IF urlbase == 'https://bugzilla.mozilla.org/' %]
+ <script type="text/javascript">
+ var _gaq = _gaq || [];
+ _gaq.push(['_setAccount', 'UA-36116321-3']);
+ _gaq.push(['_trackPageview']);
+ (function() {
+ var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
+ ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
+ var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
+ })();
+ </script>
+[% 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..e265d0bb6
--- /dev/null
+++ b/extensions/BMO/template/en/default/hook/global/header-start.html.tmpl
@@ -0,0 +1,40 @@
+[% IF !javascript_urls %]
+ [% javascript_urls = [] %]
+[% END %]
+
+[% IF template.name == 'list/list.html.tmpl' %]
+ [% javascript_urls.push('extensions/BMO/web/js/sorttable.js') %]
+[% END %]
+
+[% IF !bodyclasses %]
+ [% bodyclasses = [] %]
+[% END %]
+
+[%# Change the background/border for bugs/attachments in certain bug groups %]
+[% IF template.name == 'attachment/edit.html.tmpl'
+ || template.name == 'attachment/create.html.tmpl'
+ || template.name == 'attachment/diff-header.html.tmpl' %]
+ [% style_urls.push("skins/custom/bug_groups.css") %]
+
+ [% IF template.name == 'attachment/edit.html.tmpl'
+ || template.name == 'attachment/diff-header.html.tmpl' %]
+ [% IF bodyclasses == 'no_javascript' %]
+ [% bodyclasses = ['no_javascript'] %]
+ [% END %]
+ [% FOREACH group = attachment.bug.groups_in %]
+ [% bodyclasses.push("bz_group_$group.name") %]
+ [% END %]
+ [% END %]
+
+ [% IF template.name == 'attachment/create.html.tmpl' %]
+ [% FOREACH group = bug.groups_in %]
+ [% bodyclasses.push("bz_group_$group.name") %]
+ [% END %]
+ [% END %]
+[% END %]
+
+[% IF user.in_group('canconfirm') %]
+ [% yui.push('container', 'menu') %]
+ [% style_urls.push('js/yui/assets/skins/sam/menu.css') %]
+ [% javascript_urls.push('extensions/BMO/web/js/edituser_menu.js') %]
+[% END %]
diff --git a/extensions/BMO/template/en/default/hook/global/messages-messages.html.tmpl b/extensions/BMO/template/en/default/hook/global/messages-messages.html.tmpl
new file mode 100644
index 000000000..0c90b97b9
--- /dev/null
+++ b/extensions/BMO/template/en/default/hook/global/messages-messages.html.tmpl
@@ -0,0 +1,5 @@
+[% IF message_tag == "employee_incident_creation_failed" %]
+ The [% terms.bug %] was created successfully, but the dependent
+ Employee Incident [% terms.bug %] creation failed. The error has
+ been logged and no further action is required at this time.
+[% END %]
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-auth_failure_object.html.tmpl b/extensions/BMO/template/en/default/hook/global/user-error-auth_failure_object.html.tmpl
new file mode 100644
index 000000000..0a674aa30
--- /dev/null
+++ b/extensions/BMO/template/en/default/hook/global/user-error-auth_failure_object.html.tmpl
@@ -0,0 +1,5 @@
+[% IF object == 'group_admins' %]
+ the group administrators report
+[% ELSIF object == 'email_queue' %]
+ the email queue status report
+[% END %]
diff --git a/extensions/BMO/template/en/default/hook/global/user-error-error_message.html.tmpl b/extensions/BMO/template/en/default/hook/global/user-error-error_message.html.tmpl
new file mode 100644
index 000000000..de1848495
--- /dev/null
+++ b/extensions/BMO/template/en/default/hook/global/user-error-error_message.html.tmpl
@@ -0,0 +1,15 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF error == 'illegal_change' || error == 'illegal_change_deps' %]
+ <p>
+ If you are attempting to confirm an unconfirmed [% terms.bug %] or edit the
+ fields of a [% terms.bug %], <a href="page.cgi?id=get_permissions.html">find
+ out how to get the necessary permissions</a>.
+ </p>
+[% END %]
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..eff0e35cc
--- /dev/null
+++ b/extensions/BMO/template/en/default/hook/global/user-error-errors.html.tmpl
@@ -0,0 +1,36 @@
+[%# 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 <bjones@mozilla.com>
+ #%]
+
+[% IF error == "user_activity_missing_username" %]
+ [% title = "Missing Username" %]
+ You must provide at least one email address to report on.
+
+[% ELSIF error == "report_invalid_date" %]
+ [% title = "Invalid Date" %]
+ The date '[% date FILTER html %]' is invalid.
+
+[% ELSIF error == "report_invalid_parameter" %]
+ [% title = "Invalid Parameter" %]
+ The value for parameter [% name FILTER html %] is invalid.
+
+[% ELSIF error == "invalid_object" %]
+ Invalid [% object FILTER html %]: "[% value FILTER html %]"
+
+[% 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 @@
+<!-- 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 <gerv@gerv.net>
+ # Reed Loden <reed@reedloden.com>
+ #%]
+
+[% IF (group == "canconfirm" OR group == "editbugs") AND !reason %]
+ <p>
+ If you are attempting to confirm an unconfirmed [% terms.bug %] or edit the fields of a [% terms.bug %],
+ <a href="http://www.gerv.net/hacking/before-you-mail-gerv.html#bugzilla-permissions">find
+ out how to get the necessary permissions</a>.
+ </p>
+[% END %]
diff --git a/extensions/BMO/template/en/default/hook/global/variables-end.none.tmpl b/extensions/BMO/template/en/default/hook/global/variables-end.none.tmpl
new file mode 100644
index 000000000..89eef6fc4
--- /dev/null
+++ b/extensions/BMO/template/en/default/hook/global/variables-end.none.tmpl
@@ -0,0 +1,3 @@
+[%
+ terms.BugzillaTitle = "Bugzilla@Mozilla"
+%]
diff --git a/extensions/BMO/template/en/default/hook/index-additional_links.html.tmpl b/extensions/BMO/template/en/default/hook/index-additional_links.html.tmpl
new file mode 100644
index 000000000..c628d74ea
--- /dev/null
+++ b/extensions/BMO/template/en/default/hook/index-additional_links.html.tmpl
@@ -0,0 +1,14 @@
+<li>
+|
+<a href="page.cgi?id=etiquette.html">
+ [%- terms.Bugzilla %] Etiquette</a>
+</li>
+<li>
+|
+<a href="https://developer.mozilla.org/en/Bug_writing_guidelines">
+ [%- terms.Bug %] Writing Guidelines</a>
+</li>
+|
+<a href="page.cgi?id=researchers.html">
+ Data for Researchers</a>
+</li>
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 @@
+<a id="get_help" class="bz_common_actions"
+ href="page.cgi?id=get_help.html"><span>Get Help</span></a> \ No newline at end of file
diff --git a/extensions/BMO/template/en/default/hook/pages/fields-open-status.html.tmpl b/extensions/BMO/template/en/default/hook/pages/fields-open-status.html.tmpl
new file mode 100644
index 000000000..8f3407aa7
--- /dev/null
+++ b/extensions/BMO/template/en/default/hook/pages/fields-open-status.html.tmpl
@@ -0,0 +1,11 @@
+<dt>
+ <b>[% display_value("bug_status", "READY") FILTER html %]</b>
+</dt>
+<dd>
+ This [% terms.bug %] has enough information so that the developer can
+ start working on a fix. The [% terms.bug %] has the required testcases,
+ crash data, detailed specs, etc. [% terms.Bugs %] in this state may be
+ accepted, and become <b>[% display_value("bug_status", "ASSIGNED") FILTER html %]</b>,
+ passed on to someone else, and remain <b>[% display_value("bug_status", "READY") FILTER html %]</b>,
+ or resolved and marked <b>[% display_value("bug_status", "RESOLVED") FILTER html %]</b>.
+</dd>
diff --git a/extensions/BMO/template/en/default/hook/pages/fields-resolution.html.tmpl b/extensions/BMO/template/en/default/hook/pages/fields-resolution.html.tmpl
new file mode 100644
index 000000000..4d12ab345
--- /dev/null
+++ b/extensions/BMO/template/en/default/hook/pages/fields-resolution.html.tmpl
@@ -0,0 +1,13 @@
+<dt>
+ [% display_value("resolution", "INCOMPLETE") FILTER html %]
+</dt>
+<dd>
+ 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.
+</dd>
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..dae7f9108
--- /dev/null
+++ b/extensions/BMO/template/en/default/hook/reports/menu-end.html.tmpl
@@ -0,0 +1,49 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+<h2>Other Reports</h2>
+
+<ul>
+ <li>
+ <strong>
+ <a href="[% urlbase FILTER none %]page.cgi?id=user_activity.html">User Changes</a>
+ </strong> - Show changes made by an individual user.
+ </li>
+ <li>
+ <strong>
+ <a href="[% urlbase FILTER none %]page.cgi?id=triage_reports.html">Triage Report</a>
+ </strong> - Report on UNCONFIRMED [% terms.bugs %] to assist triage.
+ </li>
+ <li>
+ <strong>
+ <a href="[% urlbase FILTER none %]page.cgi?id=release_tracking_report.html">Release Tracking Report</a>
+ </strong> - For triaging release-train flag information.
+ </li>
+ [% IF user.in_group('editusers') %]
+ <li>
+ <strong>
+ <a href="[% urlbase FILTER none %]page.cgi?id=group_admins.html">Group Admins</a>
+ </strong> - Group Admins Report
+ </li>
+ [% END %]
+ [% IF user.in_group('editusers') || user.in_group('infrasec') %]
+ <li>
+ <strong>
+ <a href="[% urlbase FILTER none %]page.cgi?id=group_membership.html">Group Membership Report</a>
+ </strong> - Lists the groups a user is a member of.
+ </li>
+ [% END %]
+ [% IF user.in_group('admin') || user.in_group('infra') %]
+ <li>
+ <strong>
+ <a href="[% urlbase FILTER none %]page.cgi?id=email_queue.html">Email Queue</a>
+ </strong> - TheSchwartz queue
+ </li>
+ [% END %]
+</ul>
+
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..8925db8dd
--- /dev/null
+++ b/extensions/BMO/template/en/default/list/list.microsummary.tmpl
@@ -0,0 +1,29 @@
+[%# 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 <rmaia@everythingsolved.com>
+ #%]
+
+[% 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 <myk@mozilla.org>
+ #%]
+
+[%# 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 %]
+
+<html>
+ <head>
+ <title>[% terms.Bugzilla %] is pondering your search</title>
+ </head>
+ <body>
+ <div style="margin-top: 15%; text-align: center;">
+ <center><img src="extensions/BMO/web/images/mozchomp.gif" alt=""
+ width="160" height="87"></center>
+ <h1>Please wait while your [% terms.bugs %] are retrieved.</h1>
+ </div>
+
+ [% IF debug %]
+ <p>
+ [% FOREACH debugline = debugdata %]
+ <code>[% debugline FILTER html %]</code><br>
+ [% END %]
+ </p>
+ <p>
+ <code>[% query FILTER html %]</code>
+ </p>
+ [% END %]
+
+ </body>
+</html>
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 <dkl@mozilla.com>
+ #%]
+
+<html>
+ <head>
+ <meta http-equiv="refresh" content="0;url=https://developer.mozilla.org/en/Bug_writing_guidelines">
+ </head>
+</html>
diff --git a/extensions/BMO/template/en/default/pages/email_queue.html.tmpl b/extensions/BMO/template/en/default/pages/email_queue.html.tmpl
new file mode 100644
index 000000000..0e4a37551
--- /dev/null
+++ b/extensions/BMO/template/en/default/pages/email_queue.html.tmpl
@@ -0,0 +1,66 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% INCLUDE global/header.html.tmpl
+ title = "Job Queue Status"
+ style_urls = [ "extensions/BMO/web/styles/reports.css" ]
+%]
+
+[% IF jobs.size %]
+
+ <p><i>[% jobs.size FILTER none %] email(s) in the queue.</i></p>
+
+ <table id="report" cellspacing="0" border="0">
+ <tr id="report-header">
+ <th>Insert Time</th>
+ <th>Run Time</th>
+ <th>Age</th>
+ <th>Error Count</th>
+ <th>Last Error</th>
+ <th>Error Message</th>
+ </tr>
+ [% FOREACH job IN jobs %]
+ <tr class="report item [% loop.count % 2 == 1 ? "report_row_odd" : "report_row_even" %]">
+ <td nowrap>[% time2str("%Y-%m-%d %H:%M:%S %Z", job.insert_time) FILTER html %]</td>
+ <td nowrap>[% time2str("%Y-%m-%d %H:%M:%S %Z", job.run_time) FILTER html %]</td>
+ <td nowrap>
+ [% age = now - job.insert_time %]
+ [% IF age < 60 %]
+ [% age FILTER none %]s
+ [% ELSIF age < 60 * 60 %]
+ [% age / 60 FILTER format('%.0f') %]m
+ [% ELSE %]
+ [% age / (60 * 60) FILTER format('%.0f') %]h
+ [% END %]
+ </td>
+ <td nowrap>[% job.error_count FILTER html %]</td>
+ <td nowrap>
+ [% IF job.error_count %]
+ [% time2str("%Y-%m-%d %H:%M:%S %Z", job.error_time) FILTER html %]
+ [% ELSE %]
+ -
+ [% END %]
+ </td>
+ <td>
+ [% IF job.error_count %]
+ [% job.error_message FILTER html %]
+ [% ELSE %]
+ -
+ [% END %]
+ </td>
+ </tr>
+ [% END %]
+ </table>
+
+[% ELSE %]
+
+<p><i>The email queue is empty.</i></p>
+
+[% END %]
+
+[% INCLUDE global/footer.html.tmpl %]
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..2f8a89503
--- /dev/null
+++ b/extensions/BMO/template/en/default/pages/etiquette.html.tmpl
@@ -0,0 +1,146 @@
+<!-- 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): Stefan Seifert <nine@detonation.org>
+ # Gervase Markham <gerv@gerv.net>
+ #%]
+
+[% PROCESS global/header.html.tmpl
+ title = "Bugzilla Etiquette"
+ style = "li { margin: 5px } .heading { font-weight: bold }" %]
+
+<p>
+ There's a number of <i lang="fr">faux pas</i> 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.
+</p>
+
+<p>
+ That said, Mozilla developers are generally a friendly bunch, and will be
+ friendly towards you as long as you follow these guidelines.
+</p>
+
+<h3>1. Commenting</h3>
+
+<p>
+ This is the most important section.
+</p>
+
+<ol>
+ <li>
+ <span class="heading">No pointless comments</span>.
+ 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
+ <a href="http://www.mozilla.org/about/forums/">newsgroup</a>.
+ </li>
+
+ <li>
+ <span class="heading">No obligation</span>.
+ "Open Source" is not the same as "the developers must do my bidding."
+ Everyone here wants to help, but no one else has any <i>obligation</i> to fix
+ the [% terms.bugs %] you want fixed. 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.
+ </li>
+
+ <li>
+ <span class="heading">No abusing people</span>.
+ 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
+ <i>things</i>, not <i>people</i>. Examples of things include: interfaces,
+ algorithms, and schedules. Examples of people include: developers,
+ designers and users. <b>Attacking a person may result in you being banned
+ from [% terms.Bugzilla %].</b>
+ </li>
+
+ <li>
+ <span class="heading">No private email</span>.
+ 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.
+ </li>
+</ol>
+
+<h3>2. Changing Fields</h3>
+
+<ol>
+ <li>
+ <span class="heading">No messing with other people's [% terms.bugs %]</span>.
+ 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.
+ </li>
+
+ <li>
+ <span class="heading">No whining about decisions</span>.
+ 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.
+ </li>
+
+</ol>
+
+<h3>3. Applicability</h3>
+
+<ol>
+ <li>
+ 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.
+ </li>
+</ol>
+
+<p>
+ 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 <em>private</em> 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
+ <a href="mailto:gerv@mozilla.org">Gerv</a>.
+</p>
+
+<p>
+ This entire document can be summed up in one sentence:
+ do unto others as you would have them do unto you.
+</p>
+
+<p>
+ Other useful documents:
+ <a href="page.cgi?id=bug-writing.html">The [% terms.Bug %] Writing Guidelines</a>.
+</p>
+
+[% 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 <justdave@bugzilla.org>
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+[% INCLUDE global/header.html.tmpl title = "Get Help with Mozilla Products" %]
+
+<div id="steps">
+<h2>Got a problem?</h2>
+
+<ul>
+<li><a href="http://www.mozilla.org/support/">Get help with your mozilla.org product</a></li>
+<li><a href="http://hendrix.mozilla.org/">Leave quick feedback</a></li>
+<li><a href="http://input.mozilla.com/feedback">Report a broken website</a></li>
+<li><a href="enter_bug.cgi">Report a [% terms.bug %]</a> - latest release only
+ [% IF NOT user.id %]
+ (you'll need an
+ <a href="createaccount.cgi">account</a>)
+ [% END %]
+</li>
+</ul>
+</div>
+
+<br>
+
+[% INCLUDE global/footer.html.tmpl %]
diff --git a/extensions/BMO/template/en/default/pages/get_permissions.html.tmpl b/extensions/BMO/template/en/default/pages/get_permissions.html.tmpl
new file mode 100644
index 000000000..b70aa488f
--- /dev/null
+++ b/extensions/BMO/template/en/default/pages/get_permissions.html.tmpl
@@ -0,0 +1,44 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% PROCESS global/header.html.tmpl
+ title = "Upgrade Permissions"
+%]
+
+<h3>How to apply for upgraded permissions</h3>
+
+<p>
+ If you want <kbd>canconfirm</kbd>, email <a href="mailto:bmo-perms@mozilla.org">
+ bmo-perms@mozilla.org</a> the URLs of three good [% terms.bug %] reports you have filed.
+</p>
+
+<p>
+ If you want <kbd>editbugs</kbd>, email <a href="mailto:bmo-perms@mozilla.org">
+ bmo-perms@mozilla.org</a> either:
+ <ul>
+ <li>
+ The URLs of two [% terms.bugs %] to which you have attached patches
+ or testcases; or
+ </li>
+ <li>
+ The URLs of the relevant comment on three [% terms.bugs %] which you
+ wanted to change, but couldn't, and so added a comment instead.
+ </li>
+ </ul>
+</p>
+
+<p>
+ <kbd>editbugs</kbd> implies <kbd>canconfirm</kbd>; there's no need to apply for both.
+</p>
+
+<p>
+ Don't forget to include your [% terms.Bugzilla %] ID if it's not the email address
+ you are emailing from.
+</p>
+
+[% PROCESS global/footer.html.tmpl %]
diff --git a/extensions/BMO/template/en/default/pages/group_admins.html.tmpl b/extensions/BMO/template/en/default/pages/group_admins.html.tmpl
new file mode 100644
index 000000000..1afcdb0b8
--- /dev/null
+++ b/extensions/BMO/template/en/default/pages/group_admins.html.tmpl
@@ -0,0 +1,54 @@
+[%# 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):
+ # David Lawrence <dkl@mozilla.com>
+ #%]
+
+[% INCLUDE global/header.html.tmpl
+ title = "Group Admins Report"
+ style_urls = [ "extensions/BMO/web/styles/reports.css" ]
+ yui = [ "datasource" ]
+%]
+
+[% IF groups.size > 0 %]
+ <table border="0" cellspacing="0" id="report" width="100%">
+ <tr id="report-header">
+ <th align="left">Name</th>
+ <th align="left">Admins</th>
+ </tr>
+
+ [% FOREACH group = groups %]
+ [% count = loop.count() %]
+ <tr class="report_item [% count % 2 == 1 ? "report_row_odd" : "report_row_even" %]">
+ <td>
+ [% group.name FILTER html %]
+ </td>
+ <td>
+ [% FOREACH admin = group.admins %]
+ [% INCLUDE global/user.html.tmpl who = admin %][% ", " UNLESS loop.last %]
+ [% END %]
+ </td>
+ </tr>
+ [% END %]
+ </table>
+[% ELSE %]
+ <p>
+ No groups found.
+ </p>
+[% END %]
+
+[% INCLUDE global/footer.html.tmpl %]
diff --git a/extensions/BMO/template/en/default/pages/group_membership.html.tmpl b/extensions/BMO/template/en/default/pages/group_membership.html.tmpl
new file mode 100644
index 000000000..2680c7da2
--- /dev/null
+++ b/extensions/BMO/template/en/default/pages/group_membership.html.tmpl
@@ -0,0 +1,75 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% PROCESS global/header.html.tmpl
+ title = "Group Membership Report"
+ yui = [ 'autocomplete' ]
+ style_urls = [ "extensions/BMO/web/styles/reports.css" ]
+ javascript_urls = [ "js/field.js" ]
+%]
+
+<form method="GET" action="page.cgi">
+<input type="hidden" name="id" id="id" value="group_membership.html">
+
+<table id="parameters">
+<tr>
+ <th>User(s):</th>
+ <td>
+ [% INCLUDE global/userselect.html.tmpl
+ id => "who"
+ name => "who"
+ value => who.join(', ')
+ size => 40
+ classes => ["bz_userfield"]
+ multiple => 5
+ field_title => "One or more email address (comma delimited)"
+ %]
+ </td>
+ <td>&nbsp;</td>
+ <td>
+ <select name="output"
+ onchange="document.getElementById('id').value = 'group_membership.' + this.value">
+ <option value="html" [% 'selected' IF output == 'html' %]>HTML</option>
+ <option value="txt" [% 'selected' IF output == 'txt' %]>Text</option>
+ </select>
+ </td>
+ <td>
+ <input type="submit" value="Generate">
+ </td>
+</tr>
+</table>
+
+</form>
+
+[% IF users.size %]
+
+ <table border="0" cellspacing="0" id="report" width="100%">
+ [% FOREACH u = users %]
+ <tr>
+ <th colspan="3">[% u.user.identity FILTER html %]</th>
+ </tr>
+ [% FOREACH g = u.groups %]
+ <tr>
+ <td>&nbsp;</td>
+ <td>[% g.name FILTER html %]</td>
+ <td>[% g.desc FILTER html %]</td>
+ <td>
+ [% IF g.via == '' %]
+ direct
+ [% ELSE %]
+ <i>[% g.via FILTER html %]</i>
+ [% END %]
+ </td>
+ </tr>
+ [% END %]
+ [% END %]
+ </table>
+
+[% END %]
+
+[% INCLUDE global/footer.html.tmpl %]
diff --git a/extensions/BMO/template/en/default/pages/group_membership.txt.tmpl b/extensions/BMO/template/en/default/pages/group_membership.txt.tmpl
new file mode 100644
index 000000000..9958f0877
--- /dev/null
+++ b/extensions/BMO/template/en/default/pages/group_membership.txt.tmpl
@@ -0,0 +1,16 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% FOREACH u = users %]
+[% u.user.login FILTER none%]:
+ [% FOREACH g = u.groups %]
+ [% g.name FILTER none %]
+ [% ',' UNLESS loop.last %]
+ [% END %]
+ [% "\n" %]
+[% END %]
diff --git a/extensions/BMO/template/en/default/pages/release_tracking_report.html.tmpl b/extensions/BMO/template/en/default/pages/release_tracking_report.html.tmpl
new file mode 100644
index 000000000..71228014a
--- /dev/null
+++ b/extensions/BMO/template/en/default/pages/release_tracking_report.html.tmpl
@@ -0,0 +1,103 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% INCLUDE global/header.html.tmpl
+ title = "Release Tracking Report"
+ style_urls = [ "extensions/BMO/web/styles/reports.css" ]
+ javascript_urls = [ "extensions/BMO/web/js/release_tracking_report.js" ]
+%]
+
+<noscript>
+<h1>JavaScript is required to use this report.</h1>
+</noscript>
+
+<script>
+var flags_data = [% flags_json FILTER none %];
+var products_data = [% products_json FILTER none %];
+var fields_data = [% fields_json FILTER none %];
+var default_query = '[% default_query FILTER js %]';
+</script>
+
+<form action="page.cgi" method="get" onSubmit="return onFormSubmit()">
+<input type="hidden" name="id" value="release_tracking_report.html">
+<input type="hidden" name="q" id="q" value="">
+<table>
+
+<tr>
+ <th>Approval:</th>
+ <td>
+ Show [% terms.bugs %] where
+ <select id="flag" onChange="onFlagChange()">
+ [% FOREACH flag_name = flag_names %]
+ <option value="[% flag_name FILTER html %]">[% flag_name FILTER html %]</option>
+ [% END %]
+ </select>
+
+ was changed to (and is currently)
+ <select id="flag_value">
+ <option value="?">?</option>
+ <option value="-">-</option>
+ <option value="+">+</option>
+ </select>
+
+ between
+ <select id="range" onChange="serialiseForm()">
+ [% FOREACH range = ranges %]
+ <option value="[% range.value FILTER html %]">
+ [% range.label FILTER html %]
+ </option>
+ [% END %]
+ </select>
+ </td>
+</tr>
+
+<tr>
+ <th>Status:</th>
+ <td>
+ for the product
+ <select id="product" onChange="onProductChange()">
+ </select>
+ </td>
+</tr>
+
+<tr>
+ <td>&nbsp;</td>
+ <td>
+ <select id="op" onChange="serialiseForm()">
+ <option value="and">All selected tracking fields (AND)</option>
+ <option value="or">Any selected tracking fields (OR)</option>
+ </select>
+ [
+ <a href="javascript:void(0)" onClick="selectAllFields()">All</a> |
+ <a href="javascript:void(0)" onClick="selectNoFields()">None</a>
+ ]
+ [
+ <a href="javascript:void(0)" onClick="invertFields()">Invert</a>
+ ]
+ <br>
+ <span id="tracking_span">
+ </span>
+ </td>
+</tr>
+
+<tr>
+ <td>&nbsp;</td>
+ <td colspan="2">
+ <input type="submit" value="Search">
+ <input type="submit" value="Reset" onClick="onFormReset(); return false">
+ <a href="?" id="bookmark">Bookmarkable Link</a>
+ </td>
+</tr>
+</table>
+</form>
+
+<p>
+ <i>"fixed" in the status field checks for the "verified" status as well as "fixed".</i>
+</p>
+
+[% INCLUDE global/footer.html.tmpl %]
diff --git a/extensions/BMO/template/en/default/pages/researchers.html.tmpl b/extensions/BMO/template/en/default/pages/researchers.html.tmpl
new file mode 100644
index 000000000..892384798
--- /dev/null
+++ b/extensions/BMO/template/en/default/pages/researchers.html.tmpl
@@ -0,0 +1,40 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+[% INCLUDE global/header.html.tmpl
+ title = "$terms.Bugzilla Data For Researchers"
+%]
+
+<h2>[% terms.Bugzilla %] Data For Researchers</h2>
+
+<p>In the past we have made sanitized (that is, with all security [% terms.bug %] information removed) dumps of the
+ bugzilla.mozilla.org database available to researchers from <i>bona fide</i> academic institutions who
+ are willing to agree to our requirements for data handling and user privacy. We would much prefer
+ individuals to request this service rather than attempt to spider the data using the web interface.</p>
+
+<p>Because we are familiar with the [% terms.Bugzilla %] data and its possible flaws and biases, we are
+ also happy to answer questions about it, and review papers which make use of it pre-publication. Please
+ let us know if you would like us to do that.</p>
+
+<p>To request a sanitized copy of the database, please file a [% terms.bug %] against
+ <a href="enter_bug.cgi?product=bugzilla.mozilla.org&component=Administration">
+ bugzilla.mozilla.org/Administration</a> with the following information included.</p>
+
+<ul>
+ <li>Description of why you need the copy.</li>
+ <li>The academic institution you are associated with along with any course related information.</li>
+ <li>Contact information for yourself and your professor, mentor, or person overseeing your work.</li>
+ <li>How soon you will need the data.</li>
+</ul>
+
+<p>Once the request has been approved you will be asked to sign an <i>Agreement for Receipt and Use
+of [% terms.Bugzilla %] Data</i>. An administrator will then create the MySQL dump of the sanitized data and place
+it somewhere you will be able to access it.</p>
+
+[% INCLUDE 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 <bjones@mozilla.com>
+ #%]
+
+[% 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" ]
+%]
+
+<noscript>
+<h2>Javascript is required to use this report.</h2>
+</noscript>
+
+[% PROCESS "global/field-descs.none.tmpl" %]
+
+<form id="activity_form" name="activity_form" action="page.cgi" method="get"
+ onSubmit="return onGenerateReport()">
+<input type="hidden" name="id" value="triage_reports.html">
+<input type="hidden" name="action" value="run">
+
+Show UNCONFIRMED [% terms.bugs %] with:
+<table id="triage_form">
+
+<tr>
+ <th>Product:</th>
+ <td>
+ <select name="product" id="product" onChange="onSelectProduct()">
+ <option value=""></option>
+ [% FOREACH p = user.get_selectable_products %]
+ <option value="[% p.name FILTER html %]"
+ [% " selected" IF input.product == p.name %]>
+ [% p.name FILTER html %]
+ </option>
+ [% END %]
+ </select>
+ </td>
+ <td rowspan="2" valign="top">
+ <b>Comment:</b><br>
+
+ <input type="checkbox" name="filter_commenter" id="filter_commenter" value="1"
+ [% 'checked' IF input.filter_commenter %]>
+ <label for="filter_commenter">where the last commenter</label>
+ <select name="commenter" id="commenter" onChange="onCommenterChange()">
+ <option value="reporter" [% 'selected' IF input.commenter == 'reporter' %]>is the reporter</option>
+ <option value="noconfirm" [% 'selected' IF input.commenter == 'noconfirm' %]>does not have canconfirm</option>
+ <option value="is" [% 'selected' IF input.commenter == 'is' %]>is</option>
+ </select>
+ [%+ INCLUDE global/userselect.html.tmpl
+ id => "commenter_is"
+ name => "commenter_is"
+ value => input.commenter_is
+ size => 20
+ emptyok => 0
+ classes = input.commenter == "is" ? "" : "hidden"
+ %]
+ <br>
+
+ <input type="checkbox" name="filter_last" id="filter_last" value="1"
+ [% 'checked' IF input.filter_last %]>
+ <label for="filter_last">where the last comment is older than</label>
+ <select name="last" id="last" onChange="onLastChange()">
+ <option value="30" [% 'selected' IF input.last == '30' %]>30 days</option>
+ <option value="60" [% 'selected' IF input.last == '60' %]>60 days</option>
+ <option value="90" [% 'selected' IF input.last == '90' %]>90 days</option>
+ <option value="365" [% 'selected' IF input.last == '365' %]>one year</option>
+ <option value="is" [% 'selected' IF input.last == 'is' %]>the date</option>
+ </select>
+ <span id="last_is_span" class="[% 'hidden' IF input.last != 'is' %]">
+ <input type="text" id="last_is" name="last_is" size="11" maxlength="10"
+ value="[% input.last_is FILTER html %]"
+ onChange="updateCalendarLastIs(this)">
+ <button type="button" class="calendar_button" id="button_calendar_last_is"
+ onClick="showCalendar('last_is')"><span>Calendar</span>
+ </button>
+ <div id="con_calendar_last_is"></div>
+ </span>
+ <br>
+ </td>
+</tr>
+
+<tr>
+ <th>Component:</th>
+ <td>
+ <select name="component" id="component" multiple size="5">
+ </select>
+ </td>
+</tr>
+
+<tr>
+ <td>&nbsp;</td>
+ <td>
+ <input type="submit" value="Generate Report">
+ </td>
+</tr>
+
+</table>
+
+</form>
+<script>
+ createCalendar('last_is');
+</script>
+
+[% IF input.action == 'run' %]
+<hr>
+[% IF bugs.size > 0 %]
+ <p>
+ Found [% bugs.size %] [%+ terms.bug %][% 's' IF bugs.size != 1 %]:
+ </p>
+ <table border="0" cellspacing="0" id="report" width="100%">
+ <tr id="report-header">
+ <th>[% terms.Bug %] / Date</th>
+ <th>Summary</th>
+ <th>Reporter / Commenter</th>
+ <th>Comment Date</th>
+ <th>Last Comment</th>
+ </tr>
+
+ [% FOREACH bug = bugs %]
+ [% count = loop.count() %]
+ <tr class="bz_bugitem [% count % 2 == 1 ? "bz_row_odd" : "bz_row_even" %]">
+ <td>
+ [% bug.id FILTER bug_link(bug.id) FILTER none %]<br>
+ [% bug.creation_ts.replace(' .*' '') FILTER html FILTER no_break %]
+ </td>
+ <td>
+ [% bug.summary FILTER html %]
+ </td>
+ <td>
+ [% INCLUDE global/user.html.tmpl who = bug.reporter %]
+ [% IF bug.commenter.id != bug.reporter.id %]
+ <br>[% INCLUDE global/user.html.tmpl who = bug.commenter %]
+ [% END %]
+ </td>
+ <td>
+ [% bug.comment_ts FILTER html FILTER no_break %]
+ </td>
+ <td>
+ [% bug.comment FILTER html %]
+ </td>
+ </tr>
+ [% END %]
+ </table>
+
+ <p>
+ <a href="buglist.cgi?bug_id=
+ [%- FOREACH bug = bugs %][% bug.id FILTER uri %],[% END -%]
+ ">Show as a [% terms.Bug %] List</a>
+ </p>
+
+[% ELSE %]
+ <p>
+ No [% terms.bugs %] found.
+ </p>
+[% 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 <justdave@bugzilla.org>
+ # Reed Loden <reed@reedloden.com>
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+[% INCLUDE global/header.html.tmpl
+ title = "Bugzilla 3.6 Upgrade"
+%]
+[% USE date %]
+
+<p><b>Last Updated:</b> [% date.format(template.modtime, "%d-%b-%Y %H:%M %Z") %]</p>
+
+<p>On Friday, July 9, 2010, at 11:40pm PDT (0640 UTC), bugzilla.mozilla.org was
+ <a href="show_bug.cgi?id=558044">upgraded</a> to Bugzilla 3.6.1+. Please
+ <a href="enter_bug.cgi?product=mozilla.org&amp;component=Bugzilla:+Other+b.m.o+Issues&amp;blocked=bmo-regressions">file
+ any regressions</a> for tracking purposes.</p>
+
+<h3>Known Issues</h3>
+
+<p>The following is a list of issues which are known to be broken or incomplete with this upgrade so far.</p>
+
+<ul>
+
+<li>The <a href="https://bugzilla.mozilla.org/showdependencytree.cgi?id=577801&hide_resolved=1">stuff filed in Bugzilla</a>.</li>
+
+</ul>
+
+<h3>What's New</h3>
+
+<h4>Custom bugzilla.mozilla.org Changes</h4>
+
+<ul>
+ <li>Addition of autocomplete support for all user-related fields (assignee,
+ QA contact, and CC list) and the keywords field.</li>
+ <li>New attachment details UI.</li>
+ <li>New icons for the front page.</li>
+ <li>Removal of unused "Patches" column from buglist.</li>
+ <li>Initial support for <a href="http://en.wikipedia.org/wiki/Strict_Transport_Security">Strict-Transport-Security</a> (STS) header.</li>
+</ul>
+
+<h4>General Usability Improvements</h4>
+
+<p>A <a href="https://wiki.mozilla.org/Bugzilla:CMU_HCI_Research_2008">scientific
+ usability study</a> was done on [% terms.Bugzilla %] by researchers
+ from Carnegie-Mellon University. As a result of this study,
+ <a href="https://bugzilla.mozilla.org/showdependencytree.cgi?id=490786&amp;hide_resolved=0">several
+ usability issues</a> were prioritized to be fixed, based on specific data
+ from the study.</p>
+
+<p>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.</p>
+
+<p>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.</p>
+
+<h4>Improved Quicksearch</h4>
+
+<p>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
+ <kbd>[?]</kbd> link next to the box that will take you to
+ the simplified <a href="page.cgi?id=quicksearch.html">Quicksearch Help</a>,
+ 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.</p>
+
+<p>Quicksearch should also be much faster than it was before, particularly
+ on large installations.</p>
+
+<p>Note that in order to implement the new quicksearch, certain old
+ and rarely-used features had to be removed:
+
+<ul>
+ <li><b>+</b> as a prefix to mean "search additional resolutions", and
+ <b>+</b> as a prefix to mean "search just the summary". You can
+ instead use <kbd>summary:</kbd> to explicitly search summaries.</li>
+ <li>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.</li>
+ <li>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.</li>
+ <li>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.</li>
+</ul>
+
+<h4>Simple "Browse" Interface</h4>
+
+<p>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.</p>
+
+<h4>JSON-RPC Interface</h4>
+
+<p>[% terms.Bugzilla %] now has support for the
+ <a href="http://json-rpc.org/">JSON-RPC</a> WebServices protocol via
+ <a href="[% docs_urlbase FILTER html %]api/Bugzilla/WebService/Server/JSONRPC.html">jsonrpc.cgi</a>.
+ The JSON-RPC interface is experimental in this release--if you want any
+ fundamental changes in how it works,
+ <a href="http://www.bugzilla.org/developers/reporting_bugs.html">let us
+ know</a>, for the next release of [% terms.Bugzilla %].</p>
+
+<h3>New Features</h3>
+
+<h4>Enhancements for Users</h4>
+
+<ul>
+ <li><b>[% terms.Bug %] Filing:</b> When filing [% terms.abug %],
+ [%+ terms.Bugzilla %] now visually indicates which fields are
+ mandatory.</li>
+ <li><b>[% terms.Bug %] Filing:</b> "Bookmarkable templates" now
+ support the "alias" and "estimated hours" fields.</li>
+
+ <li><b>[% terms.Bug %] Editing:</b> In previous versions of
+ [%+ terms.Bugzilla %], if you added a private comment to [% terms.abug %],
+ then <em>none</em> 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.</li>
+ <li><b>[% terms.Bug %] Editing:</b> The controls for groups now
+ appear to the right of the attachment and time-tracking tables,
+ when editing [% terms.abug %].</li>
+ <li><b>[% terms.Bug %] Editing:</b> The "Collapse All Comments"
+ and "Expand All Comments" links now appear to the right of the
+ comment list instead of above it.</li>
+ <li><b>[% terms.Bug %] Editing:</b> The See Also field now supports
+ URLs for Google Code Issues and the Debian B[% %]ug-Tracking System.</li>
+ <li><b>[% terms.Bug %] Editing:</b> There have been significant performance
+ improvements in <kbd>show_bug.cgi</kbd> (the script that displays the
+ [% terms.bug %]-editing form), particularly for [% terms.bugs %] that
+ have lots of comments or attachments.</li>
+
+ <li><b>Attachments:</b> The "Details" page of an attachment
+ now displays itself as uneditable if you can't edit the fields
+ there.</li>
+ <li><b>Attachments:</b> We now make sure that there is
+ a Description specified for an attachment, using JavaScript, before
+ the form is submitted.</li>
+ <li><b>Attachments:</b> There is now a link back to the [% terms.bug %]
+ at the bottom of the "Details" page for an attachment.</li>
+ <li><b>Attachments:</b> 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.</li>
+ <li><b>Attachments</b>: 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.</li>
+
+ <li><b>Search:</b> You can now display [% terms.bug %] flags as a column
+ in search results.</li>
+ <li><b>Search:</b> 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.</li>
+ <li><b>Search:</b> You can now search the Deadline field using relative
+ dates (like "1d", "2w", etc.).</li>
+ <li><b>Search:</b> The iCalendar format of search results now includes
+ a PRIORITY field.</li>
+ <li><b>Search:</b> 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.</li>
+ <li><b>Search:</b> When there are no search results, some helpful
+ links are displayed, offering actions you might want to take.</li>
+ <li><b>Search:</b> For those who like to make their own
+ <kbd>buglist.cgi</kbd> URLs (and for people working on customizations),
+ <kbd>buglist.cgi</kbd> now accepts nearly every valid field in
+ [%+ terms.Bugzilla %] as a direct URL parameter, like
+ <kbd>&amp;field=value</kbd>.</li>
+
+ <li><b>Requests:</b> 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.</li>
+ <li><b>Requests:</b> When viewing the "My Requests" page, if you are
+ using Classifications, the Product drop-down will be grouped by
+ Classification.</li>
+
+ <li>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.</li>
+ <li>When creating a new account, you will be automatically logged in
+ after setting your password.</li>
+ <li>There is no longer a maximum password length for accounts.</li>
+ <li>In the Dusk skin, it's now easier to see links.</li>
+ <li>In the Whining system, you can now choose to receive emails even
+ if there are no [% terms.bugs %] that match your searches.</li>
+ <li>The arrows in dependency graphs now point the other way, so that
+ [%+ terms.bugs %] point at their dependencies.</li>
+
+ <li><b>New Charts:</b> You can now convert an existing Saved Search
+ into a data series for New Charts.</li>
+ <li><b>New Charts:</b> There is now an interface that allows you to
+ delete data series.</li>
+ <li><b>New Charts:</b> When deleting a product, you now have the option
+ to delete the data series that are associated with that product.</li>
+</ul>
+
+<h4>Enhancements for Administrators and Developers</h4>
+
+<ul>
+ <li>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).</li>
+ <li>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.</li>
+ <li>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.</li>
+ <li>The minimum length allowed for a password is now 6 characters.</li>
+ <li>The <kbd>UNCONFIRMED</kbd> status being enabled in a product
+ is now unrelated to the voting parameters. Instead, there is a checkbox
+ to enable the <kbd>UNCONFIRMED</kbd> status in a product.</li>
+ <li>Information about duplicates is now stored in the database instead
+ of being stored in the <kbd>data/</kbd> directory. On large installations
+ this could save several hundred megabytes of disk space.</li>
+
+ <li>When editing a group, you can now specify that members of a group
+ are allowed to grant others membership in that group itself.</li>
+ <li>The ability to compress BMP attachments to PNGs is now an Extension.
+ To enable the feature, remove the file
+ <kbd>extensions/BmpConvert/disabled</kbd> and then run checksetup.pl.</li>
+ <li>The default list of values for the Priority field are now clear English
+ words instead of P1, P2, etc.</li>
+ <li><kbd>config.cgi</kbd> now returns an ETag header and understands
+ the If-None-Match header in HTTP requests.</li>
+ <li>The XML format of <kbd>show_bug.cgi</kbd> 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.</li>
+</ul>
+
+<h4>WebService Changes</h4>
+
+<ul>
+ <li>The WebService now returns all dates and times in the UTC timezone.
+ <kbd>B[% %]ugzilla.time</kbd> 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 <kbd>B[% %]ugzilla.timezone</kbd> or
+ <kbd>B[% %]ugzilla.time</kbd>, and always input times in that timezone
+ and expect times to be returned in that format.</li>
+ <li>You can now log in by passing <kbd>Bugzilla_login</kbd> and
+ <kbd>Bugzilla_password</kbd> as arguments to any WebService function.
+ See the
+ <a href="[% docs_urlbase FILTER html %]api/Bugzilla/WebService.html#LOGGING_IN">Bugzilla::WebService</a>
+ documentation for details.</li>
+ <li>New Method:
+ <a href="[% docs_urlbase FILTER html %]api/Bugzilla/WebService/Bug.html#attachments">B[% %]ug.attachments</a>
+ which allows getting information about attachments.</li>
+ <li>New Method:
+ <a href="[% docs_urlbase FILTER html %]api/Bugzilla/WebService/Bug.html#fields">B[% %]ug.fields</a>,
+ 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 <kbd>B[% %]ug.legal_values</kbd> method is now deprecated.</li>
+ <li>In the <kbd>B[% %]ug.add_comment</kbd> method, the "private" parameter
+ has been renamed to "is_private" (for consistency with other methods).
+ You can still use "private", though, for backwards-compatibility.</li>
+ <li>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.</li>
+ <li>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.</li>
+</ul>
+
+<h3>Last Ten Commits</h3>
+
+<pre>[% bzr_history.join('') FILTER html %]</pre>
+
+<br>
+
+[% 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..377d7c244
--- /dev/null
+++ b/extensions/BMO/template/en/default/pages/user_activity.html.tmpl
@@ -0,0 +1,226 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF who %]
+[% who_title = ' (' _ who _ ')' %]
+[% ELSE %]
+[% who_title = '' %]
+[% END %]
+
+[% INCLUDE global/header.html.tmpl
+ title = "User Activity Report" _ who_title
+ yui = [ 'autocomplete', 'calendar' ]
+ javascript_urls = [ "js/util.js", "js/field.js" ]
+ style_urls = [ "extensions/BMO/web/styles/reports.css" ]
+
+%]
+
+[% PROCESS "global/field-descs.none.tmpl" %]
+[% PROCESS bug/time.html.tmpl %]
+
+<form id="activity_form" name="activity_form" action="page.cgi" method="get">
+<input type="hidden" name="id" value="user_activity.html">
+<input type="hidden" name="action" value="run">
+<table id="parameters">
+
+<tr>
+ <th>
+ Who:
+ </th>
+ <td>
+ [% INCLUDE global/userselect.html.tmpl
+ id => "who"
+ name => "who"
+ value => who
+ size => 40
+ emptyok => 0
+ title => "One or more email address (comma delimited)"
+ %]
+ &nbsp;
+ </td>
+ <th>
+ Period:
+ </th>
+ <td>
+ <input type="text" id="from" name="from" size="11"
+ align="right" value="[% from FILTER html %]" maxlength="10"
+ onchange="updateCalendarFromField(this)">
+ <button type="button" class="calendar_button" id="button_calendar_from"
+ onclick="showCalendar('from')"><span>Calendar</span>
+ </button>
+ <div id="con_calendar_from"></div>
+ to
+ <input type="text" name="to" size="11" id="to"
+ align="right" value ="[% to FILTER html %]" maxlength="10"
+ onchange="updateCalendarFromField(this)">
+ <button type="button" class="calendar_button" id="button_calendar_to"
+ onclick="showCalendar('to')"><span>Calendar</span>
+ </button>
+ <div id="con_calendar_to"></div>
+ </td>
+ <th>
+ Sort:
+ </th>
+ <td>
+ <select name="sort">
+ <option value="when" [% 'selected' IF sort == 'when' %]>When</option>
+ <option value="bug" [% 'selected' IF sort == 'bug' %]>[% terms.Bug %]</option>
+ </select>
+ </td>
+ <td>
+ <input type="submit" id="run" value="Generate Report">
+ </td>
+</tr>
+
+</table>
+[% IF debug_sql %]
+ <input type="hidden" name="debug" value="1">
+[% END %]
+</form>
+
+<script type="text/javascript">
+ createCalendar('from');
+ createCalendar('to');
+</script>
+
+[% IF action == 'run' %]
+
+[% IF debug_sql %]
+ <pre>[% debug_sql FILTER html %]</pre>
+[% END %]
+
+[% IF incomplete_data %]
+ <p>
+ There used to be an issue in <a href="http://www.bugzilla.org/">Bugzilla</a>
+ 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 '?'.
+ </p>
+[% END %]
+
+[% IF operations.size > 0 %]
+ <br>
+ <table border="1" cellpadding="4" cellspacing="0" id="report">
+ <tr id="report-header">
+ [% IF who_count > 1 %]
+ <th>Who</th>
+ [% END %]
+ [% IF sort == 'when' %]
+ <th class="sorted">[% INCLUDE sort_when_link %]</th>
+ <th>[% INCLUDE sort_bug_link %]</th>
+ [% ELSE %]
+ <th class="sorted">[% INCLUDE sort_bug_link %]</th>
+ <th>[% INCLUDE sort_when_link %]</th>
+ [% END %]
+ <th>What</th>
+ <th>Removed</th>
+ <th>Added</th>
+ </tr>
+
+ [% FOREACH operation = operations %]
+ [% tr_class = loop.count % 2 ? 'report_row_even' : 'report_row_odd' %]
+ [% FOREACH change = operation.changes %]
+ <tr class="[% tr_class FILTER none %]">
+ [% IF loop.count == 1 %]
+ [% IF who_count > 1 %]
+ <td>[% operation.who FILTER email FILTER html %]</td>
+ [% END %]
+ [% IF sort == 'when' %]
+ <td>[% change.when FILTER time FILTER no_break %]</td>
+ <td>[% operation.bug FILTER bug_link(operation.bug) FILTER none %]</td>
+ [% ELSE %]
+ <td>[% operation.bug FILTER bug_link(operation.bug) FILTER none %]</td>
+ <td>[% change.when FILTER time FILTER no_break %]</td>
+ [% END %]
+ [% ELSE %]
+ [% IF who_count > 1 %]
+ <td>&nbsp;</td>
+ [% END %]
+ <td>&nbsp;</td>
+ [% IF sort == 'when' %]
+ <td>&nbsp;</td>
+ [% ELSE %]
+ <td>[% change.when FILTER time FILTER no_break %]</td>
+ [% END %]
+ [% END %]
+ <td>
+ [% IF change.attachid %]
+ <a href="attachment.cgi?id=[% change.attachid FILTER uri %]"
+ title="[% change.attach.description FILTER html %]
+ [%- %] - [% change.attach.filename FILTER html %]"
+ >Attachment #[% change.attachid FILTER html %]</a>
+ [% END %]
+ [%IF change.comment.defined && change.fieldname == 'longdesc' %]
+ [% "Comment $change.comment.count"
+ FILTER bug_link(operation.bug, comment_num => change.comment.count)
+ FILTER none %]
+ [% ELSE %]
+ [%+ field_descs.${change.fieldname} FILTER html %]
+ [% END %]
+ </td>
+ [% PROCESS change_column change_type = change.removed %]
+ [% PROCESS change_column change_type = change.added %]
+ </tr>
+ [% END %]
+ [% END %]
+ </table>
+ <p>
+ <a href="buglist.cgi?bug_id=[% bug_ids.join(',') FILTER uri %]">
+ Show as a [% terms.Bug %] List</a>
+ </p>
+
+[% ELSE %]
+ <p>
+ No changes.
+ </p>
+[% END %]
+
+[% BLOCK change_column %]
+ <td>
+ [% IF change_type.defined %]
+ [% IF change.fieldname == 'estimated_time' ||
+ change.fieldname == 'remaining_time' ||
+ change.fieldname == 'work_time' %]
+ [% PROCESS formattimeunit time_unit=change_type %]
+ [% ELSIF change.fieldname == 'blocked' ||
+ change.fieldname == 'dependson' %]
+ [% change_type FILTER bug_list_link FILTER none %]
+ [% ELSIF change.fieldname == 'assigned_to' ||
+ change.fieldname == 'reporter' ||
+ change.fieldname == 'qa_contact' ||
+ change.fieldname == 'cc' ||
+ change.fieldname == 'flagtypes.name' %]
+ [% display_value(change.fieldname, change_type) FILTER email FILTER html %]
+ [% ELSE %]
+ [% display_value(change.fieldname, change_type) FILTER html %]
+ [% END %]
+ [% ELSE %]
+ &nbsp;
+ [% END %]
+ </td>
+[% END %]
+[% END %]
+
+[% INCLUDE global/footer.html.tmpl %]
+
+[% BLOCK sort_when_link %]
+ <a href="page.cgi?id=user_activity.html&amp;action=run&amp;
+ [%~%]who=[% who FILTER uri %]&amp;
+ [%~%]from=[% from FILTER uri %]&amp;
+ [%~%]to=[% to FILTER uri %]&amp;
+ [%~%]sort=when">When</a>
+[% END %]
+
+[% BLOCK sort_bug_link %]
+ <a href="page.cgi?id=user_activity.html&amp;action=run&amp;
+ [%~%]who=[% who FILTER uri %]&amp;
+ [%~%]from=[% from FILTER uri %]&amp;
+ [%~%]to=[% to FILTER uri %]&amp;
+ [%~%]sort=bug">[% terms.Bug %]</a>
+[% END %]
diff --git a/extensions/BMO/template/en/default/search/search-plugin.xml.tmpl b/extensions/BMO/template/en/default/search/search-plugin.xml.tmpl
new file mode 100644
index 000000000..5d187bf40
--- /dev/null
+++ b/extensions/BMO/template/en/default/search/search-plugin.xml.tmpl
@@ -0,0 +1,24 @@
+[%# 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.
+ #
+ # Contributor(s): Frédéric Buclin <LpSolit@gmail.com>
+ #
+ #%]
+[% PROCESS global/variables.none.tmpl %]
+<?xml version="1.0" encoding="UTF-8"?>
+<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">
+<ShortName>[% terms.BugzillaTitle %]</ShortName>
+<Description>[% terms.BugzillaTitle %] Quick Search</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8%2F9hAAAABGdBTUEAAK%2FINwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz%2F%2Fz8DJQAggJiQOe%2Ffv2fv7Oz8rays%2FN%2BVkfG%2FiYnJfyD%2F1%2BrVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw%2F8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi%2FG%2BQKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo%2BMXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia%2BCuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq%2FvLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg%2FkdypqCg4H8lUIACnQ%2FSOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD%2BaDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg%3D%3D</Image>
+<Url type="text/html" method="GET" template="[% urlbase FILTER xml %]buglist.cgi?quicksearch={searchTerms}"/>
+</OpenSearchDescription>
diff --git a/extensions/BMO/web/images/background.png b/extensions/BMO/web/images/background.png
new file mode 100644
index 000000000..eb254aab9
--- /dev/null
+++ b/extensions/BMO/web/images/background.png
Binary files differ
diff --git a/extensions/BMO/web/images/bugzilla.png b/extensions/BMO/web/images/bugzilla.png
new file mode 100644
index 000000000..4b7c10284
--- /dev/null
+++ b/extensions/BMO/web/images/bugzilla.png
Binary files differ
diff --git a/extensions/BMO/web/images/favicon.ico b/extensions/BMO/web/images/favicon.ico
new file mode 100644
index 000000000..c14fec40a
--- /dev/null
+++ b/extensions/BMO/web/images/favicon.ico
Binary files differ
diff --git a/extensions/BMO/web/images/groups/bugzilla-approvers.png b/extensions/BMO/web/images/groups/bugzilla-approvers.png
new file mode 100644
index 000000000..d2414e041
--- /dev/null
+++ b/extensions/BMO/web/images/groups/bugzilla-approvers.png
Binary files differ
diff --git a/extensions/BMO/web/images/groups/calendar-drivers.png b/extensions/BMO/web/images/groups/calendar-drivers.png
new file mode 100644
index 000000000..fc2c1d1e5
--- /dev/null
+++ b/extensions/BMO/web/images/groups/calendar-drivers.png
Binary files differ
diff --git a/extensions/BMO/web/images/guided.png b/extensions/BMO/web/images/guided.png
new file mode 100644
index 000000000..46ba060f8
--- /dev/null
+++ b/extensions/BMO/web/images/guided.png
Binary files differ
diff --git a/extensions/BMO/web/images/mozchomp.gif b/extensions/BMO/web/images/mozchomp.gif
new file mode 100644
index 000000000..ac6549527
--- /dev/null
+++ b/extensions/BMO/web/images/mozchomp.gif
Binary files differ
diff --git a/extensions/BMO/web/images/presshat.png b/extensions/BMO/web/images/presshat.png
new file mode 100644
index 000000000..a61de59e5
--- /dev/null
+++ b/extensions/BMO/web/images/presshat.png
Binary files differ
diff --git a/extensions/BMO/web/images/stop-sign.gif b/extensions/BMO/web/images/stop-sign.gif
new file mode 100644
index 000000000..9b420ec6c
--- /dev/null
+++ b/extensions/BMO/web/images/stop-sign.gif
Binary files differ
diff --git a/extensions/BMO/web/images/throbber.gif b/extensions/BMO/web/images/throbber.gif
new file mode 100644
index 000000000..bc4fa6561
--- /dev/null
+++ b/extensions/BMO/web/images/throbber.gif
Binary files differ
diff --git a/extensions/BMO/web/js/edit_bug.js b/extensions/BMO/web/js/edit_bug.js
new file mode 100644
index 000000000..e630eb995
--- /dev/null
+++ b/extensions/BMO/web/js/edit_bug.js
@@ -0,0 +1,91 @@
+/* ***** 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 <glob@mozilla.com>
+ *
+ * ***** END LICENSE BLOCK *****
+ */
+
+// --- custom flags
+var Dom = YAHOO.util.Dom;
+
+function bmo_hide_tracking_flags() {
+ for (var field in bmo_custom_flags) {
+ var el = Dom.get(field);
+ var value = el ? el.value : bmo_custom_flags[field];
+ if (el && (value != bmo_custom_flags[field])) {
+ bmo_show_tracking_flags();
+ return;
+ }
+ if (value == '---') {
+ Dom.addClass('row_' + field, 'bz_hidden');
+ } else {
+ Dom.addClass(field, 'bz_hidden');
+ Dom.removeClass('ro_' + field, 'bz_hidden');
+ }
+ }
+}
+
+function bmo_show_tracking_flags() {
+ Dom.addClass('edit_tracking_fields_action', 'bz_hidden');
+ for (var field in bmo_custom_flags) {
+ if (Dom.get(field).value == '---') {
+ Dom.removeClass('row_' + field, 'bz_hidden');
+ } else {
+ Dom.removeClass(field, 'bz_hidden');
+ Dom.addClass('ro_' + field, 'bz_hidden');
+ }
+ }
+}
+
+function init_clone_bug_menu(el, bug_id, product, component) {
+ var diff_url = 'enter_bug.cgi?cloned_bug_id=' + bug_id;
+ var cur_url = diff_url +
+ '&product=' + encodeURIComponent(product) +
+ '&component=' + encodeURIComponent(component);
+ var menu = new YAHOO.widget.Menu('clone_bug_menu', { position : 'dynamic' });
+ menu.addItems([
+ { text: 'Clone to the current product', url: cur_url },
+ { text: 'Clone to a different product', url: diff_url }
+ ]);
+ menu.render(document.body);
+ YAHOO.util.Event.addListener(el, 'click', show_clone_bug_menu, menu);
+}
+
+function show_clone_bug_menu(event, menu) {
+ menu.cfg.setProperty('xy', YAHOO.util.Event.getXY(event));
+ menu.show();
+ event.preventDefault();
+}
+
+// -- make attachment table, comments, new comment textarea equal widths
+
+YAHOO.util.Event.onDOMReady(function() {
+ var comment_tables = Dom.getElementsByClassName('bz_comment_table', 'table', 'comments');
+ if (comment_tables.length) {
+ var comment_width = comment_tables[0].getElementsByTagName('td')[0].clientWidth + 'px';
+ var attachment_table = Dom.get('attachment_table');
+ if (attachment_table)
+ attachment_table.style.width = comment_width;
+ var new_comment = Dom.get('comment');
+ if (new_comment)
+ new_comment.style.width = comment_width;
+ }
+});
diff --git a/extensions/BMO/web/js/edituser_menu.js b/extensions/BMO/web/js/edituser_menu.js
new file mode 100644
index 000000000..4f6d6ec69
--- /dev/null
+++ b/extensions/BMO/web/js/edituser_menu.js
@@ -0,0 +1,33 @@
+var usermenu_widget;
+
+YAHOO.util.Event.onDOMReady(function() {
+ usermenu_widget = new YAHOO.widget.Menu('usermenu_widget', { position : 'dynamic' });
+ usermenu_widget.addItems([
+ { text: 'Activity', url: '#', target: '_blank' },
+ { text: 'Mail', url: '#', target: '_blank' },
+ { text: 'Edit', url: '#', target: '_blank' }
+ ]);
+ usermenu_widget.render(document.body);
+});
+
+function show_usermenu(event, id, email, show_edit) {
+ if (!usermenu_widget)
+ return true;
+ if (event.ctrlKey || event.shiftKey || event.altKey || event.metaKey)
+ return true;
+ usermenu_widget.getItem(0).cfg.setProperty('url',
+ 'page.cgi?id=user_activity.html&action=run' +
+ '&from=' + YAHOO.util.Date.format(new Date(new Date() - (1000 * 60 * 60 * 24 * 14)), {format: '%Y-%m-%d'}) +
+ '&to=' + YAHOO.util.Date.format(new Date(), {format: '%Y-%m-%d'}) +
+ '&who=' + encodeURIComponent(email));
+ usermenu_widget.getItem(1).cfg.setProperty('url', 'mailto:' + encodeURIComponent(email));
+ if (show_edit) {
+ usermenu_widget.getItem(2).cfg.setProperty('url', 'editusers.cgi?action=edit&userid=' + id);
+ } else {
+ usermenu_widget.removeItem(2);
+ }
+ usermenu_widget.cfg.setProperty('xy', YAHOO.util.Event.getXY(event));
+ usermenu_widget.show();
+ return false;
+}
+
diff --git a/extensions/BMO/web/js/form_validate.js b/extensions/BMO/web/js/form_validate.js
new file mode 100644
index 000000000..6c8fa6f07
--- /dev/null
+++ b/extensions/BMO/web/js/form_validate.js
@@ -0,0 +1,21 @@
+/**
+ * Some Form Validation and Interaction
+ **/
+//Makes sure that there is an '@' in the address with a '.'
+//somewhere after it (and at least one character in between them
+
+function isValidEmail(email) {
+ var at_index = email.indexOf("@");
+ var last_dot = email.lastIndexOf(".");
+ return at_index > 0 && last_dot > (at_index + 1);
+}
+
+//Takes a DOM element id and makes sure that it is filled out
+function isFilledOut(elem_id) {
+ var str = document.getElementById(elem_id).value;
+ return str.length>0 && str!="noneselected";
+}
+
+function isChecked(elem_id) {
+ return document.getElementById(elem_id).checked;
+}
diff --git a/extensions/BMO/web/js/prod_comp_search.js b/extensions/BMO/web/js/prod_comp_search.js
new file mode 100644
index 000000000..ada296f52
--- /dev/null
+++ b/extensions/BMO/web/js/prod_comp_search.js
@@ -0,0 +1,85 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0.
+ */
+
+YAHOO.bugzilla.prodCompSearch = {
+ counter : 0,
+ format : '',
+ cloned_bug_id : '',
+ dataSource : null,
+ autoComplete: null,
+ generateRequest : function (enteredText) {
+ YAHOO.bugzilla.prodCompSearch.counter = YAHOO.bugzilla.prodCompSearch.counter + 1;
+ YAHOO.util.Connect.setDefaultPostHeader('application/json', true);
+ var json_object = {
+ method : "BMO.prod_comp_search",
+ id : YAHOO.bugzilla.prodCompSearch.counter,
+ params : [ {
+ search : decodeURIComponent(enteredText)
+ } ]
+ };
+ YAHOO.util.Dom.removeClass('prod_comp_throbber', 'hidden');
+ return YAHOO.lang.JSON.stringify(json_object);
+ },
+ resultListFormat : function(oResultData, enteredText, sResultMatch) {
+ return YAHOO.lang.escapeHTML(oResultData[0]) + " :: " +
+ YAHOO.lang.escapeHTML(oResultData[1]);
+ },
+ init_ds : function(){
+ this.dataSource = new YAHOO.util.XHRDataSource("jsonrpc.cgi");
+ this.dataSource.connTimeout = 30000;
+ this.dataSource.connMethodPost = true;
+ this.dataSource.connXhrMode = "cancelStaleRequests";
+ this.dataSource.maxCacheEntries = 5;
+ this.dataSource.responseType = YAHOO.util.DataSource.TYPE_JSON;
+ this.dataSource.responseSchema = {
+ resultsList : "result.products",
+ metaFields : { error: "error", jsonRpcId: "id"},
+ fields : [ "product", "component" ]
+ };
+ },
+ init : function(field, container, format, cloned_bug_id) {
+ if (this.dataSource == null)
+ this.init_ds();
+ this.format = format;
+ this.cloned_bug_id = cloned_bug_id;
+ this.autoComplete = new YAHOO.widget.AutoComplete(field, container, this.dataSource);
+ this.autoComplete.generateRequest = this.generateRequest;
+ this.autoComplete.formatResult = this.resultListFormat;
+ this.autoComplete.minQueryLength = 3;
+ this.autoComplete.autoHighlight = false;
+ this.autoComplete.queryDelay = 0.05;
+ this.autoComplete.useIFrame = true;
+ this.autoComplete.maxResultsDisplayed = 25;
+ this.autoComplete.suppressInputUpdate = true;
+ this.autoComplete.doBeforeLoadData = function(sQuery, oResponse, oPayload) {
+ YAHOO.util.Dom.addClass('prod_comp_throbber', 'hidden');
+ return true;
+ };
+ this.autoComplete.textboxFocusEvent.subscribe(function () {
+ var input = YAHOO.util.Dom.get(field);
+ if (input.value && input.value.length > 3) {
+ this.sendQuery(input.value);
+ }
+ });
+ this.autoComplete.itemSelectEvent.subscribe(function (e, args) {
+ var oData = args[2];
+ var url = "enter_bug.cgi?product=" + encodeURIComponent(oData[0]) +
+ "&component=" + encodeURIComponent(oData[1]);
+ var format = YAHOO.bugzilla.prodCompSearch.format;
+ if (format)
+ url += "&format=" + encodeURIComponent(format);
+ var cloned_bug_id = YAHOO.bugzilla.prodCompSearch.cloned_bug_id;
+ if (cloned_bug_id)
+ url += "&cloned_bug_id=" + encodeURIComponent(cloned_bug_id);
+ window.location.href = url;
+ });
+ this.autoComplete.dataReturnEvent.subscribe(function(type, args) {
+ args[0].autoHighlight = args[2].length == 1;
+ });
+ }
+}
diff --git a/extensions/BMO/web/js/release_tracking_report.js b/extensions/BMO/web/js/release_tracking_report.js
new file mode 100644
index 000000000..840b57df1
--- /dev/null
+++ b/extensions/BMO/web/js/release_tracking_report.js
@@ -0,0 +1,203 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0. */
+
+var Dom = YAHOO.util.Dom;
+var flagEl;
+var productEl;
+var trackingEl;
+var selectedFields;
+
+// events
+
+function onFieldToggle(cbEl, id) {
+ if (cbEl.checked) {
+ Dom.removeClass('field_' + id + '_td', 'disabled');
+ selectedFields['field_' + id] = id;
+ } else {
+ Dom.addClass('field_' + id + '_td', 'disabled');
+ selectedFields['field_' + id] = false;
+ }
+ Dom.get('field_' + id + '_select').disabled = !cbEl.checked;
+ serialiseForm();
+}
+
+function onProductChange() {
+ var product = productEl.value;
+ var productData = product == '0' ? getFlagByName(flagEl.value) : getProductById(product);
+ var html = '';
+ selectedFields = new Array();
+
+ if (productData) {
+ // update status fields
+ html = '<table>';
+ for(var i = 0, l = productData.fields.length; i < l; i++) {
+ var field = getFieldById(productData.fields[i]);
+ selectedFields['field_' + field.id] = false;
+ html += '<tr>' +
+ '<td>' +
+ '<input type="checkbox" id="field_' + field.id + '_cb" ' +
+ 'onClick="onFieldToggle(this,' + field.id + ')">' +
+ '</td>' +
+ '<td class="disabled" id="field_' + field.id + '_td">' +
+ '<label for="field_' + field.id + '_cb">' +
+ YAHOO.lang.escapeHTML(field.name) + ':</label>' +
+ '</td>' +
+ '<td>' +
+ '<select disabled id="field_' + field.id + '_select">' +
+ '<option value="+">fixed</option>' +
+ '<option value="-">not fixed</option>' +
+ '</select>' +
+ '</td>' +
+ '</tr>';
+ }
+ html += '</table>';
+ }
+ trackingEl.innerHTML = html;
+ serialiseForm();
+}
+
+function onFlagChange() {
+ var flag = flagEl.value;
+ var flagData = getFlagByName(flag);
+ productEl.options.length = 0;
+
+ if (flagData) {
+ // update product select
+ var currentProduct = productEl.value;
+ productEl.options[0] = new Option('(Any Product)', '0');
+ for(var i = 0, l = flagData.products.length; i < l; i++) {
+ var product = getProductById(flagData.products[i]);
+ var n = productEl.length;
+ productEl.options[n] = new Option(product.name, product.id);
+ productEl.options[n].selected = product.id == currentProduct;
+ }
+ }
+ onProductChange();
+}
+
+// form
+
+function selectAllFields() {
+ for(var i = 0, l = fields_data.length; i < l; i++) {
+ var cb = Dom.get('field_' + fields_data[i].id + '_cb');
+ cb.checked = true;
+ onFieldToggle(cb, fields_data[i].id);
+ }
+ serialiseForm();
+}
+
+function selectNoFields() {
+ for(var i = 0, l = fields_data.length; i < l; i++) {
+ var cb = Dom.get('field_' + fields_data[i].id + '_cb');
+ cb.checked = false;
+ onFieldToggle(cb, fields_data[i].id);
+ }
+ serialiseForm();
+}
+
+function invertFields() {
+ for(var i = 0, l = fields_data.length; i < l; i++) {
+ var el = Dom.get('field_' + fields_data[i].id + '_select');
+ if (el.value == '+') {
+ el.options[1].selected = true;
+ } else {
+ el.options[0].selected = true;
+ }
+ }
+ serialiseForm();
+}
+
+function onFormSubmit() {
+ serialiseForm();
+ return true;
+}
+
+function onFormReset() {
+ deserialiseForm('');
+}
+
+function serialiseForm() {
+ var q = flagEl.value + ':' +
+ Dom.get('flag_value').value + ':' +
+ Dom.get('range').value + ':' +
+ productEl.value + ':' +
+ Dom.get('op').value + ':';
+
+ for(var id in selectedFields) {
+ if (selectedFields[id]) {
+ q += selectedFields[id] + Dom.get(id + '_select').value + ':';
+ }
+ }
+
+ Dom.get('q').value = q;
+ Dom.get('bookmark').href = 'page.cgi?id=release_tracking_report.html&q=' +
+ encodeURIComponent(q);
+}
+
+function deserialiseForm(q) {
+ var parts = q.split(/:/);
+ selectValue(flagEl, parts[0]);
+ onFlagChange();
+ selectValue(Dom.get('flag_value'), parts[1]);
+ selectValue(Dom.get('range'), parts[2]);
+ selectValue(productEl, parts[3]);
+ onProductChange();
+ selectValue(Dom.get('op'), parts[4]);
+ for(var i = 5, l = parts.length; i < l; i++) {
+ var part = parts[i];
+ if (part.length) {
+ var value = part.substr(part.length - 1, 1);
+ var id = part.substr(0, part.length - 1);
+ var cb = Dom.get('field_' + id + '_cb');
+ cb.checked = true;
+ onFieldToggle(cb, id);
+ selectValue(Dom.get('field_' + id + '_select'), value);
+ }
+ }
+ serialiseForm();
+}
+
+// utils
+
+YAHOO.util.Event.onDOMReady(function() {
+ flagEl = Dom.get('flag');
+ productEl = Dom.get('product');
+ trackingEl = Dom.get('tracking_span');
+ onFlagChange();
+ deserialiseForm(default_query);
+});
+
+function getFlagByName(name) {
+ for(var i = 0, l = flags_data.length; i < l; i++) {
+ if (flags_data[i].name == name)
+ return flags_data[i];
+ }
+}
+
+function getProductById(id) {
+ for(var i = 0, l = products_data.length; i < l; i++) {
+ if (products_data[i].id == id)
+ return products_data[i];
+ }
+}
+
+function getFieldById(id) {
+ for(var i = 0, l = fields_data.length; i < l; i++) {
+ if (fields_data[i].id == id)
+ return fields_data[i];
+ }
+}
+
+function selectValue(el, value) {
+ for(var i = 0, l = el.options.length; i < l; i++) {
+ if (el.options[i].value == value) {
+ el.options[i].selected = true;
+ return;
+ }
+ }
+ el.options[0].selected = true;
+}
diff --git a/extensions/BMO/web/js/sorttable.js b/extensions/BMO/web/js/sorttable.js
new file mode 100644
index 000000000..0873dc20a
--- /dev/null
+++ b/extensions/BMO/web/js/sorttable.js
@@ -0,0 +1,709 @@
+/*
+ SortTable
+ version 2
+ 7th April 2007
+ Stuart Langridge, http://www.kryogenix.org/code/browser/sorttable/
+
+ Instructions:
+ Download this file
+ Add <script src="sorttable.js"></script> to your HTML
+ Add class="sortable" to any table you'd like to make sortable
+ Click on the headers to sort
+
+ Thanks to many, many people for contributions and suggestions.
+ Licenced as X11: http://www.kryogenix.org/code/browser/licence.html
+ This basically means: do what you want with it.
+*/
+
+var stIsIE = /*@cc_on!@*/false;
+
+sorttable = {
+ init: function() {
+ // quit if this function has already been called
+ if (arguments.callee.done) return;
+ // flag this function so we don't do the same thing twice
+ arguments.callee.done = true;
+ // kill the timer
+ if (_timer) clearInterval(_timer);
+
+ if (!document.createElement || !document.getElementsByTagName) return;
+
+ sorttable.DATE_RE = /^(\d\d?)[\/\.-](\d\d?)[\/\.-]((\d\d)?\d\d)$/;
+
+ forEach(document.getElementsByTagName('table'), function(table) {
+ if (table.className.search(/\bsortable\b/) != -1) {
+ sorttable.makeSortable(table);
+ }
+ });
+
+ },
+
+ /*
+ * Prepares the table so that it can be sorted
+ *
+ */
+ makeSortable: function(table) {
+
+ if (table.getElementsByTagName('thead').length == 0) {
+ // table doesn't have a tHead. Since it should have, create one and
+ // put the first table row in it.
+ the = document.createElement('thead');
+ the.appendChild(table.rows[0]);
+ table.insertBefore(the,table.firstChild);
+ }
+ // Safari doesn't support table.tHead, sigh
+ if (table.tHead == null) table.tHead = table.getElementsByTagName('thead')[0];
+
+ //if (table.tHead.rows.length != 1) return; // can't cope with two header rows
+
+ // Sorttable v1 put rows with a class of "sortbottom" at the bottom (as
+ // "total" rows, for example). This is B&R, since what you're supposed
+ // to do is put them in a tfoot. So, if there are sortbottom rows,
+ // for backwards compatibility, move them to tfoot (creating it if needed).
+ sortbottomrows = [];
+ for (var i=0; i<table.rows.length; i++) {
+ if (table.rows[i].className.search(/\bsortbottom\b/) != -1) {
+ sortbottomrows[sortbottomrows.length] = table.rows[i];
+ }
+ }
+
+ if (sortbottomrows) {
+ if (table.tFoot == null) {
+ // table doesn't have a tfoot. Create one.
+ tfo = document.createElement('tfoot');
+ table.appendChild(tfo);
+ }
+ for (var i=0; i<sortbottomrows.length; i++) {
+ tfo.appendChild(sortbottomrows[i]);
+ }
+ delete sortbottomrows;
+ }
+
+ sorttable._walk_through_headers(table);
+ },
+
+ /*
+ * Helper function for preparing the table
+ *
+ */
+ _walk_through_headers: function(table) {
+ // First, gather some information we need to sort the table.
+ var bodies = [];
+ var table_rows = [];
+ var body_size = table.tBodies[0].rows.length;
+
+ // We need to get all the rows
+ for (var i=0; i<table.tBodies.length; i++) {
+ if (!table.tBodies[i].className.match(/\bsorttable_body\b/))
+ continue;
+
+ bodies[bodies.length] = table.tBodies[i];
+ for (j=0; j<table.tBodies[i].rows.length; j++) {
+ table_rows[table_rows.length] = table.tBodies[i].rows[j];
+ }
+ }
+
+ table.sorttable_rows = table_rows;
+ table.sorttable_body_size = body_size;
+ table.sorttable_bodies = bodies;
+
+
+ // work through each column and calculate its type
+
+ // For each row in the header..
+ for (var row_index=0; row_index < table.tHead.rows.length; row_index++) {
+
+ headrow = table.tHead.rows[row_index].cells;
+ // ... Walk through each column and calculate the type.
+ for (var i=0; i<headrow.length; i++) {
+ // Don't sort this column, please
+ if (headrow[i].className.match(/\bsorttable_nosort\b/)) continue;
+
+ // Override sort column index.
+ column_index = i;
+ mtch = headrow[i].className.match(/\bsortable_column_([a-z0-9]+)\b/);
+ if (mtch) column_index = mtch[1];
+
+
+ // Manually override the type with a sorttable_type attribute
+ // Override sort function
+ mtch = headrow[i].className.match(/\bsorttable_([a-z0-9]+)\b/);
+ if (mtch) override = mtch[1];
+
+ if (mtch && typeof sorttable["sort_"+override] == 'function') {
+ headrow[i].sorttable_sortfunction = sorttable["sort_"+override];
+ } else {
+ headrow[i].sorttable_sortfunction = sorttable.guessType(table, column_index);
+ }
+
+ // make it clickable to sort
+ headrow[i].sorttable_columnindex = column_index;
+ headrow[i].table = table;
+
+ // If the header contains a link, clear the href.
+ for (var k=0; k<headrow[i].childNodes.length; k++) {
+ if (headrow[i].childNodes[k].tagName == 'A') {
+ headrow[i].childNodes[k].href = "javascript:void(0);";
+ }
+ }
+
+ dean_addEvent(headrow[i], "click", sorttable._on_column_header_clicked);
+
+ } // inner for (var i=0; i<headrow.length; i++)
+ } // outer for
+ },
+
+
+
+
+ /*
+ * Helper function for the _on_column_header_clicked handler
+ *
+ */
+
+ _remove_sorted_classes: function(header) {
+ // For each row in the header..
+ for (var j=0; j< header.rows.length; j++) {
+ // ... Walk through each column and calculate the type.
+ row = header.rows[j].cells;
+
+ for (var i=0; i<row.length; i++) {
+ cell = row[i];
+ if (cell.nodeType != 1) return; // an element
+
+ mtch = cell.className.match(/\bsorted_([0-9]+)\b/);
+ if (mtch) {
+ cell.className = cell.className.replace('sorted_'+mtch[1],
+ 'sorted_'+(parseInt(mtch[1])+1));
+ }
+
+ cell.className = cell.className.replace('sorttable_sorted_reverse','');
+ cell.className = cell.className.replace('sorttable_sorted','');
+ }
+ }
+ },
+
+ _check_already_sorted: function(cell) {
+ if (cell.className.search(/\bsorttable_sorted\b/) != -1) {
+ // if we're already sorted by this column, just
+ // reverse the table, which is quicker
+ sorttable.reverse_table(cell);
+
+ sorttable._mark_column_as_sorted(cell, '&#x25BC;', 1);
+ return 1;
+ }
+
+ if (cell.className.search(/\bsorttable_sorted_reverse\b/) != -1) {
+ // if we're already sorted by this column in reverse, just
+ // re-reverse the table, which is quicker
+ sorttable.reverse_table(cell);
+
+ sorttable._mark_column_as_sorted(cell, '&#x25B2;', 0);
+
+ return 1;
+ }
+
+ return 0;
+ },
+
+ /* Visualy mark the cell as sorted.
+ *
+ * @param cell: the cell being marked
+ * @param text: the text being used to mark. you can use html
+ * @param reversed: whether the column is reversed or not.
+ *
+ */
+ _mark_column_as_sorted: function(cell, text, reversed) {
+ // remove eventual class
+ cell.className = cell.className.replace('sorttable_sorted', '');
+ cell.className = cell.className.replace('sorttable_sorted_reverse', '');
+
+ // the column is reversed
+ if (reversed) {
+ cell.className += ' sorttable_sorted_reverse';
+ }
+ else {
+ // remove eventual class
+ cell.className += ' sorttable_sorted';
+ }
+
+ sorttable._remove_sorting_marker();
+
+ marker = document.createElement('span');
+ marker.id = "sorttable_sort_mark";
+ marker.className = "bz_sort_order_primary";
+ marker.innerHTML = text;
+ cell.appendChild(marker);
+ },
+
+ _remove_sorting_marker: function() {
+ mark = document.getElementById('sorttable_sort_mark');
+ if (mark) { mark.parentNode.removeChild(mark); }
+ els = sorttable._getElementsByClassName('bz_sort_order_primary');
+ for(var i=0,j=els.length; i<j; i++) {
+ els[i].parentNode.removeChild(els[i]);
+ }
+ els = sorttable._getElementsByClassName('bz_sort_order_secondary');
+ for(var i=0,j=els.length; i<j; i++) {
+ els[i].parentNode.removeChild(els[i]);
+ }
+ },
+
+ _getElementsByClassName: function(classname, node) {
+ if(!node) node = document.getElementsByTagName("body")[0];
+ var a = [];
+ var re = new RegExp('\\b' + classname + '\\b');
+ var els = node.getElementsByTagName("*");
+ for(var i=0,j=els.length; i<j; i++)
+ if(re.test(els[i].className))a.push(els[i]);
+ return a;
+ },
+
+ /*
+ * This is the callback for when the table header is clicked.
+ *
+ * @param evt: the event that triggered this callback
+ */
+ _on_column_header_clicked: function(evt) {
+
+ // The table is already sorted by this column. Just reverse it.
+ if (sorttable._check_already_sorted(this))
+ return;
+
+
+ // First, remove sorttable_sorted classes from the other header
+ // that is currently sorted and its marker (the simbol indicating
+ // that its sorted.
+ sorttable._remove_sorted_classes(this.table.tHead);
+ mtch = this.className.match(/\bsorted_([0-9]+)\b/);
+ if (mtch) {
+ this.className = this.className.replace('sorted_'+mtch[1], '');
+ }
+ this.className += ' sorted_0 ';
+
+ // This is the text that indicates that the column is sorted.
+ sorttable._mark_column_as_sorted(this, '&#x25BC;', 0);
+
+ sorttable.sort_table(this);
+
+ },
+
+ sort_table: function(cell) {
+ // build an array to sort. This is a Schwartzian transform thing,
+ // i.e., we "decorate" each row with the actual sort key,
+ // sort based on the sort keys, and then put the rows back in order
+ // which is a lot faster because you only do getInnerText once per row
+ col = cell.sorttable_columnindex;
+ rows = cell.table.sorttable_rows;
+
+ var BUGLIST = '';
+
+ for (var j = 0; j < cell.table.sorttable_rows.length; j++) {
+ rows[j].sort_data = sorttable.getInnerText(rows[j].cells[col]);
+ }
+
+ /* If you want a stable sort, uncomment the following line */
+ sorttable.shaker_sort(rows, cell.sorttable_sortfunction);
+ /* and comment out this one */
+ //rows.sort(cell.sorttable_sortfunction);
+
+ // Rebuild the table, using he sorted rows.
+ tb = cell.table.sorttable_bodies[0];
+ body_size = cell.table.sorttable_body_size;
+ body_index = 0;
+
+ for (var j=0; j<rows.length; j++) {
+ if (j % 2)
+ rows[j].className = rows[j].className.replace('bz_row_even',
+ 'bz_row_odd');
+ else
+ rows[j].className = rows[j].className.replace('bz_row_odd',
+ 'bz_row_even');
+
+ tb.appendChild(rows[j]);
+ var bug_id = sorttable.getInnerText(rows[j].cells[0].childNodes[1]);
+ BUGLIST = BUGLIST ? BUGLIST+':'+bug_id : bug_id;
+
+ if (j % body_size == body_size-1) {
+ body_index++;
+ if (body_index < cell.table.sorttable_bodies.length) {
+ tb = cell.table.sorttable_bodies[body_index];
+ }
+ }
+ }
+
+ document.cookie = 'BUGLIST='+BUGLIST;
+
+ cell.table.sorttable_rows = rows;
+ },
+
+ reverse_table: function(cell) {
+ oldrows = cell.table.sorttable_rows;
+ newrows = [];
+
+ for (var i=0; i < oldrows.length; i++) {
+ newrows[newrows.length] = oldrows[i];
+ }
+
+ tb = cell.table.sorttable_bodies[0];
+ body_size = cell.table.sorttable_body_size;
+ body_index = 0;
+
+ var BUGLIST = '';
+
+ cell.table.sorttable_rows = [];
+ for (var i = newrows.length-1; i >= 0; i--) {
+ if (i % 2)
+ newrows[i].className = newrows[i].className.replace('bz_row_even',
+ 'bz_row_odd');
+ else
+ newrows[i].className = newrows[i].className.replace('bz_row_odd',
+ 'bz_row_even');
+
+ tb.appendChild(newrows[i]);
+ cell.table.sorttable_rows.push(newrows[i]);
+
+ var bug_id = sorttable.getInnerText(newrows[i].cells[0].childNodes[1]);
+ BUGLIST = BUGLIST ? BUGLIST+':'+bug_id : bug_id;
+
+ if ((newrows.length-1-i) % body_size == body_size-1) {
+ body_index++;
+ if (body_index < cell.table.sorttable_bodies.length) {
+ tb = cell.table.sorttable_bodies[body_index];
+ }
+ }
+
+ }
+
+ document.cookie = 'BUGLIST='+BUGLIST;
+
+ delete newrows;
+ },
+
+ guessType: function(table, column) {
+ // guess the type of a column based on its first non-blank row
+ sortfn = sorttable.sort_alpha;
+ for (var i=0; i<table.sorttable_bodies[0].rows.length; i++) {
+ text = sorttable.getInnerText(table.sorttable_bodies[0].rows[i].cells[column]);
+ if (text != '') {
+ if (text.match(/^-?[$]?[\d,.]+%?$/)) {
+ return sorttable.sort_numeric;
+ }
+ // check for a date: dd/mm/yyyy or dd/mm/yy
+ // can have / or . or - as separator
+ // can be mm/dd as well
+ possdate = text.match(sorttable.DATE_RE)
+ if (possdate) {
+ // looks like a date
+ first = parseInt(possdate[1]);
+ second = parseInt(possdate[2]);
+ if (first > 12) {
+ // definitely dd/mm
+ return sorttable.sort_ddmm;
+ } else if (second > 12) {
+ return sorttable.sort_mmdd;
+ } else {
+ // looks like a date, but we can't tell which, so assume
+ // that it's dd/mm (English imperialism!) and keep looking
+ sortfn = sorttable.sort_ddmm;
+ }
+ }
+ }
+ }
+ return sortfn;
+ },
+
+ getInnerText: function(node) {
+ // gets the text we want to use for sorting for a cell.
+ // strips leading and trailing whitespace.
+ // this is *not* a generic getInnerText function; it's special to sorttable.
+ // for example, you can override the cell text with a customkey attribute.
+ // it also gets .value for <input> fields.
+
+ hasInputs = (typeof node.getElementsByTagName == 'function') &&
+ node.getElementsByTagName('input').length;
+
+ if (typeof node.getAttribute != 'undefined' && node.getAttribute("sorttable_customkey") != null) {
+ return node.getAttribute("sorttable_customkey");
+ }
+ else if (typeof node.textContent != 'undefined' && !hasInputs) {
+ return node.textContent.replace(/^\s+|\s+$/g, '');
+ }
+ else if (typeof node.innerText != 'undefined' && !hasInputs) {
+ return node.innerText.replace(/^\s+|\s+$/g, '');
+ }
+ else if (typeof node.text != 'undefined' && !hasInputs) {
+ return node.text.replace(/^\s+|\s+$/g, '');
+ }
+ else {
+ switch (node.nodeType) {
+ case 3:
+ if (node.nodeName.toLowerCase() == 'input') {
+ return node.value.replace(/^\s+|\s+$/g, '');
+ }
+ case 4:
+ return node.nodeValue.replace(/^\s+|\s+$/g, '');
+ break;
+ case 1:
+ case 11:
+ var innerText = '';
+ for (var i = 0; i < node.childNodes.length; i++) {
+ innerText += sorttable.getInnerText(node.childNodes[i]);
+ }
+ return innerText.replace(/^\s+|\s+$/g, '');
+ break;
+ default:
+ return '';
+ }
+ }
+ },
+
+ /* sort functions
+ each sort function takes two parameters, a and b
+ you are comparing a.sort_data and b.sort_data */
+ sort_numeric: function(a,b) {
+ aa = parseFloat(a.sort_data.replace(/[^0-9.-]/g,''));
+ if (isNaN(aa)) aa = 0;
+ bb = parseFloat(b.sort_data.replace(/[^0-9.-]/g,''));
+ if (isNaN(bb)) bb = 0;
+ return aa-bb;
+ },
+
+ sort_alpha: function(a,b) {
+ if (a.sort_data.toLowerCase()==b.sort_data.toLowerCase()) return 0;
+ if (a.sort_data.toLowerCase()<b.sort_data.toLowerCase()) return -1;
+ return 1;
+ },
+
+ sort_ddmm: function(a,b) {
+ mtch = a.sort_data.match(sorttable.DATE_RE);
+ y = mtch[3]; m = mtch[2]; d = mtch[1];
+ if (m.length == 1) m = '0'+m;
+ if (d.length == 1) d = '0'+d;
+ dt1 = y+m+d;
+ mtch = b.sort_data.match(sorttable.DATE_RE);
+ y = mtch[3]; m = mtch[2]; d = mtch[1];
+ if (m.length == 1) m = '0'+m;
+ if (d.length == 1) d = '0'+d;
+ dt2 = y+m+d;
+ if (dt1==dt2) return 0;
+ if (dt1<dt2) return -1;
+ return 1;
+ },
+
+ sort_mmdd: function(a,b) {
+ mtch = a.sort_data.match(sorttable.DATE_RE);
+ y = mtch[3]; d = mtch[2]; m = mtch[1];
+ if (m.length == 1) m = '0'+m;
+ if (d.length == 1) d = '0'+d;
+ dt1 = y+m+d;
+ mtch = b.sort_data.match(sorttable.DATE_RE);
+ y = mtch[3]; d = mtch[2]; m = mtch[1];
+ if (m.length == 1) m = '0'+m;
+ if (d.length == 1) d = '0'+d;
+ dt2 = y+m+d;
+ if (dt1==dt2) return 0;
+ if (dt1<dt2) return -1;
+ return 1;
+ },
+
+ shaker_sort: function(list, comp_func) {
+ // A stable sort function to allow multi-level sorting of data
+ // see: http://en.wikipedia.org/wiki/Cocktail_sort
+ // thanks to Joseph Nahmias
+ var b = 0;
+ var t = list.length - 1;
+ var swap = true;
+
+ while(swap) {
+ swap = false;
+ for(var i = b; i < t; ++i) {
+ if ( comp_func(list[i], list[i+1]) > 0 ) {
+ var q = list[i]; list[i] = list[i+1]; list[i+1] = q;
+ swap = true;
+ }
+ } // for
+ t--;
+
+ if (!swap) break;
+
+ for(var i = t; i > b; --i) {
+ if ( comp_func(list[i], list[i-1]) < 0 ) {
+ var q = list[i]; list[i] = list[i-1]; list[i-1] = q;
+ swap = true;
+ }
+ } // for
+ b++;
+
+ } // while(swap)
+ }
+}
+
+/* ******************************************************************
+ Supporting functions: bundled here to avoid depending on a library
+ ****************************************************************** */
+
+// Dean Edwards/Matthias Miller/John Resig
+
+/* for Mozilla/Opera9 */
+if (document.addEventListener) {
+ document.addEventListener("DOMContentLoaded", sorttable.init, false);
+}
+
+/* for Internet Explorer */
+/*@cc_on @*/
+/*@if (@_win32)
+ // IE doesn't have a way to test if the DOM is loaded
+ // doing a deferred script load with onReadyStateChange checks is
+ // problematic, so poll the document until it is scrollable
+ // http://blogs.atlassian.com/developer/2008/03/when_ie_says_dom_is_ready_but.html
+ var loadTestTimer = function() {
+ try {
+ if (document.readyState != "loaded" && document.readyState != "complete") {
+ document.documentElement.doScroll("left");
+ }
+ sorttable.init(); // call the onload handler
+ } catch(error) {
+ setTimeout(loadTestTimer, 100);
+ }
+ };
+ loadTestTimer();
+/*@end @*/
+
+/* for Safari */
+if (/WebKit/i.test(navigator.userAgent)) { // sniff
+ var _timer = setInterval(function() {
+ if (/loaded|complete/.test(document.readyState)) {
+ sorttable.init(); // call the onload handler
+ }
+ }, 10);
+}
+
+/* for other browsers */
+window.onload = sorttable.init;
+
+// written by Dean Edwards, 2005
+// with input from Tino Zijdel, Matthias Miller, Diego Perini
+
+// http://dean.edwards.name/weblog/2005/10/add-event/
+
+function dean_addEvent(element, type, handler) {
+ if (element.addEventListener) {
+ element.addEventListener(type, handler, false);
+ } else {
+ // assign each event handler a unique ID
+ if (!handler.$$guid) handler.$$guid = dean_addEvent.guid++;
+ // create a hash table of event types for the element
+ if (!element.events) element.events = {};
+ // create a hash table of event handlers for each element/event pair
+ var handlers = element.events[type];
+ if (!handlers) {
+ handlers = element.events[type] = {};
+ // store the existing event handler (if there is one)
+ if (element["on" + type]) {
+ handlers[0] = element["on" + type];
+ }
+ }
+ // store the event handler in the hash table
+ handlers[handler.$$guid] = handler;
+ // assign a global event handler to do all the work
+ element["on" + type] = handleEvent;
+ }
+};
+// a counter used to create unique IDs
+dean_addEvent.guid = 1;
+
+function removeEvent(element, type, handler) {
+ if (element.removeEventListener) {
+ element.removeEventListener(type, handler, false);
+ } else {
+ // delete the event handler from the hash table
+ if (element.events && element.events[type]) {
+ delete element.events[type][handler.$$guid];
+ }
+ }
+};
+
+function handleEvent(event) {
+ var returnValue = true;
+ // grab the event object (IE uses a global event object)
+ event = event || fixEvent(((this.ownerDocument || this.document || this).parentWindow || window).event);
+ // get a reference to the hash table of event handlers
+ var handlers = this.events[event.type];
+ // execute each event handler
+ for (var i in handlers) {
+ this.$$handleEvent = handlers[i];
+ if (this.$$handleEvent(event) === false) {
+ returnValue = false;
+ }
+ }
+ return returnValue;
+};
+
+function fixEvent(event) {
+ // add W3C standard event methods
+ event.preventDefault = fixEvent.preventDefault;
+ event.stopPropagation = fixEvent.stopPropagation;
+ return event;
+};
+fixEvent.preventDefault = function() {
+ this.returnValue = false;
+};
+fixEvent.stopPropagation = function() {
+ this.cancelBubble = true;
+}
+
+// Dean's forEach: http://dean.edwards.name/base/forEach.js
+/*
+ forEach, version 1.0
+ Copyright 2006, Dean Edwards
+ License: http://www.opensource.org/licenses/mit-license.php
+*/
+
+// array-like enumeration
+if (!Array.forEach) { // mozilla already supports this
+ Array.forEach = function(array, block, context) {
+ for (var i = 0; i < array.length; i++) {
+ block.call(context, array[i], i, array);
+ }
+ };
+}
+
+// generic enumeration
+Function.prototype.forEach = function(object, block, context) {
+ for (var key in object) {
+ if (typeof this.prototype[key] == "undefined") {
+ block.call(context, object[key], key, object);
+ }
+ }
+};
+
+// character enumeration
+String.forEach = function(string, block, context) {
+ Array.forEach(string.split(""), function(chr, index) {
+ block.call(context, chr, index, string);
+ });
+};
+
+// globally resolve forEach enumeration
+var forEach = function(object, block, context) {
+ if (object) {
+ var resolve = Object; // default
+ if (object instanceof Function) {
+ // functions have a "length" property
+ resolve = Function;
+ } else if (object.forEach instanceof Function) {
+ // the object implements a custom forEach method so use that
+ object.forEach(block, context);
+ return;
+ } else if (typeof object == "string") {
+ // the object is a string
+ resolve = String;
+ } else if (typeof object.length == "number") {
+ // the object is array-like
+ resolve = Array;
+ }
+ resolve.forEach(object, block, context);
+ }
+};
+
diff --git a/extensions/BMO/web/js/swag.js b/extensions/BMO/web/js/swag.js
new file mode 100644
index 000000000..cd9561b54
--- /dev/null
+++ b/extensions/BMO/web/js/swag.js
@@ -0,0 +1,60 @@
+/**
+ * Swag Request Form Functions
+ * Form Interal Swag Request Form
+ * dtran
+ * 7/6/09
+ **/
+
+
+function evalToNumber(numberString) {
+ if(numberString=='') return 0;
+ return parseInt(numberString);
+}
+
+function evalToNumberString(numberString) {
+ if(numberString=='') return '0';
+ return numberString;
+}
+//item_array should be an array of DOM element ids
+function getTotal(item_array) {
+ var total = 0;
+ for(var i in item_array) {
+ total += evalToNumber(document.getElementById(item_array[i]).value);
+ }
+ return total;
+}
+
+function calculateTotalSwag() {
+ document.getElementById('Totalswag').value =
+ getTotal( new Array('Lanyards',
+ 'Stickers',
+ 'Bracelets',
+ 'Tattoos',
+ 'Buttons',
+ 'Posters'));
+
+}
+
+
+function calculateTotalMensShirts() {
+ document.getElementById('mens_total').value =
+ getTotal( new Array('mens_s',
+ 'mens_m',
+ 'mens_l',
+ 'mens_xl',
+ 'mens_xxl',
+ 'mens_xxxl'));
+
+}
+
+
+function calculateTotalWomensShirts() {
+ document.getElementById('womens_total').value =
+ getTotal( new Array('womens_s',
+ 'womens_m',
+ 'womens_l',
+ 'womens_xl',
+ 'womens_xxl',
+ 'womens_xxxl'));
+
+}
diff --git a/extensions/BMO/web/js/triage_reports.js b/extensions/BMO/web/js/triage_reports.js
new file mode 100644
index 000000000..855b577d7
--- /dev/null
+++ b/extensions/BMO/web/js/triage_reports.js
@@ -0,0 +1,83 @@
+var Dom = YAHOO.util.Dom;
+
+function onSelectProduct() {
+ var component = Dom.get('component');
+ if (Dom.get('product').value == '') {
+ bz_clearOptions(component);
+ return;
+ }
+ selectProduct(Dom.get('product'), component);
+ // selectProduct only supports __Any__ on both elements
+ // we only want it on component, so add it back in
+ try {
+ component.add(new Option('__Any__', ''), component.options[0]);
+ } catch(e) {
+ // support IE
+ component.add(new Option('__Any__', ''), 0);
+ }
+ component.value = '';
+}
+
+function onCommenterChange() {
+ var commenter_is = Dom.get('commenter_is');
+ if (Dom.get('commenter').value == 'is') {
+ Dom.removeClass(commenter_is, 'hidden');
+ } else {
+ Dom.addClass(commenter_is, 'hidden');
+ }
+}
+
+function onLastChange() {
+ var last_is_span = Dom.get('last_is_span');
+ if (Dom.get('last').value == 'is') {
+ Dom.removeClass(last_is_span, 'hidden');
+ } else {
+ Dom.addClass(last_is_span, 'hidden');
+ }
+}
+
+function onGenerateReport() {
+ if (Dom.get('product').value == '') {
+ alert('You must select a product.');
+ return false;
+ }
+ if (Dom.get('component').value == '' && !Dom.get('component').options[0].selected) {
+ alert('You must select at least one component.');
+ return false;
+ }
+ if (!(Dom.get('filter_commenter').checked || Dom.get('filter_last').checked)) {
+ alert('You must select at least one comment filter.');
+ return false;
+ }
+ if (Dom.get('filter_commenter').checked
+ && Dom.get('commenter').value == 'is'
+ && Dom.get('commenter_is').value == '')
+ {
+ alert('You must specify the last commenter\'s email address.');
+ return false;
+ }
+ if (Dom.get('filter_last').checked
+ && Dom.get('last').value == 'is'
+ && Dom.get('last_is').value == '')
+ {
+ alert('You must specify the "comment is older than" date.');
+ return false;
+ }
+ return true;
+}
+
+YAHOO.util.Event.onDOMReady(function() {
+ onSelectProduct();
+ onCommenterChange();
+ onLastChange();
+
+ var component = Dom.get('component');
+ if (selected_components.length == 0)
+ return;
+ component.options[0].selected = false;
+ for (var i = 0, n = selected_components.length; i < n; i++) {
+ var index = bz_optionIndex(component, selected_components[i]);
+ if (index != -1)
+ component.options[index].selected = true;
+ }
+});
diff --git a/extensions/BMO/web/js/webtrends.js b/extensions/BMO/web/js/webtrends.js
new file mode 100644
index 000000000..fd0aca29e
--- /dev/null
+++ b/extensions/BMO/web/js/webtrends.js
@@ -0,0 +1,213 @@
+function WebTrends(options){var that=this;this.dcsid="dcsis0ifv10000gg3ag82u4rf_7b1e";this.rate=100;this.fpcdom=".mozilla.org";this.trackevents=false;if(typeof(options)!="undefined")
+{if(typeof(options.dcsid)!="undefined")this.dcsid=options.dcsid;if(typeof(options.rate)!="undefined")this.rate=options.rate;if(typeof(options.fpcdom)!="undefined")this.fpcdom=options.fpcdom;if(typeof(this.fpcdom)!="undefined"&&this.fpcdom.substring(0,1)!='.')this.fpcdom='.'+this.fpcdom;if(typeof(options.trackevents)!="undefined")this.trackevents=options.trackevents;}
+this.domain="statse.webtrendslive.com";this.timezone=0;this.onsitedoms="";this.downloadtypes="xls,doc,pdf,txt,csv,zip,dmg,exe";this.navigationtag="div,table";this.enabled=true;this.i18n=false;this.fpc="WT_FPC";this.paidsearchparams="gclid";this.splitvalue="";this.preserve=true;this.DCSdir={};this.DCS={};this.WT={};this.DCSext={};this.images=[];this.index=0;this.exre=(function()
+{return(window.RegExp?new RegExp("dcs(uri)|(ref)|(aut)|(met)|(sta)|(sip)|(pro)|(byt)|(dat)|(p3p)|(cfg)|(redirect)|(cip)","i"):"");})();this.re=(function()
+{return(window.RegExp?(that.i18n?{"%25":/\%/g,"%26":/\&/g}:{"%09":/\t/g,"%20":/ /g,"%23":/\#/g,"%26":/\&/g,"%2B":/\+/g,"%3F":/\?/g,"%5C":/\\/g,"%22":/\"/g,"%7F":/\x7F/g,"%A0":/\xA0/g}):"");})();}
+WebTrends.prototype.dcsGetId=function(){if(this.enabled&&(document.cookie.indexOf(this.fpc+"=")==-1)&&(document.cookie.indexOf("WTLOPTOUT=")==-1)){document.write("<scr"+"ipt type='text/javascript' src='"+"http"+(window.location.protocol.indexOf('https:')==0?'s':'')+"://"+this.domain+"/"+this.dcsid+"/wtid.js"+"'><\/scr"+"ipt>");}}
+WebTrends.prototype.dcsGetCookie=function(name)
+{var cookies=document.cookie.split("; ");var cmatch=[];var idx=0;var i=0;var namelen=name.length;var clen=cookies.length;for(i=0;i<clen;i++)
+{var c=cookies[i];if((c.substring(0,namelen+1))==(name+"=")){cmatch[idx++]=c;}}
+var cmatchCount=cmatch.length;if(cmatchCount>0)
+{idx=0;if((cmatchCount>1)&&(name==this.fpc))
+{var dLatest=new Date(0);for(i=0;i<cmatchCount;i++)
+{var lv=parseInt(this.dcsGetCrumb(cmatch[i],"lv"));var dLst=new Date(lv);if(dLst>dLatest)
+{dLatest.setTime(dLst.getTime());idx=i;}}}
+return unescape(cmatch[idx].substring(namelen+1));}
+else
+{return null;}}
+WebTrends.prototype.dcsGetCrumb=function(cval,crumb,sep){var aCookie=cval.split(sep||":");for(var i=0;i<aCookie.length;i++){var aCrumb=aCookie[i].split("=");if(crumb==aCrumb[0]){return aCrumb[1];}}
+return null;}
+WebTrends.prototype.dcsGetIdCrumb=function(cval,crumb){var id=cval.substring(0,cval.indexOf(":lv="));var aCrumb=id.split("=");for(var i=0;i<aCrumb.length;i++){if(crumb==aCrumb[0]){return aCrumb[1];}}
+return null;}
+WebTrends.prototype.dcsIsFpcSet=function(name,id,lv,ss){var c=this.dcsGetCookie(name);if(c){return((id==this.dcsGetIdCrumb(c,"id"))&&(lv==this.dcsGetCrumb(c,"lv"))&&(ss==this.dcsGetCrumb(c,"ss")))?0:3;}
+return 2;}
+WebTrends.prototype.dcsFPC=function(){if(document.cookie.indexOf("WTLOPTOUT=")!=-1){return;}
+var WT=this.WT;var name=this.fpc;var dCur=new Date();var adj=(dCur.getTimezoneOffset()*60000)+(this.timezone*3600000);dCur.setTime(dCur.getTime()+adj);var dExp=new Date(dCur.getTime()+315360000000);var dSes=new Date(dCur.getTime());WT.co_f=WT.vtid=WT.vtvs=WT.vt_f=WT.vt_f_a=WT.vt_f_s=WT.vt_f_d=WT.vt_f_tlh=WT.vt_f_tlv="";if(document.cookie.indexOf(name+"=")==-1){if((typeof(gWtId)!="undefined")&&(gWtId!="")){WT.co_f=gWtId;}
+else if((typeof(gTempWtId)!="undefined")&&(gTempWtId!="")){WT.co_f=gTempWtId;WT.vt_f="1";}
+else{WT.co_f="2";var curt=dCur.getTime().toString();for(var i=2;i<=(32-curt.length);i++){WT.co_f+=Math.floor(Math.random()*16.0).toString(16);}
+WT.co_f+=curt;WT.vt_f="1";}
+if(typeof(gWtAccountRollup)=="undefined"){WT.vt_f_a="1";}
+WT.vt_f_s=WT.vt_f_d="1";WT.vt_f_tlh=WT.vt_f_tlv="0";}
+else{var c=this.dcsGetCookie(name);var id=this.dcsGetIdCrumb(c,"id");var lv=parseInt(this.dcsGetCrumb(c,"lv"));var ss=parseInt(this.dcsGetCrumb(c,"ss"));if((id==null)||(id=="null")||isNaN(lv)||isNaN(ss)){return;}
+WT.co_f=id;var dLst=new Date(lv);WT.vt_f_tlh=Math.floor((dLst.getTime()-adj)/1000);dSes.setTime(ss);if((dCur.getTime()>(dLst.getTime()+1800000))||(dCur.getTime()>(dSes.getTime()+28800000))){WT.vt_f_tlv=Math.floor((dSes.getTime()-adj)/1000);dSes.setTime(dCur.getTime());WT.vt_f_s="1";}
+if((dCur.getDay()!=dLst.getDay())||(dCur.getMonth()!=dLst.getMonth())||(dCur.getYear()!=dLst.getYear())){WT.vt_f_d="1";}}
+WT.co_f=escape(WT.co_f);WT.vtid=(typeof(this.vtid)=="undefined")?WT.co_f:(this.vtid||"");WT.vtvs=(dSes.getTime()-adj).toString();var expiry="; expires="+dExp.toGMTString();var cur=dCur.getTime().toString();var ses=dSes.getTime().toString();document.cookie=name+"="+"id="+WT.co_f+":lv="+cur+":ss="+ses+expiry+"; path=/"+(((this.fpcdom!=""))?("; domain="+this.fpcdom):(""));var rc=this.dcsIsFpcSet(name,WT.co_f,cur,ses);if(rc!=0){WT.co_f=WT.vtvs=WT.vt_f_s=WT.vt_f_d=WT.vt_f_tlh=WT.vt_f_tlv="";if(typeof(this.vtid)=="undefined"){WT.vtid="";}
+WT.vt_f=WT.vt_f_a=rc;}}
+WebTrends.prototype.dcsIsOnsite=function(host){if(host.length>0){host=host.toLowerCase();if(host==window.location.hostname.toLowerCase()){return true;}
+if(typeof(this.onsitedoms.test)=="function"){return this.onsitedoms.test(host);}
+else if(this.onsitedoms.length>0){var doms=this.dcsSplit(this.onsitedoms);var len=doms.length;for(var i=0;i<len;i++){if(host==doms[i]){return true;}}}}
+return false;}
+WebTrends.prototype.dcsTypeMatch=function(pth,typelist){var type=pth.toLowerCase().substring(pth.lastIndexOf(".")+1,pth.length);var types=this.dcsSplit(typelist);var tlen=types.length;for(var i=0;i<tlen;i++){if(type==types[i]){return true;}}
+return false;}
+WebTrends.prototype.dcsEvt=function(evt,tag){var e=evt.target||evt.srcElement;while(e.tagName&&(e.tagName.toLowerCase()!=tag.toLowerCase())){e=e.parentElement||e.parentNode;}
+return e;}
+WebTrends.prototype.dcsNavigation=function(evt){var id="";var cname="";var elems=this.dcsSplit(this.navigationtag);var elen=elems.length;var i,e,elem;for(i=0;i<elen;i++)
+{elem=elems[i];if(elem.length)
+{e=this.dcsEvt(evt,elem);id=(e.getAttribute&&e.getAttribute("id"))?e.getAttribute("id"):"";cname=e.className||"";if(id.length||cname.length){break;}}}
+return id.length?id:cname;}
+WebTrends.prototype.dcsBind=function(event,func){if((typeof(func)=="function")&&document.body){if(document.body.addEventListener){document.body.addEventListener(event,func.wtbind(this),true);}
+else if(document.body.attachEvent){document.body.attachEvent("on"+event,func.wtbind(this));}}}
+WebTrends.prototype.dcsET=function(){var e=(navigator.appVersion.indexOf("MSIE")!=-1)?"click":"mousedown";this.dcsBind(e,this.dcsDownload);this.dcsBind("contextmenu",this.dcsRightClick);this.dcsBind(e,this.dcsLinkTrack);}
+WebTrends.prototype.dcsMultiTrack=function(){var args=dcsMultiTrack.arguments?dcsMultiTrack.arguments:arguments;if(args.length%2==0){this.dcsSaveProps(args);this.dcsSetProps(args);var dCurrent=new Date();this.DCS.dcsdat=dCurrent.getTime();this.dcsFPC();this.dcsTag();this.dcsRestoreProps();}}
+WebTrends.prototype.dcsLinkTrack=function(evt)
+{evt=evt||(window.event||"");if(evt&&((typeof(evt.which)!="number")||(evt.which==1)))
+{var e=this.dcsEvt(evt,"A");var f=this.dcsEvt(evt,"IMG");if(e.href&&e.protocol&&e.protocol.indexOf("http")!=-1&&!this.dcsLinkTrackException(e))
+{if((navigator.appVersion.indexOf("MSIE")==-1)&&((e.onclick)||(e.onmousedown)))
+{this.dcsSetVarCap(e);}
+var hn=e.hostname?(e.hostname.split(":")[0]):"";var qry=e.search?e.search.substring(e.search.indexOf("?")+1,e.search.length):"";var pth=e.pathname?((e.pathname.indexOf("/")!=0)?"/"+e.pathname:e.pathname):"/";var ti='';if(f.alt)
+{ti=f.alt;}
+else
+{if(document.all)
+{ti=e.title||e.innerText||e.innerHTML||"";}
+else
+{ti=e.title||e.text||e.innerHTML||"";}}
+hn=this.DCS.setvar_dcssip||hn;pth=this.DCS.setvar_dcsuri||pth;qry=this.DCS.setvar_dcsqry||qry;ti=this.WT.setvar_ti||ti;ti=this.dcsTrim(ti);this.WT.mc_id=this.WT.setvar_mc_id||"";this.WT.sp=this.WT.ad=this.DCS.setvar_dcsuri=this.DCS.setvar_dcssip=this.DCS.setvar_dcsqry=this.WT.setvar_ti=this.WT.setvar_mc_id="";this.dcsMultiTrack("DCS.dcssip",hn,"DCS.dcsuri",pth,"DCS.dcsqry",this.trimoffsiteparams?"":qry,"DCS.dcsref",window.location,"WT.ti","Link:"+ti,"WT.dl","1","WT.nv",this.dcsNavigation(evt),"WT.sp","","WT.ad","","WT.AutoLinkTrack","1");this.DCS.dcssip=this.DCS.dcsuri=this.DCS.dcsqry=this.DCS.dcsref=this.WT.ti=this.WT.dl=this.WT.nv="";}}}
+WebTrends.prototype.dcsTrim=function(sString)
+{while(sString.substring(0,1)==' ')
+{sString=sString.substring(1,sString.length);}
+while(sString.substring(sString.length-1,sString.length)==' ')
+{sString=sString.substring(0,sString.length-1);}
+return sString;}
+WebTrends.prototype.dcsSetVarCap=function(e)
+{if(e.onclick)
+var gCap=e.onclick.toString();else if(e.onmousedown)
+var gCap=e.onmousedown.toString();var gStart=gCap.substring(gCap.indexOf("dcsSetVar(")+10,gCap.length)||gCap.substring(gCap.indexOf("_tag.dcsSetVar(")+16,gCap.length);var gEnd=gStart.substring(0,gStart.indexOf(");")).replace(/\s"/gi,"").replace(/"/gi,"");var gSplit=gEnd.split(",");if(gSplit.length!=-1)
+{for(var i=0;i<gSplit.length;i+=2)
+{if(gSplit[i].indexOf('WT.')==0)
+{if(this.dcsSetVarValidate(gSplit[i]))
+{this.WT["setvar_"+gSplit[i].substring(3)]=gSplit[i+1];}
+else
+{this.WT[gSplit[i].substring(3)]=gSplit[i+1];}}
+else if(gSplit[i].indexOf('DCS.')==0)
+{if(this.dcsSetVarValidate(gSplit[i]))
+{this.DCS["setvar_"+gSplit[i].substring(4)]=gSplit[i+1];}
+else
+{this.DCS[gSplit[i].substring(4)]=gSplit[i+1];}}
+else if(gSplit[i].indexOf('DCSext.')==0)
+{if(this.dcsSetVarValidate(gSplit[i]))
+{this.DCSext["setvar_"+gSplit[i].substring(7)]=gSplit[i+1];}
+else
+{this.DCSext[gSplit[i].substring(7)]=gSplit[i+1];}}
+else if(gSplit[i].indexOf('DCSdir.')==0)
+{if(this.dcsSetVarValidate(gSplit[i]))
+{this.DCSdir["setvar_"+gSplit[i].substring(7)]=gSplit[i+1];}
+else
+{this.DCSdir[gSplit[i].substring(7)]=gSplit[i+1];}}}}}
+WebTrends.prototype.dcsSetVarValidate=function(validate)
+{var wtParamList="DCS.dcssip,DCS.dcsuri,DCS.dcsqry,WT.ti,WT.mc_id".split(",");for(var i=0;i<wtParamList.length;i++)
+{if(wtParamList[i]==validate)
+{return 1;}}
+return 0;}
+WebTrends.prototype.dcsSetVar=function()
+{var args=dcsSetVar.arguments?dcsSetVar.arguments:arguments;if((args.length%2==0)&&(navigator.appVersion.indexOf("MSIE")!=-1)){for(var i=0;i<args.length;i+=2){if(args[i].indexOf('WT.')==0){if(this.dcsSetVarValidate(args[i])){this.WT["setvar_"+args[i].substring(3)]=args[i+1];}
+else{this.WT[args[i].substring(3)]=args[i+1];}}
+else if(args[i].indexOf('DCS.')==0){if(this.dcsSetVarValidate(args[i])){this.DCS["setvar_"+args[i].substring(4)]=args[i+1];}
+else{this.DCS[args[i].substring(4)]=args[i+1];}}
+else if(args[i].indexOf('DCSext.')==0){if(this.dcsSetVarValidate(args[i])){this.DCSext["setvar_"+args[i].substring(7)]=args[i+1];}
+else{this.DCSext[args[i].substring(7)]=args[i+1];}}
+else if(args[i].indexOf('DCSdir.')==0){if(this.dcsSetVarValidate(args[i])){this.DCSdir["setvar_"+args[i].substring(7)]=args[i+1];}
+else{this.DCSdir[args[i].substring(7)]=args[i+1];}}}}}
+WebTrends.prototype.dcsLinkTrackException=function(n)
+{try
+{var b=0;if(this.DCSdir.gTrackExceptions)
+{var e=this.DCSdir.gTrackExceptions.split(",");while(b!=1)
+{if(n.tagName&&n.tagName=="body")
+{b=1;return false}
+else
+{if(n.className)
+{var f=String(n.className).split(" ");for(var c=0;c<e.length;c++)for(var d=0;d<f.length;d++)
+{if(f[d]==e[c])
+{b=1;return true}}}}
+n=n.parentNode}}
+else
+{return false;}}
+catch(g){}}
+WebTrends.prototype.dcsCleanUp=function(){this.DCS={};this.WT={};this.DCSext={};if(arguments.length%2==0){this.dcsSetProps(arguments);}}
+WebTrends.prototype.dcsSetProps=function(args){for(var i=0;i<args.length;i+=2){if(args[i].indexOf('WT.')==0){this.WT[args[i].substring(3)]=args[i+1];}
+else if(args[i].indexOf('DCS.')==0){this.DCS[args[i].substring(4)]=args[i+1];}
+else if(args[i].indexOf('DCSext.')==0){this.DCSext[args[i].substring(7)]=args[i+1];}}}
+WebTrends.prototype.dcsSaveProps=function(args){var i,key,param;if(this.preserve){this.args=[];for(i=0;i<args.length;i+=2){param=args[i];if(param.indexOf('WT.')==0){key=param.substring(3);this.args[i]=param;this.args[i+1]=this.WT[key]||"";}
+else if(param.indexOf('DCS.')==0){key=param.substring(4);this.args[i]=param;this.args[i+1]=this.DCS[key]||"";}
+else if(param.indexOf('DCSext.')==0){key=param.substring(7);this.args[i]=param;this.args[i+1]=this.DCSext[key]||"";}}}}
+WebTrends.prototype.dcsRestoreProps=function(){if(this.preserve){this.dcsSetProps(this.args);this.args=[];}}
+WebTrends.prototype.dcsSplit=function(list){var items=list.toLowerCase().split(",");var len=items.length;for(var i=0;i<len;i++){items[i]=items[i].replace(/^\s*/,"").replace(/\s*$/,"");}
+return items;}
+WebTrends.prototype.dcsDownload=function(evt){evt=evt||(window.event||"");if(evt&&((typeof(evt.which)!="number")||(evt.which==1))){var e=this.dcsEvt(evt,"A");if(e.href){var hn=e.hostname?(e.hostname.split(":")[0]):"";if(this.dcsIsOnsite(hn)&&this.dcsTypeMatch(e.pathname,this.downloadtypes)){var qry=e.search?e.search.substring(e.search.indexOf("?")+1,e.search.length):"";var pth=e.pathname?((e.pathname.indexOf("/")!=0)?"/"+e.pathname:e.pathname):"/";var ttl="";var text=document.all?e.innerText:e.text;var img=this.dcsEvt(evt,"IMG");if(img.alt){ttl=img.alt;}
+else if(text){ttl=text;}
+else if(e.innerHTML){ttl=e.innerHTML;}
+this.dcsMultiTrack("DCS.dcssip",hn,"DCS.dcsuri",pth,"DCS.dcsqry",e.search||"","WT.ti","Download:"+ttl,"WT.dl","20","WT.nv",this.dcsNavigation(evt));}}}}
+WebTrends.prototype.dcsRightClick=function(evt){evt=evt||(window.event||"");if(evt){var btn=evt.which||evt.button;if((btn!=1)||(navigator.userAgent.indexOf("Safari")!=-1)){var e=this.dcsEvt(evt,"A");if((typeof(e.href)!="undefined")&&e.href){if((typeof(e.protocol)!="undefined")&&e.protocol&&(e.protocol.indexOf("http")!=-1)){if((typeof(e.pathname)!="undefined")&&this.dcsTypeMatch(e.pathname,this.downloadtypes)){var pth=e.pathname?((e.pathname.indexOf("/")!=0)?"/"+e.pathname:e.pathname):"/";var hn=e.hostname?(e.hostname.split(":")[0]):"";this.dcsMultiTrack("DCS.dcssip",hn,"DCS.dcsuri",pth,"DCS.dcsqry","","WT.ti","RightClick:"+pth,"WT.dl","25");}}}}}}
+WebTrends.prototype.dcsAdv=function(){if(this.trackevents&&(typeof(this.dcsET)=="function")){if(window.addEventListener){window.addEventListener("load",this.dcsET.wtbind(this),false);}
+else if(window.attachEvent){window.attachEvent("onload",this.dcsET.wtbind(this));}}
+this.dcsFPC();}
+WebTrends.prototype.dcsVar=function(){var dCurrent=new Date();var WT=this.WT;var DCS=this.DCS;WT.tz=parseInt(dCurrent.getTimezoneOffset()/60*-1)||"0";WT.bh=dCurrent.getHours()||"0";WT.ul=navigator.appName=="Netscape"?navigator.language:navigator.userLanguage;if(typeof(screen)=="object"){WT.cd=navigator.appName=="Netscape"?screen.pixelDepth:screen.colorDepth;WT.sr=screen.width+"x"+screen.height;}
+if(typeof(navigator.javaEnabled())=="boolean"){WT.jo=navigator.javaEnabled()?"Yes":"No";}
+if(document.title){if(window.RegExp){var tire=new RegExp("^"+window.location.protocol+"//"+window.location.hostname+"\\s-\\s");WT.ti=document.title.replace(tire,"");}
+else{WT.ti=document.title;}}
+WT.js="Yes";WT.jv=(function(){var agt=navigator.userAgent.toLowerCase();var major=parseInt(navigator.appVersion);var mac=(agt.indexOf("mac")!=-1);var ff=(agt.indexOf("firefox")!=-1);var ff0=(agt.indexOf("firefox/0.")!=-1);var ff10=(agt.indexOf("firefox/1.0")!=-1);var ff15=(agt.indexOf("firefox/1.5")!=-1);var ff20=(agt.indexOf("firefox/2.0")!=-1);var ff3up=(ff&&!ff0&&!ff10&!ff15&!ff20);var nn=(!ff&&(agt.indexOf("mozilla")!=-1)&&(agt.indexOf("compatible")==-1));var nn4=(nn&&(major==4));var nn6up=(nn&&(major>=5));var ie=((agt.indexOf("msie")!=-1)&&(agt.indexOf("opera")==-1));var ie4=(ie&&(major==4)&&(agt.indexOf("msie 4")!=-1));var ie5up=(ie&&!ie4);var op=(agt.indexOf("opera")!=-1);var op5=(agt.indexOf("opera 5")!=-1||agt.indexOf("opera/5")!=-1);var op6=(agt.indexOf("opera 6")!=-1||agt.indexOf("opera/6")!=-1);var op7up=(op&&!op5&&!op6);var jv="1.1";if(ff3up){jv="1.8";}
+else if(ff20){jv="1.7";}
+else if(ff15){jv="1.6";}
+else if(ff0||ff10||nn6up||op7up){jv="1.5";}
+else if((mac&&ie5up)||op6){jv="1.4";}
+else if(ie5up||nn4||op5){jv="1.3";}
+else if(ie4){jv="1.2";}
+return jv;})();WT.ct="unknown";if(document.body&&document.body.addBehavior){try{document.body.addBehavior("#default#clientCaps");WT.ct=document.body.connectionType||"unknown";document.body.addBehavior("#default#homePage");WT.hp=document.body.isHomePage(location.href)?"1":"0";}
+catch(e){}}
+if(document.all){WT.bs=document.body?document.body.offsetWidth+"x"+document.body.offsetHeight:"unknown";}
+else{WT.bs=window.innerWidth+"x"+window.innerHeight;}
+WT.fv=(function(){var i,flash;if(window.ActiveXObject){for(i=15;i>0;i--){try{flash=new ActiveXObject("ShockwaveFlash.ShockwaveFlash."+i);return i+".0";}
+catch(e){}}}
+else if(navigator.plugins&&navigator.plugins.length){for(i=0;i<navigator.plugins.length;i++){if(navigator.plugins[i].name.indexOf('Shockwave Flash')!=-1){return navigator.plugins[i].description.split(" ")[2];}}}
+return"Not enabled";})();WT.slv=(function(){var slv="Not enabled";try{if(navigator.userAgent.indexOf('MSIE')!=-1){var sli=new ActiveXObject('AgControl.AgControl');if(sli){slv="Unknown";}}
+else if(navigator.plugins["Silverlight Plug-In"]){slv="Unknown";}}
+catch(e){}
+if(slv!="Not enabled"){var i,m,M,F;if((typeof(Silverlight)=="object")&&(typeof(Silverlight.isInstalled)=="function")){for(i=9;i>0;i--){M=i;if(Silverlight.isInstalled(M+".0")){break;}
+if(slv==M){break;}}
+for(m=9;m>=0;m--){F=M+"."+m;if(Silverlight.isInstalled(F)){slv=F;break;}
+if(slv==F){break;}}}}
+return slv;})();if(this.i18n){if(typeof(document.defaultCharset)=="string"){WT.le=document.defaultCharset;}
+else if(typeof(document.characterSet)=="string"){WT.le=document.characterSet;}
+else{WT.le="unknown";}}
+WT.tv="9.3.0";WT.sp=this.splitvalue;WT.dl="0";WT.ssl=(window.location.protocol.indexOf('https:')==0)?"1":"0";DCS.dcsdat=dCurrent.getTime();DCS.dcssip=window.location.hostname;DCS.dcsuri=window.location.pathname;WT.es=DCS.dcssip+DCS.dcsuri;if(window.location.search){DCS.dcsqry=window.location.search;}
+if(DCS.dcsqry){var dcsqry=DCS.dcsqry.toLowerCase();var params=this.paidsearchparams.length?this.paidsearchparams.toLowerCase().split(","):[];for(var i=0;i<params.length;i++){if(dcsqry.indexOf(params[i]+"=")!=-1){WT.srch="1";break;}}}
+if((window.document.referrer!="")&&(window.document.referrer!="-")){if(!(navigator.appName=="Microsoft Internet Explorer"&&parseInt(navigator.appVersion)<4)){DCS.dcsref=window.document.referrer;}}}
+WebTrends.prototype.dcsEscape=function(S,REL){if(REL!=""){S=S.toString();for(var R in REL){if(REL[R]instanceof RegExp){S=S.replace(REL[R],R);}}
+return S;}
+else{return escape(S);}}
+WebTrends.prototype.dcsA=function(N,V){if(this.i18n&&(this.exre!="")&&!this.exre.test(N)){if(N=="dcsqry"){var newV="";var params=V.substring(1).split("&");for(var i=0;i<params.length;i++){var pair=params[i];var pos=pair.indexOf("=");if(pos!=-1){var key=pair.substring(0,pos);var val=pair.substring(pos+1);if(i!=0){newV+="&";}
+newV+=key+"="+this.dcsEncode(val);}}
+V=V.substring(0,1)+newV;}
+else{V=this.dcsEncode(V);}}
+return"&"+N+"="+this.dcsEscape(V,this.re);}
+WebTrends.prototype.dcsEncode=function(S){return(typeof(encodeURIComponent)=="function")?encodeURIComponent(S):escape(S);}
+WebTrends.prototype.dcsCreateImage=function(dcsSrc){if(document.images){this.images[this.index]=new Image();this.images[this.index].src=dcsSrc;this.index++;}
+else{document.write('<img alt="" border="0" name="DCSIMG" width="1" height="1" src="'+dcsSrc+'">');}}
+WebTrends.prototype.dcsMeta=function(){var elems;if(document.documentElement){elems=document.getElementsByTagName("meta");}
+else if(document.all){elems=document.all.tags("meta");}
+if(typeof(elems)!="undefined"){var length=elems.length;for(var i=0;i<length;i++){var name=elems.item(i).name;var content=elems.item(i).content;var equiv=elems.item(i).httpEquiv;if(name.length>0){if(name.toUpperCase().indexOf("WT.")==0){this.WT[name.substring(3)]=content;}
+else if(name.toUpperCase().indexOf("DCSEXT.")==0){this.DCSext[name.substring(7)]=content;}
+else if(name.toUpperCase().indexOf("DCSDIR.")==0){this.DCSdir[name.substring(7)]=content;}
+else if(name.toUpperCase().indexOf("DCS.")==0){this.DCS[name.substring(4)]=content;}}}}}
+WebTrends.prototype.dcsTag=function(){if(document.cookie.indexOf("WTLOPTOUT=")!=-1||!this.dcsChk()){return;}
+var WT=this.WT;var DCS=this.DCS;var DCSext=this.DCSext;var i18n=this.i18n;var P="http"+(window.location.protocol.indexOf('https:')==0?'s':'')+"://"+this.domain+(this.dcsid==""?'':'/'+this.dcsid)+"/dcs.gif?";if(i18n){WT.dep="";}
+for(var N in DCS){if(DCS[N]&&(typeof DCS[N]!="function")){P+=this.dcsA(N,DCS[N]);}}
+for(N in WT){if(WT[N]&&(typeof WT[N]!="function")){P+=this.dcsA("WT."+N,WT[N]);}}
+for(N in DCSext){if(DCSext[N]&&(typeof DCSext[N]!="function")){if(i18n){WT.dep=(WT.dep.length==0)?N:(WT.dep+";"+N);}
+P+=this.dcsA(N,DCSext[N]);}}
+if(i18n&&(WT.dep.length>0)){P+=this.dcsA("WT.dep",WT.dep);}
+if(P.length>2048&&navigator.userAgent.indexOf('MSIE')>=0){P=P.substring(0,2040)+"&WT.tu=1";}
+this.dcsCreateImage(P);this.WT.ad="";}
+WebTrends.prototype.dcsDebug=function(){var t=this;var i=t.images[0].src;var q=i.indexOf("?");var r=i.substring(0,q).split("/");var m="<b>Protocol</b><br><code>"+r[0]+"<br></code>";m+="<b>Domain</b><br><code>"+r[2]+"<br></code>";m+="<b>Path</b><br><code>/"+r[3]+"/"+r[4]+"<br></code>";m+="<b>Query Params</b><code>"+i.substring(q+1).replace(/\&/g,"<br>")+"</code>";m+="<br><b>Cookies</b><br><code>"+document.cookie.replace(/\;/g,"<br>")+"</code>";if(t.w&&!t.w.closed){t.w.close();}
+t.w=window.open("","dcsDebug","width=500,height=650,scrollbars=yes,resizable=yes");t.w.document.write(m);t.w.focus();}
+WebTrends.prototype.dcsCollect=function(){if(this.enabled){this.dcsVar();this.dcsMeta();this.dcsAdv();this.dcsBounce();if(typeof(this.dcsCustom)=="function"){this.dcsCustom();}
+this.dcsTag();}}
+function dcsMultiTrack(){if(typeof(_tag)!="undefined"){return(_tag.dcsMultiTrack());}}
+function dcsSetVar(){if(typeof(_tag)!="undefined"){return(_tag.dcsSetVar());}}
+function dcsDebug(){if(typeof(_tag)!="undefined"){return(_tag.dcsDebug());}}
+Function.prototype.wtbind=function(obj){var method=this;var temp=function(){return method.apply(obj,arguments);};return temp;}
+WebTrends.prototype.dcsBounce=function(){if(typeof(this.WT.vt_f_s)!="undefined"&&this.WT.vt_f_s==1){this.WT.z_bounce="1";}else{this.WT.z_bounce="0";}}
+WebTrends.prototype.dcsChk=function()
+{if(this.rate==100){return"true";}
+var cname='wtspl';cval=this.dcsGetCookie(cname);if(cval==null)
+{cval=Math.floor(Math.random()*1000000);var date=new Date();date.setTime(date.getTime()+(30*24*60*60*1000));document.cookie=cname+"="+cval+"; expires="+date.toGMTString()+"; path=/; domain="+this.fpcdom+";";}
+return((cval%1000)<(this.rate*10));} \ No newline at end of file
diff --git a/extensions/BMO/web/producticons/camino.png b/extensions/BMO/web/producticons/camino.png
new file mode 100644
index 000000000..c833b4d04
--- /dev/null
+++ b/extensions/BMO/web/producticons/camino.png
Binary files differ
diff --git a/extensions/BMO/web/producticons/dino.png b/extensions/BMO/web/producticons/dino.png
new file mode 100644
index 000000000..9e0470a07
--- /dev/null
+++ b/extensions/BMO/web/producticons/dino.png
Binary files differ
diff --git a/extensions/BMO/web/producticons/fennec.png b/extensions/BMO/web/producticons/fennec.png
new file mode 100644
index 000000000..ebad7e358
--- /dev/null
+++ b/extensions/BMO/web/producticons/fennec.png
Binary files differ
diff --git a/extensions/BMO/web/producticons/firefox.png b/extensions/BMO/web/producticons/firefox.png
new file mode 100644
index 000000000..582a6952a
--- /dev/null
+++ b/extensions/BMO/web/producticons/firefox.png
Binary files differ
diff --git a/extensions/BMO/web/producticons/idea.png b/extensions/BMO/web/producticons/idea.png
new file mode 100644
index 000000000..9480dce62
--- /dev/null
+++ b/extensions/BMO/web/producticons/idea.png
Binary files differ
diff --git a/extensions/BMO/web/producticons/input.png b/extensions/BMO/web/producticons/input.png
new file mode 100644
index 000000000..81f355d85
--- /dev/null
+++ b/extensions/BMO/web/producticons/input.png
Binary files differ
diff --git a/extensions/BMO/web/producticons/labs.png b/extensions/BMO/web/producticons/labs.png
new file mode 100644
index 000000000..346e0ef06
--- /dev/null
+++ b/extensions/BMO/web/producticons/labs.png
Binary files differ
diff --git a/extensions/BMO/web/producticons/mozilla.png b/extensions/BMO/web/producticons/mozilla.png
new file mode 100644
index 000000000..e506328bc
--- /dev/null
+++ b/extensions/BMO/web/producticons/mozilla.png
Binary files differ
diff --git a/extensions/BMO/web/producticons/other.png b/extensions/BMO/web/producticons/other.png
new file mode 100644
index 000000000..e436c22ae
--- /dev/null
+++ b/extensions/BMO/web/producticons/other.png
Binary files differ
diff --git a/extensions/BMO/web/producticons/seamonkey.png b/extensions/BMO/web/producticons/seamonkey.png
new file mode 100644
index 000000000..fcb261ae1
--- /dev/null
+++ b/extensions/BMO/web/producticons/seamonkey.png
Binary files differ
diff --git a/extensions/BMO/web/producticons/sunbird.png b/extensions/BMO/web/producticons/sunbird.png
new file mode 100644
index 000000000..6b15c257d
--- /dev/null
+++ b/extensions/BMO/web/producticons/sunbird.png
Binary files differ
diff --git a/extensions/BMO/web/producticons/thunderbird.png b/extensions/BMO/web/producticons/thunderbird.png
new file mode 100644
index 000000000..f3523183a
--- /dev/null
+++ b/extensions/BMO/web/producticons/thunderbird.png
Binary files differ
diff --git a/extensions/BMO/web/styles/choose_product.css b/extensions/BMO/web/styles/choose_product.css
new file mode 100644
index 000000000..053af542f
--- /dev/null
+++ b/extensions/BMO/web/styles/choose_product.css
@@ -0,0 +1,16 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0. */
+
+#choose_product h2,
+#choose_product p {
+ text-align: center;
+}
+
+#choose_product td h2,
+#choose_product td p {
+ text-align: left;
+}
diff --git a/extensions/BMO/web/styles/create_account.css b/extensions/BMO/web/styles/create_account.css
new file mode 100644
index 000000000..0ab527629
--- /dev/null
+++ b/extensions/BMO/web/styles/create_account.css
@@ -0,0 +1,62 @@
+/* ***** 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 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):
+ * Byron Jones <glob@mozilla.com>
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+#create-account h2 {
+ margin: 0px;
+}
+
+.column-header {
+ padding: 20px 20px 20px 0px;
+}
+
+#create-account-left {
+ border-right: 2px solid #888888;
+ padding-right: 10px;
+}
+
+#product-list td {
+ padding-top: 10px;
+}
+
+#product-list img {
+ padding-right: 10px;
+}
+
+#create-account-right {
+ padding-left: 10px;
+}
+
+#right-blurb {
+ font-size: large;
+}
+
+#right-blurb li {
+ padding-bottom: 1em;
+}
+
+#create-account-right {
+ padding-bottom: 5em;
+}
+
diff --git a/extensions/BMO/web/styles/edit_bug.css b/extensions/BMO/web/styles/edit_bug.css
new file mode 100644
index 000000000..089a92fbb
--- /dev/null
+++ b/extensions/BMO/web/styles/edit_bug.css
@@ -0,0 +1,38 @@
+/* ***** 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 <glob@mozilla.com>
+ *
+ * ***** END LICENSE BLOCK *****
+ */
+
+#project-flags,
+#custom-flags {
+ width: auto;
+}
+
+.bz_hidden {
+ display: none;
+}
+
+.bz_collapse_comment {
+ font-family: monospace;
+}
+
diff --git a/extensions/BMO/web/styles/prod_comp_search.css b/extensions/BMO/web/styles/prod_comp_search.css
new file mode 100644
index 000000000..24c0a2cf8
--- /dev/null
+++ b/extensions/BMO/web/styles/prod_comp_search.css
@@ -0,0 +1,22 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0. */
+
+#prod_comp_search_main {
+ width: 400px;
+ margin-right: auto;
+ margin-left: auto;
+}
+
+#prod_comp_search_main .hidden {
+ display: none;
+}
+
+#prod_comp_search_main li.yui-ac-highlight a {
+ text-decoration: none;
+ color: #FFFFFF;
+ display: block;
+}
diff --git a/extensions/BMO/web/styles/reports.css b/extensions/BMO/web/styles/reports.css
new file mode 100644
index 000000000..2a6bc54fc
--- /dev/null
+++ b/extensions/BMO/web/styles/reports.css
@@ -0,0 +1,58 @@
+.hidden {
+ display: none;
+}
+
+#product, #component {
+ width: 20em;
+}
+
+#parameters th {
+ text-align: left;
+ vertical-align: middle !important;
+}
+
+#report tr.bugitem:hover {
+ background: #ccccff;
+}
+
+#report td, #report th {
+ padding: 3px 10px 3px 3px;
+}
+
+#report th {
+ text-align: left;
+}
+
+#report th.sorted {
+ text-decoration: underline;
+}
+
+#report-header {
+ background: #cccccc;
+}
+
+.report_row_odd {
+ background-color: #eeeeee;
+ color: #000000;
+}
+
+.report_row_even {
+ background-color: #ffffff;
+ color: #000000;
+}
+
+#report tr:hover {
+ background-color: #ccccff;
+}
+
+#report {
+ border: 1px solid #888888;
+}
+
+#report th, #report td {
+ border: 0px;
+}
+
+.disabled {
+ color: #888888;
+}
diff --git a/extensions/BMO/web/styles/triage_reports.css b/extensions/BMO/web/styles/triage_reports.css
new file mode 100644
index 000000000..6190fd32c
--- /dev/null
+++ b/extensions/BMO/web/styles/triage_reports.css
@@ -0,0 +1,23 @@
+.hidden {
+ display: none;
+}
+
+#triage_form th {
+ text-align: left;
+}
+
+#product, #component {
+ width: 20em;
+}
+
+#report tr.bugitem:hover {
+ background: #ccccff;
+}
+
+#report td {
+ padding: 1px 10px 1px 10px;
+}
+
+#report-header {
+ background: #dddddd;
+}
diff --git a/extensions/BrowserID/Config.pm b/extensions/BrowserID/Config.pm
new file mode 100644
index 000000000..a55ea8ff0
--- /dev/null
+++ b/extensions/BrowserID/Config.pm
@@ -0,0 +1,43 @@
+# -*- 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 BrowserID 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):
+# Gervase Markham <gerv@gerv.net>
+
+package Bugzilla::Extension::BrowserID;
+use strict;
+
+use constant NAME => 'BrowserID';
+
+use constant REQUIRED_MODULES => [
+ {
+ package => 'JSON',
+ module => 'JSON',
+ version => 0,
+ },
+ {
+ package => 'libwww-perl',
+ module => 'LWP::UserAgent',
+ version => 0,
+ },
+];
+
+use constant OPTIONAL_MODULES => [
+];
+
+__PACKAGE__->NAME;
diff --git a/extensions/BrowserID/Extension.pm b/extensions/BrowserID/Extension.pm
new file mode 100644
index 000000000..873cca8e3
--- /dev/null
+++ b/extensions/BrowserID/Extension.pm
@@ -0,0 +1,49 @@
+# -*- 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 BrowserID 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):
+# Gervase Markham <gerv@gerv.net>
+
+package Bugzilla::Extension::BrowserID;
+use strict;
+use base qw(Bugzilla::Extension);
+
+our $VERSION = '0.01';
+
+sub auth_login_methods {
+ my ($self, $args) = @_;
+ my $modules = $args->{'modules'};
+ if (exists($modules->{'BrowserID'})) {
+ $modules->{'BrowserID'} = 'Bugzilla/Extension/BrowserID/Login.pm';
+ }
+}
+
+sub config_modify_panels {
+ my ($self, $args) = @_;
+ my $panels = $args->{'panels'};
+ my $auth_panel_params = $panels->{'auth'}->{'params'};
+
+ my ($user_info_class) =
+ grep { $_->{'name'} eq 'user_info_class' } @$auth_panel_params;
+
+ if ($user_info_class) {
+ push(@{ $user_info_class->{'choices'} }, "BrowserID,CGI");
+ }
+}
+
+__PACKAGE__->NAME;
diff --git a/extensions/BrowserID/TODO b/extensions/BrowserID/TODO
new file mode 100644
index 000000000..ac94a3c42
--- /dev/null
+++ b/extensions/BrowserID/TODO
@@ -0,0 +1,19 @@
+ToDo:
+
+* Cache the LWP::UserAgent in Login.pm?
+
+* Fix Bugzilla::Auth::Login::Stack to allow failure part way down the chain
+ (currently, it seems that both CGI and BrowserID have to be last in order
+ to report login failures correctly.)
+
+* JS inclusions noticeably slow page load. Do we want a local copy of
+ browserid.js? Do the browserid folks object to that? How can we get good
+ performance? How can we avoid including it in every logged-in page? Can we
+ do demand loading onclick, and/or load-on-reveal?
+
+* Fix -8px margin-bottom hack in login-small-additional_methods.html.tmpl
+
+
+
+
+
diff --git a/extensions/BrowserID/lib/Login.pm b/extensions/BrowserID/lib/Login.pm
new file mode 100644
index 000000000..c3d87c958
--- /dev/null
+++ b/extensions/BrowserID/lib/Login.pm
@@ -0,0 +1,126 @@
+# -*- 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 BrowserID 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):
+# Gervase Markham <gerv@gerv.net>
+
+package Bugzilla::Extension::BrowserID::Login;
+use strict;
+use base qw(Bugzilla::Auth::Login);
+
+use Bugzilla::Constants;
+use Bugzilla::Util;
+use Bugzilla::Error;
+use Bugzilla::Token;
+
+use JSON;
+use LWP::UserAgent;
+
+use constant requires_verification => 0;
+use constant is_automatic => 1;
+use constant user_can_create_account => 1;
+
+sub get_login_info {
+ my ($self) = @_;
+
+ my $cgi = Bugzilla->cgi;
+
+ my $assertion = $cgi->param("browserid_assertion");
+ # Avoid the assertion being copied into any 'echoes' of the current URL
+ # in the page.
+ $cgi->delete('browserid_assertion');
+
+ if (!$assertion) {
+ return { failure => AUTH_NODATA };
+ }
+
+ my $token = $cgi->param("token");
+ $cgi->delete('token');
+ check_hash_token($token, ['login']);
+
+ my $urlbase = new URI(correct_urlbase());
+ my $audience = $urlbase->scheme . "://" . $urlbase->host_port;
+
+ my $ua = new LWP::UserAgent();
+
+ my $info = { 'status' => 'browserid-server-broken' };
+ eval {
+ my $response = $ua->post("https://browserid.org/verify",
+ [assertion => $assertion,
+ audience => $audience]);
+
+ $info = decode_json($response->content());
+ };
+
+ if ($info->{'status'} eq "okay" &&
+ $info->{'audience'} eq $audience &&
+ ($info->{'expires'} / 1000) > time())
+ {
+ my $login_data = {
+ 'username' => $info->{'email'}
+ };
+
+ my $result =
+ Bugzilla::Auth::Verify->create_or_update_user($login_data);
+ return $result if $result->{'failure'};
+
+ my $user = $result->{'user'};
+
+ # You can restrict people in a particular group from logging in using
+ # BrowserID by making that group a member of a group called
+ # "no-browser-id".
+ #
+ # If you have your "createemailregexp" set up in such a way that a
+ # newly-created account is a member of "no-browser-id", this code will
+ # create an account for them and then fail their login. Which isn't
+ # great, but they can still use normal-Bugzilla-login password
+ # recovery.
+ if ($user->in_group('no-browser-id')) {
+ # We use a custom error here, for greater clarity, rather than
+ # returning a failure code.
+ ThrowUserError('browserid_account_too_powerful');
+ }
+
+ $login_data->{'user'} = $user;
+ $login_data->{'user_id'} = $user->id;
+
+ return $login_data;
+ }
+ else {
+ return { failure => AUTH_LOGINFAILED };
+ }
+}
+
+# Pinched from Bugzilla::Auth::Login::CGI
+sub fail_nodata {
+ my ($self) = @_;
+ my $cgi = Bugzilla->cgi;
+ my $template = Bugzilla->template;
+
+ if (Bugzilla->usage_mode != USAGE_MODE_BROWSER) {
+ ThrowUserError('login_required');
+ }
+
+ print $cgi->header();
+ $template->process("account/auth/login.html.tmpl",
+ { 'target' => $cgi->url(-relative=>1) })
+ || ThrowTemplateError($template->error());
+ exit;
+}
+
+1;
diff --git a/extensions/BrowserID/template/en/default/hook/account/auth/login-additional_methods.html.tmpl b/extensions/BrowserID/template/en/default/hook/account/auth/login-additional_methods.html.tmpl
new file mode 100644
index 000000000..2b6f4b85a
--- /dev/null
+++ b/extensions/BrowserID/template/en/default/hook/account/auth/login-additional_methods.html.tmpl
@@ -0,0 +1,57 @@
+[% IF Param('user_info_class').split(',').contains('BrowserID') %]
+
+[% USE Bugzilla %]
+[% cgi = Bugzilla.cgi %]
+
+<script src="https://browserid.org/include.js" type="text/javascript"></script>
+
+<script type="text/javascript">
+function browserid_sign_in() {
+ navigator.id.getVerifiedEmail(function(assertion) {
+ if (assertion) {
+ // This code will be invoked once the user has successfully
+ // selected an email address they control to sign in with.
+ var browseridForm = document.createElement('form');
+ browseridForm.action = '[% target FILTER js %]';
+ browseridForm.method = 'POST';
+ browseridForm.style.display = 'none';
+
+ var tokenField = document.createElement('input');
+ tokenField.type = 'hidden';
+ tokenField.name = 'token';
+ tokenField.value = '[% issue_hash_token(['login']) FILTER js %]';
+ browseridForm.appendChild(tokenField);
+
+ var assertionField = document.createElement('input');
+ assertionField.type = 'hidden';
+ assertionField.name = 'browserid_assertion';
+ assertionField.value = assertion;
+ browseridForm.appendChild(assertionField);
+
+ var hidden_fields =[];
+ var field_count = 0;
+ [% FOREACH field = cgi.param() %]
+ [% NEXT IF field.search("^(Bugzilla_(login|password|restrictlogin)|token|browserid_assertion)$") %]
+ [% FOREACH mvalue = cgi.param(field).slice(0) %]
+ hidden_fields[field_count] = document.createElement('input');
+ hidden_fields[field_count].type = 'hidden';
+ hidden_fields[field_count].name = '[% field FILTER js %]';
+ hidden_fields[field_count].value = '[% mvalue FILTER html_linebreak FILTER js %]';
+ browseridForm.appendChild(hidden_fields[field_count]);
+ [% END %]
+ field_count++;
+ [% END %]
+
+ document.body.appendChild(browseridForm);
+ browseridForm.submit();
+ return true;
+ }
+ });
+}
+</script>
+
+<p>
+Or, log in with BrowserID:
+<img src="extensions/BrowserID/web/sign_in_orange.png" onclick="browserid_sign_in()">
+</p>
+[% END %]
diff --git a/extensions/BrowserID/template/en/default/hook/account/auth/login-small-additional_methods.html.tmpl b/extensions/BrowserID/template/en/default/hook/account/auth/login-small-additional_methods.html.tmpl
new file mode 100644
index 000000000..444bc1d14
--- /dev/null
+++ b/extensions/BrowserID/template/en/default/hook/account/auth/login-small-additional_methods.html.tmpl
@@ -0,0 +1,46 @@
+[% IF Param('user_info_class').split(',').contains('BrowserID') %]
+<script src="https://browserid.org/include.js" type="text/javascript"></script>
+
+<script type="text/javascript">
+function browserid_sign_in() {
+ navigator.id.getVerifiedEmail(function(assertion) {
+ if (assertion) {
+ // This code will be invoked once the user has successfully
+ // selected an email address they control to sign in with.
+ var browseridForm = document.createElement('form');
+ browseridForm.action = '[% login_target FILTER js %]';
+ browseridForm.method = 'POST';
+ browseridForm.style.display = 'none';
+
+ var tokenField = document.createElement('input');
+ tokenField.type = 'hidden';
+ tokenField.name = 'token';
+ tokenField.value = '[% issue_hash_token(['login']) FILTER js %]';
+ browseridForm.appendChild(tokenField);
+
+ var assertionField = document.createElement('input');
+ assertionField.type = 'hidden';
+ assertionField.name = 'browserid_assertion';
+ assertionField.value = assertion;
+ browseridForm.appendChild(assertionField);
+
+ document.body.appendChild(browseridForm);
+ browseridForm.submit();
+ return true;
+ }
+ });
+}
+YAHOO.util.Event.addListener('login_link[% qs_suffix FILTER js %]','click', function () {
+ var login_link = YAHOO.util.Dom.get('browserid_mini_login[% qs_suffix FILTER js %]');
+ YAHOO.util.Dom.removeClass(login_link, 'bz_default_hidden');
+});
+YAHOO.util.Event.addListener('hide_mini_login[% qs_suffix FILTER js %]','click', function () {
+ var login_link = YAHOO.util.Dom.get('browserid_mini_login[% qs_suffix FILTER js %]');
+ YAHOO.util.Dom.addClass(login_link, 'bz_default_hidden');
+});
+</script>
+
+<span id="browserid_mini_login[% qs_suffix FILTER html %]" class="bz_default_hidden">
+ <img src="extensions/BrowserID/web/sign_in_orange.png" onclick="browserid_sign_in()" style="margin-bottom: -8px"> or
+</span>
+[% END %]
diff --git a/extensions/BrowserID/template/en/default/hook/account/create-additional_methods.html.tmpl b/extensions/BrowserID/template/en/default/hook/account/create-additional_methods.html.tmpl
new file mode 100644
index 000000000..6f75f5cd7
--- /dev/null
+++ b/extensions/BrowserID/template/en/default/hook/account/create-additional_methods.html.tmpl
@@ -0,0 +1,31 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF Param('user_info_class').split(',').contains('BrowserID') %]
+<script type="text/javascript">
+function browserid_create_account() {
+ navigator.id.getVerifiedEmail(function(assertion) {
+ if (assertion) {
+ // This code will be invoked once the user has successfully
+ // selected an email address they control to sign in with.
+ document.getElementById('browserid_assertion').value = assertion;
+ document.getElementById('browserid_form').submit();
+ return true;
+ }
+ });
+}
+</script>
+
+Or, use your BrowserID account:
+<img src="extensions/BrowserID/web/sign_in_orange.png" onclick="browserid_create_account()">
+
+<form id="browserid_form" method="POST" action="index.cgi">
+ <input type="hidden" name="token" value="[% issue_hash_token(['login']) FILTER html %]">
+ <input type="hidden" name="browserid_assertion" id="browserid_assertion" value="">
+</form>
+[% END %]
diff --git a/extensions/BrowserID/template/en/default/hook/global/user-error-errors.html.tmpl b/extensions/BrowserID/template/en/default/hook/global/user-error-errors.html.tmpl
new file mode 100644
index 000000000..ce872abda
--- /dev/null
+++ b/extensions/BrowserID/template/en/default/hook/global/user-error-errors.html.tmpl
@@ -0,0 +1,10 @@
+[% IF error == "browserid_account_too_powerful" %]
+ [% title = "Account Too Powerful" %]
+ Your account is a member of a group which is not permitted to use
+ BrowserID to log in. Please log in with your [% terms.Bugzilla %] username
+ and password.
+ <br><br>
+ (BrowserID logins are disabled for accounts which are members of certain
+ particularly sensitive groups, while we gain experience with the
+ technology.)
+[% END %]
diff --git a/extensions/BrowserID/web/sign_in_orange.png b/extensions/BrowserID/web/sign_in_orange.png
new file mode 100644
index 000000000..65ccda473
--- /dev/null
+++ b/extensions/BrowserID/web/sign_in_orange.png
Binary files differ
diff --git a/extensions/BzAPI/Config.pm b/extensions/BzAPI/Config.pm
new file mode 100644
index 000000000..0de081097
--- /dev/null
+++ b/extensions/BzAPI/Config.pm
@@ -0,0 +1,63 @@
+# ***** BEGIN LICENSE BLOCK *****
+# Version: MPL 1.1/GPL 2.0/LGPL 2.1
+#
+# The contents of this file are subject to the Mozilla Public License Version
+# 1.1 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+# http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS IS" basis,
+# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+# for the specific language governing rights and limitations under the
+# License.
+#
+# The Original Code is the BzAPI 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 <gerv@gerv.net>
+#
+# Alternatively, the contents of this file may be used under the terms of
+# either the GNU General Public License Version 2 or later (the "GPL"), or
+# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+# in which case the provisions of the GPL or the LGPL are applicable instead
+# of those above. If you wish to allow use of your version of this file only
+# under the terms of either the GPL or the LGPL, and not to allow others to
+# use your version of this file under the terms of the MPL, indicate your
+# decision by deleting the provisions above and replace them with the notice
+# and other provisions required by the GPL or the LGPL. If you do not delete
+# the provisions above, a recipient may use your version of this file under
+# the terms of any one of the MPL, the GPL or the LGPL.
+#
+# ***** END LICENSE BLOCK *****
+
+package Bugzilla::Extension::BzAPI;
+use strict;
+
+use constant NAME => 'BzAPI';
+
+use constant REQUIRED_MODULES => [
+ {
+ package => 'SOAP-Lite',
+ module => 'SOAP::Lite',
+ # 0.710.04 is required for correct UTF-8 handling, but .04 and .05 are
+ # affected by bug 468009.
+ version => '0.710.06',
+ },
+ {
+ package => 'Test-Taint',
+ module => 'Test::Taint',
+ version => 0,
+ },
+ {
+ package => 'JSON',
+ module => 'JSON',
+ version => 0,
+ },
+];
+
+__PACKAGE__->NAME;
diff --git a/extensions/BzAPI/Extension.pm b/extensions/BzAPI/Extension.pm
new file mode 100644
index 000000000..aeaa0bce4
--- /dev/null
+++ b/extensions/BzAPI/Extension.pm
@@ -0,0 +1,71 @@
+# ***** BEGIN LICENSE BLOCK *****
+# Version: MPL 1.1/GPL 2.0/LGPL 2.1
+#
+# The contents of this file are subject to the Mozilla Public License Version
+# 1.1 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+# http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS IS" basis,
+# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+# for the specific language governing rights and limitations under the
+# License.
+#
+# The Original Code is the BzAPI 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 <gerv@gerv.net>
+#
+# Alternatively, the contents of this file may be used under the terms of
+# either the GNU General Public License Version 2 or later (the "GPL"), or
+# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+# in which case the provisions of the GPL or the LGPL are applicable instead
+# of those above. If you wish to allow use of your version of this file only
+# under the terms of either the GPL or the LGPL, and not to allow others to
+# use your version of this file under the terms of the MPL, indicate your
+# decision by deleting the provisions above and replace them with the notice
+# and other provisions required by the GPL or the LGPL. If you do not delete
+# the provisions above, a recipient may use your version of this file under
+# the terms of any one of the MPL, the GPL or the LGPL.
+#
+# ***** END LICENSE BLOCK *****
+
+package Bugzilla::Extension::BzAPI;
+use strict;
+use base qw(Bugzilla::Extension);
+
+our $VERSION = '0.1';
+
+# Add JSON filter for JSON templates
+sub template_before_create {
+ my ($self, $args) = @_;
+ my $config = $args->{'config'};
+
+ $config->{'FILTERS'}->{'json'} = sub {
+ my ($var) = @_;
+ $var =~ s/([\\\"\/])/\\$1/g;
+ $var =~ s/\n/\\n/g;
+ $var =~ s/\r/\\r/g;
+ $var =~ s/\f/\\f/g;
+ $var =~ s/\t/\\t/g;
+ return $var;
+ };
+}
+
+sub template_before_process {
+ my ($self, $args) = @_;
+ my $vars = $args->{'vars'};
+ my $file = $args->{'file'};
+
+ if ($file =~ /config\.json\.tmpl$/) {
+ $vars->{'initial_status'} = Bugzilla::Status->can_change_to;
+ $vars->{'status_objects'} = [Bugzilla::Status->get_all];
+ }
+}
+
+__PACKAGE__->NAME;
diff --git a/extensions/BzAPI/template/en/default/config.json.tmpl b/extensions/BzAPI/template/en/default/config.json.tmpl
new file mode 100644
index 000000000..9c6852346
--- /dev/null
+++ b/extensions/BzAPI/template/en/default/config.json.tmpl
@@ -0,0 +1,315 @@
+[%# 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 <gerv@gerv.net>
+ #%]
+
+[%
+ # Pinched from Bugzilla/API/Model/Utils.pm in BzAPI - need to keep in sync
+OLD2NEW = {
+ 'opendate' => 'creation_time', # query
+ 'creation_ts' => 'creation_time',
+ 'changeddate' => 'last_change_time', # query
+ 'delta_ts' => 'last_change_time',
+ 'bug_id' => 'id',
+ 'rep_platform' => 'platform',
+ 'bug_severity' => 'severity',
+ 'bug_status' => 'status',
+ 'short_desc' => 'summary',
+ 'short_short_desc' => 'summary',
+ 'bug_file_loc' => 'url',
+ 'status_whiteboard' => 'whiteboard',
+ 'reporter' => 'creator',
+ 'reporter_realname' => 'creator_realname',
+ 'cclist_accessible' => 'is_cc_accessible',
+ 'reporter_accessible' => 'is_creator_accessible',
+ 'everconfirmed' => 'is_confirmed',
+ 'dependson' => 'depends_on',
+ 'blocked' => 'blocks',
+ 'attachment' => 'attachments',
+ 'flag' => 'flags',
+ 'flagtypes.name' => 'flag',
+ 'bug_group' => 'group',
+ 'group' => 'groups',
+ 'longdesc' => 'comment',
+ 'bug_file_loc_type' => 'url_type',
+ 'bugidtype' => 'id_mode',
+ 'longdesc_type' => 'comment_type',
+ 'short_desc_type' => 'summary_type',
+ 'status_whiteboard_type' => 'whiteboard_type',
+ 'emailassigned_to1' => 'email1_assigned_to',
+ 'emailassigned_to2' => 'email2_assigned_to',
+ 'emailcc1' => 'email1_cc',
+ 'emailcc2' => 'email2_cc',
+ 'emailqa_contact1' => 'email1_qa_contact',
+ 'emailqa_contact2' => 'email2_qa_contact',
+ 'emailreporter1' => 'email1_creator',
+ 'emailreporter2' => 'email2_creator',
+ 'emaillongdesc1' => 'email1_comment_creator',
+ 'emaillongdesc2' => 'email2_comment_creator',
+ 'emailtype1' => 'email1_type',
+ 'emailtype2' => 'email2_type',
+ 'chfieldfrom' => 'changed_after',
+ 'chfieldto' => 'changed_before',
+ 'chfield' => 'changed_field',
+ 'chfieldvalue' => 'changed_field_to',
+ 'deadlinefrom' => 'deadline_after',
+ 'deadlineto' => 'deadline_before',
+ 'attach_data.thedata' => 'attachment.data',
+ 'longdescs.isprivate' => 'comment.is_private',
+ 'commenter' => 'comment.creator',
+ 'flagtypes.name' => 'flag',
+ 'requestees.login_name' => 'flag.requestee',
+ 'setters.login_name' => 'flag.setter',
+ 'days_elapsed' => 'idle',
+ 'owner_idle_time' => 'assignee_idle',
+ 'dup_id' => 'dupe_of',
+ 'isopened' => 'is_open',
+ 'flag_type' => 'flag_types',
+ 'token' => 'update_token'
+};
+
+OLDATTACH2NEW = {
+ 'submitter' => 'attacher',
+ 'description' => 'description',
+ 'filename' => 'file_name',
+ 'delta_ts' => 'last_change_time',
+ 'isobsolete' => 'is_obsolete',
+ 'ispatch' => 'is_patch',
+ 'isprivate' => 'is_private',
+ 'mimetype' => 'content_type',
+ 'contenttypeentry' => 'content_type',
+ 'date' => 'creation_time',
+ 'attachid' => 'id',
+ 'desc' => 'description',
+ 'flag' => 'flags',
+ 'type' => 'content_type',
+ 'token' => 'update_token'
+};
+
+%]
+
+[%# Add attachment stuff to the main hash - but with right prefix. (This is
+ # the way the code is structured in BzAPI, and changing it makes it harder
+ # to keep the two in sync.)
+ #%]
+[% FOREACH entry IN OLDATTACH2NEW %]
+ [% newkey = 'attachments.' _ entry.key %]
+ [% OLD2NEW.${newkey} = 'attachment.' _ OLDATTACH2NEW.${entry.key} %]
+[% END %]
+
+[% all_visible_flag_types = {} %]
+
+{
+ "version": "[% constants.BUGZILLA_VERSION FILTER json %]",
+ "maintainer": "[% Param('maintainer') FILTER json %]",
+ "announcement": "[% Param('announcehtml') FILTER json %]",
+ "max_attachment_size": [% (Param('maxattachmentsize') * 1000) FILTER json %],
+
+[% IF Param('useclassification') %]
+ [% cl_name_for = {} %]
+ "classification": {
+ [% FOREACH cl IN user.get_selectable_classifications() %]
+ [% cl_name_for.${cl.id} = cl.name %]
+ "[% cl.name FILTER json %]": {
+ "id": [% cl.id FILTER json %],
+ "description": "[% cl.description FILTER json %]",
+ "products": [
+ [% FOREACH product IN user.get_selectable_products(cl.id) %]
+ "[% product.name FILTER json %]"[% ',' UNLESS loop.last() %]
+ [% END %]
+ ]
+ }[% ',' UNLESS loop.last() %]
+ [% END %]
+ },
+[% END %]
+
+ "product": {
+ [% FOREACH product = products %]
+ "[% product.name FILTER json %]": {
+ "id": [% product.id FILTER json %],
+ "description": "[% product.description FILTER json %]",
+ "is_active": [% product.isactive ? "true" : "false" %],
+ "is_permitting_unconfirmed": [% product.allows_unconfirmed ? "true" : "false" %],
+[% IF Param('useclassification') %]
+ "classification": "[% cl_name_for.${product.classification_id} FILTER json %]",
+[% END %]
+ "component": {
+ [% FOREACH component = product.components %]
+ "[% component.name FILTER json %]": {
+ "id": [% component.id FILTER json %],
+[% IF show_flags %]
+ "flag_type": [
+ [% flag_types =
+ component.flag_types(is_active=>1).bug.merge(component.flag_types(is_active=>1).attachment) %]
+ [%-# "first" flag used to get commas right; can't use loop.last() in case
+ # last flag is inactive %]
+ [% first = 1 %]
+ [% FOREACH flag_type = flag_types %]
+ [% all_visible_flag_types.${flag_type.id} = flag_type %]
+ [% ',' UNLESS first %][% flag_type.id FILTER json %][% first = 0 %]
+ [% END %]],
+[% END %]
+ "description": "[% component.description FILTER json %]"
+ } [% ',' UNLESS loop.last() %]
+ [% END %]
+ },
+ "version": [
+ [% FOREACH version = product.versions %]
+ "[% version.name FILTER json %]"[% ',' UNLESS loop.last() %]
+ [% END %]
+ ],
+
+[% IF Param('usetargetmilestone') %]
+ "default_target_milestone": "[% product.defaultmilestone FILTER json %]",
+ "target_milestone": [
+ [% FOREACH milestone = product.milestones %]
+ "[% milestone.name FILTER json %]"[% ',' UNLESS loop.last() %]
+ [% END %]
+ ],
+[% END %]
+
+ "group": [
+ [% FOREACH group = product.groups_valid %]
+ [% group.id FILTER json %][% ',' UNLESS loop.last() %]
+ [% END %]
+ ]
+ }[% ',' UNLESS loop.last() %]
+ [% END %]
+ },
+
+ "group": {
+ [% FOREACH group = product.groups_valid %]
+ "[% group.id FILTER json %]": {
+ "name": "[% group.name FILTER json %]",
+ "description": "[% group.description FILTER json %]",
+ "is_accepting_bugs": [% group.is_bug_group ? 'true' : 'false' %],
+ "is_active": [% group.is_active ? 'true' : 'false' %]
+ }[% ',' UNLESS loop.last() %]
+ [% END %]
+ },
+
+[% IF show_flags %]
+ "flag_type": {
+ [% FOREACH flag_type = all_visible_flag_types.values.sort('name') %]
+ "[%+ flag_type.id FILTER json %]": {
+ "name": "[% flag_type.name FILTER json %]",
+ "description": "[% flag_type.description FILTER json %]",
+ [% IF user.in_group("editcomponents") %]
+ [% IF flag_type.request_group_id %]
+ "request_group": [% flag_type.request_group_id FILTER json %],
+ [% END %]
+ [% IF flag_type.grant_group_id %]
+ "grant_group": [% flag_type.grant_group_id FILTER json %],
+ [% END %]
+ [% END %]
+ "is_for_bugs": [% flag_type.target_type == "bug" ? 'true' : 'false' %],
+ "is_requestable": [% flag_type.is_requestable ? 'true' : 'false' %],
+ "is_specifically_requestable": [% flag_type.is_requesteeble ? 'true' : 'false' %],
+ "is_multiplicable": [% flag_type.is_multiplicable ? 'true' : 'false' %]
+ }[% ',' UNLESS loop.last() %]
+ [% END %]
+ },
+[% END %]
+
+ [% PROCESS "global/field-descs.none.tmpl" %]
+
+ [%# Put custom field value data where below loop expects to find it %]
+ [% FOREACH cf = custom_fields %]
+ [% ${cf.name} = [] %]
+ [% FOREACH value = cf.legal_values %]
+ [% ${cf.name}.push(value.name) %]
+ [% END %]
+ [% END %]
+
+ [%# Built-in fields do not have type IDs. There aren't ID values for all
+ # the types of the built-in fields, but we do what we can, and leave the
+ # rest as "0" (unknown).
+ #%]
+ [% type_id_for = {
+ "id" => 6,
+ "summary" => 1,
+ "classification" => 2,
+ "version" => 2,
+ "url" => 1,
+ "whiteboard" => 1,
+ "keywords" => 3,
+ "component" => 2,
+ "attachment.description" => 1,
+ "attachment.file_name" => 1,
+ "attachment.content_type" => 1,
+ "target_milestone" => 2,
+ "comment" => 4,
+ "alias" => 1,
+ "deadline" => 5,
+ } %]
+
+ "field": {
+ [% FOREACH item = field %]
+ [% newname = OLD2NEW.${item.name} || item.name %]
+ "[% newname FILTER json %]": {
+ "description": "[% (field_descs.${item.name} OR
+ item.description) FILTER json %]",
+ "is_active": [% field.obsolete ? "false" : "true" %],
+ [% blacklist = ["version", "group", "product", "component"] %]
+ [% IF ${newname} AND NOT blacklist.contains(newname) %]
+ "values": [
+ [% FOREACH value = ${newname} %]
+ "[% value FILTER json %]"[% ',' UNLESS loop.last() %]
+ [% END %]
+ ],
+ [% END %]
+ [% paramname = newname.replace("_", "") %] [%# For op_sys... %]
+ [% IF paramname != "query" AND Param('default' _ paramname) %]
+ "default": "[% Param('default' _ paramname) %]",
+ [% END %]
+ [%-# The 'status' hash has a lot of extra stuff %]
+ [% IF newname == "status" %]
+ "open": [
+ [% FOREACH value = open_status %]
+ "[% value FILTER json %]"[% ',' UNLESS loop.last() %]
+ [% END %]
+ ],
+ "closed": [
+ [% FOREACH value = closed_status %]
+ "[% value FILTER json %]"[% ',' UNLESS loop.last() %]
+ [% END %]
+ ],
+ "transitions": {
+ "{Start}": [
+ [% FOREACH target = initial_status %]
+ "[% target.name FILTER json %]"[% ',' UNLESS loop.last() %]
+ [% END %]
+ ],
+ [% FOREACH status = status_objects %]
+ [% targets = status.can_change_to() %]
+ "[% status.name FILTER json %]": [
+ [% FOREACH target = targets %]
+ "[% target.name FILTER json %]"[% ',' UNLESS loop.last() %]
+ [% END %]
+ ][% ',' UNLESS loop.last() %]
+ [% END %]
+ },
+ [% END %]
+ [% IF newname.match("^cf_") %]
+ "is_on_bug_entry": [% item.enter_bug ? 'true' : 'false' %],
+ [% END %]
+ "type": [% item.type || type_id_for.$newname || 0 FILTER json %]
+ }[% ',' UNLESS loop.last() %]
+ [% END %]
+ }
+}
diff --git a/extensions/ComponentWatching/Config.pm b/extensions/ComponentWatching/Config.pm
new file mode 100644
index 000000000..560b5c3c5
--- /dev/null
+++ b/extensions/ComponentWatching/Config.pm
@@ -0,0 +1,12 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::ComponentWatching;
+use strict;
+use constant NAME => 'ComponentWatching';
+
+__PACKAGE__->NAME;
diff --git a/extensions/ComponentWatching/Extension.pm b/extensions/ComponentWatching/Extension.pm
new file mode 100644
index 000000000..e8e62b8b6
--- /dev/null
+++ b/extensions/ComponentWatching/Extension.pm
@@ -0,0 +1,499 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::ComponentWatching;
+use strict;
+use base qw(Bugzilla::Extension);
+
+use Bugzilla::Constants;
+use Bugzilla::Error;
+use Bugzilla::Group;
+use Bugzilla::User;
+use Bugzilla::User::Setting;
+use Bugzilla::Util qw(trim);
+
+our $VERSION = '2';
+
+use constant REL_COMPONENT_WATCHER => 15;
+
+#
+# installation
+#
+
+sub db_schema_abstract_schema {
+ my ($self, $args) = @_;
+ $args->{'schema'}->{'component_watch'} = {
+ FIELDS => [
+ user_id => {
+ TYPE => 'INT3',
+ NOTNULL => 1,
+ REFERENCES => {
+ TABLE => 'profiles',
+ COLUMN => 'userid',
+ DELETE => 'CASCADE',
+ }
+ },
+ component_id => {
+ TYPE => 'INT2',
+ NOTNULL => 0,
+ REFERENCES => {
+ TABLE => 'components',
+ COLUMN => 'id',
+ DELETE => 'CASCADE',
+ }
+ },
+ product_id => {
+ TYPE => 'INT2',
+ NOTNULL => 0,
+ REFERENCES => {
+ TABLE => 'products',
+ COLUMN => 'id',
+ DELETE => 'CASCADE',
+ }
+ },
+ ],
+ };
+}
+
+sub install_update_db {
+ my $dbh = Bugzilla->dbh;
+ $dbh->bz_add_column(
+ 'components',
+ 'watch_user',
+ {
+ TYPE => 'INT3',
+ #REFERENCES => {
+ # TABLE => 'profiles',
+ # COLUMN => 'userid',
+ # DELETE => 'SET NULL',
+ #}
+ }
+ );
+}
+
+#
+# templates
+#
+
+sub template_before_create {
+ my ($self, $args) = @_;
+ my $config = $args->{config};
+ my $constants = $config->{CONSTANTS};
+ $constants->{REL_COMPONENT_WATCHER} = REL_COMPONENT_WATCHER;
+}
+
+#
+# user-watch
+#
+
+BEGIN {
+ *Bugzilla::Component::watch_user = \&_component_watch_user;
+}
+
+sub _component_watch_user {
+ my ($self) = @_;
+ return unless $self->{watch_user};
+ $self->{watch_user_object} ||= Bugzilla::User->new($self->{watch_user});
+ return $self->{watch_user_object};
+}
+
+sub object_columns {
+ my ($self, $args) = @_;
+ my $class = $args->{class};
+ my $columns = $args->{columns};
+ return unless $class->isa('Bugzilla::Component');
+
+ push(@$columns, 'watch_user');
+}
+
+sub object_update_columns {
+ my ($self, $args) = @_;
+ my $object = $args->{object};
+ my $columns = $args->{columns};
+ return unless $object->isa('Bugzilla::Component');
+
+ push(@$columns, 'watch_user');
+
+ # editcomponents.cgi doesn't call set_all, so we have to do this here
+ my $input = Bugzilla->input_params;
+ $object->set('watch_user', $input->{watch_user});
+}
+
+sub object_validators {
+ my ($self, $args) = @_;
+ my $class = $args->{class};
+ my $validators = $args->{validators};
+ return unless $class->isa('Bugzilla::Component');
+
+ $validators->{watch_user} = \&_check_watch_user;
+}
+
+sub object_before_create {
+ my ($self, $args) = @_;
+ my $class = $args->{class};
+ my $params = $args->{params};
+ return unless $class->isa('Bugzilla::Component');
+
+ my $input = Bugzilla->input_params;
+ $params->{watch_user} = $input->{watch_user};
+}
+
+sub object_end_of_update {
+ my ($self, $args) = @_;
+ my $object = $args->{object};
+ my $old_object = $args->{old_object};
+ my $changes = $args->{changes};
+ return unless $object->isa('Bugzilla::Component');
+
+ my $old_id = $old_object->watch_user ? $old_object->watch_user->id : 0;
+ my $new_id = $object->watch_user ? $object->watch_user->id : 0;
+ return if $old_id == $new_id;
+
+ $changes->{watch_user} = [ $old_id ? $old_id : undef, $new_id ? $new_id : undef ];
+}
+
+sub _check_watch_user {
+ my ($self, $value, $field) = @_;
+ return 0;
+ $value = trim($value || '');
+ if ($value eq '') {
+ ThrowUserError('component_watch_missing_watch_user');
+ }
+ if ($value !~ /\.bugs$/i) {
+ ThrowUserError('component_watch_invalid_watch_user');
+ }
+ return Bugzilla::User->check($value)->id;
+}
+
+#
+# preferences
+#
+
+sub user_preferences {
+ my ($self, $args) = @_;
+ my $tab = $args->{'current_tab'};
+ return unless $tab eq 'component_watch';
+
+ my $save = $args->{'save_changes'};
+ my $handled = $args->{'handled'};
+ my $vars = $args->{'vars'};
+ my $user = Bugzilla->user;
+ my $input = Bugzilla->input_params;
+
+ if ($save) {
+ my ($sth, $sthAdd, $sthDel);
+
+ if ($input->{'add'} && $input->{'add_product'}) {
+ # add watch
+
+ my $productName = $input->{'add_product'};
+ my $ra_componentNames = $input->{'add_component'};
+ $ra_componentNames = [$ra_componentNames || ''] unless ref($ra_componentNames);
+
+ # load product and verify access
+ my $product = Bugzilla::Product->new({ name => $productName });
+ unless ($product && $user->can_access_product($product)) {
+ ThrowUserError('product_access_denied', { product => $productName });
+ }
+
+ if (grep { $_ eq '' } @$ra_componentNames) {
+ # watching a product
+ _addProductWatch($user, $product);
+
+ } else {
+ # watching specific components
+ foreach my $componentName (@$ra_componentNames) {
+ my $component = Bugzilla::Component->new({ name => $componentName, product => $product });
+ unless ($component) {
+ ThrowUserError('product_access_denied', { product => $productName });
+ }
+ _addComponentWatch($user, $component);
+ }
+ }
+
+ _addDefaultSettings($user);
+
+ } else {
+ # remove watch(s)
+
+ foreach my $name (keys %$input) {
+ if ($name =~ /^del_(\d+)$/) {
+ _deleteProductWatch($user, $1);
+ } elsif ($name =~ /^del_(\d+)_(\d+)$/) {
+ _deleteComponentWatch($user, $1, $2);
+ }
+ }
+ }
+ }
+
+ $vars->{'add_product'} = $input->{'product'};
+ $vars->{'add_component'} = $input->{'component'};
+ $vars->{'watches'} = _getWatches($user);
+ $vars->{'user_watches'} = _getUserWatches($user);
+
+ $$handled = 1;
+}
+
+#
+# bugmail
+#
+
+sub bugmail_recipients {
+ my ($self, $args) = @_;
+ my $bug = $args->{'bug'};
+ my $recipients = $args->{'recipients'};
+ my $diffs = $args->{'diffs'};
+
+ my ($oldProductId, $newProductId) = ($bug->product_id, $bug->product_id);
+ my ($oldComponentId, $newComponentId) = ($bug->component_id, $bug->component_id);
+
+ # notify when the product/component is switch from one being watched
+ if (@$diffs) {
+ # we need the product to process the component, so scan for that first
+ my $product;
+ foreach my $ra (@$diffs) {
+ next if !(exists $ra->{'old'}
+ && exists $ra->{'field_name'});
+ if ($ra->{'field_name'} eq 'product') {
+ $product = Bugzilla::Product->new({ name => $ra->{'old'} });
+ $oldProductId = $product->id;
+ }
+ }
+ if (!$product) {
+ $product = Bugzilla::Product->new($oldProductId);
+ }
+ foreach my $ra (@$diffs) {
+ next if !(exists $ra->{'old'}
+ && exists $ra->{'field_name'});
+ if ($ra->{'field_name'} eq 'component') {
+ my $component = Bugzilla::Component->new({ name => $ra->{'old'}, product => $product });
+ $oldComponentId = $component->id;
+ }
+ }
+ }
+
+ # add component watchers
+ my $dbh = Bugzilla->dbh;
+ my $sth = $dbh->prepare("
+ SELECT user_id
+ FROM component_watch
+ WHERE ((product_id = ? OR product_id = ?) AND component_id IS NULL)
+ OR (component_id = ? OR component_id = ?)
+ ");
+ $sth->execute($oldProductId, $newProductId, $oldComponentId, $newComponentId);
+ while (my ($uid) = $sth->fetchrow_array) {
+ if (!exists $recipients->{$uid}) {
+ $recipients->{$uid}->{+REL_COMPONENT_WATCHER} = Bugzilla::BugMail::BIT_WATCHING();
+ }
+ }
+
+ # add component watchers from watch-users
+ my $uidList = join(',', keys %$recipients);
+ $sth = $dbh->prepare("
+ SELECT component_watch.user_id
+ FROM components
+ INNER JOIN component_watch ON component_watch.component_id = components.id
+ WHERE components.watch_user in ($uidList)
+ ");
+ $sth->execute();
+ while (my ($uid) = $sth->fetchrow_array) {
+ if (!exists $recipients->{$uid}) {
+ $recipients->{$uid}->{+REL_COMPONENT_WATCHER} = Bugzilla::BugMail::BIT_WATCHING();
+ }
+ }
+
+ # add watch-users from component watchers
+ $sth = $dbh->prepare("
+ SELECT watch_user
+ FROM components
+ WHERE (id = ? OR id = ?)
+ AND (watch_user IS NOT NULL)
+ ");
+ $sth->execute($oldComponentId, $newComponentId);
+ while (my ($uid) = $sth->fetchrow_array) {
+ if (!exists $recipients->{$uid}) {
+ $recipients->{$uid}->{+REL_COMPONENT_WATCHER} = Bugzilla::BugMail::BIT_DIRECT();
+ }
+ }
+}
+
+sub bugmail_relationships {
+ my ($self, $args) = @_;
+ my $relationships = $args->{relationships};
+ $relationships->{+REL_COMPONENT_WATCHER} = 'Component-Watcher';
+}
+
+#
+# db
+#
+
+sub _getWatches {
+ my ($user) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ my $sth = $dbh->prepare("
+ SELECT product_id, component_id
+ FROM component_watch
+ WHERE user_id = ?
+ ");
+ $sth->execute($user->id);
+ my @watches;
+ while (my ($productId, $componentId) = $sth->fetchrow_array) {
+ my $product = Bugzilla::Product->new($productId);
+ next unless $product && $user->can_access_product($product);
+
+ my %watch = ( product => $product );
+ if ($componentId) {
+ my $component = Bugzilla::Component->new($componentId);
+ next unless $component;
+ $watch{'component'} = $component;
+ }
+
+ push @watches, \%watch;
+ }
+
+ @watches = sort {
+ $a->{'product'}->name cmp $b->{'product'}->name
+ || $a->{'component'}->name cmp $b->{'component'}->name
+ } @watches;
+
+ return \@watches;
+}
+
+sub _getUserWatches {
+ my ($user) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ my $sth = $dbh->prepare("
+ SELECT components.product_id, components.id as component, profiles.login_name
+ FROM watch
+ INNER JOIN components ON components.watch_user = watched
+ INNER JOIN profiles ON profiles.userid = watched
+ WHERE watcher = ?
+ ");
+ $sth->execute($user->id);
+ my @watches;
+ while (my ($productId, $componentId, $login) = $sth->fetchrow_array) {
+ my $product = Bugzilla::Product->new($productId);
+ next unless $product && $user->can_access_product($product);
+
+ my %watch = (
+ product => $product,
+ component => Bugzilla::Component->new($componentId),
+ user => Bugzilla::User->check($login),
+ );
+ push @watches, \%watch;
+ }
+
+ @watches = sort {
+ $a->{'product'}->name cmp $b->{'product'}->name
+ || $a->{'component'}->name cmp $b->{'component'}->name
+ } @watches;
+
+ return \@watches;
+}
+
+sub _addProductWatch {
+ my ($user, $product) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ my $sth = $dbh->prepare("
+ SELECT 1
+ FROM component_watch
+ WHERE user_id = ? AND product_id = ? AND component_id IS NULL
+ ");
+ $sth->execute($user->id, $product->id);
+ return if $sth->fetchrow_array;
+
+ $sth = $dbh->prepare("
+ DELETE FROM component_watch
+ WHERE user_id = ? AND product_id = ?
+ ");
+ $sth->execute($user->id, $product->id);
+
+ $sth = $dbh->prepare("
+ INSERT INTO component_watch(user_id, product_id)
+ VALUES (?, ?)
+ ");
+ $sth->execute($user->id, $product->id);
+}
+
+sub _addComponentWatch {
+ my ($user, $component) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ my $sth = $dbh->prepare("
+ SELECT 1
+ FROM component_watch
+ WHERE user_id = ?
+ AND (component_id = ? OR (product_id = ? AND component_id IS NULL))
+ ");
+ $sth->execute($user->id, $component->id, $component->product_id);
+ return if $sth->fetchrow_array;
+
+ $sth = $dbh->prepare("
+ INSERT INTO component_watch(user_id, product_id, component_id)
+ VALUES (?, ?, ?)
+ ");
+ $sth->execute($user->id, $component->product_id, $component->id);
+}
+
+sub _deleteProductWatch {
+ my ($user, $productId) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ my $sth = $dbh->prepare("
+ DELETE FROM component_watch
+ WHERE user_id = ? AND product_id = ? AND component_id IS NULL
+ ");
+ $sth->execute($user->id, $productId);
+}
+
+sub _deleteComponentWatch {
+ my ($user, $productId, $componentId) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ my $sth = $dbh->prepare("
+ DELETE FROM component_watch
+ WHERE user_id = ? AND product_id = ? AND component_id = ?
+ ");
+ $sth->execute($user->id, $productId, $componentId);
+}
+
+sub _addDefaultSettings {
+ my ($user) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ my $sth = $dbh->prepare("
+ SELECT 1
+ FROM email_setting
+ WHERE user_id = ? AND relationship = ?
+ ");
+ $sth->execute($user->id, REL_COMPONENT_WATCHER);
+ return if $sth->fetchrow_array;
+
+ my @defaultEvents = (
+ EVT_OTHER,
+ EVT_COMMENT,
+ EVT_ATTACHMENT,
+ EVT_ATTACHMENT_DATA,
+ EVT_PROJ_MANAGEMENT,
+ EVT_OPENED_CLOSED,
+ EVT_KEYWORD,
+ EVT_DEPEND_BLOCK,
+ EVT_BUG_CREATED,
+ );
+ foreach my $event (@defaultEvents) {
+ $dbh->do(
+ "INSERT INTO email_setting(user_id,relationship,event) VALUES (?,?,?)",
+ undef,
+ $user->id, REL_COMPONENT_WATCHER, $event
+ );
+ }
+}
+
+__PACKAGE__->NAME;
diff --git a/extensions/ComponentWatching/template/en/default/account/prefs/component_watch.html.tmpl b/extensions/ComponentWatching/template/en/default/account/prefs/component_watch.html.tmpl
new file mode 100644
index 000000000..8c193a056
--- /dev/null
+++ b/extensions/ComponentWatching/template/en/default/account/prefs/component_watch.html.tmpl
@@ -0,0 +1,232 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[%# initialise product to component mapping #%]
+
+[% SET selectable_products = user.get_selectable_products %]
+[% SET dont_show_button = 1 %]
+
+<script>
+var Dom = YAHOO.util.Dom;
+var useclassification = false;
+var first_load = true;
+var last_sel = [];
+var cpts = new Array();
+var watch_users = new Array();
+[% n = 0 %]
+[% FOREACH prod = selectable_products %]
+ cpts['[% n %]'] = [
+ [%- FOREACH comp = prod.components %]'[% comp.name FILTER js %]'[% ", " UNLESS loop.last %] [%- END -%] ];
+ [% n = n + 1 %]
+ [% FOREACH comp = prod.components %]
+ [% IF comp.watch_user %]
+ if (!watch_users['[% prod.name FILTER js %]'])
+ watch_users['[% prod.name FILTER js %]'] = new Array();
+ watch_users['[% prod.name FILTER js %]']['[% comp.name FILTER js %]'] = '[% comp.watch_user.login FILTER js %]';
+ [% END %]
+ [% END %]
+[% END %]
+</script>
+<script type="text/javascript" src="[% 'js/productform.js' FILTER mtime FILTER html %]">
+</script>
+
+<script>
+function onSelectProduct() {
+ var component = Dom.get('component');
+ selectProduct(Dom.get('product'), component);
+ // selectProduct only supports __Any__ on both elements
+ // we only want it on component, so add it back in
+ try {
+ component.add(new Option('__Any__', ''), component.options[0]);
+ } catch(e) {
+ // support IE
+ component.add(new Option('__Any__', ''), 0);
+ }
+ if ('[% add_component FILTER js %]' != ''
+ && bz_valueSelected(Dom.get('product'), '[% add_product FILTER js %]')
+ ) {
+ var index = bz_optionIndex(Dom.get('component'), '[% add_component FILTER js %]');
+ if (index != -1)
+ Dom.get('component').options[index].selected = true;
+ }
+ onSelectComponent();
+}
+
+function onSelectComponent() {
+ var product_select = Dom.get('product');
+ var product = product_select.options[product_select.selectedIndex].value;
+ var component = Dom.get('component').value;
+ if (component && watch_users[product] && watch_users[product][component]) {
+ Dom.get('watch-user-email').innerHTML = watch_users[product][component];
+ Dom.get('watch-user-div').style.display = '';
+ } else {
+ Dom.get('watch-user-div').style.display = 'none';
+ }
+ Dom.get('add').disabled = Dom.get('component').selectedIndex == -1;
+}
+
+YAHOO.util.Event.onDOMReady(onSelectProduct);
+
+function onRemoveChange() {
+ var cbs = Dom.get('remove_table').getElementsByTagName('input');
+ for (var i = 0, l = cbs.length; i < l; i++) {
+ if (cbs[i].checked) {
+ Dom.get('remove').disabled = false;
+ return;
+ }
+ }
+ Dom.get('remove').disabled = true;
+}
+
+YAHOO.util.Event.onDOMReady(onRemoveChange);
+
+</script>
+
+<p>
+ Select the components you want to watch.
+ To watch all components in a product, watch "__Any__".<br>
+ Use <a href="userprefs.cgi?tab=email">Email Preferences</a> to filter which
+ notification emails you receive.
+</p>
+
+<table border="0" cellpadding="3" cellspacing="0">
+<tr>
+ <td align="right">Product:</td>
+ <td colspan="2">
+ <select name="add_product" id="product" onChange="onSelectProduct()">
+ [% FOREACH product IN selectable_products %]
+ <option [% 'selected' IF add_product == product.name %]>
+ [%~ product.name FILTER html %]</option>
+ [% END %]
+ </select>
+ </td>
+</tr>
+<tr>
+ <td align="right" valign="top">Component:</td>
+ <td>
+ <select name="add_component" id="component" multiple size="10" onChange="onSelectComponent()">
+ <option value="">__Any__</option>
+ [% FOREACH product IN selectable_products %]
+ [% FOREACH component IN product.components %]
+ <option [% 'selected' IF add_component == component.name %]>
+ [%~ component.name FILTER html %]</option>
+ [% END %]
+ [% END %]
+ </select>
+ </td>
+ <td valign="top">
+ <div id="watch-user-div"
+ title="You can also watch a component by following this user. [% ~%]
+ CC'ing this user on a [% terms.bug %] will trigger notifications to all watchers of this component."
+ style="cursor:help">
+ Watch User: <span id="watch-user-email"></span>
+ </div>
+ </td>
+</tr>
+<tr>
+ <td>&nbsp;</td>
+ <td><input type="submit" id="add" name="add" value="Add"></td>
+</tr>
+</table>
+
+<hr>
+<p>
+ You are currently watching:
+</p>
+
+[% IF watches.size %]
+
+ <table border="0" cellpadding="3" cellspacing="0" id="remove_table">
+ <tr>
+ <td>&nbsp;</td>
+ <td><b>Product</b></td>
+ <td>&nbsp;<b>Component</b></td>
+ </tr>
+ [% FOREACH watch IN watches %]
+ <tr>
+ [% IF (watch.component) %]
+ <td>
+ <input type="checkbox" onChange="onRemoveChange()" id="cwdel_[% loop.count %]" value="1"
+ name="del_[% watch.product.id FILTER html %]_[% watch.component.id FILTER html %]">
+ </td>
+ <td>
+ <label for="cwdel_[% loop.count %]">
+ [% watch.component.product.name FILTER html %]
+ </label>
+ </td>
+ <td>&nbsp;
+ <a href="buglist.cgi?product=[% watch.product.name FILTER uri ~%]
+ &component=[% watch.component.name FILTER uri %]&resolution=---">
+ [% watch.component.name FILTER html %]
+ </a>
+ </td>
+ [% ELSE %]
+ <td>
+ <input type="checkbox" onChange="onRemoveChange()" id="cwdel_[% loop.count %]" value="1"
+ name="del_[% watch.product.id FILTER html %]" value="1">
+ </td>
+ <td>
+ <label for="cwdel_[% loop.count %]">
+ [% watch.product.name FILTER html %]
+ </label>
+ </td>
+ <td>&nbsp;
+ <a href="describecomponents.cgi?product=[% watch.product.name FILTER uri %]">
+ __Any__
+ </a>
+ </td>
+ [% END %]
+ </tr>
+ [% END %]
+ </table>
+
+ <input id="remove" type="submit" value="Remove Selected">
+
+[% ELSE %]
+
+ <p>
+ <i>You are not watching any components directly.</i>
+ </p>
+
+[% END %]
+
+[% IF user_watches.size %]
+
+ <hr>
+ <p>
+ [% watches.size ? "In addition," : "However," %]
+ you are watching the following components by watching users:
+ </p>
+
+ <table border="0" cellpadding="3" cellspacing="0">
+ <tr>
+ <td><b>User</b></td>
+ <td>&nbsp;<b>Product</b></td>
+ <td>&nbsp;<b>Component</b></td>
+ </tr>
+ [% FOREACH watch IN user_watches %]
+ <tr>
+ <td>[% watch.user.login FILTER html %]</td>
+ <td>&nbsp;[% watch.component.product.name FILTER html %]</td>
+ <td>&nbsp;
+ <a href="buglist.cgi?product=[% watch.product.name FILTER uri ~%]
+ &component=[% watch.component.name FILTER uri %]&resolution=---">
+ [% watch.component.name FILTER html %]
+ </a>
+ </td>
+ </tr>
+ [% END %]
+ </table>
+
+ <p>
+ Use <a href="userprefs.cgi?tab=email#new_watched_by_you">Email Preferences</a>
+ to manage this list.
+ </p>
+
+[% 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..69ab53751
--- /dev/null
+++ b/extensions/ComponentWatching/template/en/default/hook/account/prefs/email-relationships.html.tmpl
@@ -0,0 +1,10 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% 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..9af22ed39
--- /dev/null
+++ b/extensions/ComponentWatching/template/en/default/hook/account/prefs/prefs-tabs.html.tmpl
@@ -0,0 +1,14 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% 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/admin/components/edit-common-rows.html.tmpl b/extensions/ComponentWatching/template/en/default/hook/admin/components/edit-common-rows.html.tmpl
new file mode 100644
index 000000000..154ba089e
--- /dev/null
+++ b/extensions/ComponentWatching/template/en/default/hook/admin/components/edit-common-rows.html.tmpl
@@ -0,0 +1,20 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+<tr>
+ <td valign="top"><label for="watch_user">Watch User:</label></td>
+ <td>
+ [% INCLUDE global/userselect.html.tmpl
+ name => "watch_user"
+ id => "watch_user"
+ value => comp.watch_user.login
+ size => 64
+ emptyok => 1
+ %]
+ </td>
+</tr>
diff --git a/extensions/ComponentWatching/template/en/default/hook/admin/components/list-before_table.html.tmpl b/extensions/ComponentWatching/template/en/default/hook/admin/components/list-before_table.html.tmpl
new file mode 100644
index 000000000..ed8d6e350
--- /dev/null
+++ b/extensions/ComponentWatching/template/en/default/hook/admin/components/list-before_table.html.tmpl
@@ -0,0 +1,17 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% CALL columns.splice(5, 0, { name => 'watch_user', heading => 'Watch User' }) %]
+
+[% FOREACH my_component = product.components %]
+ [% overrides.watch_user.name.${my_component.name} = {
+ override_content => 1
+ content => my_component.watch_user.login
+ }
+ %]
+[% END %]
diff --git a/extensions/ComponentWatching/template/en/default/hook/global/messages-component_updated_fields.html.tmpl b/extensions/ComponentWatching/template/en/default/hook/global/messages-component_updated_fields.html.tmpl
new file mode 100644
index 000000000..38c7e8c8a
--- /dev/null
+++ b/extensions/ComponentWatching/template/en/default/hook/global/messages-component_updated_fields.html.tmpl
@@ -0,0 +1,15 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF changes.watch_user.defined %]
+ [% IF comp.watch_user %]
+ <li>Watch User updated to '[% comp.watch_user.login FILTER html %]'</li>
+ [% ELSE %]
+ <li>Watch User deleted</li>
+ [% END %]
+[% END %]
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..8cd67bdff
--- /dev/null
+++ b/extensions/ComponentWatching/template/en/default/hook/global/reason-descs-end.none.tmpl
@@ -0,0 +1,10 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% watch_reason_descs.${constants.REL_COMPONENT_WATCHER} =
+ "You are watching the component for the ${terms.bug}." %]
diff --git a/extensions/ComponentWatching/template/en/default/hook/global/user-error-errors.html.tmpl b/extensions/ComponentWatching/template/en/default/hook/global/user-error-errors.html.tmpl
new file mode 100644
index 000000000..01dbb5114
--- /dev/null
+++ b/extensions/ComponentWatching/template/en/default/hook/global/user-error-errors.html.tmpl
@@ -0,0 +1,17 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF error == "component_watch_invalid_watch_user" %]
+ [% title = "Invalid Watch User" %]
+ The "Watch User" must be a <b>.bugs</b> email address.<br>
+ For example: <i>accessibility-apis@core.bugs</i>
+[% ELSIF error == "component_watch_missing_watch_user" %]
+ [% title = "Missing Watch User" %]
+ You must provide a <b>.bugs</b> email address for the "Watch User".<br>
+ For example: <i>accessibility-apis@core.bugs</i>
+[% END %]
diff --git a/extensions/ContributorEngagement/Config.pm b/extensions/ContributorEngagement/Config.pm
new file mode 100644
index 000000000..3984dd60e
--- /dev/null
+++ b/extensions/ContributorEngagement/Config.pm
@@ -0,0 +1,19 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::ContributorEngagement;
+use strict;
+
+use constant NAME => 'ContributorEngagement';
+
+use constant REQUIRED_MODULES => [
+];
+
+use constant OPTIONAL_MODULES => [
+];
+
+__PACKAGE__->NAME;
diff --git a/extensions/ContributorEngagement/Extension.pm b/extensions/ContributorEngagement/Extension.pm
new file mode 100644
index 000000000..7e7031b33
--- /dev/null
+++ b/extensions/ContributorEngagement/Extension.pm
@@ -0,0 +1,134 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::ContributorEngagement;
+
+use strict;
+use warnings;
+
+use base qw(Bugzilla::Extension);
+
+use Bugzilla::User;
+use Bugzilla::Util qw(format_time);
+use Bugzilla::Mailer;
+use Bugzilla::Install::Util qw(indicate_progress);
+
+use Bugzilla::Extension::ContributorEngagement::Constants;
+
+our $VERSION = '1.0';
+
+BEGIN {
+ *Bugzilla::User::first_patch_approved_id = \&_first_patch_approved_id;
+}
+
+sub _first_patch_approved_id { return $_[0]->{'first_patch_approved_id'}; }
+
+sub install_update_db {
+ my ($self) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ if (!$dbh->bz_column_info('profiles', 'first_patch_approved_id')) {
+ $dbh->bz_add_column('profiles', 'first_patch_approved_id',
+ { TYPE => 'INT3' });
+ _migrate_first_approved_ids();
+ }
+}
+
+sub _migrate_first_approved_ids {
+ my $dbh = Bugzilla->dbh;
+
+ my $sth = $dbh->prepare('UPDATE profiles SET first_patch_approved_id = ? WHERE userid = ?');
+ my $ra = $dbh->selectall_arrayref("SELECT attachments.submitter_id,
+ attachments.attach_id,
+ flagtypes.name
+ FROM attachments
+ INNER JOIN flags ON attachments.attach_id = flags.attach_id
+ INNER JOIN flagtypes ON flags.type_id = flagtypes.id
+ WHERE flags.status = '+'
+ ORDER BY flags.modification_date");
+ my $count = 1;
+ my $total = scalar @$ra;
+ my %user_seen;
+ foreach my $ra_row (@$ra) {
+ my ($user_id, $attach_id, $flag_name) = @$ra_row;
+ next if $user_seen{$user_id};
+ my $found_flag = 0;
+ foreach my $flag_re (FLAG_REGEXES) {
+ $found_flag = 1 if ($flag_name =~ $flag_re);
+ }
+ next if !$found_flag;
+ indicate_progress({ current => $count++, total => $total, every => 25 });
+ $sth->execute($attach_id, $user_id);
+ $user_seen{$user_id} = 1;
+ }
+
+ print "done\n";
+}
+
+sub object_columns {
+ my ($self, $args) = @_;
+ my ($class, $columns) = @$args{qw(class columns)};
+ if ($class->isa('Bugzilla::User')) {
+ push(@$columns, 'first_patch_approved_id');
+ }
+}
+
+sub flag_end_of_update {
+ my ($self, $args) = @_;
+ my ($object, $timestamp, $new_flags) = @$args{qw(object timestamp new_flags)};
+
+ if ($object->isa('Bugzilla::Attachment')
+ && @$new_flags
+ && grep($_ eq $object->bug->product, ENABLED_PRODUCTS)
+ && !$object->attacher->first_patch_approved_id)
+ {
+ my $attachment = $object;
+
+ # Glob: Borrowed this code from your push extension :)
+ foreach my $change (@$new_flags) {
+ $change =~ s/^[^:]+://; # get rid of setter
+ $change =~ s/\([^\)]+\)$//; # get rid of requestee
+ my ($name, $value) = $change =~ /^(.+)(.)$/;
+
+ # Only interested in flags set to +
+ next if $value ne '+';
+
+ my $found_flag = 0;
+ foreach my $flag_re (FLAG_REGEXES) {
+ $found_flag = 1 if ($name =~ $flag_re);
+ }
+ next if !$found_flag;
+
+ _send_approval_mail($attachment, $timestamp);
+
+ last;
+ }
+ }
+}
+
+sub _send_approval_mail {
+ my ($attachment, $timestamp) = @_;
+
+ my $vars = {
+ date => format_time($timestamp, '%a, %d %b %Y %T %z', 'UTC'),
+ to_user => $attachment->attacher->email,
+ from_user => EMAIL_FROM,
+ };
+
+ my $msg;
+ my $template = Bugzilla->template_inner($attachment->attacher->setting('lang'));
+ $template->process("contributor/email.txt.tmpl", $vars, \$msg)
+ || ThrowTemplateError($template->error());
+
+ MessageToMTA($msg);
+
+ # Make sure we don't do this again
+ Bugzilla->dbh->do("UPDATE profiles SET first_patch_approved_id = ? WHERE userid = ?",
+ undef, $attachment->id, $attachment->attacher->id);
+}
+
+__PACKAGE__->NAME;
diff --git a/extensions/ContributorEngagement/lib/Constants.pm b/extensions/ContributorEngagement/lib/Constants.pm
new file mode 100644
index 000000000..851c0dbc2
--- /dev/null
+++ b/extensions/ContributorEngagement/lib/Constants.pm
@@ -0,0 +1,36 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::ContributorEngagement::Constants;
+
+use strict;
+
+use base qw(Exporter);
+
+our @EXPORT = qw(
+ EMAIL_FROM
+ ENABLED_PRODUCTS
+ FLAG_REGEXES
+);
+
+use constant EMAIL_FROM => 'bugzilla-daemon@mozilla.org';
+
+use constant ENABLED_PRODUCTS => (
+ "Core",
+ "Firefox for Android",
+ "Firefox",
+ "Testing",
+ "Toolkit",
+ "Mozilla Services",
+ "TestProduct",
+);
+
+use constant FLAG_REGEXES => (
+ qr/^approval/
+);
+
+1;
diff --git a/extensions/ContributorEngagement/template/en/default/contributor/email.txt.tmpl b/extensions/ContributorEngagement/template/en/default/contributor/email.txt.tmpl
new file mode 100644
index 000000000..b403a4bfb
--- /dev/null
+++ b/extensions/ContributorEngagement/template/en/default/contributor/email.txt.tmpl
@@ -0,0 +1,46 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+[% PROCESS "global/variables.none.tmpl" %]
+From: [% from_user FILTER none %]
+To: [% to_user FILTER none %]
+Subject: Congratulations on having your first patch approved
+Date: [% date FILTER none %]
+
+Congratulations on having your first patch approved, and thank you
+for your contribution to Mozilla.
+
+The next step is to get the patch actually checked in to our repository.
+For more information about how to make that happen, check out this post:
+
+https://developer.mozilla.org/en/Creating_a_patch_that_can_be_checked_in
+
+While you are going through those final steps, if you're looking for a
+new project to take on, have a look at our list of 'mentored' [% terms.bugs %] ([% terms.bugs %] where
+someone is specifically available to help you):
+
+https://bugzil.la/sw:mentor
+
+Alternatively, you could join us on our IRC chat server in the #introduction
+channel and ask for suggestions about what would be a good [% terms.bugs %] to work on.
+There's more about using our chat server at:
+
+http://irc.mozilla.org/
+
+If you haven't done so already, this is also a good time to sign up to the
+Mozilla Contributor Directory and create a profile for yourself. Doing this
+will give you access to community members' profiles so you can reach out and
+connect with other Mozillians. You will need someone to 'vouch for' your
+profile; if you don't know any other Mozillians well, why not contact the
+person who approved your patch?
+
+The directory is here:
+
+https://mozillians.org/
+
+Thanks again for your help :-)
+Josh, Kyle, Dietrich and Brian; Coding Stewards
diff --git a/extensions/Example/Extension.pm b/extensions/Example/Extension.pm
index 885a8e8ff..8eef19a6e 100644
--- a/extensions/Example/Extension.pm
+++ b/extensions/Example/Extension.pm
@@ -44,6 +44,20 @@ use constant REL_EXAMPLE => -127;
our $VERSION = '1.0';
+sub admin_editusers_action {
+ my ($self, $args) = @_;
+ my ($vars, $action, $user) = @$args{qw(vars action user)};
+ my $template = Bugzilla->template;
+
+ if ($action eq 'my_action') {
+ # Allow to restrict the search to any group the user is allowed to bless.
+ $vars->{'restrictablegroups'} = $user->bless_groups();
+ $template->process('admin/users/search.html.tmpl', $vars)
+ || ThrowTemplateError($template->error());
+ exit;
+ }
+}
+
sub attachment_process_data {
my ($self, $args) = @_;
my $type = $args->{attributes}->{mimetype};
@@ -80,6 +94,44 @@ sub auth_verify_methods {
}
}
+sub bug_check_can_change_field {
+ my ($self, $args) = @_;
+
+ my ($bug, $field, $new_value, $old_value, $priv_results)
+ = @$args{qw(bug field new_value old_value priv_results)};
+
+ my $user = Bugzilla->user;
+
+ # Disallow a bug from being reopened if currently closed unless user
+ # is in 'admin' group
+ if ($field eq 'bug_status' && $bug->product_obj->name eq 'Example') {
+ if (!is_open_state($old_value) && is_open_state($new_value)
+ && !$user->in_group('admin'))
+ {
+ push(@$priv_results, PRIVILEGES_REQUIRED_EMPOWERED);
+ return;
+ }
+ }
+
+ # Disallow a bug's keywords from being edited unless user is the
+ # reporter of the bug
+ if ($field eq 'keywords' && $bug->product_obj->name eq 'Example'
+ && $user->login ne $bug->reporter->login)
+ {
+ push(@$priv_results, PRIVILEGES_REQUIRED_REPORTER);
+ return;
+ }
+
+ # Allow updating of priority even if user cannot normally edit the bug
+ # and they are in group 'engineering'
+ if ($field eq 'priority' && $bug->product_obj->name eq 'Example'
+ && $user->in_group('engineering'))
+ {
+ push(@$priv_results, PRIVILEGES_REQUIRED_NONE);
+ return;
+ }
+}
+
sub bug_columns {
my ($self, $args) = @_;
my $columns = $args->{'columns'};
@@ -691,6 +743,12 @@ sub page_before_template {
}
}
+sub path_info_whitelist {
+ my ($self, $args) = @_;
+ my $whitelist = $args->{whitelist};
+ push(@$whitelist, "page.cgi");
+}
+
sub post_bug_after_creation {
my ($self, $args) = @_;
@@ -819,58 +877,6 @@ sub template_before_process {
}
}
-sub bug_check_can_change_field {
- my ($self, $args) = @_;
-
- my ($bug, $field, $new_value, $old_value, $priv_results)
- = @$args{qw(bug field new_value old_value priv_results)};
-
- my $user = Bugzilla->user;
-
- # Disallow a bug from being reopened if currently closed unless user
- # is in 'admin' group
- if ($field eq 'bug_status' && $bug->product_obj->name eq 'Example') {
- if (!is_open_state($old_value) && is_open_state($new_value)
- && !$user->in_group('admin'))
- {
- push(@$priv_results, PRIVILEGES_REQUIRED_EMPOWERED);
- return;
- }
- }
-
- # Disallow a bug's keywords from being edited unless user is the
- # reporter of the bug
- if ($field eq 'keywords' && $bug->product_obj->name eq 'Example'
- && $user->login ne $bug->reporter->login)
- {
- push(@$priv_results, PRIVILEGES_REQUIRED_REPORTER);
- return;
- }
-
- # Allow updating of priority even if user cannot normally edit the bug
- # and they are in group 'engineering'
- if ($field eq 'priority' && $bug->product_obj->name eq 'Example'
- && $user->in_group('engineering'))
- {
- push(@$priv_results, PRIVILEGES_REQUIRED_NONE);
- return;
- }
-}
-
-sub admin_editusers_action {
- my ($self, $args) = @_;
- my ($vars, $action, $user) = @$args{qw(vars action user)};
- my $template = Bugzilla->template;
-
- if ($action eq 'my_action') {
- # Allow to restrict the search to any group the user is allowed to bless.
- $vars->{'restrictablegroups'} = $user->bless_groups();
- $template->process('admin/users/search.html.tmpl', $vars)
- || ThrowTemplateError($template->error());
- exit;
- }
-}
-
sub user_preferences {
my ($self, $args) = @_;
my $tab = $args->{current_tab};
diff --git a/extensions/FlagDefaultRequestee/Config.pm b/extensions/FlagDefaultRequestee/Config.pm
new file mode 100644
index 000000000..70c5ca33a
--- /dev/null
+++ b/extensions/FlagDefaultRequestee/Config.pm
@@ -0,0 +1,17 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::FlagDefaultRequestee;
+
+use strict;
+
+use constant NAME => 'FlagDefaultRequestee';
+
+use constant REQUIRED_MODULES => [];
+use constant OPTIONAL_MODULES => [];
+
+__PACKAGE__->NAME;
diff --git a/extensions/FlagDefaultRequestee/Extension.pm b/extensions/FlagDefaultRequestee/Extension.pm
new file mode 100644
index 000000000..b444bce49
--- /dev/null
+++ b/extensions/FlagDefaultRequestee/Extension.pm
@@ -0,0 +1,144 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::FlagDefaultRequestee;
+
+use strict;
+use base qw(Bugzilla::Extension);
+
+use Bugzilla::FlagType;
+use Bugzilla::User;
+
+use Bugzilla::Extension::FlagDefaultRequestee::Constants;
+
+our $VERSION = '1';
+
+################
+# Installation #
+################
+
+sub install_update_db {
+ my $dbh = Bugzilla->dbh;
+ if (!$dbh->bz_column_info('flagtypes', 'default_requestee')) {
+ $dbh->bz_add_column('flagtypes', 'default_requestee', {
+ TYPE => 'INT3', NOTNULL => 0,
+ REFERENCES => { TABLE => 'profiles',
+ COLUMN => 'userid',
+ DELETE => 'SET NULL' }
+ });
+ }
+}
+
+#############
+# Templates #
+#############
+
+sub template_before_process {
+ my ($self, $args) = @_;
+ my ($vars, $file) = @$args{qw(vars file)};
+ my $dbh = Bugzilla->dbh;
+
+ return unless Bugzilla->user->id;
+
+ return unless grep { $_ eq $file } FLAGTYPE_TEMPLATES;
+
+ my $flag_types = [];
+ if (exists $vars->{bug} || exists $vars->{attachment}) {
+ my $bug;
+ if (exists $vars->{bug}) {
+ $bug = $vars->{'bug'};
+ } elsif (exists $vars->{'attachment'}) {
+ $bug = $vars->{'attachment'}->{bug};
+ }
+
+ $flag_types = Bugzilla::FlagType::match({
+ 'target_type' => ($file =~ /^bug/ ? 'bug' : 'attachment'),
+ 'product_id' => $bug->product_id,
+ 'component_id' => $bug->component_id,
+ 'bug_id' => $bug->id,
+ 'active_or_has_flags' => $bug->id,
+ });
+
+ $vars->{flag_currently_requested} ||= {};
+ foreach my $type (@$flag_types) {
+ my $flags = Bugzilla::Flag->match({
+ type_id => $type->id,
+ bug_id => $bug->id,
+ status => '?'
+ });
+ map { $vars->{flag_currently_requested}->{$_->id} = 1 } @$flags;
+ }
+ }
+ elsif ($file =~ /^bug\/create/ && exists $vars->{product}) {
+ my $bug_flags = $vars->{product}->flag_types->{bug};
+ my $attachment_flags = $vars->{product}->flag_types->{attachment};
+ $flag_types = [ map { $_ } (@$bug_flags, @$attachment_flags) ];
+ }
+
+ return if !@$flag_types;
+
+ $vars->{flag_default_requestees} ||= {};
+ foreach my $type (@$flag_types) {
+ next if !$type->default_requestee;
+ $vars->{flag_default_requestees}->{$type->id} = $type->default_requestee->login;
+ }
+}
+
+#########
+# Admin #
+#########
+
+sub flagtype_end_of_create {
+ my ($self, $args) = @_;
+ _set_default_requestee($args->{id});
+}
+
+sub flagtype_end_of_update {
+ my ($self, $args) = @_;
+ _set_default_requestee($args->{id});
+}
+
+sub _set_default_requestee {
+ my $type_id = shift;
+ my $input = Bugzilla->input_params;
+ my $dbh = Bugzilla->dbh;
+
+ my $requestee_login = $input->{'default_requestee'};
+
+ my $requestee_id = undef;
+ if ($requestee_login) {
+ my $requestee = Bugzilla::User->check($requestee_login);
+ $requestee_id = $requestee->id;
+ }
+
+ $dbh->do("UPDATE flagtypes SET default_requestee = ? WHERE id = ?",
+ undef, $requestee_id, $type_id);
+}
+
+##################
+# Object Methods #
+##################
+
+BEGIN {
+ *Bugzilla::FlagType::default_requestee = \&_default_requestee;
+}
+
+sub _default_requestee {
+ my ($self) = @_;
+ my $dbh = Bugzilla->dbh;
+ return $self->{default_requestee} if exists $self->{default_requestee};
+ my $requestee_id = $dbh->selectrow_array("SELECT default_requestee
+ FROM flagtypes
+ WHERE id = ?",
+ undef, $self->id);
+ $self->{default_requestee} = $requestee_id
+ ? Bugzilla::User->new($requestee_id)
+ : undef;
+ return $self->{default_requestee};
+}
+
+__PACKAGE__->NAME;
diff --git a/extensions/FlagDefaultRequestee/lib/Constants.pm b/extensions/FlagDefaultRequestee/lib/Constants.pm
new file mode 100644
index 000000000..467028423
--- /dev/null
+++ b/extensions/FlagDefaultRequestee/lib/Constants.pm
@@ -0,0 +1,25 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::FlagDefaultRequestee::Constants;
+
+use strict;
+
+use base qw(Exporter);
+
+our @EXPORT = qw(
+ FLAGTYPE_TEMPLATES
+);
+
+use constant FLAGTYPE_TEMPLATES => (
+ "attachment/edit.html.tmpl",
+ "attachment/createformcontents.html.tmpl",
+ "bug/edit.html.tmpl",
+ "bug/create/create.html.tmpl"
+);
+
+1;
diff --git a/extensions/FlagDefaultRequestee/template/en/default/flag/default_requestees.html.tmpl b/extensions/FlagDefaultRequestee/template/en/default/flag/default_requestees.html.tmpl
new file mode 100644
index 000000000..db728c168
--- /dev/null
+++ b/extensions/FlagDefaultRequestee/template/en/default/flag/default_requestees.html.tmpl
@@ -0,0 +1,105 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF flag_default_requestees.keys.size %]
+ <script type="text/javascript">
+ var currently_requested = new Array();
+ var default_requestees = new Array();
+ [% FOREACH id = flag_currently_requested.keys %]
+ currently_requested.push('[% id FILTER js %]');
+ [% END %]
+ [% FOREACH id = flag_default_requestees.keys %]
+ default_requestees['id_[% id FILTER js %]'] = '[% flag_default_requestees.$id FILTER js %]';
+ [% END %]
+
+ function fdrSetDefaultRequestee(field, default_requestee) {
+ field.value = default_requestee;
+ field.focus();
+ field.select();
+ }
+
+ function fdrOnChange(ev) {
+ var parts = ev.target.id.split('-');
+ var flag = parts[0];
+ var id = parts[1];
+ var state = ev.target.value;
+ var requestee_field;
+
+ if (flag.search(/_type/) == -1) {
+ for (var i = 0; i < currently_requested.length; i++) {
+ if (id == currently_requested[i]) {
+ return;
+ }
+ }
+ requestee_field = YAHOO.util.Dom.get('requestee-' + id);
+ parts = ev.target.className.split('-');
+ id = parts[1];
+ }
+ else {
+ requestee_field = YAHOO.util.Dom.get('requestee_type-' + id);
+ }
+ if (!requestee_field) return;
+
+ var current_requestee = requestee_field.value;
+ var default_requestee = default_requestees['id_' + id];
+ if (!default_requestee) return;
+
+ if (state == '?' && !current_requestee && default_requestee) {
+ fdrSetDefaultRequestee(requestee_field, default_requestees['id_' + id]);
+ }
+ else if (state == '?' && current_requestee != default_requestee) {
+ fdrShowDefaultLink(requestee_field, id);
+ }
+ }
+
+ YAHOO.util.Event.onDOMReady(function() {
+ var selects = YAHOO.util.Dom.getElementsByClassName('flag_select');
+ for (var i = 0; i < selects.length; i++) {
+ YAHOO.util.Event.on(selects[i], 'change', fdrOnChange);
+ }
+
+ for (var i = 0; i < currently_requested.length; i++) {
+ var flag_id = currently_requested[i];
+ var flag_field = YAHOO.util.Dom.get('flag-' + flag_id);
+ var requestee_field = YAHOO.util.Dom.get('requestee-' + flag_id);
+ if (!requestee_field) continue;
+ var parts = flag_field.className.split('-');
+ var type_id = parts[1];
+ var current_requestee = requestee_field.value;
+ var default_requestee = default_requestees['id_' + type_id];
+ if (!default_requestee) continue;
+ if (current_requestee != default_requestee) {
+ fdrShowDefaultLink(requestee_field, type_id, flag_id);
+ }
+ }
+ });
+
+ function fdrHideDefaultLink (flag_id) {
+ YAHOO.util.Dom.addClass('default_requestee_' + flag_id, 'bz_default_hidden');
+ }
+
+ function fdrShowDefaultLink (requestee_field, type_id, flag_id) {
+ var default_requestee = default_requestees['id_' + type_id];
+
+ var default_link = document.createElement('a');
+ YAHOO.util.Dom.setAttribute(default_link, 'href', 'javascript:void(0)');
+ default_link.appendChild(document.createTextNode('default requestee'));
+ YAHOO.util.Event.addListener(default_link, 'click', function() {
+ fdrSetDefaultRequestee(requestee_field, default_requestee);
+ fdrHideDefaultLink(flag_id);
+ });
+
+ var default_span = document.createElement('span');
+ YAHOO.util.Dom.setAttribute(default_span, 'id', 'default_requestee_' + flag_id);
+ default_span.appendChild(document.createTextNode("\u00a0("));
+ default_span.appendChild(default_link);
+ default_span.appendChild(document.createTextNode(')'));
+ requestee_field.parentNode.parentNode.appendChild(default_span);
+ }
+ </script>
+[% END %]
diff --git a/extensions/FlagDefaultRequestee/template/en/default/hook/admin/flag-type/edit-rows.html.tmpl b/extensions/FlagDefaultRequestee/template/en/default/hook/admin/flag-type/edit-rows.html.tmpl
new file mode 100644
index 000000000..edefca370
--- /dev/null
+++ b/extensions/FlagDefaultRequestee/template/en/default/hook/admin/flag-type/edit-rows.html.tmpl
@@ -0,0 +1,21 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+<tr>
+ <th>Default Requestee:</th>
+ <td>
+ If flag is specifically requestable, this user will be entered in the
+ requestee field by default unless the user changes it.<br>
+ [% INCLUDE global/userselect.html.tmpl
+ name => 'default_requestee'
+ id => 'default_requestee'
+ value => type.default_requestee.login
+ classes => ['requestee']
+ %]
+ </td>
+</tr>
diff --git a/extensions/FlagDefaultRequestee/template/en/default/hook/attachment/create-end.html.tmpl b/extensions/FlagDefaultRequestee/template/en/default/hook/attachment/create-end.html.tmpl
new file mode 100644
index 000000000..20b2526d0
--- /dev/null
+++ b/extensions/FlagDefaultRequestee/template/en/default/hook/attachment/create-end.html.tmpl
@@ -0,0 +1,9 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% INCLUDE flag/default_requestees.html.tmpl %]
diff --git a/extensions/FlagDefaultRequestee/template/en/default/hook/attachment/edit-end.html.tmpl b/extensions/FlagDefaultRequestee/template/en/default/hook/attachment/edit-end.html.tmpl
new file mode 100644
index 000000000..20b2526d0
--- /dev/null
+++ b/extensions/FlagDefaultRequestee/template/en/default/hook/attachment/edit-end.html.tmpl
@@ -0,0 +1,9 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% INCLUDE flag/default_requestees.html.tmpl %]
diff --git a/extensions/FlagDefaultRequestee/template/en/default/hook/bug/create/create-form.html.tmpl b/extensions/FlagDefaultRequestee/template/en/default/hook/bug/create/create-form.html.tmpl
new file mode 100644
index 000000000..20b2526d0
--- /dev/null
+++ b/extensions/FlagDefaultRequestee/template/en/default/hook/bug/create/create-form.html.tmpl
@@ -0,0 +1,9 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% INCLUDE flag/default_requestees.html.tmpl %]
diff --git a/extensions/FlagDefaultRequestee/template/en/default/hook/bug/edit-after_custom_fields.html.tmpl b/extensions/FlagDefaultRequestee/template/en/default/hook/bug/edit-after_custom_fields.html.tmpl
new file mode 100644
index 000000000..20b2526d0
--- /dev/null
+++ b/extensions/FlagDefaultRequestee/template/en/default/hook/bug/edit-after_custom_fields.html.tmpl
@@ -0,0 +1,9 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% INCLUDE flag/default_requestees.html.tmpl %]
diff --git a/extensions/FlagTypeComment/Config.pm b/extensions/FlagTypeComment/Config.pm
new file mode 100644
index 000000000..e20be10e3
--- /dev/null
+++ b/extensions/FlagTypeComment/Config.pm
@@ -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 FlagTypeComment Bugzilla Extension.
+#
+# The Initial Developer of the Original Code is Alex Keybl
+# Portions created by the Initial Developer are Copyright (C) 2011 the
+# Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+# Alex Keybl <akeybl@mozilla.com>
+# byron jones <glob@mozilla.com>
+
+package Bugzilla::Extension::FlagTypeComment;
+use strict;
+
+use constant NAME => 'FlagTypeComment';
+
+use constant REQUIRED_MODULES => [];
+use constant OPTIONAL_MODULES => [];
+
+__PACKAGE__->NAME;
diff --git a/extensions/FlagTypeComment/Extension.pm b/extensions/FlagTypeComment/Extension.pm
new file mode 100644
index 000000000..d9098a5db
--- /dev/null
+++ b/extensions/FlagTypeComment/Extension.pm
@@ -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 FlagTypeComment Bugzilla Extension.
+#
+# The Initial Developer of the Original Code is Alex Keybl
+# Portions created by the Initial Developer are Copyright (C) 2011 the
+# Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+# Alex Keybl <akeybl@mozilla.com>
+# byron jones <glob@mozilla.com>
+
+package Bugzilla::Extension::FlagTypeComment;
+use strict;
+use base qw(Bugzilla::Extension);
+
+use Bugzilla::Extension::FlagTypeComment::Constants;
+
+use Bugzilla::FlagType;
+use Bugzilla::Util qw(trick_taint);
+use Scalar::Util qw(blessed);
+
+our $VERSION = '1';
+
+################
+# Installation #
+################
+
+sub db_schema_abstract_schema {
+ my ($self, $args) = @_;
+ $args->{'schema'}->{'flagtype_comments'} = {
+ FIELDS => [
+ type_id => {
+ TYPE => 'SMALLINT(6)',
+ NOTNULL => 1,
+ REFERENCES => {
+ TABLE => 'flagtypes',
+ COLUMN => 'id',
+ DELETE => 'CASCADE'
+ }
+ },
+ on_status => {
+ TYPE => 'CHAR(1)',
+ NOTNULL => 1
+ },
+ comment => {
+ TYPE => 'MEDIUMTEXT',
+ NOTNULL => 1
+ },
+ ],
+ INDEXES => [
+ flagtype_comments_idx => ['type_id'],
+ ],
+ };
+}
+
+#############
+# Templates #
+#############
+
+sub template_before_process {
+ my ($self, $args) = @_;
+ my ($vars, $file) = @$args{qw(vars file)};
+
+ return unless Bugzilla->user->id;
+ if (grep { $_ eq $file } FLAGTYPE_COMMENT_TEMPLATES) {
+ _set_ftc_states($file, $vars);
+ }
+}
+
+sub _set_ftc_states {
+ my ($file, $vars) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ my $ftc_flags;
+ my $db_result;
+ if ($file =~ /^admin\//) {
+ # admin
+ my $type = $vars->{'type'} || return;
+ my ($target_type, $id);
+ if (blessed($type)) {
+ ($target_type, $id) = ($type->target_type, $type->id);
+ } else {
+ ($target_type, $id) = ($type->{target_type}, $type->{id});
+ trick_taint($id) if $id;
+ }
+ if ($target_type eq 'bug') {
+ return unless FLAGTYPE_COMMENT_BUG_FLAGS;
+ } else {
+ return unless FLAGTYPE_COMMENT_ATTACHMENT_FLAGS;
+ }
+ if ($id) {
+ $db_result = $dbh->selectall_arrayref(
+ "SELECT type_id AS flagtype, on_status AS state, comment AS text
+ FROM flagtype_comments
+ WHERE type_id = ?",
+ { Slice => {} }, $id);
+ }
+ } else {
+ # creating/editing attachment / viewing bug
+ my $bug;
+ if (exists $vars->{'bug'}) {
+ $bug = $vars->{'bug'};
+ } elsif (exists $vars->{'attachment'}) {
+ $bug = $vars->{'attachment'}->{bug};
+ } else {
+ return;
+ }
+
+ my $flag_types = Bugzilla::FlagType::match({
+ 'target_type' => ($file =~ /^bug/ ? 'bug' : 'attachment'),
+ 'product_id' => $bug->product_id,
+ 'component_id' => $bug->component_id,
+ 'bug_id' => $bug->id,
+ 'active_or_has_flags' => $bug->id,
+ });
+
+ my $types = join(',', map { $_->id } @$flag_types);
+ my $states = "'" . join("','", FLAGTYPE_COMMENT_STATES) . "'";
+ $db_result = $dbh->selectall_arrayref(
+ "SELECT type_id AS flagtype, on_status AS state, comment AS text
+ FROM flagtype_comments
+ WHERE type_id IN ($types) AND on_status IN ($states)",
+ { Slice => {} });
+ }
+
+ foreach my $row (@$db_result) {
+ $ftc_flags->{$row->{'flagtype'}} ||= {};
+ $ftc_flags->{$row->{'flagtype'}}{$row->{'state'}} = $row->{text};
+ }
+
+ $vars->{'ftc_states'} = [ FLAGTYPE_COMMENT_STATES ];
+ $vars->{'ftc_flags'} = $ftc_flags;
+}
+
+#########
+# Admin #
+#########
+
+sub flagtype_end_of_create {
+ my ($self, $args) = @_;
+ _set_flagtypes($args->{id});
+}
+
+sub flagtype_end_of_update {
+ my ($self, $args) = @_;
+ _set_flagtypes($args->{id});
+}
+
+sub _set_flagtypes {
+ my $flagtype_id = shift;
+ my $input = Bugzilla->input_params;
+ my $dbh = Bugzilla->dbh;
+
+ foreach my $state (FLAGTYPE_COMMENT_STATES) {
+ next if (!defined $input->{"ftc_${flagtype_id}_$state"}
+ && !defined $input->{"ftc_new_$state"});
+
+ my $text = $input->{"ftc_${flagtype_id}_$state"} || $input->{"ftc_new_$state"} || '';
+ $text =~ s/\r\n/\n/g;
+ trick_taint($text);
+
+ if ($text ne '') {
+ if ($dbh->selectrow_array(
+ "SELECT 1 FROM flagtype_comments WHERE type_id=? AND on_status=?",
+ undef,
+ $flagtype_id, $state)
+ ) {
+ $dbh->do(
+ "UPDATE flagtype_comments SET comment=?
+ WHERE type_id=? AND on_status=?",
+ undef,
+ $text, $flagtype_id, $state);
+ } else {
+ $dbh->do(
+ "INSERT INTO flagtype_comments(type_id, on_status, comment)
+ VALUES (?, ?, ?)",
+ undef,
+ $flagtype_id, $state, $text);
+ }
+
+ } else {
+ $dbh->do(
+ "DELETE FROM flagtype_comments WHERE type_id=? AND on_status=?",
+ undef,
+ $flagtype_id, $state);
+ }
+ }
+}
+
+__PACKAGE__->NAME;
diff --git a/extensions/FlagTypeComment/lib/Constants.pm b/extensions/FlagTypeComment/lib/Constants.pm
new file mode 100644
index 000000000..e1a99e5b3
--- /dev/null
+++ b/extensions/FlagTypeComment/lib/Constants.pm
@@ -0,0 +1,50 @@
+# 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 FlagTypeComment Bugzilla Extension.
+#
+# The Initial Developer of the Original Code is Alex Keybl
+# Portions created by the Initial Developer are Copyright (C) 2011 the
+# Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+# Alex Keybl <akeybl@mozilla.com>
+# byron jones <glob@mozilla.com>
+
+package Bugzilla::Extension::FlagTypeComment::Constants;
+use strict;
+
+use base qw(Exporter);
+our @EXPORT = qw(
+ FLAGTYPE_COMMENT_TEMPLATES
+ FLAGTYPE_COMMENT_STATES
+ FLAGTYPE_COMMENT_BUG_FLAGS
+ FLAGTYPE_COMMENT_ATTACHMENT_FLAGS
+);
+
+use constant FLAGTYPE_COMMENT_STATES => ("?", "+", "-");
+use constant FLAGTYPE_COMMENT_BUG_FLAGS => 0;
+use constant FLAGTYPE_COMMENT_ATTACHMENT_FLAGS => 1;
+
+sub FLAGTYPE_COMMENT_TEMPLATES {
+ my @result = ("admin/flag-type/edit.html.tmpl");
+ if (FLAGTYPE_COMMENT_BUG_FLAGS) {
+ push @result, ("bug/comments.html.tmpl");
+ }
+ if (FLAGTYPE_COMMENT_ATTACHMENT_FLAGS) {
+ push @result, (
+ "attachment/edit.html.tmpl",
+ "attachment/createformcontents.html.tmpl",
+ );
+ }
+ return @result;
+}
+
+1;
diff --git a/extensions/FlagTypeComment/template/en/default/flag/type_comment.html.tmpl b/extensions/FlagTypeComment/template/en/default/flag/type_comment.html.tmpl
new file mode 100644
index 000000000..95c0cb283
--- /dev/null
+++ b/extensions/FlagTypeComment/template/en/default/flag/type_comment.html.tmpl
@@ -0,0 +1,54 @@
+[%# 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 FlagTypeComment 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):
+ # Alex Keybl <akeybl@mozilla.com>
+ # byron jones <glob@mozilla.com>
+ #%]
+
+[% IF ftc_flags.keys.size %]
+ <script type="text/javascript">
+ YAHOO.util.Event.onDOMReady(function() {
+ var selects = YAHOO.util.Dom.getElementsByClassName('flag_select');
+ for (var i = 0; i < selects.length; i++) {
+ YAHOO.util.Event.on(selects[i], 'change', ftc_on_change);
+ }
+ });
+
+ function ftc_on_change(ev) {
+ var id = ev.target.id.split('-')[1];
+ var state = ev.target.value;
+ var commentEl = document.getElementById('comment');
+ if (!commentEl) return;
+ [% FOREACH type_id = ftc_flags.keys %]
+ [% FOREACH state = ftc_states %]
+ if ([% type_id FILTER none %] == id && '[% state FILTER js %]' == state) {
+ var text = '[% ftc_flags.$type_id.$state FILTER js %]';
+ var value = commentEl.value;
+ if (value == text) {
+ return;
+ } else if (value == '') {
+ commentEl.value = text;
+ } else {
+ commentEl.value = text + "\n\n" + value;
+ }
+ }
+ [% END %]
+ [% END %]
+ }
+ </script>
+[% END %]
diff --git a/extensions/FlagTypeComment/template/en/default/hook/admin/flag-type/edit-rows.html.tmpl b/extensions/FlagTypeComment/template/en/default/hook/admin/flag-type/edit-rows.html.tmpl
new file mode 100644
index 000000000..3ca5e8aa7
--- /dev/null
+++ b/extensions/FlagTypeComment/template/en/default/hook/admin/flag-type/edit-rows.html.tmpl
@@ -0,0 +1,45 @@
+[%# 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 FlagTypeComment 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):
+ # Alex Keybl <akeybl@mozilla.com>
+ # byron jones <glob@mozilla.com>
+ #%]
+
+[% IF ftc_states %]
+ <tr>
+ <th>Flag Comments:</th>
+ <td>add text into the comment box when flag is changed to a state</td>
+ </tr>
+
+ [% FOREACH state = ftc_states %]
+ [% ftc_type_id = "ftc_${type.id}_$state" %]
+ [% IF action == 'insert' %]
+ [% ftc_type_id = "ftc_new_$state" %]
+ [% END %]
+ <tr>
+ <td>&nbsp;</td>
+ <td>
+ for [% state FILTER html %]<br>
+ <textarea
+ id="[% ftc_type_id FILTER html %]"
+ name="[% ftc_type_id FILTER html %]"
+ cols="50" rows="2">[% ftc_flags.${type.id}.$state FILTER html %]</textarea>
+ </td>
+ </tr>
+ [% END %]
+[% END %]
diff --git a/extensions/FlagTypeComment/template/en/default/hook/attachment/create-end.html.tmpl b/extensions/FlagTypeComment/template/en/default/hook/attachment/create-end.html.tmpl
new file mode 100644
index 000000000..dfa010d7c
--- /dev/null
+++ b/extensions/FlagTypeComment/template/en/default/hook/attachment/create-end.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 Original Code is FlagTypeComment 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):
+ # Alex Keybl <akeybl@mozilla.com>
+ # byron jones <glob@mozilla.com>
+ #%]
+
+[% INCLUDE flag/type_comment.html.tmpl %]
diff --git a/extensions/FlagTypeComment/template/en/default/hook/attachment/edit-end.html.tmpl b/extensions/FlagTypeComment/template/en/default/hook/attachment/edit-end.html.tmpl
new file mode 100644
index 000000000..dfa010d7c
--- /dev/null
+++ b/extensions/FlagTypeComment/template/en/default/hook/attachment/edit-end.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 Original Code is FlagTypeComment 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):
+ # Alex Keybl <akeybl@mozilla.com>
+ # byron jones <glob@mozilla.com>
+ #%]
+
+[% INCLUDE flag/type_comment.html.tmpl %]
diff --git a/extensions/FlagTypeComment/template/en/default/hook/bug/edit-after_custom_fields.html.tmpl b/extensions/FlagTypeComment/template/en/default/hook/bug/edit-after_custom_fields.html.tmpl
new file mode 100644
index 000000000..dfa010d7c
--- /dev/null
+++ b/extensions/FlagTypeComment/template/en/default/hook/bug/edit-after_custom_fields.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 Original Code is FlagTypeComment 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):
+ # Alex Keybl <akeybl@mozilla.com>
+ # byron jones <glob@mozilla.com>
+ #%]
+
+[% INCLUDE flag/type_comment.html.tmpl %]
diff --git a/extensions/GuidedBugEntry/Config.pm b/extensions/GuidedBugEntry/Config.pm
new file mode 100644
index 000000000..e4bc9c70b
--- /dev/null
+++ b/extensions/GuidedBugEntry/Config.pm
@@ -0,0 +1,19 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+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..5665e18ae
--- /dev/null
+++ b/extensions/GuidedBugEntry/Extension.pm
@@ -0,0 +1,116 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+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')) .
+ '|' . url_quote($cgi->param('component') || '')
+ );
+ 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.
+ if (
+ ($cgi->param('format') && $cgi->param('format') eq "__default__")
+ && ($cgi->param('product') && $cgi->param('product') ne '')
+ ) {
+ $cgi->delete('format');
+ }
+}
+
+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('create_bug');
+
+ $vars->{'platform'} = detect_platform();
+ $vars->{'op_sys'} = detect_op_sys();
+
+ eval 'use Bugzilla::Extension::BMO::Data';
+ $vars->{'BMO'} = $@ ? 0 : 1;
+}
+
+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..93d036f7b
--- /dev/null
+++ b/extensions/GuidedBugEntry/template/en/default/guided/guided.html.tmpl
@@ -0,0 +1,545 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+
+[% js_urls = [ 'extensions/GuidedBugEntry/web/js/products.js',
+ 'extensions/GuidedBugEntry/web/js/guided.js',
+ 'js/field.js', 'js/TUI.js', 'js/bug.js' ] %]
+[% js_urls.push('extensions/BMO/web/js/prod_comp_search.js') IF BMO %]
+
+[% yui_modules = [ 'history', 'datatable', 'container' ] %]
+[% yui_modules.push('autocomplete') IF BMO %]
+
+[% PROCESS global/header.html.tmpl
+ title = "Enter A Bug"
+ javascript_urls = js_urls
+ style_urls = [ 'extensions/GuidedBugEntry/web/style/guided.css',
+ 'js/yui/assets/skins/sam/container.css' ]
+ yui = yui_modules
+%]
+
+<iframe id="yui-history-iframe" src="extensions/GuidedBugEntry/web/yui-history-iframe.txt"></iframe>
+<input id="yui-history-field" type="hidden">
+
+<noscript>
+You require JavaScript to use this [% terms.bug %] entry form.<br><br>
+Please use the <a href="enter_bug.cgi?format=__default__">advanced [% terms.bug %] entry form</a>.
+</noscript>
+
+<div id="loading" class="hidden">
+Please wait...
+</div>
+<script type="text/javascript">
+YAHOO.util.Dom.removeClass('loading', 'hidden');
+</script>
+
+<div id="steps">
+[% INCLUDE product_step %]
+[% INCLUDE otherProducts_step %]
+[% INCLUDE dupes_step %]
+[% INCLUDE bugForm_step %]
+</div>
+
+<div id="advanced">
+ <a id="advanced_img" href="enter_bug.cgi?format=__default__"><img
+ src="extensions/GuidedBugEntry/web/images/advanced.png" width="16" height="16" border="0"></a>
+ <a id="advanced_link" href="enter_bug.cgi?format=__default__">Switch to the advanced [% terms.bug %] entry form</a>
+</div>
+
+<script type="text/javascript">
+YAHOO.util.Dom.addClass('loading', 'hidden');
+guided.init();
+guided.detectedPlatform = '[% platform FILTER js %]';
+guided.detectedOpSys = '[% op_sys FILTER js %]';
+guided.currentUser = '[% user.login FILTER js %]';
+guided.openStates = [
+[% FOREACH state = open_states %]
+ '[% state FILTER js%]'
+ [%- "," UNLESS loop.last %]
+[% END %]
+];
+dupes.setLabels(
+ {
+ id: "[% field_descs.bug_id FILTER js %]",
+ summary: "[% field_descs.short_desc FILTER js %]",
+ component: "[% field_descs.component FILTER js %]",
+ status: "[% field_descs.bug_status FILTER js %]",
+ }
+);
+</script>
+<script type="text/javascript" src="page.cgi?id=guided_products.js"></script>
+[% PROCESS global/footer.html.tmpl %]
+
+[%############################################################################%]
+[%# page title #%]
+[%############################################################################%]
+
+[% BLOCK page_title %]
+ <div id="page_title">
+ <h2>Enter A [% terms.Bug %]</h2>
+ <h3>Step [% step_number FILTER html %] of 3</h3>
+ </div>
+[% END %]
+
+[%############################################################################%]
+[%# product step #%]
+[%############################################################################%]
+
+[% BLOCK product_step %]
+<div id="product_step" class="step hidden">
+
+[% INCLUDE page_title
+ step_number = "1"
+%]
+
+[% INCLUDE exits
+ show = "all"
+%]
+
+<table id="products">
+[% INCLUDE 'guided/products.html.tmpl' %]
+[% INCLUDE product_block
+ name="Other Products"
+ icon="other.png"
+ desc="Other Mozilla products which aren't listed here"
+ onclick="guided.setStep('otherProducts')"
+%]
+</table>
+
+[% IF BMO %]
+ <h3>
+ Or search for a Product:
+ </h3>
+
+ <div id="prod_comp_search_main">
+ <div id="prod_comp_search_autocomplete">
+ <div id="prod_comp_search_label">
+ Type to find product and component by name or description:
+ <img id="prod_comp_throbber" src="extensions/GuidedBugEntry/web/images/throbber.gif"
+ class="hidden" width="16" height="11">
+ </div>
+ <input id="prod_comp_search" type="text" size="60">
+ <div id="prod_comp_search_autocomplete_container"></div>
+ </div>
+ </div>
+ <script type="text/javascript">
+ if (typeof(YAHOO.bugzilla.prodCompSearch) !== 'undefined' && YAHOO.bugzilla.prodCompSearch != null)
+ YAHOO.bugzilla.prodCompSearch.init('prod_comp_search', 'prod_comp_search_autocomplete_container', 'guided');
+ </script>
+[% END %]
+
+</div>
+[% END %]
+
+[% BLOCK product_block %]
+ [% IF !caption %]
+ [% caption = name %]
+ [% END %]
+ [% IF !desc %]
+ [% FOREACH c = classifications %]
+ [% FOREACH p = c.products %]
+ [% IF p.name == name %]
+ [% desc = p.description %]
+ [% LAST %]
+ [% END %]
+ [% END %]
+ [% END %]
+ [% END %]
+ <tr>
+ <td class="product_img">
+ <a href="javascript:void(0)"
+ [% IF onclick %]
+ onclick="[% onclick FILTER html %]"
+ [% ELSE %]
+ onclick="product.select('[% name FILTER js %]')"
+ [% END %]
+ ><img src="extensions/GuidedBugEntry/web/images/products/[% icon FILTER uri %]" width="64" height="64"
+ ></a>
+ </td>
+ <td>
+ <h2>
+ <a href="javascript:void(0)"
+ [% IF onclick %]
+ onclick="[% onclick FILTER html %]"
+ [% ELSE %]
+ onclick="product.select('[% name FILTER js %]')"
+ [% END %]
+ >[% caption FILTER html %]</a>
+ </h2>
+ <p>
+ [% desc FILTER html_light %]
+ </p>
+ </td>
+ </tr>
+[% END %]
+
+[%############################################################################%]
+[%# other products step #%]
+[%############################################################################%]
+
+[% BLOCK otherProducts_step %]
+<div id="otherProducts_step" class="step hidden">
+
+[% INCLUDE page_title
+ step_number = "1"
+%]
+
+[% INCLUDE exits
+ show = "all"
+%]
+
+<table id="other_products">
+[% FOREACH c = classifications %]
+ [% IF c.object %]
+ <tr class="classification">
+ <th align="right" valign="top">
+ [% c.object.name FILTER html %]:&nbsp;
+ </th>
+ <td>
+ [% c.object.description FILTER html_light %]
+ </td>
+ </tr>
+ [% END %]
+ [% FOREACH p = c.products %]
+ <tr>
+ <th align="right" valign="top">
+ <a href="javascript:void(0)" onclick="product.select('[% p.name FILTER js %]')">
+ [% p.name FILTER html FILTER no_break %]</a>:&nbsp;
+ </th>
+
+ <td valign="top">[% p.description FILTER html_light %]</td>
+ </tr>
+ [% END %]
+ <tr>
+ <td>&nbsp;</td>
+ </tr>
+[% END %]
+</table>
+
+</div>
+[% END %]
+
+[%############################################################################%]
+[%# exits (support/input) #%]
+[%############################################################################%]
+
+[% BLOCK exits %]
+<table class="exits">
+ <tr>
+ <td>
+ <div class="exit_img">
+ <a href="http://www.mozilla.org/support/"
+ ><img src="extensions/GuidedBugEntry/web/images/support.png" width="32" height="32"
+ ></a>
+ </div>
+ </td>
+ <td width="100%">
+ <h2>
+ <a href="http://www.mozilla.org/support/">I need technical support</a>
+ </h2>
+ For technical support or help getting your site to work with Mozilla.
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <div class="exit_img">
+ <a href="http://input.mozilla.org/idea/"
+ ><img src="extensions/GuidedBugEntry/web/images/idea.png" width="32" height="32"
+ ></a>
+ </div>
+ </td>
+ <td width="100%">
+ <h2>
+ <a href="http://input.mozilla.org/idea/">I have an idea for firefox</a>
+ </h2>
+ For offering us ideas on how to enhance Firefox.
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <div class="exit_img">
+ <a href="http://input.mozilla.org/feedback/"
+ ><img src="extensions/GuidedBugEntry/web/images/input.png" width="32" height="32"
+ ></a>
+ </div>
+ </td>
+ <td width="100%">
+ &bull; <a href="http://input.mozilla.org/feedback/">Provide other feedback about Firefox</a><br>
+ &bull; <a href="http://input.mozilla.org/feedback/#sad">Report an issue with a web site that I use</a><br>
+ &bull; <a href="enter_bug.cgi?format=guided&product=Core">Report an issue with Firefox on a site that I've developed</a><br>
+ </td>
+ </tr>
+</table>
+<h3>
+ None of the above; my [% terms.bug %] is in:
+</h3>
+[% END %]
+
+[% BLOCK exit_block %]
+ <tr>
+ <td>
+ <div class="exit_img">
+ <a href="[% href FILTER none %]"
+ ><img src="extensions/GuidedBugEntry/web/images/[% icon FILTER uri %]" width="32" height="32"
+ ></a>
+ </div>
+ </td>
+ <td width="100%">
+ <h2>
+ <a href="[% href FILTER none %]">[% name FILTER html %]</a>
+ </h2>
+ [% desc FILTER html %]
+ </td>
+ </tr>
+[% END %]
+
+[%############################################################################%]
+[%# duplicates step #%]
+[%############################################################################%]
+
+[% BLOCK dupes_step %]
+<div id="dupes_step" class="step hidden">
+
+[% INCLUDE page_title
+ step_number = "2"
+%]
+
+<p>
+Product: <b><span id="dupes_product_name">?</span></b>:
+(<a href="javascript:void(0)" onclick="guided.setStep('product')">Change</a>)
+</p>
+
+<table border="0" cellpadding="5" cellspacing="0" id="product_support" class="hidden">
+<tr>
+<td>
+ <img src="extensions/GuidedBugEntry/web/images/message.png" width="24" height="24">
+</td>
+<td id="product_support_message">&nbsp;</td>
+</table>
+
+<div id="dupe_form">
+ <p>
+ Please summarise your issue or request in one sentence:
+ </p>
+ <input id="dupes_summary" value="Short summary of issue" spellcheck="true" placeholder="Short summary of issue">
+ <button id="dupes_search">Find similar issues</button>
+ <button id="dupes_continue_button_top" onclick="guided.setStep('bugForm')">My issue is not listed</button>
+</div>
+
+<div id="dupes_list"></div>
+<div id="dupes_continue">
+<button id="dupes_continue_button_bottom" onclick="guided.setStep('bugForm')">My issue is not listed</button>
+</div>
+
+</div>
+[% END %]
+
+[%############################################################################%]
+[%# bug form step #%]
+[%############################################################################%]
+
+[% BLOCK bugForm_step %]
+<div id="bugForm_step" class="step hidden">
+
+[% INCLUDE page_title
+ step_number = "3"
+%]
+
+<form method="post" action="post_bug.cgi" enctype="multipart/form-data" onsubmit="return bugForm.validate()">
+<input type="hidden" name="token" value="[% token FILTER html %]">
+<input type="hidden" name="product" id="product" value="">
+<input type="hidden" name="component" id="component" value="">
+<input type="hidden" name="bug_severity" value="normal">
+<input type="hidden" name="rep_platform" id="rep_platform" value="All">
+<input type="hidden" name="priority" value="--">
+<input type="hidden" name="op_sys" id="op_sys" value="All">
+<input type="hidden" name="version" id="version" value="">
+<input type="hidden" name="comment" id="comment" value="">
+<input type="hidden" name="format" value="guided">
+<input type="hidden" name="user_agent" id="user_agent" value="">
+<input type="hidden" name="build_id" id="build_id" value="">
+
+<ul>
+<li>Please fill out this form clearly, precisely and in as much detail as you can manage.</li>
+<li>Please report only a single problem at a time.</li>
+<li><a href="https://developer.mozilla.org/en/Bug_writing_guidelines" target="_blank">These guidelines</a>
+explain how to write effective [% terms.bug %] reports.</li>
+</ul>
+
+<table id="bugForm" cellspacing="0">
+
+<tr class="odd">
+ <td class="label">Summary:</td>
+ <td width="100%" colspan="2">
+ <input name="short_desc" id="short_desc" class="textInput" spellcheck="true">
+ </td>
+ <td valign="top">
+ [% PROCESS help id="summary_help" %]
+ <div id="summary_help" class="hidden help">
+ A sentence which summarises the problem. Please be descriptive and use lots of keywords.<br>
+ <br>
+ <span class="help-bad">Bad example</span>: mail crashed<br>
+ <span class="help-good">Good example</span>: crash if I close the mail window while checking for new POP mail
+ </div>
+ </td>
+</tr>
+
+<tr class="even">
+ <td class="label">Product:</td>
+ <td id="productTD">
+ <span id="product_label"></span>
+ (<a href="javascript:void(0)" onclick="guided.setStep('product')">Change</a>)
+ </td>
+ <td id="versionTD" class="hidden">
+ <span class="label">Version:
+ <select id="version_select" onchange="bugForm.onVersionChange(this.value)">
+ </select>
+ </td>
+ <td valign="top">
+ [% PROCESS help id="product_help" %]
+ <div id="product_help" class="hidden help">
+ The Product and Version you are reporting the issue with.
+ </div>
+</tr>
+
+<tr class="odd" id="componentTR">
+ <td valign="top">
+ <div class="label">
+ Component:
+ </div>
+ (<a id="list_comp" href="describecomponents.cgi" target="_blank"
+ title="Show a list of all components and descriptions (in a new window)."
+ >List</a>)
+ </td>
+ <td valign="top" colspan="2">
+ <select id="component_select" onchange="bugForm.onComponentChange(this.value)" class="mandatory">
+ </select>
+ <div id="component_description"></div>
+ </td>
+ <td valign="top">
+ [% PROCESS help id="component_help" %]
+ <div id="component_help" class="hidden help">
+ The area where the problem occurs.<br>
+ <br>
+ If you are unsure which component to use, select a 'General' component.
+ </div>
+</tr>
+
+<tr class="even">
+ <td class="label" colspan="3">What did you do?</td>
+ <td valign="top">
+ [% PROCESS help id="steps_help" %]
+ <div id="steps_help" class="hidden help">
+ Please be as specific as possible about what what you did
+ to cause the problem. Providing step-by-step instructions
+ would be ideal.<br>
+ <br>
+ Include any relevant URLs and special setup steps.<br>
+ <br>
+ <span class="help-bad">Bad example</span>: Mozilla crashed. You suck!<br>
+ <span class="help-good">Good example</span>: 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.
+ </div>
+ </td>
+</tr>
+<tr class="even">
+ <td colspan="3"><textarea id="bug_steps" name="bug_steps" rows="5"></textarea></td>
+ <td>&nbsp;</td>
+</tr>
+
+<tr class="odd">
+ <td class="label" colspan="3">What happened?</td>
+ <td valign="top">
+ [% PROCESS help id="actual_help" %]
+ <div id="actual_help" class="hidden help">
+ What happened after you performed the steps above?
+ </div>
+</tr>
+<tr class="odd">
+ <td colspan="3"><textarea id="actual" name="actual" rows="5"></textarea></td>
+ <td>&nbsp;</td>
+</tr>
+
+<tr class="even">
+ <td class="label" colspan="3">What should have happened?</td>
+ <td valign="top">
+ [% PROCESS help id="expected_help" %]
+ <div id="expected_help" class="hidden help">
+ What should the software have done instead?
+ </div>
+</tr>
+<tr class="even">
+ <td colspan="3"><textarea id="expected" name="expected" rows="5"></textarea></td>
+ <td>&nbsp;</td>
+</tr>
+
+<tr class="odd">
+ <td class="label">Attach a file:</td>
+ <td colspan="2">
+ <input type="file" name="data" id="data" size="50" onchange="bugForm.onFileChange()">
+ <input type="hidden" name="contenttypemethod" value="autodetect">
+ <button id="reset_data" onclick="return bugForm.onFileClear()" disabled>Clear</button>
+ </td>
+ <td valign="top">
+ [% PROCESS help id="file_help" %]
+ <div id="file_help" class="hidden help">
+ If a file helps explain the issue better, such as a screenshot, please
+ attach one here.
+ </div>
+ </td>
+</tr>
+<tr class="odd">
+ <td class="label">File Description:</td>
+ <td colspan="2"><input type="text" name="description" id="data_description" class="textInput" disabled></td>
+ <td>&nbsp;</td>
+</tr>
+
+<tr class="even">
+ <td class="label">Security:</td>
+ <td colspan="2">
+ <table border="0" cellpadding="0" cellspacing="0">
+ <tr>
+ <td>
+ <input type="checkbox" name="groups" value="core-security" id="groups">
+ </td>
+ <td>
+ <label for="groups">Many users could be harmed by this security problem:
+ it should be kept hidden from the public until it is resolved.</label>
+ </td>
+ </tr>
+ </table>
+ </td>
+ <td>&nbsp;</td>
+</tr>
+
+<tr class="odd">
+ <td>&nbsp;</td>
+ <td colspan="2" id="submitTD">
+ <input type="submit" id="submit" value="Submit [% terms.Bug %]">
+ </td>
+ <td>&nbsp;</td>
+</tr>
+
+</table>
+
+</form>
+
+</div>
+[% END %]
+
+[%############################################################################%]
+[%# help block #%]
+[%############################################################################%]
+
+[% BLOCK help %]
+<img src="extensions/GuidedBugEntry/web/images/help.png" width="16" height="16" class="help_image"
+ helpid="[% id FILTER html %]" onMouseOver="bugForm.showHelp(this)" onMouseOut="bugForm.hideHelp(this)"
+ >
+[% 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..22c93a354
--- /dev/null
+++ b/extensions/GuidedBugEntry/template/en/default/guided/products.html.tmpl
@@ -0,0 +1,44 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% INCLUDE product_block
+ name="Firefox"
+ icon="firefox.png"
+%]
+[% INCLUDE product_block
+ name="Firefox for Android"
+ icon="firefox.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..231681085
--- /dev/null
+++ b/extensions/GuidedBugEntry/template/en/default/pages/guided_products.js.tmpl
@@ -0,0 +1,18 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[%# 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
--- /dev/null
+++ b/extensions/GuidedBugEntry/web/images/advanced.png
Binary files differ
diff --git a/extensions/GuidedBugEntry/web/images/help.png b/extensions/GuidedBugEntry/web/images/help.png
new file mode 100644
index 000000000..5c870176d
--- /dev/null
+++ b/extensions/GuidedBugEntry/web/images/help.png
Binary files differ
diff --git a/extensions/GuidedBugEntry/web/images/idea.png b/extensions/GuidedBugEntry/web/images/idea.png
new file mode 100644
index 000000000..0a0ce6c79
--- /dev/null
+++ b/extensions/GuidedBugEntry/web/images/idea.png
Binary files differ
diff --git a/extensions/GuidedBugEntry/web/images/input.png b/extensions/GuidedBugEntry/web/images/input.png
new file mode 100644
index 000000000..34c10e989
--- /dev/null
+++ b/extensions/GuidedBugEntry/web/images/input.png
Binary files differ
diff --git a/extensions/GuidedBugEntry/web/images/message.png b/extensions/GuidedBugEntry/web/images/message.png
new file mode 100644
index 000000000..55b6add19
--- /dev/null
+++ b/extensions/GuidedBugEntry/web/images/message.png
Binary files 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
--- /dev/null
+++ b/extensions/GuidedBugEntry/web/images/products/camino.png
Binary files 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
--- /dev/null
+++ b/extensions/GuidedBugEntry/web/images/products/core.png
Binary files 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
--- /dev/null
+++ b/extensions/GuidedBugEntry/web/images/products/dino.png
Binary files 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
--- /dev/null
+++ b/extensions/GuidedBugEntry/web/images/products/fennec.png
Binary files 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
--- /dev/null
+++ b/extensions/GuidedBugEntry/web/images/products/firefox.png
Binary files 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
--- /dev/null
+++ b/extensions/GuidedBugEntry/web/images/products/labs.png
Binary files 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
--- /dev/null
+++ b/extensions/GuidedBugEntry/web/images/products/mozilla.png
Binary files 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
--- /dev/null
+++ b/extensions/GuidedBugEntry/web/images/products/other.png
Binary files 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
--- /dev/null
+++ b/extensions/GuidedBugEntry/web/images/products/seamonkey.png
Binary files 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
--- /dev/null
+++ b/extensions/GuidedBugEntry/web/images/products/sunbird.png
Binary files 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
--- /dev/null
+++ b/extensions/GuidedBugEntry/web/images/products/thunderbird.png
Binary files differ
diff --git a/extensions/GuidedBugEntry/web/images/sumo.png b/extensions/GuidedBugEntry/web/images/sumo.png
new file mode 100644
index 000000000..d5773647c
--- /dev/null
+++ b/extensions/GuidedBugEntry/web/images/sumo.png
Binary files differ
diff --git a/extensions/GuidedBugEntry/web/images/support.png b/extensions/GuidedBugEntry/web/images/support.png
new file mode 100644
index 000000000..2320ea74a
--- /dev/null
+++ b/extensions/GuidedBugEntry/web/images/support.png
Binary files differ
diff --git a/extensions/GuidedBugEntry/web/images/throbber.gif b/extensions/GuidedBugEntry/web/images/throbber.gif
new file mode 100644
index 000000000..bc4fa6561
--- /dev/null
+++ b/extensions/GuidedBugEntry/web/images/throbber.gif
Binary files differ
diff --git a/extensions/GuidedBugEntry/web/images/warning.png b/extensions/GuidedBugEntry/web/images/warning.png
new file mode 100644
index 000000000..86bed170d
--- /dev/null
+++ b/extensions/GuidedBugEntry/web/images/warning.png
Binary files differ
diff --git a/extensions/GuidedBugEntry/web/js/guided.js b/extensions/GuidedBugEntry/web/js/guided.js
new file mode 100644
index 000000000..1883e4eb6
--- /dev/null
+++ b/extensions/GuidedBugEntry/web/js/guided.js
@@ -0,0 +1,909 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0. */
+
+// 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
+ switch(newStep) {
+ case 'product':
+ product.onShow();
+ break;
+ case 'otherProducts':
+ otherProducts.onShow();
+ break;
+ case 'dupes':
+ dupes.onShow();
+ break;
+ case 'bugForm':
+ bugForm.onShow();
+ break;
+ default:
+ guided.setStep('product');
+ return;
+ }
+
+ // 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() +
+ (product.getPreselectedComponent() ? '|' + product.getPreselectedComponent() : '')
+ );
+ }
+ },
+
+ 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] || '');
+ product.setPreselectedComponent(state[2] || '');
+ guided.setStep(state[0], noSetHistory);
+ },
+
+ setAdvancedLink: function() {
+ href = 'enter_bug.cgi?format=__default__' +
+ '&product=' + encodeURIComponent(product.getName()) +
+ '&short_desc=' + encodeURIComponent(dupes.getSummary());
+ Dom.get('advanced_img').href = href;
+ Dom.get('advanced_link').href = href;
+ }
+};
+
+// product step
+
+var product = {
+ details: false,
+ _counter: 0,
+ _loaded: '',
+ _preselectedComponent: '',
+
+ 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;
+ },
+
+ getPreselectedComponent: function() {
+ return this._preselectedComponent;
+ },
+
+ setPreselectedComponent: function(value) {
+ this._preselectedComponent = 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=' + encodeURIComponent(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 = products[productName].support;
+ Dom.removeClass("product_support", "hidden");
+ } else {
+ Dom.addClass("product_support", "hidden");
+ }
+
+ // show/hide component selection row
+ if (products[productName] && products[productName].noComponentSelection) {
+ if (!Dom.hasClass('componentTR', 'hidden')) {
+ Dom.addClass('componentTR', 'hidden');
+ bugForm.toggleOddEven();
+ }
+ } else {
+ if (Dom.hasClass('componentTR', 'hidden')) {
+ Dom.removeClass('componentTR', 'hidden');
+ bugForm.toggleOddEven();
+ }
+ }
+
+ if (this._loaded == productName)
+ return;
+
+ // grab the product information
+ this.details = false;
+ this._loaded = productName;
+ YAHOO.util.Connect.setDefaultPostHeader('application/json; 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, 'input', 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: "component", label: labels.component, 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 = '<a href="show_bug.cgi?id=' + oData +
+ '" target="_blank">' + oData + '</a>';
+ },
+
+ _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&nbsp;following';
+ button.onclick = function() {
+ dupes.updateFollowing(el, id, bugStatus, button, false); return false;
+ };
+ } else {
+ button.innerHTML = 'Follow&nbsp;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('application/json; 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 <enter> 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...&nbsp;&nbsp;&nbsp;' +
+ '<img src="extensions/GuidedBugEntry/web/images/throbber.gif"' +
+ ' width="16" height="11">',
+ 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", "component" ]
+ }
+ };
+
+ 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: [],
+
+ 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;
+
+ // filter components
+ if (products[productName] && products[productName].componentFilter) {
+ product.details.components = products[productName].componentFilter(product.details.components);
+ }
+
+ // build components
+
+ var elComponent = Dom.get('component');
+ if (products[productName] && products[productName].noComponentSelection) {
+
+ elComponent.value = products[productName].defaultComponent;
+ bugForm._mandatoryFields = [ 'short_desc', 'version_select' ];
+
+ } else {
+
+ bugForm._mandatoryFields = [ 'short_desc', 'component_select', 'version_select' ];
+
+ // check for the default component
+ var defaultRegex;
+ if (product.getPreselectedComponent()) {
+ defaultRegex = new RegExp('^' + quoteMeta(product.getPreselectedComponent()) + '$', 'i')
+ } else if(products[productName] && products[productName].defaultComponent) {
+ defaultRegex = new RegExp('^' + quoteMeta(products[productName].defaultComponent) + '$', 'i')
+ } else {
+ defaultRegex = new RegExp('General', 'i');
+ }
+
+ var preselectedComponent = false;
+ for (var i = 0, n = product.details.components.length; i < n; i++) {
+ var component = product.details.components[i];
+ if (component.is_active == '1') {
+ if (defaultRegex.test(component.name)) {
+ preselectedComponent = component.name;
+ break;
+ }
+ }
+ }
+
+ // if there isn't a default component, default to blank
+ if (!preselectedComponent) {
+ elComponents.options[elComponents.options.length] = new Option('', '');
+ }
+
+ // build component select
+ 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);
+ }
+ }
+
+ var validComponent = false;
+ for (var i = 0, n = elComponents.options.length; i < n && !validComponent; i++) {
+ if (elComponents.options[i].value == elComponent.value)
+ validComponent = true;
+ }
+ if (!validComponent)
+ elComponent.value = '';
+ if (elComponent.value == '' && preselectedComponent)
+ elComponent.value = preselectedComponent;
+ 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;
+ },
+
+ toggleOddEven: function() {
+ var rows = Dom.get('bugForm').getElementsByTagName('TR');
+ var doToggle = false;
+ for (var i = 0, n = rows.length; i < n; i++) {
+ if (doToggle) {
+ rows[i].className = rows[i].className == 'odd' ? 'even' : 'odd';
+ } else {
+ doToggle = rows[i].id == 'componentTR';
+ }
+ }
+ },
+
+ _getFilename: function() {
+ var filename = Dom.get('data').value;
+ if (!filename)
+ return '';
+ filename = filename.replace(/^.+[\\\/]/, '');
+ return filename;
+ },
+
+ _mandatoryMissing: function() {
+ var result = new Array();
+ 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.push(id);
+ } else {
+ Dom.removeClass(id, 'missing');
+ }
+ }
+ return result;
+ },
+
+ validate: function() {
+
+ // check mandatory fields
+
+ var missing = bugForm._mandatoryMissing();
+ if (missing.length) {
+ var message = 'The following field' +
+ (missing.length == 1 ? ' is' : 's are') + ' required:\n\n';
+ for (var i = 0, n = missing.length; i < n; i++ ) {
+ var id = missing[i];
+ if (id == 'short_desc') message += ' Summary\n';
+ if (id == 'component_select') message += ' Component\n';
+ if (id == 'version_select') message += ' Version\n';
+ }
+ alert(message);
+ 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;
+ }
+}
+
+function quoteMeta(value) {
+ return value.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
+}
diff --git a/extensions/GuidedBugEntry/web/js/products.js b/extensions/GuidedBugEntry/web/js/products.js
new file mode 100644
index 000000000..026e94f0d
--- /dev/null
+++ b/extensions/GuidedBugEntry/web/js/products.js
@@ -0,0 +1,128 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0. */
+
+/* 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
+ * defaultComponent: the default compoent to select. Defaults to 'General'
+ * noComponentSelection: when true, the default component will always be
+ * used. Defaults to 'false';
+ * 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;
+ }
+ },
+ defaultComponent: "Untriaged",
+ noComponentSelection: true,
+ detectPlatform: true,
+ support:
+ 'If you are new to Firefox or Bugzilla, please consider checking ' +
+ '<a href="http://support.mozilla.com/">' +
+ '<img src="extensions/GuidedBugEntry/web/images/sumo.png" width="16" height="16" align="absmiddle">' +
+ ' <b>Firefox Help</b></a> instead of creating a bug.'
+ },
+
+ "Fennec": {
+ related: [ "Firefox for Android", "Core", "Toolkit" ],
+ detectPlatform: true,
+ support:
+ 'If you are new to Firefox or Bugzilla, please consider checking ' +
+ '<a href="http://support.mozilla.com/">' +
+ '<img src="extensions/GuidedBugEntry/web/images/sumo.png" width="16" height="16" align="absmiddle">' +
+ ' <b>Firefox Help</b></a> instead of creating a bug.'
+ },
+
+ "Firefox for Android": {
+ related: [ "Fennec", "Core", "Toolkit" ],
+ detectPlatform: true,
+ support:
+ 'If you are new to Firefox or Bugzilla, please consider checking ' +
+ '<a href="http://support.mozilla.com/">' +
+ '<img src="extensions/GuidedBugEntry/web/images/sumo.png" width="16" height="16" align="absmiddle">' +
+ ' <b>Firefox Help</b></a> 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,
+ defaultComponent: "Untriaged",
+ componentFilter : function(components) {
+ var index = -1;
+ for (var i = 0, l = components.length; i < l; i++) {
+ if (components[i].name == 'General') {
+ index = i;
+ break;
+ }
+ }
+ if (index != -1) {
+ components.splice(index, 1);
+ }
+ return components;
+ }
+ },
+
+ "Penelope": {
+ related: [ "Core", "Toolkit", "MailNews Core" ]
+ },
+
+ "Bugzilla": {
+ support:
+ 'Please use <a href="http://landfill.bugzilla.org/">Bugzilla Landfill</a> to file "test bugs".'
+ },
+
+ "bugzilla.mozilla.org": {
+ related: [ "Bugzilla" ],
+ support:
+ 'Please use <a href="http://landfill.bugzilla.org/">Bugzilla Landfill</a> 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..55550933f
--- /dev/null
+++ b/extensions/GuidedBugEntry/web/style/guided.css
@@ -0,0 +1,229 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0. */
+
+/* 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;
+ border: 1px solid #aaa;
+ border-radius: 5px;
+}
+
+.exits td {
+ padding: 5px;
+}
+
+.exits h2 {
+ margin: 0px;
+ font-size: 90%;
+}
+
+.exit_img {
+ width: 64px;
+ text-align: right;
+}
+
+#prod_comp_search_main {
+ width: 400px;
+}
+
+#prod_comp_search_label {
+ margin-bottom: 1px;
+}
+
+#prod_comp_search_main li.yui-ac-highlight a {
+ text-decoration: none;
+ color: #FFFFFF;
+ display: block;
+}
+
+#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/BmpConvert/disabled b/extensions/GuidedBugEntry/web/yui-history-iframe.txt
index e69de29bb..e69de29bb 100644
--- a/extensions/BmpConvert/disabled
+++ b/extensions/GuidedBugEntry/web/yui-history-iframe.txt
diff --git a/extensions/InlineHistory/Config.pm b/extensions/InlineHistory/Config.pm
new file mode 100644
index 000000000..3834bd81d
--- /dev/null
+++ b/extensions/InlineHistory/Config.pm
@@ -0,0 +1,13 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::InlineHistory;
+use strict;
+
+use constant NAME => 'InlineHistory';
+
+__PACKAGE__->NAME;
diff --git a/extensions/InlineHistory/Extension.pm b/extensions/InlineHistory/Extension.pm
new file mode 100644
index 000000000..2e388994a
--- /dev/null
+++ b/extensions/InlineHistory/Extension.pm
@@ -0,0 +1,206 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::InlineHistory;
+use strict;
+use base qw(Bugzilla::Extension);
+
+use Bugzilla::User::Setting;
+use Bugzilla::Constants;
+use Bugzilla::Attachment;
+
+our $VERSION = '1.5';
+
+# don't show inline history for bugs with lots of changes
+use constant MAXIMUM_ACTIVITY_COUNT => 500;
+
+sub template_before_process {
+ my ($self, $args) = @_;
+ my $file = $args->{'file'};
+ my $vars = $args->{'vars'};
+
+ return if $file ne 'bug/edit.html.tmpl';
+
+ my $user = Bugzilla->user;
+ my $dbh = Bugzilla->dbh;
+ return unless $user->id && $user->settings->{'inline_history'}->{'value'} eq 'on';
+
+ # note: bug/edit.html.tmpl doesn't support multiple bugs
+ my $bug = exists $vars->{'bugs'} ? $vars->{'bugs'}[0] : $vars->{'bug'};
+ my $bug_id = $bug->id;
+
+ # build bug activity
+ my ($activity) = Bugzilla::Bug::GetBugActivity($bug_id);
+ $activity = _add_duplicates($bug_id, $activity);
+
+ if (scalar @$activity > MAXIMUM_ACTIVITY_COUNT) {
+ $activity = [];
+ $vars->{'ih_activity'} = 0;
+ $vars->{'ih_activity_max'} = 1;
+ return;
+ }
+
+ # prime caches with objects already loaded
+ my %user_cache;
+ foreach my $comment (@{$bug->comments}) {
+ $user_cache{$comment->{author}->login} = $comment->{author};
+ }
+
+ my %attachment_cache;
+ foreach my $attachment (@{$bug->attachments}) {
+ $attachment_cache{$attachment->id} = $attachment;
+ }
+
+ # build a list of bugs we need to check visibility of, so we can check with a single query
+ my %visible_bug_ids;
+
+ # augment and tweak
+ foreach my $operation (@$activity) {
+ # make operation.who an object
+ $user_cache{$operation->{who}} ||= Bugzilla::User->new({ name => $operation->{who} });
+ $operation->{who} = $user_cache{$operation->{who}};
+
+ for (my $i = 0; $i < scalar(@{$operation->{changes}}); $i++) {
+ my $change = $operation->{changes}->[$i];
+
+ # make an attachment object
+ if ($change->{attachid}) {
+ $change->{attach} = $attachment_cache{$change->{attachid}};
+ }
+
+ # empty resolutions are displayed as --- by default
+ # make it explicit here to enable correct display of the change
+ if ($change->{fieldname} eq 'resolution') {
+ $change->{removed} = '---' if $change->{removed} eq '';
+ $change->{added} = '---' if $change->{added} eq '';
+ }
+
+ # make boolean fields true/false instead of 1/0
+ my ($table, $field) = ('bugs', $change->{fieldname});
+ if ($field =~ /^([^\.]+)\.(.+)$/) {
+ ($table, $field) = ($1, $2);
+ }
+ my $column = $dbh->bz_column_info($table, $field);
+ if ($column && $column->{TYPE} eq 'BOOLEAN') {
+ $change->{removed} = '';
+ $change->{added} = $change->{added} ? 'true' : 'false';
+ }
+
+ my $field_obj;
+ if ($change->{fieldname} =~ /^cf_/) {
+ $field_obj = Bugzilla::Field->new({ name => $change->{fieldname} });
+ }
+
+ # identify buglist changes
+ if ($change->{fieldname} eq 'blocked' ||
+ $change->{fieldname} eq 'dependson' ||
+ $change->{fieldname} eq 'dupe' ||
+ ($field_obj && $field_obj->type == FIELD_TYPE_BUG_ID)
+ ) {
+ $change->{buglist} = 1;
+ foreach my $what (qw(removed added)) {
+ my @buglist = split(/[\s,]+/, $change->{$what});
+ foreach my $id (@buglist) {
+ if ($id && $id =~ /^\d+$/) {
+ $visible_bug_ids{$id} = 1;
+ }
+ }
+ }
+ }
+
+ # split multiple flag changes (must be processed last)
+ if ($change->{fieldname} eq 'flagtypes.name') {
+ my @added = split(/, /, $change->{added});
+ my @removed = split(/, /, $change->{removed});
+ next if scalar(@added) <= 1 && scalar(@removed) <= 1;
+ # remove current change
+ splice(@{$operation->{changes}}, $i, 1);
+ # restructure into added/removed for each flag
+ my %flags;
+ foreach my $added (@added) {
+ my ($value, $name) = $added =~ /^((.+).)$/;
+ $flags{$name}{added} = $value;
+ $flags{$name}{removed} |= '';
+ }
+ foreach my $removed (@removed) {
+ my ($value, $name) = $removed =~ /^((.+).)$/;
+ $flags{$name}{added} |= '';
+ $flags{$name}{removed} = $value;
+ }
+ # clone current change, modify and insert
+ foreach my $flag (sort keys %flags) {
+ my $flag_change = {};
+ foreach my $key (keys %$change) {
+ $flag_change->{$key} = $change->{$key};
+ }
+ $flag_change->{removed} = $flags{$flag}{removed};
+ $flag_change->{added} = $flags{$flag}{added};
+ splice(@{$operation->{changes}}, $i, 0, $flag_change);
+ }
+ $i--;
+ }
+ }
+ }
+
+ $user->visible_bugs([keys %visible_bug_ids]);
+
+ $vars->{'ih_activity'} = $activity;
+}
+
+sub _add_duplicates {
+ # insert 'is a dupe of this bug' comment to allow js to display
+ # as activity
+
+ my ($bug_id, $activity) = @_;
+
+ my $dbh = Bugzilla->dbh;
+ my $sth = $dbh->prepare("
+ SELECT profiles.login_name, " .
+ $dbh->sql_date_format('bug_when', '%Y.%m.%d %H:%i:%s') . ",
+ extra_data,
+ thetext
+ FROM longdescs
+ INNER JOIN profiles ON profiles.userid = longdescs.who
+ WHERE bug_id = ?
+ AND (
+ type = ?
+ OR thetext LIKE '%has been marked as a duplicate of this%'
+ )
+ ORDER BY bug_when
+ ");
+ $sth->execute($bug_id, CMT_HAS_DUPE);
+
+ while (my($who, $when, $dupe_id, $the_text) = $sth->fetchrow_array) {
+ if (!$dupe_id) {
+ next unless $the_text =~ / (\d+) has been marked as a duplicate of this/;
+ $dupe_id = $1;
+ }
+ my $entry = {
+ 'when' => $when,
+ 'who' => $who,
+ 'changes' => [
+ {
+ 'removed' => '',
+ 'added' => $dupe_id,
+ 'attachid' => undef,
+ 'fieldname' => 'dupe',
+ 'dupe' => 1,
+ }
+ ],
+ };
+ push @$activity, $entry;
+ }
+
+ return [ sort { $a->{when} cmp $b->{when} } @$activity ];
+}
+
+sub install_before_final_checks {
+ my ($self, $args) = @_;
+ add_setting('inline_history', ['on', 'off'], 'off');
+}
+
+__PACKAGE__->NAME;
diff --git a/extensions/InlineHistory/README b/extensions/InlineHistory/README
new file mode 100644
index 000000000..f5aaf163f
--- /dev/null
+++ b/extensions/InlineHistory/README
@@ -0,0 +1,10 @@
+InlineHistory inserts bug activity inline with the comments when viewing a bug.
+It was derived from the Bugzilla Tweaks Addon by Ehasn Akhgari.
+
+For technical and performance reasons it is only available to logged in users,
+and is enabled by a User Preference.
+
+It works with an unmodified install of Bugzilla 4.0 and 4.2.
+
+If you have modified your show_bug template, the javascript in
+web/inline-history.js may need to be updated to suit your installation.
diff --git a/extensions/InlineHistory/template/en/default/hook/bug/comments-aftercomments.html.tmpl b/extensions/InlineHistory/template/en/default/hook/bug/comments-aftercomments.html.tmpl
new file mode 100644
index 000000000..1c47fd21c
--- /dev/null
+++ b/extensions/InlineHistory/template/en/default/hook/bug/comments-aftercomments.html.tmpl
@@ -0,0 +1,152 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% RETURN UNLESS ih_activity %]
+[%# this div exists to allow bugzilla-tweaks to detect when we're active %]
+<div id="inline-history-ext"></div>
+
+<script>
+ var ih_activity = new Array();
+ var ih_activity_flags = new Array();
+ var ih_activity_sort_order = '[% user.settings.comment_sort_order.value FILTER js %]';
+ [% FOREACH operation = ih_activity %]
+ var html = '';
+ [% has_cc = 0 %]
+ [% has_flag = 0 %]
+ [% changer_identity = operation.who.identity %]
+ [% changer_login = operation.who.login %]
+ [% change_date = operation.when FILTER time %]
+
+ [% FOREACH change = operation.changes %]
+ [%# track flag changes %]
+ [% IF change.fieldname == 'flagtypes.name' && change.added != '' %]
+ var item = new Array(5);
+ item[0] = '[% changer_login FILTER js %]';
+ item[1] = '[% change_date FILTER js %]';
+ item[2] = '[% change.attachid FILTER js %]';
+ item[3] = '[% change.added FILTER js %]';
+ item[4] = '[% changer_identity FILTER js %]';
+ ih_activity_flags.push(item);
+ [% has_flag = 1 %]
+ [% END %]
+
+ [%# wrap CC changes in a span for toggling visibility %]
+ [% IF change.fieldname == 'cc' %]
+ html += '<span class="ih_cc">';
+ [% has_cc = 1 %]
+ [% END %]
+
+ [%# make attachment changes better %]
+ [% IF change.attachid %]
+ html += '<a '
+ + 'href="attachment.cgi?id=[% change.attachid FILTER none %]&amp;action=edit" '
+ + 'title="[% change.attach.description FILTER html FILTER js %]" '
+ + 'class="[% "bz_obsolete" IF change.attach.isobsolete %]"'
+ + '>Attachment #[% change.attachid FILTER none %]</a> - ';
+ [% END %]
+
+ [%# buglists need to be displayed differently, as we shouldn't use strike-out %]
+ [% IF change.buglist %]
+ [% IF change.dupe %]
+ [% label = 'Duplicate of this ' _ terms.bug %]
+ [% ELSE %]
+ [% label = field_descs.${change.fieldname} %]
+ [% END %]
+ [% IF change.added != '' %]
+ html += '[% label FILTER js %]: ';
+ [% PROCESS add_change value = change.added %]
+ [% END %]
+ [% IF change.removed != '' %]
+ [% "html += '<br>';" IF change.added != '' %]
+ html += 'No longer [% label FILTER lcfirst FILTER js %]: ';
+ [% PROCESS add_change value = change.removed %]
+ [% END %]
+ [% ELSE %]
+ [% IF change.fieldname == 'longdescs.isprivate' %]
+ [%# reference the comment that was made private/public in the field label %]
+ html += '<a href="#c[% change.comment.count FILTER js %]">'
+ + 'Comment [% change.comment.count FILTER js %]</a> is private: ';
+ [% ELSE %]
+ [%# normal label %]
+ html += '[% field_descs.${change.fieldname} FILTER js %]: ';
+ [% END %]
+ [% IF change.removed != '' %]
+ [% IF change.added == '' %]
+ html += '<span class="ih_deleted">';
+ [% END %]
+ [% PROCESS add_change value = change.removed %]
+ [% IF change.added == '' %]
+ html += '</span>';
+ [% ELSE %]
+ html += ' &rarr; ';
+ [% END %]
+ [% END %]
+ [% PROCESS add_change value = change.added %]
+ [% END %]
+ [% "html += '<br>';" UNLESS loop.last %]
+
+ [% IF change.fieldname == 'cc' %]
+ html += '</span>';
+ [% END %]
+ [% END %]
+
+ [% changer_id = operation.who.id %]
+ [% UNLESS user_cache.$changer_id %]
+ [% user_cache.$changer_id = BLOCK %]
+ [% INCLUDE global/user.html.tmpl who = operation.who %]
+ [% END %]
+ [% END %]
+
+ var item = new Array(7);
+ item[0] = '[% changer_login FILTER js %]';
+ item[1] = '[% change_date FILTER js %]';
+ item[2] = html;
+ item[3] = '<div class="bz_comment_head">'
+ + '<span class="bz_comment_user">'
+ + '[% user_cache.$changer_id FILTER js %]'
+ + '</span>'
+ + '<span class="bz_comment_time"> ' + item[1] + ' </span>'
+ + '</div>';
+ item[4] = [% IF has_cc && (operation.changes.size == 1) %]true[% ELSE %]false[% END %];
+ item[5] = [% IF change.dupe %][% change.added FILTER js %][% ELSE %]0[% END %];
+ item[6] = [% IF has_flag %]true[% ELSE %]false[% END %];
+ ih_activity[[% loop.index %]] = item;
+ [% END %]
+ inline_history.init();
+</script>
+
+[% BLOCK add_change %]
+ html += '[%~%]
+ [% IF change.fieldname == 'estimated_time' ||
+ change.fieldname == 'remaining_time' ||
+ change.fieldname == 'work_time' %]
+ [% PROCESS formattimeunit time_unit = value FILTER html FILTER js %]
+ [% ELSIF change.buglist %]
+ [% value FILTER bug_list_link FILTER js %]
+ [% ELSIF change.fieldname == 'bug_file_loc' %]
+ [%~%]<a href="[% value FILTER html FILTER js %]" target="_blank"
+ [%~ ' onclick="return inline_history.confirmUnsafeUrl(this.href)"'
+ UNLESS is_safe_url(value) %]>
+ [%~%][% value FILTER html FILTER js %]</a>
+ [% ELSIF change.fieldname == 'see_also' %]
+ [% FOREACH see_also = value.split(', ') %]
+ [%~%]<a href="[% see_also FILTER html FILTER js %]" target="_blank">
+ [%~%][% see_also FILTER html FILTER js %]</a>
+ [%- ", " IF NOT loop.last %]
+ [% END %]
+ [% ELSIF change.fieldname == 'assigned_to' ||
+ change.fieldname == 'reporter' ||
+ change.fieldname == 'qa_contact' ||
+ change.fieldname == 'cc' ||
+ change.fieldname == 'flagtypes.name' %]
+ [% value FILTER email FILTER js %]
+ [% ELSE %]
+ [% value FILTER html FILTER js %]
+ [% END %]
+ [%~ %]';
+[% END %]
diff --git a/extensions/InlineHistory/template/en/default/hook/bug/comments-comment_banner.html.tmpl b/extensions/InlineHistory/template/en/default/hook/bug/comments-comment_banner.html.tmpl
new file mode 100644
index 000000000..133005f4f
--- /dev/null
+++ b/extensions/InlineHistory/template/en/default/hook/bug/comments-comment_banner.html.tmpl
@@ -0,0 +1,13 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF ih_activity_max %]
+<p>
+ <i>This [% terms.bug %] contains too many changes to be displayed inline.</i>
+</p>
+[% END %]
diff --git a/extensions/InlineHistory/template/en/default/hook/bug/show-header-end.html.tmpl b/extensions/InlineHistory/template/en/default/hook/bug/show-header-end.html.tmpl
new file mode 100644
index 000000000..7e54b8380
--- /dev/null
+++ b/extensions/InlineHistory/template/en/default/hook/bug/show-header-end.html.tmpl
@@ -0,0 +1,12 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF user.id && user.settings.inline_history.value == "on" %]
+ [% style_urls.push('extensions/InlineHistory/web/style.css') %]
+ [% javascript_urls.push('extensions/InlineHistory/web/inline-history.js') %]
+[% END %]
diff --git a/extensions/InlineHistory/template/en/default/hook/global/setting-descs-settings.none.tmpl b/extensions/InlineHistory/template/en/default/hook/global/setting-descs-settings.none.tmpl
new file mode 100644
index 000000000..e1ff4c0f6
--- /dev/null
+++ b/extensions/InlineHistory/template/en/default/hook/global/setting-descs-settings.none.tmpl
@@ -0,0 +1,11 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[%
+ setting_descs.inline_history = "When viewing a $terms.bug, show all $terms.bug activity",
+%]
diff --git a/extensions/InlineHistory/web/inline-history.js b/extensions/InlineHistory/web/inline-history.js
new file mode 100644
index 000000000..0d38edf7f
--- /dev/null
+++ b/extensions/InlineHistory/web/inline-history.js
@@ -0,0 +1,385 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0. */
+
+var inline_history = {
+ _ccDivs: null,
+ _hasAttachmentFlags: false,
+ _hasBugFlags: false,
+
+ init: function() {
+ Dom = YAHOO.util.Dom;
+
+ // remove 'has been marked as a duplicate of this bug' comments
+ var reDuplicate = /^\*\*\* \S+ \d+ has been marked as a duplicate of this/;
+ var reBugId = /show_bug\.cgi\?id=(\d+)/;
+ var comments = Dom.getElementsByClassName("bz_comment", 'div', 'comments');
+ for (var i = 1, il = comments.length; i < il; i++) {
+ var textDiv = Dom.getElementsByClassName('bz_comment_text', 'pre', comments[i]);
+ if (textDiv) {
+ var match = reDuplicate.exec(textDiv[0].textContent || textDiv[0].innerText);
+ if (match) {
+ // grab the comment and bug number from the element
+ var comment = comments[i];
+ var number = comment.id.substr(1);
+ var time = this.trim(Dom.getElementsByClassName('bz_comment_time', 'span', comment)[0].innerHTML);
+ var dupeId = 0;
+ match = reBugId.exec(Dom.get('comment_text_' + number).innerHTML);
+ if (match)
+ dupeId = match[1];
+ // remove the element
+ comment.parentNode.removeChild(comment);
+ // update the html for the history item to include the comment number
+ if (dupeId == 0)
+ continue;
+ for (var j = 0, jl = ih_activity.length; j < jl; j++) {
+ var item = ih_activity[j];
+ if (item[5] == dupeId && item[1] == time) {
+ // insert comment number and link into the header
+ item[3] = item[3].substr(0, item[3].length - 6) // remove trailing </div>
+ // add comment number
+ + '<span class="bz_comment_number" id="c' + number + '">'
+ + '<a href="#c' + number + '">Comment ' + number + '</a>'
+ + '</span>'
+ + '</div>';
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ // ensure new items are placed immediately after the last comment
+ var commentDivs = Dom.getElementsByClassName('bz_comment', 'div', 'comments');
+ if (!commentDivs.length) return;
+ var lastCommentDiv = commentDivs[commentDivs.length - 1];
+
+ // insert activity into the correct location
+ var commentTimes = Dom.getElementsByClassName('bz_comment_time', 'span', 'comments');
+ for (var i = 0, il = ih_activity.length; i < il; i++) {
+ var item = ih_activity[i];
+ // item[0] : who
+ // item[1] : when
+ // item[2] : change html
+ // item[3] : header html
+ // item[4] : bool; cc-only
+ // item[5] : int; dupe bug id (or 0)
+ // item[6] : bool; is flag
+ var user = item[0];
+ var time = item[1];
+
+ var reachedEnd = false;
+ var start_index = ih_activity_sort_order == 'newest_to_oldest_desc_first' ? 1 : 0;
+ for (var j = start_index, jl = commentTimes.length; j < jl; j++) {
+ var commentHead = commentTimes[j].parentNode;
+ var mainUser = Dom.getElementsByClassName('email', 'a', commentHead)[0].href.substr(7);
+ var text = commentTimes[j].textContent || commentTimes[j].innerText;
+ var mainTime = this.trim(text);
+
+ if (ih_activity_sort_order == 'oldest_to_newest' ? time > mainTime : time < mainTime) {
+ if (j < commentTimes.length - 1) {
+ continue;
+ } else {
+ reachedEnd = true;
+ }
+ }
+
+ var inline = (mainUser == user && time == mainTime);
+ var currentDiv = document.createElement("div");
+
+ // place ih_cc class on parent container if it's the only child
+ var containerClass = '';
+ if (item[4]) {
+ item[2] = item[2].replace('"ih_cc"', '""');
+ containerClass = 'ih_cc';
+ }
+
+ if (inline) {
+ // assume that the change was made by the same user
+ commentHead.parentNode.appendChild(currentDiv);
+ currentDiv.innerHTML = item[2];
+ Dom.addClass(currentDiv, 'ih_inlinehistory');
+ Dom.addClass(currentDiv, containerClass);
+ if (item[6])
+ this.setFlagChangeID(item, commentHead.parentNode.id);
+
+ } else {
+ // the change was made by another user
+ if (!reachedEnd) {
+ var parentDiv = commentHead.parentNode;
+ var previous = this.previousElementSibling(parentDiv);
+ if (previous && previous.className.indexOf("ih_history") >= 0) {
+ currentDiv = this.previousElementSibling(parentDiv);
+ } else {
+ parentDiv.parentNode.insertBefore(currentDiv, parentDiv);
+ }
+ } else {
+ var parentDiv = commentHead.parentNode;
+ var next = this.nextElementSibling(parentDiv);
+ if (next && next.className.indexOf("ih_history") >= 0) {
+ currentDiv = this.nextElementSibling(parentDiv);
+ } else {
+ lastCommentDiv.parentNode.insertBefore(currentDiv, lastCommentDiv.nextSibling);
+ }
+ }
+
+ var itemHtml = '<div class="ih_history_item ' + containerClass + '" '
+ + 'id="h' + i + '">'
+ + item[3] + item[2]
+ + '</div>';
+
+ if (ih_activity_sort_order == 'oldest_to_newest') {
+ currentDiv.innerHTML = currentDiv.innerHTML + itemHtml;
+ } else {
+ currentDiv.innerHTML = itemHtml + currentDiv.innerHTML;
+ }
+ currentDiv.setAttribute("class", "bz_comment ih_history");
+ if (item[6])
+ this.setFlagChangeID(item, 'h' + i);
+ }
+ break;
+ }
+ }
+
+ // find comment blocks which only contain cc changes, shift the ih_cc
+ var historyDivs = Dom.getElementsByClassName('ih_history', 'div', 'comments');
+ for (var i = 0, il = historyDivs.length; i < il; i++) {
+ var historyDiv = historyDivs[i];
+ var itemDivs = Dom.getElementsByClassName('ih_history_item', 'div', historyDiv);
+ var ccOnly = true;
+ for (var j = 0, jl = itemDivs.length; j < jl; j++) {
+ if (!Dom.hasClass(itemDivs[j], 'ih_cc')) {
+ ccOnly = false;
+ break;
+ }
+ }
+ if (ccOnly) {
+ for (var j = 0, jl = itemDivs.length; j < jl; j++) {
+ Dom.removeClass(itemDivs[j], 'ih_cc');
+ }
+ Dom.addClass(historyDiv, 'ih_cc');
+ }
+ }
+
+ if (this._hasAttachmentFlags)
+ this.linkAttachmentFlags();
+ if (this._hasBugFlags)
+ this.linkBugFlags();
+
+ ih_activity = undefined;
+ ih_activity_flags = undefined;
+
+ this._ccDivs = Dom.getElementsByClassName('ih_cc', '', 'comments');
+ this.hideCC();
+ YAHOO.util.Event.onDOMReady(this.addCCtoggler);
+ },
+
+ setFlagChangeID: function(changeItem, id) {
+ // put the ID for the change into ih_activity_flags
+ for (var i = 0, il = ih_activity_flags.length; i < il; i++) {
+ var flagItem = ih_activity_flags[i];
+ // flagItem[0] : who.login
+ // flagItem[1] : when
+ // flagItem[2] : attach id
+ // flagItem[3] : flag
+ // flagItem[4] : who.identity
+ // flagItem[5] : change div id
+ if (flagItem[0] == changeItem[0] && flagItem[1] == changeItem[1]) {
+ // store the div
+ flagItem[5] = id;
+ // tag that we have flags to process
+ if (flagItem[2]) {
+ this._hasAttachmentFlags = true;
+ } else {
+ this._hasBugFlags = true;
+ }
+ // don't break as there may be multiple flag changes at once
+ }
+ }
+ },
+
+ linkAttachmentFlags: function() {
+ var rows = Dom.get('attachment_table').getElementsByTagName('tr');
+ for (var i = 0, il = rows.length; i < il; i++) {
+
+ // deal with attachments with flags only
+ var tr = rows[i];
+ if (!tr.id || tr.id == 'a0')
+ continue;
+ var attachFlagTd = Dom.getElementsByClassName('bz_attach_flags', 'td', tr);
+ if (attachFlagTd.length == 0)
+ continue;
+ attachFlagTd = attachFlagTd[0];
+
+ // get the attachment id
+ var attachId = 0;
+ var anchors = tr.getElementsByTagName('a');
+ for (var j = 0, jl = anchors.length; j < jl; j++) {
+ var match = anchors[j].href.match(/attachment\.cgi\?id=(\d+)/);
+ if (match) {
+ attachId = match[1];
+ break;
+ }
+ }
+ if (!attachId)
+ continue;
+
+ var html = '';
+
+ // there may be multiple flags, split by <br>
+ var attachFlags = attachFlagTd.innerHTML.split('<br>');
+ for (var j = 0, jl = attachFlags.length; j < jl; j++) {
+ var match = attachFlags[j].match(/^\s*(<span.+\/span>):([^\?\-\+]+[\?\-\+])([\s\S]*)/);
+ if (!match) continue;
+ var setterSpan = match[1];
+ var flag = this.trim(match[2].replace('\u2011', '-', 'g'));
+ var requestee = this.trim(match[3]);
+ var requesteeLogin = '';
+
+ match = setterSpan.match(/title="([^"]+)"/);
+ if (!match) continue;
+ var setterIdentity = this.htmlDecode(match[1]);
+
+ if (requestee) {
+ match = requestee.match(/title="([^"]+)"/);
+ if (!match) continue;
+ requesteeLogin = this.htmlDecode(match[1]);
+ match = requesteeLogin.match(/<([^>]+)>/);
+ if (match)
+ requesteeLogin = match[1];
+ }
+
+ var flagValue = requestee ? flag + '(' + requesteeLogin + ')' : flag;
+ // find the id for this change
+ var found = false;
+ for (var k = 0, kl = ih_activity_flags.length; k < kl; k++) {
+ flagItem = ih_activity_flags[k];
+ if (
+ flagItem[2] == attachId
+ && flagItem[3] == flagValue
+ && flagItem[4] == setterIdentity
+ ) {
+ html +=
+ setterSpan + ': '
+ + '<a href="#' + flagItem[5] + '">' + flag + '</a> '
+ + requestee + '<br>';
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ // something went wrong, insert the flag unlinked
+ html += attachFlags[j] + '<br>';
+ }
+ }
+
+ if (html)
+ attachFlagTd.innerHTML = html;
+ }
+ },
+
+ linkBugFlags: function() {
+ var flags = Dom.get('flags');
+ if (!flags) return;
+ var rows = flags.getElementsByTagName('tr');
+ for (var i = 0, il = rows.length; i < il; i++) {
+ var cells = rows[i].getElementsByTagName('td');
+ if (!cells[1]) continue;
+
+ var match = cells[0].innerHTML.match(/title="([^"]+)"/);
+ if (!match) continue;
+ var setterIdentity = this.htmlDecode(match[1]);
+
+ var flagValue = cells[2].getElementsByTagName('select');
+ if (!flagValue.length) continue;
+ flagValue = flagValue[0].value;
+
+ var flagLabel = cells[1].getElementsByTagName('label');
+ if (!flagLabel.length) continue;
+ flagLabel = flagLabel[0];
+ var flagName = this.trim(flagLabel.innerHTML).replace('\u2011', '-', 'g');
+
+ for (var j = 0, jl = ih_activity_flags.length; j < jl; j++) {
+ flagItem = ih_activity_flags[j];
+ if (
+ !flagItem[2]
+ && flagItem[3] == flagName + flagValue
+ && flagItem[4] == setterIdentity
+ ) {
+ flagLabel.innerHTML =
+ '<a href="#' + flagItem[5] + '">' + flagName + '</a>';
+ break;
+ }
+ }
+ }
+ },
+
+ hideCC: function() {
+ Dom.addClass(this._ccDivs, 'ih_hidden');
+ },
+
+ showCC: function() {
+ Dom.removeClass(this._ccDivs, 'ih_hidden');
+ },
+
+ addCCtoggler: function() {
+ var ul = Dom.getElementsByClassName('bz_collapse_expand_comments');
+ if (ul.length == 0)
+ return;
+ ul = ul[0];
+ var a = document.createElement('a');
+ a.href = 'javascript:void(0)';
+ a.id = 'ih_toggle_cc';
+ YAHOO.util.Event.addListener(a, 'click', function(e) {
+ if (Dom.get('ih_toggle_cc').innerHTML == 'Show CC Changes') {
+ a.innerHTML = 'Hide CC Changes';
+ inline_history.showCC();
+ } else {
+ a.innerHTML = 'Show CC Changes';
+ inline_history.hideCC();
+ }
+ });
+ a.innerHTML = 'Show CC Changes';
+ var li = document.createElement('li');
+ li.appendChild(a);
+ ul.appendChild(li);
+ },
+
+ confirmUnsafeUrl: function(url) {
+ return confirm(
+ 'This is considered an unsafe URL and could possibly be harmful.\n'
+ + 'The full URL is:\n\n' + url + '\n\nContinue?');
+ },
+
+ previousElementSibling: function(el) {
+ if (el.previousElementSibling)
+ return el.previousElementSibling;
+ while (el = el.previousSibling) {
+ if (el.nodeType == 1)
+ return el;
+ }
+ },
+
+ nextElementSibling: function(el) {
+ if (el.nextElementSibling)
+ return el.nextElementSibling;
+ while (el = el.nextSibling) {
+ if (el.nodeType == 1)
+ return el;
+ }
+ },
+
+ htmlDecode: function(v) {
+ if (!v.match(/&/)) return v;
+ var e = document.createElement('textarea');
+ e.innerHTML = v;
+ return e.value;
+ },
+
+ trim: function(s) {
+ return s.replace(/^\s+|\s+$/g, '');
+ }
+}
diff --git a/extensions/InlineHistory/web/style.css b/extensions/InlineHistory/web/style.css
new file mode 100644
index 000000000..af76eba82
--- /dev/null
+++ b/extensions/InlineHistory/web/style.css
@@ -0,0 +1,35 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0. */
+
+.ih_history {
+ background: none !important;
+ color: #444;
+}
+
+.ih_inlinehistory {
+ font-weight: normal;
+ font-size: small;
+ color: #444;
+ border-top: 1px dotted #C8C8BA;
+ padding-top: 5px;
+}
+
+.bz_comment.ih_history {
+ padding: 5px 5px 0px 5px
+}
+
+.ih_history_item {
+ margin-bottom: 5px;
+}
+
+.ih_hidden {
+ display: none;
+}
+
+.ih_deleted {
+ text-decoration: line-through;
+}
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 <guy.pyrzak@gmail.com>
+
+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 <guy.pyrzak@gmail.com>
+# Gervase Markham <gerv@gerv.net>
+
+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/Voting/disabled b/extensions/InlineImages/disabled
index e69de29bb..e69de29bb 100644
--- a/extensions/Voting/disabled
+++ b/extensions/InlineImages/disabled
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 <guy.pyrzak@gmail.com>
+# Gervase Markham <gerv@gerv.net>
+#%]
+
+[% IF Param("allow_attachment_display") %]
+<script>
+ YAHOO.util.Event.onDOMReady(function() {
+ var Dom = YAHOO.util.Dom;
+
+ // Don't bother doing this if there are no images as attachments
+ if (Dom.getElementsByClassName("is_image", "a", "comments").length == 0) {
+ return;
+ }
+
+ var comments_expand_collapse =
+ Dom.getElementsByClassName('bz_collapse_expand_comments',
+ 'ul',
+ 'comments');
+
+ // Check that what we're looking for is here
+ if (comments_expand_collapse.length == 0) {
+ // Find the table we're looking for
+ var commentsTable = Dom.getElementsByClassName("bz_comment_table",
+ "table",
+ "comments");
+ secondColumn = commentsTable[0].getElementsByTagName("td")[1];
+ var newUL = document.createElement("ul");
+ if (secondColumn) {
+ secondColumn.appendChild(newUL);
+ comments_expand_collapse[0] = newUL;
+ }
+ }
+
+ // Insert the li into the dom
+ var li = document.createElement("li");
+ var link = document.createElement("a");
+ link.id = "toggle_images";
+ link.href = "#";
+ link.innerHTML = "Show Inline Images";
+ link.onclick = YAHOO.bz_ext_inlineImage.toggleImages;
+ li.appendChild(link);
+ if (comments_expand_collapse.length > 0) {
+ comments_expand_collapse[0].appendChild(li);
+ }
+
+ // Check to see if user has the inlineImagesCookie == on.
+ // If it is, go ahead and show images for the user
+ var inlineImagesCookie = YAHOO.util.Cookie.get("inlineImagesCookie");
+ if (inlineImagesCookie && inlineImagesCookie == "on") {
+ YAHOO.bz_ext_inlineImage.toggleImages();
+ }
+ });
+
+ var Dom = YAHOO.util.Dom;
+ YAHOO.namespace("bz_ext_inlineImage");
+ YAHOO.bz_ext_inlineImage.toggleImages = function(event) {
+ var imgs = Dom.getElementsByClassName("inline_image", "img", "comments");
+ var toggle_link_text = "";
+
+ if (imgs.length == 0) {
+ // Show inline images
+
+ var alinks = Dom.getElementsByClassName("is_image", "a", "comments");
+ for (var i = 0; i < alinks.length; i++) {
+ var img = document.createElement("img");
+ img.src = alinks[i].href;
+ // Might want to add some support to hide obsolete images
+ img.className = "inline_image";
+ img.style.display = "block";
+ Dom.insertAfter(img, alinks[i].parentNode);
+ }
+
+ YAHOO.util.Cookie.set("inlineImagesCookie", "on");
+ toggle_link_text = "Hide Inline Images";
+ }
+ else {
+ // Hide inline images
+
+ for (var i = 0; i < imgs.length; i++) {
+ imgs[i].parentNode.removeChild(imgs[i]);
+ }
+
+ YAHOO.util.Cookie.set("inlineImagesCookie", "off");
+ toggle_link_text = "Show Inline Images";
+ }
+
+ var link = document.getElementById("toggle_images");
+ link.innerHTML = toggle_link_text;
+
+ return false;
+ }
+</script>
+[% END %]
diff --git a/extensions/LastResolved/Config.pm b/extensions/LastResolved/Config.pm
new file mode 100644
index 000000000..f763167e2
--- /dev/null
+++ b/extensions/LastResolved/Config.pm
@@ -0,0 +1,20 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::LastResolved;
+
+use strict;
+
+use constant NAME => 'LastResolved';
+
+use constant REQUIRED_MODULES => [
+];
+
+use constant OPTIONAL_MODULES => [
+];
+
+__PACKAGE__->NAME;
diff --git a/extensions/LastResolved/Extension.pm b/extensions/LastResolved/Extension.pm
new file mode 100644
index 000000000..3627330c2
--- /dev/null
+++ b/extensions/LastResolved/Extension.pm
@@ -0,0 +1,112 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::LastResolved;
+
+use strict;
+
+use base qw(Bugzilla::Extension);
+
+use Bugzilla::Bug qw(LogActivityEntry);
+use Bugzilla::Util qw(format_time);
+use Bugzilla::Constants;
+use Bugzilla::Field;
+use Bugzilla::Install::Util qw(indicate_progress);
+
+our $VERSION = '0.01';
+
+sub install_update_db {
+ my ($self, $args) = @_;
+ my $last_resolved = Bugzilla::Field->new({'name' => 'cf_last_resolved'});
+ if (!$last_resolved) {
+ Bugzilla::Field->create({
+ name => 'cf_last_resolved',
+ description => 'Last Resolved',
+ type => FIELD_TYPE_DATETIME,
+ mailhead => 0,
+ enter_bug => 0,
+ obsolete => 0,
+ custom => 1,
+ buglist => 1,
+ });
+ _migrate_last_resolved();
+ }
+}
+
+sub _migrate_last_resolved {
+ my $dbh = Bugzilla->dbh;
+ my $field_id = get_field_id('bug_status');
+ my $resolved_activity = $dbh->selectall_arrayref(
+ "SELECT bugs_activity.bug_id, bugs_activity.bug_when, bugs_activity.who
+ FROM bugs_activity
+ WHERE bugs_activity.fieldid = ?
+ AND bugs_activity.added = 'RESOLVED'
+ ORDER BY bugs_activity.bug_when",
+ undef, $field_id);
+
+ my $count = 1;
+ my $total = scalar @$resolved_activity;
+ my %current_last_resolved;
+ foreach my $activity (@$resolved_activity) {
+ indicate_progress({ current => $count++, total => $total, every => 25 });
+ my ($id, $new, $who) = @$activity;
+ my $old = $current_last_resolved{$id} ? $current_last_resolved{$id} : "";
+ $dbh->do("UPDATE bugs SET cf_last_resolved = ? WHERE bug_id = ?", undef, $new, $id);
+ LogActivityEntry($id, 'cf_last_resolved', $old, $new, $who, $new);
+ $current_last_resolved{$id} = $new;
+ }
+}
+
+sub active_custom_fields {
+ my ($self, $args) = @_;
+ my $fields = $args->{'fields'};
+ my @tmp_fields = grep($_->name ne 'cf_last_resolved', @$$fields);
+ $$fields = \@tmp_fields;
+}
+
+sub bug_end_of_update {
+ my ($self, $args) = @_;
+ my $dbh = Bugzilla->dbh;
+ my ($bug, $old_bug, $timestamp, $changes) =
+ @$args{qw(bug old_bug timestamp changes)};
+ if ($changes->{'bug_status'}) {
+ # If the bug has been resolved then update the cf_last_resolved
+ # value to the current timestamp if cf_last_resolved exists
+ if ($bug->bug_status eq 'RESOLVED') {
+ $dbh->do("UPDATE bugs SET cf_last_resolved = ? WHERE bug_id = ?",
+ undef, $timestamp, $bug->id);
+ my $old_value = $bug->cf_last_resolved || '';
+ LogActivityEntry($bug->id, 'cf_last_resolved', $old_value,
+ $timestamp, Bugzilla->user->id, $timestamp);
+ }
+ }
+}
+
+sub bug_fields {
+ my ($self, $args) = @_;
+ my $fields = $args->{'fields'};
+ push (@$fields, 'cf_last_resolved')
+}
+
+sub object_columns {
+ my ($self, $args) = @_;
+ my ($class, $columns) = @$args{qw(class columns)};
+ if ($class->isa('Bugzilla::Bug')) {
+ push(@$columns, 'cf_last_resolved');
+ }
+}
+
+sub buglist_columns {
+ my ($self, $args) = @_;
+ my $columns = $args->{columns};
+ $columns->{'cf_last_resolved'} = {
+ name => 'bugs.cf_last_resolved',
+ title => 'Last Resolved',
+ };
+}
+
+__PACKAGE__->NAME;
diff --git a/extensions/LastResolved/template/en/default/hook/global/field-descs-end.none.tmpl b/extensions/LastResolved/template/en/default/hook/global/field-descs-end.none.tmpl
new file mode 100644
index 000000000..4457ccd9b
--- /dev/null
+++ b/extensions/LastResolved/template/en/default/hook/global/field-descs-end.none.tmpl
@@ -0,0 +1,11 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF in_template_var %]
+ [% vars.field_descs.cf_last_resolved = "Last Resolved" %]
+[% END %]
diff --git a/extensions/LimitedEmail/Config.pm b/extensions/LimitedEmail/Config.pm
new file mode 100644
index 000000000..f1ab104bc
--- /dev/null
+++ b/extensions/LimitedEmail/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 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 <bjones@mozilla.com>
+
+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/^byron\.jones\@gmail\.com$/i,
+ qr/^gerv\@mozilla\.org$/i,
+ qr/^reed\@reedloden\.com$/i,
+ qr/^shyam\@mozilla\.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 <bjones@mozilla.com>
+
+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
--- /dev/null
+++ b/extensions/LimitedEmail/disabled
diff --git a/extensions/MozProjectReview/Config.pm b/extensions/MozProjectReview/Config.pm
new file mode 100644
index 000000000..5a9d2b730
--- /dev/null
+++ b/extensions/MozProjectReview/Config.pm
@@ -0,0 +1,19 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+package Bugzilla::Extension::MozProjectReview;
+
+use strict;
+
+use constant NAME => 'MozProjectReview';
+
+use constant REQUIRED_MODULES => [
+];
+
+use constant OPTIONAL_MODULES => [
+];
+
+__PACKAGE__->NAME;
diff --git a/extensions/MozProjectReview/Extension.pm b/extensions/MozProjectReview/Extension.pm
new file mode 100644
index 000000000..4bf71188f
--- /dev/null
+++ b/extensions/MozProjectReview/Extension.pm
@@ -0,0 +1,253 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+package Bugzilla::Extension::MozProjectReview;
+
+use strict;
+
+use base qw(Bugzilla::Extension);
+
+use Bugzilla::User;
+use Bugzilla::Group;
+use Bugzilla::Error;
+use Bugzilla::Constants;
+
+our $VERSION = '0.01';
+
+sub post_bug_after_creation {
+ my ($self, $args) = @_;
+ my $vars = $args->{vars};
+ my $bug = $vars->{bug};
+
+ my $user = Bugzilla->user;
+ my $params = Bugzilla->input_params;
+ my $template = Bugzilla->template;
+
+ return if !($params->{format}
+ && $params->{format} eq 'moz-project-review'
+ && $bug->component eq 'Project Review');
+
+ my $error_mode_cache = Bugzilla->error_mode;
+ Bugzilla->error_mode(ERROR_MODE_DIE);
+
+ # do a match if applicable
+ Bugzilla::User::match_field({
+ 'legal_cc' => { 'type' => 'multi' }
+ });
+
+ my ($do_sec_review, $do_legal, $do_finance, $do_privacy_vendor,
+ $do_data_safety, $do_privacy_tech, $do_privacy_policy);
+
+ if ($params->{mozilla_data} eq 'Yes') {
+ $do_legal = 1;
+ $do_privacy_policy = 1;
+ $do_privacy_tech = 1;
+ $do_sec_review = 1;
+ }
+
+ if ($params->{mozilla_data} eq 'Yes'
+ && $params->{data_safety_user_data} eq 'Yes')
+ {
+ $do_data_safety = 1;
+ }
+
+ if ($params->{new_or_change} eq 'New') {
+ $do_legal = 1;
+ $do_privacy_policy = 1;
+ }
+ elsif ($params->{new_or_change} eq 'Existing') {
+ $do_legal = 1;
+ }
+
+ if ($params->{separate_party} eq 'Yes') {
+ $do_legal = 1;
+ }
+
+ if ($params->{data_access} eq 'Yes') {
+ $do_privacy_policy = 1;
+ $do_sec_review = 1;
+ }
+
+ if ($params->{data_access} eq 'Yes'
+ && $params->{'privacy_policy_vendor_user_data'} eq 'Yes')
+ {
+ $do_privacy_vendor = 1;
+ }
+
+ if ($params->{vendor_cost} eq '> $25,000') {
+ $do_finance = 1;
+ }
+
+ my ($sec_review_bug, $legal_bug, $finance_bug, $privacy_vendor_bug,
+ $data_safety_bug, $privacy_tech_bug, $privacy_policy_bug);
+
+ eval {
+ if ($do_sec_review) {
+ my $bug_data = {
+ short_desc => 'Security Review for ' . $bug->short_desc,
+ product => 'mozilla.org',
+ component => 'Security Assurance: Review Request',
+ bug_severity => 'normal',
+ groups => [ 'mozilla-corporation-confidential' ],
+ keywords => 'sec-review-needed',
+ op_sys => 'All',
+ rep_platform => 'All',
+ version => 'other',
+ blocked => $bug->bug_id,
+ };
+ $sec_review_bug = _file_child_bug($bug, $vars, 'sec-review', $bug_data);
+ }
+
+ if ($do_legal) {
+ my $component;
+ if ($params->{new_or_change} eq 'New') {
+ $component = 'General';
+ }
+ elsif ($params->{new_or_change} eq 'Existing') {
+ $component = $params->{mozilla_project};
+ }
+
+ if ($params->{separate_party} eq 'Yes'
+ && $params->{relationship_type})
+ {
+ $component = $params->{relationship_type} eq 'unspecified'
+ ? 'General'
+ : $params->{relationship_type};
+ }
+
+ my $bug_data = {
+ short_desc => 'Complete Legal Review for ' . $bug->short_desc,
+ product => 'Legal',
+ component => $component,
+ bug_severity => 'normal',
+ priority => '--',
+ groups => [ 'legal' ],
+ op_sys => 'All',
+ rep_platform => 'All',
+ version => 'unspecified',
+ blocked => $bug->bug_id,
+ cc => $params->{'legal_cc'},
+ };
+ $legal_bug = _file_child_bug($bug, $vars, 'legal', $bug_data);
+
+ }
+
+ if ($do_finance) {
+ my $bug_data = {
+ short_desc => 'Complete Finance Review for ' . $bug->short_desc,
+ product => 'Finance',
+ component => 'Purchase Request Form',
+ bug_severity => 'normal',
+ priority => '--',
+ groups => [ 'finance' ],
+ op_sys => 'All',
+ rep_platform => 'All',
+ version => 'unspecified',
+ blocked => $bug->bug_id,
+ };
+ $finance_bug = _file_child_bug($bug, $vars, 'finance', $bug_data);
+ }
+
+ if ($do_data_safety) {
+ my $bug_data = {
+ short_desc => 'Data Safety Review for ' . $bug->short_desc,
+ product => 'Data Safety',
+ component => 'General',
+ bug_severity => 'normal',
+ priority => '--',
+ op_sys => 'All',
+ rep_platform => 'All',
+ version => 'unspecified',
+ blocked => $bug->bug_id,
+ };
+ $data_safety_bug = _file_child_bug($bug, $vars, 'data-safety', $bug_data);
+ }
+
+ if ($do_privacy_tech) {
+ my $bug_data = {
+ short_desc => 'Complete Privacy-Technical Review for ' . $bug->short_desc,
+ product => 'mozilla.org',
+ component => 'Security Assurance: Review Request',
+ bug_severity => 'normal',
+ priority => '--',
+ keywords => 'privacy-review-needed',
+ groups => [ 'mozilla-corporation-confidential' ],
+ op_sys => 'All',
+ rep_platform => 'All',
+ version => 'other',
+ blocked => $bug->bug_id,
+ };
+ $privacy_tech_bug = _file_child_bug($bug, $vars, 'privacy-tech', $bug_data);
+ }
+
+ if ($do_privacy_policy) {
+ my $bug_data = {
+ short_desc => 'Complete Privacy-Policy Review for ' . $bug->short_desc,
+ product => 'Privacy',
+ component => 'Privacy Review',
+ bug_severity => 'normal',
+ priority => '--',
+ op_sys => 'All',
+ rep_platform => 'All',
+ version => 'unspecified',
+ blocked => $bug->bug_id,
+ };
+ $privacy_policy_bug = _file_child_bug($bug, $vars, 'privacy-policy', $bug_data);
+ }
+
+ if ($do_privacy_vendor) {
+ my $bug_data = {
+ short_desc => 'Complete Privacy / Vendor Review for ' . $bug->short_desc,
+ product => 'Privacy',
+ component => 'Vendor Review',
+ bug_severity => 'normal',
+ priority => '--',
+ op_sys => 'All',
+ rep_platform => 'All',
+ version => 'unspecified',
+ blocked => $bug->bug_id,
+ };
+ $privacy_vendor_bug = _file_child_bug($bug, $vars, 'privacy-vendor', $bug_data);
+ }
+ };
+
+ my $error = $@;
+ Bugzilla->error_mode($error_mode_cache);
+
+ if ($error
+ || ($do_legal && !$legal_bug)
+ || ($do_sec_review && !$sec_review_bug)
+ || ($do_finance && !$finance_bug)
+ || ($do_data_safety && !$data_safety_bug)
+ || ($do_privacy_tech && !$privacy_tech_bug)
+ || ($do_privacy_policy && !$privacy_policy_bug)
+ || ($do_privacy_vendor && !$privacy_vendor_bug))
+ {
+ warn "Failed to create additional moz-project-review bugs: $error" if $error;
+ $vars->{message} = 'moz_project_review_creation_failed';
+ $vars->{message_error} = $error;
+ }
+}
+
+sub _file_child_bug {
+ my ($parent_bug, $vars, $template_suffix, $bug_data) = @_;
+ my $template = Bugzilla->template;
+ my $comment = "";
+
+ my $full_template = "bug/create/comment-moz-project-review-$template_suffix.txt.tmpl";
+ $template->process($full_template, $vars, \$comment)
+ || ThrowTemplateError($template->error());
+
+ $bug_data->{comment} = $comment;
+ my $new_bug = Bugzilla::Bug->create($bug_data);
+
+ $parent_bug->set_all({ dependson => { add => [ $new_bug->bug_id ] }});
+ Bugzilla::BugMail::Send($new_bug->id, { changer => Bugzilla->user });
+
+ return $new_bug;
+}
+
+__PACKAGE__->NAME;
diff --git a/extensions/MozProjectReview/template/en/default/bug/create/comment-moz-project-review-data-safety.txt.tmpl b/extensions/MozProjectReview/template/en/default/bug/create/comment-moz-project-review-data-safety.txt.tmpl
new file mode 100644
index 000000000..5a6ffbbbd
--- /dev/null
+++ b/extensions/MozProjectReview/template/en/default/bug/create/comment-moz-project-review-data-safety.txt.tmpl
@@ -0,0 +1,40 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% USE Bugzilla %]
+[% cgi = Bugzilla.cgi %]
+
+[% PROCESS "bug/create/comment-moz-project-review.txt.tmpl" %]
+
+Data Safety Questions:
+
+User Data: [% cgi.param('data_safety_user_data') %]
+How many involved?: [% cgi.param('data_safety_user_count') %]
+How many users do you anticipate to be involved?: [% cgi.param('data_safety_user_count_anticipated') %]
+Type of Data:
+[%+ cgi.param('data_safety_data_type') %]
+Data Reason:
+[%+ cgi.param('data_safety_data_reason') %]
+Community Benefit:
+[%+ cgi.param('data_safety_community_benefit') %]
+Data Collection:
+[%+ cgi.param('data_safety_community_collection') %]
+Data Retention: [% cgi.param('data_safety_retention') %]
+Data Retention Length: [% cgi.param('data_safety_retention_length') %]
+Separate Party: [% cgi.param('data_safety_separate_party') %]
+Separate Party Data Type:
+[%+ cgi.param('data_safety_separate_party_data') %]
+Separate Party Data Communication:
+[%+ cgi.param('data_safety_separate_party_data_communication') %]
+Who are the separate parties?:
+[%+ cgi.param('data_safety_separate_party_who') %]
+Community Visibility and Input: [% cgi.param('data_safety_community_visibility') %]
+Communication Channels:
+[%+ cgi.param('data_safety_communication_channels') %]
+Public Communication Plan:
+[%+ cgi.param('data_safety_communication_plan') %]
diff --git a/extensions/MozProjectReview/template/en/default/bug/create/comment-moz-project-review-finance.txt.tmpl b/extensions/MozProjectReview/template/en/default/bug/create/comment-moz-project-review-finance.txt.tmpl
new file mode 100644
index 000000000..1fc72de6f
--- /dev/null
+++ b/extensions/MozProjectReview/template/en/default/bug/create/comment-moz-project-review-finance.txt.tmpl
@@ -0,0 +1,26 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% USE Bugzilla %]
+[% cgi = Bugzilla.cgi %]
+
+[% PROCESS "bug/create/comment-moz-project-review.txt.tmpl" %]
+
+Finance Questions:
+
+What is the purchase for?:
+[%+ cgi.param('finance_purchase_what') %]
+Why is the purchase needed?:
+[%+ cgi.param('finance_purchase_why') %]
+What is the risk if not purchased?:
+[%+ cgi.param('finance_purchase_risk') %]
+What is the alternative?:
+[%+ cgi.param('finance_purchase_alternative') %]
+Is this line item in budget?: [% cgi.param('finance_purchase_inbudget') %]
+What is the urgency?: [% cgi.param('finance_purchase_urgency') %]
+Total Cost: [% cgi.param('finance_purchase_cost') %]
diff --git a/extensions/MozProjectReview/template/en/default/bug/create/comment-moz-project-review-legal.txt.tmpl b/extensions/MozProjectReview/template/en/default/bug/create/comment-moz-project-review-legal.txt.tmpl
new file mode 100644
index 000000000..345557743
--- /dev/null
+++ b/extensions/MozProjectReview/template/en/default/bug/create/comment-moz-project-review-legal.txt.tmpl
@@ -0,0 +1,22 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% USE Bugzilla %]
+[% cgi = Bugzilla.cgi %]
+
+[% PROCESS "bug/create/comment-moz-project-review.txt.tmpl" %]
+
+Legal Questions:
+
+Priority: [% cgi.param('legal_priority') %]
+Other Party: [% cgi.param('legal_other_party') %]
+Business Objective: [% cgi.param('legal_business_objective') %]
+URL: [% cgi.param('legal_url') %]
+SOW Details: [% cgi.param('legal_sow_details') %]
+Description:
+[%+ cgi.param('legal_description') %]
diff --git a/extensions/MozProjectReview/template/en/default/bug/create/comment-moz-project-review-privacy-policy.txt.tmpl b/extensions/MozProjectReview/template/en/default/bug/create/comment-moz-project-review-privacy-policy.txt.tmpl
new file mode 100644
index 000000000..816834c40
--- /dev/null
+++ b/extensions/MozProjectReview/template/en/default/bug/create/comment-moz-project-review-privacy-policy.txt.tmpl
@@ -0,0 +1,18 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% USE Bugzilla %]
+[% cgi = Bugzilla.cgi %]
+
+[% PROCESS "bug/create/comment-moz-project-review.txt.tmpl" %]
+
+Privacy Policy: [% cgi.param('privacy_policy_project') %]
+Privacy Policy Link: [% cgi.param('privacy_policy_project_link') %]
+User Data: [% cgi.param('privacy_policy_user_data') %]
+Data Safety [% terms.Bug %] ID: [% cgi.param('privacy_policy_user_data_bug') %]
+Legal [% terms.Bug %] ID: [% cgi.param('privacy_policy_legal_bug') %]
diff --git a/extensions/MozProjectReview/template/en/default/bug/create/comment-moz-project-review-privacy-tech.txt.tmpl b/extensions/MozProjectReview/template/en/default/bug/create/comment-moz-project-review-privacy-tech.txt.tmpl
new file mode 100644
index 000000000..7b72cf1bc
--- /dev/null
+++ b/extensions/MozProjectReview/template/en/default/bug/create/comment-moz-project-review-privacy-tech.txt.tmpl
@@ -0,0 +1,12 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% USE Bugzilla %]
+[% cgi = Bugzilla.cgi %]
+
+[% PROCESS "bug/create/comment-moz-project-review.txt.tmpl" %]
diff --git a/extensions/MozProjectReview/template/en/default/bug/create/comment-moz-project-review-privacy-vendor.txt.tmpl b/extensions/MozProjectReview/template/en/default/bug/create/comment-moz-project-review-privacy-vendor.txt.tmpl
new file mode 100644
index 000000000..eaf9f12e3
--- /dev/null
+++ b/extensions/MozProjectReview/template/en/default/bug/create/comment-moz-project-review-privacy-vendor.txt.tmpl
@@ -0,0 +1,16 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% USE Bugzilla %]
+[% cgi = Bugzilla.cgi %]
+
+[% PROCESS "bug/create/comment-moz-project-review.txt.tmpl" %]
+
+Privacy Policy: [% cgi.param('privacy_policy_vendor_user_data') %]
+Vendor's Privacy Policy: [% cgi.param('privacy_policy_vendor_link') %]
+Privacy Questionnaire: [% cgi.param('privacy_policy_vendor_questionnaire') %]
diff --git a/extensions/MozProjectReview/template/en/default/bug/create/comment-moz-project-review-sec-review.txt.tmpl b/extensions/MozProjectReview/template/en/default/bug/create/comment-moz-project-review-sec-review.txt.tmpl
new file mode 100644
index 000000000..029f6df48
--- /dev/null
+++ b/extensions/MozProjectReview/template/en/default/bug/create/comment-moz-project-review-sec-review.txt.tmpl
@@ -0,0 +1,20 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% USE Bugzilla %]
+[% cgi = Bugzilla.cgi %]
+
+[% PROCESS "bug/create/comment-moz-project-review.txt.tmpl" %]
+
+Security Review Questions:
+
+Affects Products: [% cgi.param('sec_affects_products') %]
+Review Due Date: [% cgi.param('sec_review_date') %]
+Review Invitees: [% cgi.param('sec_review_invitees') %]
+Extra Information:
+[%+ cgi.param('sec_review_extra') %]
diff --git a/extensions/MozProjectReview/template/en/default/bug/create/comment-moz-project-review.txt.tmpl b/extensions/MozProjectReview/template/en/default/bug/create/comment-moz-project-review.txt.tmpl
new file mode 100644
index 000000000..16bdcb568
--- /dev/null
+++ b/extensions/MozProjectReview/template/en/default/bug/create/comment-moz-project-review.txt.tmpl
@@ -0,0 +1,34 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% USE Bugzilla %]
+[% cgi = Bugzilla.cgi %]
+
+Initial Questions:
+
+Project/Feature Name: [% cgi.param('short_desc') %]
+Tracking [% terms.Bug %] ID:[% cgi.param('tracking_id') %]
+Description:
+[%+ cgi.param('description') %]
+Additional Information:
+[%+ cgi.param('additional') %]
+Urgency: [% cgi.param('urgency') %]
+Current Goal: [% cgi.param('goal') %]
+Release Date: [% cgi.param('release_date') %]
+Project Status: [% cgi.param('project_status') %]
+Mozilla Data: [% cgi.param('mozilla_data') %]
+New or Change: [% cgi.param('new_or_change') %]
+Mozilla Project: [% cgi.param('mozilla_project') %]
+Mozilla Related: [% cgi.param('mozilla_related') %]
+Separate Party: [% cgi.param('separate_party') %]
+[% IF cgi.param('separate_part') == 'Yes' %]
+Type of Relationship: [% cgi.param('relationship_type') %]
+Data Access: [% cgi.param('data_access') %]
+Privacy Policy: [% cgi.param('privacy_policy') %]
+Vendor Cost: [% cgi.param('vendor_cost') %]
+[% END %]
diff --git a/extensions/MozProjectReview/template/en/default/bug/create/create-moz-project-review.html.tmpl b/extensions/MozProjectReview/template/en/default/bug/create/create-moz-project-review.html.tmpl
new file mode 100644
index 000000000..f11cda038
--- /dev/null
+++ b/extensions/MozProjectReview/template/en/default/bug/create/create-moz-project-review.html.tmpl
@@ -0,0 +1,702 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+
+[% PROCESS global/header.html.tmpl
+ title = "Mozilla Project Review"
+ style_urls = [ 'extensions/MozProjectReview/web/style/moz_project_review.css' ]
+ javascript_urls = [ 'js/field.js', 'js/util.js',
+ 'extensions/MozProjectReview/web/js/moz_project_review.js' ]
+ yui = [ 'autocomplete', 'calendar' ]
+%]
+
+<p>
+ <strong>Please use this form for submitting a Mozilla Project Review</strong>
+ If you have a [% terms.bug %] to file, go <a href="enter_bug.cgi">here</a>.
+</p>
+
+<p>
+ (<span class="required_star">*</span> =
+ <span class="required_explanation">Required Field</span>)
+</p>
+
+<form method="post" action="post_bug.cgi" id="incidentForm" enctype="multipart/form-data"
+ onSubmit="return MPR.validateAndSubmit();">
+ <input type="hidden" id="product" name="product" value="mozilla.org">
+ <input type="hidden" id="component" name="component" value="Project Review">
+ <input type="hidden" id="rep_platform" name="rep_platform" value="All">
+ <input type="hidden" id="op_sys" name="op_sys" value="All">
+ <input type="hidden" id="priority" name="priority" value="--">
+ <input type="hidden" id="version" name="version" value="other">
+ <input type="hidden" id="format" name="format" value="moz-project-review">
+ <input type="hidden" id="bug_severity" name="bug_severity" value="normal">
+ <input type="hidden" id="token" name="token" value="[% token FILTER html %]">
+
+ <div id="initial_questions">
+ <div class="header">Initial Questions</div>
+
+ <div id="project_feature_summary_row" class="field_row">
+ <span class="field_label required">Project/Feature Name:</span>
+ <span class="field_data">
+ <input type="text" name="short_desc" id="short_desc" size="60" maxsize="255">
+ </span>
+ </div>
+
+ <div id="tracking_id_row" class="field_row">
+ <span class="field_label">Tracking [% terms.Bug %] ID:</span>
+ <span class="field_data">
+ <div class="field_description">Master tracking [% terms.bug %] number (if it exists)?</div>
+ <input type="text" name="tracking_id" id="tracking_id" size="60">
+ </span>
+ </div>
+
+ <div id="contacts_row" class="field_row">
+ <span class="field_label required">Points of Contact:</span>
+ <span class="field_data">
+ <div class="field_description">Who are the points of contact for this review?</div>
+ [% INCLUDE global/userselect.html.tmpl
+ id => "cc"
+ name => "cc"
+ value => ""
+ size => 60
+ classes => ["bz_userfield"]
+ multiple => 5
+ %]
+ </span>
+ </div>
+
+ <div id="description_row" class="field_row">
+ <span class="field_label required">Description:</span>
+ <span class="field_data">
+ <div class="field_description">Please provide a short description of the feature / application / project /
+ business relationship (e.g. problem solved, use cases, etc.)</div>
+ <textarea name="description" id="description" rows="10" cols="80"></textarea>
+ </span>
+ </div>
+
+ <div id="additional_row" class="field_row">
+ <span class="field_label">Additional Information:</span>
+ <span class="field_data">
+ <div class="field_description">Please provide links to additional information (e.g. feature page, wiki)
+ if available and not yet included in feature description.)</div>
+ <textarea name="additional" id="additional" rows="10" cols="80"></textarea>
+ </span>
+ </div>
+
+ <div id="urgency_row" class="field_row">
+ <span class="field_label required">Urgency:</span>
+ <span class="field_data">
+ <div class="field_description">What is the urgency of this project?</div>
+ <select id="urgency" name="urgency">
+ <option value="">Select One</option>
+ <option value="2 days">2 days</option>
+ <option value="a week">a week</option>
+ <option value="2-4 weeks">2-4 weeks</option>
+ <option value="no rush">no rush</option>
+ </select>
+ </span>
+ </div>
+
+ <div id="goal_row" class="field_row">
+ <span class="field_label">Current Goal:</span>
+ <span class="field_data">
+ <div class="field_description">Does it support a current Mozilla goal (if so, which one)?</div>
+ <input type="text" name="goal" id="goal" size="60">
+ </span>
+ </div>
+
+ <div id="release_date_row" class="field_row">
+ <span class="field_label required">Release Date:</span>
+ <span class="field_data">
+ <div class="field_description">What is your key release / launch date?</div>
+ <input name="release_date" size="20" id="release_date" value=""
+ onchange="updateCalendarFromField(this)">
+ <button type="button" class="calendar_button"
+ id="button_calendar_release_date"
+ onclick="showCalendar('release_date')">
+ <span>Calendar</span>
+ </button>
+ <div id="con_calendar_release_date"></div>
+ <script type="text/javascript">
+ createCalendar('release_date')
+ </script>
+ </span>
+ </div>
+
+ <div id="project_status_row" class="field_row">
+ <span class="field_label required">Project Status:</span>
+ <span class="field_data">
+ <div class="field_description">What is the current state of your project?</div>
+ <select name="project_status" id="project_status">
+ <option value="">Select One</option>
+ <option value="future">Future project under discussion</option>
+ <option value="active">Active planning</option>
+ <option value="development">Development</option>
+ <option value="ready">Ready to launch/commit</option>
+ <option value="launched">Already launched/committed</option>
+ </select>
+ </span>
+ </div>
+
+ <div id="mozilla_data_row" class="field_row">
+ <span class="field_label required">Mozilla Data:</span>
+ <span class="field_data">
+ <div class="field_description">Does this product/service/project access, interact with, or store Mozilla
+ (customer, contributor, user, employee) data? Example of such data includes
+ email addresses, first and last name, addresses, phone numbers, credit card data.)</div>
+ <select name="mozilla_data" id="mozilla_data"
+ onchange="MPR.toggleSpecialSections();">
+ <option value="">Select One</option>
+ <option value="Yes">Yes</option>
+ <option value="No">No</option>
+ </select>
+ </span>
+ </div>
+
+ <div id="new_or_change_row" class="field_row">
+ <span class="field_label required">New or Change:</span>
+ <span class="field_data">
+ <div class="field_description">Is this a NEW product, service, project, feature, or functionality,
+ a change to an EXISTING one, or neither?</div>
+ <select name="new_or_change" id="new_or_change"
+ onchange="MPR.toggleVisibleById(this,'Existing','mozilla_project_row');">
+ <option value="">Select One</option>
+ <option value="New">New</option>
+ <option value="Existing">Existing</option>
+ <option value="Neither">Neither</option>
+ </select>
+ </span>
+ </div>
+
+ <div id="mozilla_project_row" class="field_row bz_default_hidden">
+ <span class="field_label">Mozilla Project:</span>
+ <span class="field_data">
+ <div class="field_description">What product/service/project does this pertain to?</div>
+ <select name="mozilla_project" id="mozilla_project">
+ <option value="none">None</option>
+ <option value="FirefoxOS">FirefoxOS</option>
+ <option value="Marketplace">Marketplace</option>
+ <option value="Persona">Persona</option>
+ <option value="Marketing Initiative">Marketing Initiative</option>
+ </select>
+ </span>
+ </div>
+
+ <div id="mozilla_related_row" class="field_row">
+ <span class="field_label">Mozilla Related:</span>
+ <span class="field_data">
+ <div class="field_description">What Mozilla products/services/projects does this product/service/project
+ integrate with or relate to?</div>
+ <input type="text" name="mozilla_related" id="mozilla_related" size="60">
+ </span>
+ </div>
+
+ <div id="separate_party_row" class="field_row">
+ <span class="field_label required">Separate Party:</span>
+ <span class="field_data">
+ <div class="field_description">Does this project involve a relationship with another party (such as a third
+ party vendor, hosted service provider, consultant or strategic partner (business deals))?
+ This includes NDAs, click to accept, API agreements, open source licenses, renewals,
+ additional services or goods, and any other agreements.</div>
+ <select name="separate_party" id="separate_party"
+ onchange="MPR.toggleVisibleById(this,'Yes','initial_separate_party_questions');">
+ <option value="">Select One</option>
+ <option value="Yes">Yes</option>
+ <option value="No">No</option>
+ </select>
+ </span>
+ </div>
+
+ <div id="initial_separate_party_questions" class="bz_default_hidden">
+ <div id="relation_type_row" class="field_row">
+ <span class="field_label required">Type of Relationship:</span>
+ <span class="field_data">
+ <div class="field_description">What type of relationship?</div>
+ <select name="relationship_type" id="relationship_type"
+ onchange="MPR.toggleVisibleById(this,'Vendor/Services','legal_sow_details_row');">
+ <option value="">Select One</option>
+ <option value="Vendor/Services">Vendor/Services</option>
+ <option value="Distribution/Bundling">Distribution/Bundling</option>
+ <option value="Search">Search</option>
+ <option value="NDA">NDA</option>
+ <option value="Other">Other</option>
+ </select>
+ </span>
+ </div>
+
+ <div id="data_access_row" class="field_row">
+ <span class="field_label required">Data Access:</span>
+ <span class="field_data">
+ <div class="field_description">Will the other party have access to Mozilla (customer, contributor, user,
+ employee) data? (If this is for an NDA, choose no)</div>
+ <select name="data_access" id="data_access"
+ onchange="MPR.toggleSpecialSections();">
+ <option value="">Select One</option>
+ <option value="Yes">Yes</option>
+ <option value="No">No</option>
+ </select>
+ </span>
+ </div>
+
+ <div id="privacy_policy_row" class="field_row">
+ <span class="field_label">Privacy Policy:</span>
+ <span class="field_data">
+ <div class="field_description">What is the url for their privacy policy?</div>
+ <input type="text" name="privacy_policy" id="privacy_policy" size="60">
+ </span>
+ </div>
+
+ <div id="vendor_cost_row" class="field_row">
+ <span class="field_label required">Vendor Cost:</span>
+ <span class="field_data">
+ <div class="field_description">What is the anticipated cost of the vendor relationship?
+ (Entire Contract Cost, not monthly cost)</div>
+ <select name="vendor_cost" id="vendor_cost"
+ onchange="MPR.toggleVisibleById(this,'> $25,000','finance_questions');">
+ <option value="">Select One</option>
+ <option value="N/A">N/A</option>
+ <option value="&lt= $25,000">&lt;= $25,000</option>
+ <option value="&gt $25,000">&gt; $25,000</option>
+ </select>
+ </span>
+ </div>
+ </div>
+ </div>
+
+ <div id="sec_review_questions" class="bz_default_hidden">
+ <div class="header">Security Review</div>
+
+ <div id="sec_review_affects_products_row">
+ <span class="field_label">Affects Products:</span>
+ <span class="field_data">
+ <div class="field_description">Does this feature or code change affect Firefox, Thunderbird or any
+ product or service the Mozilla ships to end users?</div>
+ <select name="sec_affects_products" id="sec_affects_products">
+ <option value="">Select One</option>
+ <option value="Yes">Yes</option>
+ <option value="No">No</option>
+ </select>
+ </span>
+ </div>
+
+ <div id="sec_review_date_row" class="field_row">
+ <span class="field_label">Review Due Date:</span>
+ <span class="field_data">
+ <div class="field_description">When would you like the review to be completed?
+ (<a href="https://mail.mozilla.com/home/ckoenig@mozilla.com/Security%20Review.html"
+ target="_blank">more info</a>)</div>
+ <input name="sec_review_date" size="20" id="sec_review_date" value=""
+ onchange="updateCalendarFromField(this)">
+ <button type="button" class="calendar_button"
+ id="button_calendar_sec_review_date"
+ onclick="showCalendar('sec_review_date')">
+ <span>Calendar</span>
+ </button>
+ <div id="con_calendar_sec_review_date"></div>
+ <script type="text/javascript">
+ createCalendar('sec_review_date')
+ </script>
+ </span>
+ </div>
+
+ <div id="sec_review_invitees_row" class="field_row">
+ <span class="field_label">Review Invitees:</span>
+ <span class="field_data">
+ <div class="field_description">Whom should be invited to the review?</div>
+ <input type="text" name="sec_review_invitees" id="sec_review_invitees" size="60">
+ </span>
+ </div>
+
+ <div id="sec_review_extra_row" class="field_row">
+ <span class="field_label">Extra Information:</span>
+ <span class="field_data">
+ <div class="field_description">If you feel something is missing here or you would like to provide other
+ kind of feedback, feel free to do so here?</div>
+ <textarea name="sec_review_extra" id="sec_review_extra" rows="10" cols="80"></textarea>
+ </span>
+ </div>
+ </div>
+
+ <div id="privacy_policy_project_questions" class="bz_default_hidden">
+ <div class="header">Privacy (Policy/Project)</div>
+
+ <div id="privacy_policy_project_row" class="field_row">
+ <span class="field_label">Privacy Policy:</span>
+ <span class="field_data">
+ <div class="field_description">Do you currently have a privacy policy for your project / site / product?</div>
+ <select name="privacy_policy_project" id="privacy_policy_project"
+ onchange="MPR.toggleVisibleById(this,'Yes','privacy_policy_project_link_row');">
+ <option value="">Select One</option>
+ <option value="Yes">Yes</option>
+ <option value="No">No</option>
+ </select>
+ </span>
+ </div>
+
+ <div id="privacy_policy_project_link_row" class="field_row bz_default_hidden">
+ <span class="field_label">Privacy Policy Link:</span>
+ <span class="field_data">
+ <div class="field_description">Please provide link to policy</div>
+ <input type="text" name="privacy_policy_project_link" id="privacy_policy_project_link" size="60">
+ </span>
+ </div>
+
+ <div id="privacy_policy_project_user_data_row" class="field_row">
+ <span class="field_label">User Data:</span>
+ <span class="field_data">
+ <div class="field_description">Does your product/service/project collect, use or maintain any user data?</div>
+ <select name="privacy_policy_user_data" id="privacy_policy_user_data"
+ onchange="MPR.toggleVisibleById(this,'Yes','privacy_policy_project_user_data_bug_row');">
+ <option value="">Select One</option>
+ <option value="Yes">Yes</option>
+ <option value="No">No</option>
+ </select>
+ </span>
+ </div>
+
+ <div id="privacy_policy_project_user_data_bug_row" class="bz_default_hidden">
+ <span class="field_label">Data Safety [% terms.Bug %] ID:</span>
+ <span class="field_data">
+ <div class="field_description">Please provide link to Data Safety [% terms.bug %]</div>
+ <input type="text" name="privacy_policy_user_data_bug" id="privacy_policy_user_data_bug" size="60">
+ </span>
+ </div>
+
+ <div id="privacy_policy_project_legal_bug_row" class="field_row">
+ <span class="field_label">Legal [% terms.Bug %]:</span>
+ <span class="field_data">
+ <div class="field_description">For reference, please provide link to related Legal [% terms.bug %] or enter
+ "not filed" if a legal [% terms.bug %] has not yet been filed.</div>
+ <input type="text" name="privacy_policy_legal_bug" id="privacy_policy_legal_bug" size="60">
+ </span>
+ </div>
+ </div>
+
+ <div id="privacy_policy_vendor_questions" class="bz_default_hidden">
+ <div class="header">Privacy (Policy/Vendor)</div>
+
+ <div id="privacy_policy_vendor_user_data_row" class="field_row">
+ <span class="field_label">Privacy Policy:</span>
+ <span class="field_data">
+ <div class="field_description">Will the vendor have access to Mozilla (customer, contributor, user, employee) data?</div>
+ <select name="privacy_policy_vendor_user_data" id="privacy_policy_vendor_user_data"
+ onchange="MPR.toggleVisibleById(this,'Yes','privacy_policy_vendor_extra');">
+ <option value="">Select One</option>
+ <option value="Yes">Yes</option>
+ <option value="No">No</option>
+ </select>
+ </span>
+ </div>
+
+ <div id="privacy_policy_vendor_extra" class="bz_default_hidden">
+ <div id="privacy_policy_vendor_link_row" class="field_row">
+ <span class="field_label">Vendor's Privacy Policy:</span>
+ <span class="field_data">
+ <div class="field_description">Please provide link to vendor's privacy policy</div>
+ <input type="text" name="privacy_policy_vendor_link" id="privacy_policy_vendor_link" size="60">
+ </span>
+ </div>
+
+ <div id="privacy_policy_vendor_questionnaire_row" class="field_row">
+ <span class="field_label">Privacy Questionnaire:</span>
+ <span class="field_data">
+ <div class="field_description">Has vendor completed Mozilla Vendor Privacy Questionnaire?</div>
+ <select name="privacy_policy_vendor_questionnaire" id="privacy_policy_vendor_questionnaire">
+ <option value="">Select One</option>
+ <option value="Yes">Yes</option>
+ <option value="No">No</option>
+ </select>
+ </span>
+ </div>
+ </div>
+ </div>
+
+ <div id="legal_questions" class="bz_default_hidden">
+ <div class="header">Legal</div>
+
+ <div id="legal_priority_row" class="field_row">
+ <span class="field_label required">Priority:</span>
+ <span class="field_data">
+ <div class="field_description">Priority to your team</div>
+ <select name="legal_priority" id="legal_priority">
+ <option value="">Select One</option>
+ <option value="high">High</option>
+ <option value="medium">Medium</option>
+ <option value="low">Low</option>
+ </select>
+ </span>
+ </div>
+
+ <div id="legal_cc_row" class="field_row">
+ <span class="field_label">Cc:</span>
+ <span class="field_data">
+ [% INCLUDE global/userselect.html.tmpl
+ id => "legal_cc"
+ name => "legal_cc"
+ value => ""
+ size => 60
+ classes => ["bz_userfield"]
+ multiple => 5
+ %]
+ </span>
+ </div>
+
+ <div id="legal_other_party_row" class="field_row">
+ <span class="field_label">Other Party:</span>
+ <span class="field_data">
+ <div class="field_description">Name of other party involved</div>
+ <input type="text" name="legal_other_party" id="legal_other_party" size="60">
+ </span>
+ </div>
+
+ <div id="legal_business_objective_row" class="field_row">
+ <span class="field_label">Business Objective:</span>
+ <span class="field_data">
+ <textarea name="legal_business_objective" id="legal_business_objective" rows="10" cols="80"></textarea>
+ </span>
+ </div>
+
+ <div id="legal_sow_details_row" class="class_row bz_default_hidden">
+ <span class="field_label">SOW Details:</span>
+ <span class="field_data">
+ <div class="field_description">If applicable</div>
+ <textarea name="legal_sow_details" id="legal_sow_details" rows="10" cols="80"></textarea>
+ </span>
+ </div>
+ </div>
+
+ <div id="finance_questions" class="bz_default_hidden">
+ <div class="header">Finance</div>
+
+ <div id="finance_purchase_what_row" class="field_row">
+ <span class="field_label required">What is the purchase for?:</span>
+ <span class="field_data">
+ <textarea name="finance_purchase_what" id="finance_purchase_what" rows="10" cols="80"></textarea>
+ </span>
+ </div>
+
+ <div id="finance_purchase_why_row" class="field_row">
+ <span class="field_label required">Why is the purchase needed?:</span>
+ <span class="field_data">
+ <textarea name="finance_purchase_why" id="finance_purchase_why" rows="10" cols="80"></textarea>
+ </span>
+ </div>
+
+ <div id="finance_purchase_risk_row" class="field_row">
+ <span class="field_label required">What is the risk<br>if not purchased?:</span>
+ <span class="field_data">
+ <textarea name="finance_purchase_risk" id="finance_purchase_risk" rows="10" cols="80"></textarea>
+ </span>
+ </div>
+
+ <div id="finance_purchase_alternative_row" class="field_row">
+ <span class="field_label required">What is the alternative?:</span>
+ <span class="field_data">
+ <textarea name="finance_purchase_alternative" id="finance_purchase_alternative" rows="10" cols="80"></textarea>
+ </span>
+ </div>
+
+ <div id="finance_purchase_inbudget_row" class="field_row">
+ <span class="field_label required">Is this line item in budget?:</span>
+ <span class="field_data">
+ <select name="finance_purchase_inbudget" id="finance_purchase_inbudget">
+ <option value="">Select One</option>
+ <option value="Yes">Yes</option>
+ <option value="No">No</option>
+ </select>
+ </span>
+ </div>
+
+ <div id="finance_purchase_urgency_row" class="field_row">
+ <span class="field_label required">What is the urgency?:</span>
+ <span class="field_data">
+ <select name="finance_purchase_urgency" id="finance_purchase_urgency">
+ <option value="within 24 hours">within 24 hours</option>
+ <option value="1 to 3 days">1 to 3 days</option>
+ <option value="a week">a week</option>
+ <option value="no rush" selected>no rush</option>
+ </select>
+ </span>
+ </div>
+
+ <div id="finance_purchase_cost_row" class="field_row">
+ <span class="field_label required">Total Cost:</span>
+ <span class="field_data">
+ <input type="text" name="finance_purchase_cost" id="finance_purchase_cost" size="60">
+ </span>
+ </div>
+ </div>
+
+ <div id="data_safety_questions" class="bz_default_hidden">
+ <div class="header">Data Safety</div>
+
+ <div id="data_safety_user_data_row" class="field_row">
+ <span class="field_label">User Data:</span>
+ <span class="field_data">
+ <div class="field_description">Does your project collect data from users?</div>
+ <select name="data_safety_user_data" id="data_safety_user_data"
+ onchange="MPR.toggleVisibleById(this,'Yes','data_safety_extra_questions');">
+ <option value="">Select One</option>
+ <option value="Yes">Yes</option>
+ <option value="No">No</option>
+ </select>
+ </span>
+ </div>
+
+ <div id="data_safety_extra_questions" class="bz_default_hidden">
+ <div id="data_safety_user_count_row" class="field_row">
+ <span class="field_label">How many involved?:</span>
+ <span class="field_data">
+ <div class="field_description">How many users are currently involved?</div>
+ <input type="text" name="data_safety_user_count" id="data_safety_user_count" size="60">
+ </span>
+ </div>
+
+ <div id="data_safety_user_count_anticipated_row" class="field_row">
+ <span class="field_label">How many antcipated?:</span>
+ <span class="field_data">
+ <div class="field_description">How many users do you anticipate to be involved?</div>
+ <input type="text" name="data_safety_user_count_anticipated" id="data_safety_user_count_anticipated" size="60">
+ </span>
+ </div>
+
+ <div id="data_safety_data_type_row" class="field_row">
+ <span class="field_label">Type of Data:</span>
+ <span class="field_data">
+ <div class="field_description">Please provide examples of the types of user data you collect.</div>
+ <textarea name="data_safety_data_type" id="data_safety_data_type" rows="10" cols="80"></textarea>
+ </span>
+ </div>
+
+ <div id="data_safety_data_reason_row" class="field_row">
+ <span class="field_label">Data Reason:</span>
+ <span class="field_data">
+ <div class="field_description">Why do you need to collect user data?</div>
+ <textarea name="data_safety_data_reason" id="data_safety_data_reason" rows="10" cols="80"></textarea>
+ </span>
+ </div>
+
+ <div id="data_safety_community_benefit_row" class="field_row">
+ <span class="field_label">Community Benefit:</span>
+ <span class="field_data">
+ <div class="field_description">What community benefits are derived from the collection of user data for your project?</div>
+ <textarea name="data_safety_community_benefit" id="data_safety_community_benefit" rows="10" cols="80"></textarea>
+ </span>
+ </div>
+
+ <div id="data_safety_community_collection_row" class="field_row">
+ <span class="field_label">Data Collection:</span>
+ <span class="field_data">
+ <div class="field_description">How is the data being collected? (e.g., forms on web site, provided directly by user,
+ observed data collection, etc.) (Consider that you may be collecting data unintentionally
+ such as automatic logging by web servers)</div>
+ <textarea name="data_safety_community_collection" id="data_safety_community_collection" rows="10" cols="80"></textarea>
+ </span>
+ </div>
+
+ <div id="data_safety_retention_row" class="field_row">
+ <span class="field_label">Data Retention:</span>
+ <span class="field_data">
+ <div class="field_description">Will your project / team members need to retain user data?</div>
+ <select name="data_safety_retention" id="data_safety_retention"
+ onchange="MPR.toggleVisibleById(this,'Yes','data_safety_retention_length_row');">
+ <option value="">Select One</option>
+ <option value="Yes">Yes</option>
+ <option value="No">No</option>
+ </select>
+ </span>
+ </div>
+
+ <div id="data_safety_retention_length_row" class="field_row bz_default_hidden">
+ <span class="field_label">Data Retention Length:</span>
+ <span class="field_data">
+ <div class="field_description">If the data is being retained, for how long?</div>
+ <input type="text" name="data_safety_retention_length" id="data_safety_retention_length" size="60"></textarea>
+ </span>
+ </div>
+
+ <div id="data_safety_separate_party_row" class="field_row">
+ <span class="field_label">Separate Party:</span>
+ <span class="field_data">
+ <div class="field_description">Will any user data be shared or accessed by third party partners, customers or providers?</div>
+ <select name="data_safety_separate_party" id="data_safety_separate_party"
+ onchange="MPR.toggleVisibleById(this,'Yes','data_safety_separate_party_data_row');">
+ <option value="">Select One</option>
+ <option value="Yes">Yes</option>
+ <option value="No">No</option>
+ </select>
+ </span>
+ </div>
+
+ <div id="data_safety_separate_party_extra" class="bz_default_hidden">
+ <div id="data_safety_separate_party_data_row" class="field_row">
+ <span class="field_label">Separate Party Data Type:</span>
+ <span class="field_data">
+ <div class="field_description">What is the data being shared or accessed?</div>
+ <textarea name="data_safety_separate_party_data" id="data_safety_community_separate_party_data" rows="10" cols="80"></textarea>
+ </span>
+ </div>
+
+ <div id="data_safety_separate_party_data_communication_row" class="field_row">
+ <span class="field_label">Separate Party<br>Data Communication:</span>
+ <span class="field_data">
+ <div class="field_description">How would the data be communicated / transferred to the third parties?</div>
+ <textarea name="data_safety_separate_party_data_communication" id="data_safety_separate_party_data_communication" rows="10" cols="80"></textarea>
+ </span>
+ </div>
+
+ <div id="data_safety_separate_party_who_row" class="field_row">
+ <span class="field_label">Who are the separate parties?:</span>
+ <span class="field_data">
+ <div class="field_description">Who are the third party vendors and in what countries are they based?</div>
+ <textarea name="data_safety_separate_party_who" id="data_safety_separate_party_who" rows="10" cols="80"></textarea>
+ </span>
+ </div>
+ </div>
+
+ <div id="data_safety_community_visibility_row" class="field_row">
+ <span class="field_label">Community Visibility and Input:</span>
+ <span class="field_data">
+ <div class="field_description">Has your proposal been shared publicly, including requirements for Mozilla to collect and host user data?</div>
+ <select name="data_safety_community_visibility" id="data_safety_community_visibility"
+ onchange="MPR.toggleVisibleById(this,'Yes','data_safety_communication_channels_row');
+ MPR.toggleVisibleById(this,'No','data_safety_communication_plan_row');">
+ <option value="">Select One</option>
+ <option value="Yes">Yes</option>
+ <option value="No">No</option>
+ </select>
+ </span>
+ </div>
+
+ <div id="data_safety_communication_channels_row" class="field_row bz_default_hidden">
+ <span class="field_label">Communication Channels:</span>
+ <span class="field_data">
+ <div class="field_description">What communication channels are you using and what kind of input have you received thus far?</div>
+ <textarea name="data_safety_communication_channels" id="data_safety_communication_channels" rows="10" cols="80"></textarea>
+ </span>
+ </div>
+
+ <div id="data_safety_communication_plan_row" class="field_row bz_default_hidden">
+ <span class="field_label">Public Communication Plan:</span>
+ <span class="field_data">
+ <div class="field_description">Data Safety discussion needed. Provide your plan for publicly sharing your proposal.</div>
+ <textarea name="data_safety_communication_plan" id="data_safety_communication_plan" rows="10" cols="80"></textarea>
+ </span>
+ </div>
+ </div>
+ </div>
+
+ <input type="submit" id="commit" value="Submit Review">
+</form>
+
+<p>
+ Thanks for contacting us. You will be notified by email of any progress made in resolving your request.
+</p>
+
+[% PROCESS global/footer.html.tmpl %]
diff --git a/extensions/MozProjectReview/template/en/default/hook/global/messages-messages.html.tmpl b/extensions/MozProjectReview/template/en/default/hook/global/messages-messages.html.tmpl
new file mode 100644
index 000000000..ac7c1f6c7
--- /dev/null
+++ b/extensions/MozProjectReview/template/en/default/hook/global/messages-messages.html.tmpl
@@ -0,0 +1,13 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF message_tag == "moz_project_review_creation_failed" %]
+ The parent [% terms.bug %] was created successfully, but creation of
+ the dependent [% terms.bugs %] failed. The error has been logged
+ and no further action is required at this time.
+[% END %]
diff --git a/extensions/MozProjectReview/web/js/moz_project_review.js b/extensions/MozProjectReview/web/js/moz_project_review.js
new file mode 100644
index 000000000..318122f19
--- /dev/null
+++ b/extensions/MozProjectReview/web/js/moz_project_review.js
@@ -0,0 +1,149 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0.
+ */
+
+YAHOO.namespace('MozProjectReview');
+
+var MPR = YAHOO.MozProjectReview;
+var Dom = YAHOO.util.Dom;
+
+MPR.required_fields = {
+ "initial_questions": {
+ "short_desc": "Please enter a value for project or feature name in the initial questions section",
+ "cc": "Please enter a value for points of contact in the initial questions section",
+ "urgency": "Please enter a value for urgency in the initial questions section",
+ "release_date": "Please enter a value for release date in the initial questions section",
+ "project_status": "Please select a value for project status in the initial questions section",
+ "mozilla_data": "Please select a value for mozilla data in the initial questions section",
+ "new_or_change": "Please select a value for new or change to existing project in the initial questions section",
+ "separate_party": "Please select a value for separate party in the initial questions section"
+ },
+ "finance_questions": {
+ "finance_purchase_what": "Please enter a value for what in the finance questions section",
+ "finance_purchase_why": "Please enter a value for why in the finance questions section",
+ "finance_purchase_risk": "Please enter a value for risk in the finance questions section",
+ "finance_purchase_alternative": "Please enter a value for alternative in the finance questions section",
+ "finance_purchase_inbudget": "Please enter a value for in budget in the finance questions section",
+ "finance_purchase_urgency": "Please select a value for urgency in the finance questions section",
+ "finance_purchase_cost": "Please enter a value for total cost in the finance questions section"
+ },
+ "legal_questions": {
+ "legal_priority": "Please select a priority for the legal questions section"
+ }
+};
+
+MPR.toggleSpecialSections = function () {
+ var mozilla_data_select = Dom.get('mozilla_data');
+ var data_access_select = Dom.get('data_access');
+ var vendor_cost_select = Dom.get('vendor_cost');
+
+ if (mozilla_data_select.value == 'Yes') {
+ Dom.removeClass('legal_questions', 'bz_default_hidden');
+ Dom.removeClass('privacy_policy_project_questions', 'bz_default_hidden');
+ Dom.removeClass('data_safety_questions', 'bz_default_hidden');
+ Dom.removeClass('sec_review_questions', 'bz_default_hidden');
+ }
+ else {
+ Dom.addClass('legal_questions', 'bz_default_hidden');
+ Dom.addClass('privacy_policy_project_questions', 'bz_default_hidden');
+ Dom.addClass('data_safety_questions', 'bz_default_hidden');
+ Dom.addClass('sec_review_questions', 'bz_default_hidden');
+ }
+
+ if (data_access_select.value == 'Yes' || mozilla_data_select.value == 'Yes') {
+ Dom.removeClass('sec_review_questions', 'bz_default_hidden');
+ }
+ else {
+ Dom.addClass('sec_review_questions', 'bz_default_hidden');
+ }
+
+ if (data_access_select.value == 'Yes') {
+ Dom.removeClass('privacy_policy_vendor_questions', 'bz_default_hidden');
+ }
+ else {
+ Dom.addClass('privacy_policy_vendor_questions', 'bz_default_hidden');
+ }
+
+ if (vendor_cost_select.value == '> $25,000') {
+ Dom.removeClass('finance_questions', 'bz_default_hidden');
+ }
+ else {
+ Dom.addClass('finance_questions', 'bz_default_hidden');
+ }
+}
+
+MPR.toggleVisibleById = function () {
+ var args = Array.prototype.slice.call(arguments);
+ var select = args.shift();
+ var value = args.shift();
+ var ids = args;
+
+ if (typeof select == 'string') {
+ select = Dom.get(select);
+ }
+
+ for (var i = 0; i < ids.length; i++) {
+ if (select.value == value) {
+ Dom.removeClass(ids[i], 'bz_default_hidden');
+ }
+ else {
+ Dom.addClass(ids[i], 'bz_default_hidden');
+ }
+ }
+}
+
+MPR.validateAndSubmit = function () {
+ var alert_text = '';
+ var section = '';
+ for (section in MPR.required_fields) {
+ console.log("section: " + section);
+ if (!Dom.hasClass(section, 'bz_default_hidden')) {
+ var field = '';
+ for (field in MPR.required_fields[section]) {
+ console.log("field: " + field);
+ if (!MPR.isFilledOut(field)) {
+ alert_text += MPR.required_fields[section][field] + "\n";
+ }
+ }
+ }
+ }
+
+ if (Dom.get('separate_party').value == 'Yes') {
+ if (!MPR.isFilledOut('relationship_type')) alert_text += "Please select a value for type of relationship\n";
+ if (!MPR.isFilledOut('data_access')) alert_text += "Please select a value for data access\n";
+ if (!MPR.isFilledOut('vendor_cost')) alert_text += "Please select a value for vendor cost\n";
+ }
+
+ if (alert_text) {
+ alert(alert_text);
+ return false;
+ }
+
+ return true;
+}
+
+YAHOO.util.Event.onDOMReady(function() {
+ MPR.toggleSpecialSections();
+ MPR.toggleVisibleById('new_or_change', 'Existing', 'mozilla_project_row');
+ MPR.toggleVisibleById('separate_party', 'Yes', 'initial_separate_party_questions');
+ MPR.toggleVisibleById('relationship_type', 'Vendor/Services', 'legal_sow_details_row');
+ MPR.toggleVisibleById('vendor_cost', '> $25,000', 'finance_questions');
+ MPR.toggleVisibleById('privacy_policy_project', 'Yes', 'privacy_policy_project_link_row');
+ MPR.toggleVisibleById('privacy_policy_user_data', 'Yes', 'privacy_policy_project_user_data_bug_row');
+ MPR.toggleVisibleById('privacy_policy_vendor_user_data', 'Yes', 'privacy_policy_vendor_extra');
+ MPR.toggleVisibleById('data_safety_user_data', 'Yes', 'data_safety_extra_questions');
+ MPR.toggleVisibleById('data_safety_retention', 'Yes', 'data_safety_retention_length_row');
+ MPR.toggleVisibleById('data_safety_separate_party', 'Yes', 'data_safety_separate_party_data_row');
+ MPR.toggleVisibleById('data_safety_community_visibility', 'Yes', 'data_safety_communication_channels_row');
+ MPR.toggleVisibleById('data_safety_community_visibility', 'No', 'data_safety_communication_plan_row');
+});
+
+//Takes a DOM element id and makes sure that it is filled out
+MPR.isFilledOut = function (elem_id) {
+ var str = Dom.get(elem_id).value;
+ return str.length > 0 ? true : false;
+}
diff --git a/extensions/MozProjectReview/web/style/moz_project_review.css b/extensions/MozProjectReview/web/style/moz_project_review.css
new file mode 100644
index 000000000..fb23a78e1
--- /dev/null
+++ b/extensions/MozProjectReview/web/style/moz_project_review.css
@@ -0,0 +1,41 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0. */
+
+.header {
+ width:95%;
+ border-bottom: 1px solid rgb(116,126,147);
+ font-size: 1.5em;
+ color: rgb(102,100,88);
+ padding-bottom: 5px;
+ margin-bottom: 5px;
+ margin-top: 12px;
+}
+.field_row {
+ width:100%;
+ min-width:700px;
+}
+.field_label {
+ float:left;
+ width:20%;
+}
+.field_data {
+ float:left;
+ width:75%;
+ margin-left:5px;
+ margin-bottom:5px;
+}
+.field_description {
+ font-style:italic;
+ font-size:90%;
+ color: rgb(102,100,88);
+}
+span.required:before {
+ content: "* ";
+}
+span.required:before, span.required_star {
+ color: red;
+}
diff --git a/extensions/MyDashboard/Config.pm b/extensions/MyDashboard/Config.pm
new file mode 100644
index 000000000..7c14936ff
--- /dev/null
+++ b/extensions/MyDashboard/Config.pm
@@ -0,0 +1,14 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::MyDashboard;
+
+use strict;
+
+use constant NAME => 'MyDashboard';
+
+__PACKAGE__->NAME;
diff --git a/extensions/MyDashboard/Extension.pm b/extensions/MyDashboard/Extension.pm
new file mode 100644
index 000000000..82c995442
--- /dev/null
+++ b/extensions/MyDashboard/Extension.pm
@@ -0,0 +1,363 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::MyDashboard;
+
+use strict;
+
+use base qw(Bugzilla::Extension);
+
+use Bugzilla;
+use Bugzilla::Constants;
+use Bugzilla::Search;
+use Bugzilla::Util;
+use Bugzilla::Status;
+use Bugzilla::Field;
+use Bugzilla::Search::Saved;
+
+use Bugzilla::Extension::MyDashboard::Util qw(open_states closed_states
+ quoted_open_states quoted_closed_states);
+use Bugzilla::Extension::MyDashboard::TimeAgo qw(time_ago);
+
+use DateTime;
+
+our $VERSION = BUGZILLA_VERSION;
+
+sub QUERY_DEFS {
+ my $user = Bugzilla->user;
+
+ my @query_defs = (
+ {
+ name => 'assignedbugs',
+ heading => 'Assigned to You',
+ description => 'The bug has been assigned to you and it is not resolved or closed yet.',
+ params => {
+ 'bug_status' => ['__open__'],
+ 'emailassigned_to1' => 1,
+ 'emailtype1' => 'exact',
+ 'email1' => $user->login
+ }
+ },
+ {
+ name => 'newbugs',
+ heading => 'New Reported by You',
+ description => 'You reported the bug but nobody has accepted it yet.',
+ params => {
+ 'bug_status' => ['NEW'],
+ 'emailreporter1' => 1,
+ 'emailtype1' => 'exact',
+ 'email1' => $user->login
+ }
+ },
+ {
+ name => 'inprogressbugs',
+ heading => "In Progress Reported by You",
+ description => 'You reported the bug, the developer accepted the bug and is hopefully working on it.',
+ params => {
+ 'bug_status' => [ map { $_->name } grep($_->name ne 'NEW' && $_->name ne 'MODIFIED', open_states()) ],
+ 'emailreporter1' => 1,
+ 'emailtype1' => 'exact',
+ 'email1' => $user->login
+ }
+ },
+ {
+ name => 'openccbugs',
+ heading => "You Are CC'd On",
+ description => 'You are in the CC list of the bug, so you are watching it.',
+ params => {
+ 'bug_status' => ['__open__'],
+ 'emailcc1' => 1,
+ 'emailtype1' => 'exact',
+ 'email1' => $user->login
+ }
+ },
+ );
+
+ if (Bugzilla->params->{'useqacontact'}) {
+ push(@query_defs, {
+ name => 'qacontactbugs',
+ heading => 'You Are QA Contact',
+ description => 'You are the qa contact on this bug and it is not resolved or closed yet.',
+ params => {
+ 'bug_status' => ['__open__'],
+ 'emailqa_contact1' => 1,
+ 'emailtype1' => 'exact',
+ 'email1' => $user->login
+ }
+ });
+ }
+
+ return @query_defs;
+}
+
+################
+# Installation #
+################
+
+sub db_schema_abstract_schema {
+ my ($self, $args) = @_;
+
+ my $schema = $args->{schema};
+
+ $schema->{'mydashboard'} = {
+ FIELDS => [
+ namedquery_id => {TYPE => 'INT3', NOTNULL => 1,
+ REFERENCES => {TABLE => 'namedqueries',
+ COLUMN => 'id',
+ DELETE => 'CASCADE'}},
+ user_id => {TYPE => 'INT3', NOTNULL => 1,
+ REFERENCES => {TABLE => 'profiles',
+ COLUMN => 'userid',
+ DELETE => 'CASCADE'}},
+ ],
+ INDEXES => [
+ mydashboard_namedquery_id_idx => {FIELDS => [qw(namedquery_id user_id)],
+ TYPE => 'UNIQUE'},
+ mydashboard_user_id_idx => ['user_id'],
+ ],
+ };
+}
+
+###########
+# Objects #
+###########
+
+BEGIN {
+ *Bugzilla::Search::Saved::in_mydashboard = \&_in_mydashboard;
+}
+
+sub _in_mydashboard {
+ my ($self) = @_;
+ my $dbh = Bugzilla->dbh;
+ return $self->{'in_mydashboard'} if exists $self->{'in_mydashboard'};
+ $self->{'in_mydashboard'} = $dbh->selectrow_array("
+ SELECT 1 FROM mydashboard WHERE namedquery_id = ? AND user_id = ?",
+ undef, $self->id, $self->user->id);
+ return $self->{'in_mydashboard'};
+}
+
+#############
+# Templates #
+#############
+
+sub page_before_template {
+ my ($self, $args) = @_;
+ my $page = $args->{'page_id'};
+ my $vars = $args->{'vars'};
+
+ return if $page ne 'mydashboard.html';
+
+ # If we're using bug groups to restrict bug entry, we need to know who the
+ # user is right from the start.
+ my $user = Bugzilla->login(LOGIN_REQUIRED);
+
+ # Switch to shadow db since we are just reading information
+ Bugzilla->switch_to_shadow_db();
+
+ _standard_saved_queries($vars);
+ _flags_requested($vars);
+
+ $vars->{'severities'} = get_legal_field_values('bug_severity');
+}
+
+sub _standard_saved_queries {
+ my ($vars) = @_;
+ my $dbh = Bugzilla->dbh;
+ my $user = Bugzilla->user;
+
+ # Default sort order
+ my $order = ["changeddate desc", "bug_id"];
+
+ # List of columns that we will be selecting. In the future this should be configurable
+ # Share with buglist.cgi?
+ my @select_columns = ('bug_id','product','bug_status','bug_severity','version', 'component','short_desc', 'changeddate');
+
+ # Define the columns that can be selected in a query
+ my $columns = Bugzilla::Search::COLUMNS;
+
+ # Weed out columns that don't actually exist and detaint along the way.
+ @select_columns = grep($columns->{$_} && trick_taint($_), @select_columns);
+
+ ### Standard query definitions
+ my @query_defs = QUERY_DEFS;
+
+ ### Saved query definitions
+ ### These are enabled through the userprefs.cgi UI
+
+ if ($user->showmybugslink) {
+ my $query = Bugzilla->params->{mybugstemplate};
+ my $login = $user->login;
+ $query =~ s/%userid%/$login/;
+ $query =~ s/^buglist.cgi\?//;
+ push(@query_defs, {
+ name => 'mybugs',
+ heading => "My Bugs",
+ saved => 1,
+ params => $query,
+ });
+ }
+
+ foreach my $q (@{$user->queries}) {
+ next if !$q->in_mydashboard;
+ push(@query_defs, { name => $q->name,
+ heading => $q->name,
+ saved => 1,
+ params => $q->url });
+ }
+
+ #my $date_now = DateTime->now(time_zone => Bugzilla->local_timezone);
+
+ ### Collect the query results for display in the template
+
+ my @results;
+ foreach my $qdef (@query_defs) {
+ my $params = new Bugzilla::CGI($qdef->{params});
+
+ my $search = new Bugzilla::Search( fields => \@select_columns,
+ params => scalar $params->Vars,
+ order => $order );
+ my $query = $search->sql();
+
+ my $sth = $dbh->prepare($query);
+ $sth->execute();
+
+ my $rows = $sth->fetchall_arrayref();
+
+ my @bugs;
+ foreach my $row (@$rows) {
+ my $bug = {};
+ foreach my $column (@select_columns) {
+ $bug->{$column} = shift @$row;
+ #if ($column eq 'changeddate') {
+ # my $date_then = datetime_from($bug->{$column});
+ # $bug->{'updated'} = time_ago($date_then, $date_now);
+ #}
+ }
+ push(@bugs, $bug);
+ }
+
+ $qdef->{bugs} = \@bugs;
+ $qdef->{buffer} = $params->canonicalise_query();
+
+ push(@results, $qdef);
+ }
+
+ $vars->{'results'} = \@results;
+}
+
+sub _flags_requested {
+ my ($vars) = @_;
+ my $user = Bugzilla->user;
+ my $dbh = Bugzilla->dbh;
+
+ my $attach_join_clause = "flags.attach_id = attachments.attach_id";
+ if (Bugzilla->params->{insidergroup} && !$user->in_group(Bugzilla->params->{insidergroup})) {
+ $attach_join_clause .= " AND attachments.isprivate < 1";
+ }
+
+ my $query =
+ # Select columns describing each flag, the bug/attachment on which
+ # it has been set, who set it, and of whom they are requesting it.
+ " SELECT flags.id AS id,
+ flagtypes.name AS type,
+ flags.status AS status,
+ flags.bug_id AS bug_id,
+ bugs.short_desc AS bug_summary,
+ flags.attach_id AS attach_id,
+ attachments.description AS attach_summary,
+ requesters.realname AS requester,
+ requestees.realname AS requestee,
+ " . $dbh->sql_date_format('flags.creation_date', '%Y:%m:%d') . " AS created
+ FROM flags
+ LEFT JOIN attachments
+ ON ($attach_join_clause)
+ INNER JOIN flagtypes
+ ON flags.type_id = flagtypes.id
+ INNER JOIN bugs
+ ON flags.bug_id = bugs.bug_id
+ LEFT JOIN profiles AS requesters
+ ON flags.setter_id = requesters.userid
+ LEFT JOIN profiles AS requestees
+ ON flags.requestee_id = requestees.userid
+ LEFT JOIN bug_group_map AS bgmap
+ ON bgmap.bug_id = bugs.bug_id
+ LEFT JOIN cc AS ccmap
+ ON ccmap.who = " . $user->id . "
+ AND ccmap.bug_id = bugs.bug_id ";
+
+ # Limit query to pending requests and open bugs only
+ $query .= " WHERE bugs.bug_status IN (" . join(',', quoted_open_states()) . ")
+ AND flags.status = '?' ";
+
+ # Weed out bug the user does not have access to
+ $query .= " AND ((bgmap.group_id IS NULL)
+ OR bgmap.group_id IN (" . $user->groups_as_string . ")
+ OR (ccmap.who IS NOT NULL AND cclist_accessible = 1)
+ OR (bugs.reporter = " . $user->id . " AND bugs.reporter_accessible = 1)
+ OR (bugs.assigned_to = " . $user->id .") ";
+ if (Bugzilla->params->{useqacontact}) {
+ $query .= " OR (bugs.qa_contact = " . $user->id . ") ";
+ }
+ $query .= ") ";
+
+ # Order the records (within each group).
+ my $group_order_by = " GROUP BY flags.bug_id ORDER BY flags.creation_date, flagtypes.name";
+
+ my $requestee_list = $dbh->selectall_arrayref($query .
+ " AND requestees.login_name = ? " .
+ $group_order_by,
+ { Slice => {} }, $user->login);
+ $vars->{'requestee_list'} = $requestee_list;
+ my $requester_list = $dbh->selectall_arrayref($query .
+ " AND requesters.login_name = ? " .
+ $group_order_by,
+ { Slice => {} }, $user->login);
+ $vars->{'requester_list'} = $requester_list;
+}
+
+#########
+# Hooks #
+#########
+
+sub user_preferences {
+ my ($self, $args) = @_;
+ my $tab = $args->{'current_tab'};
+ return unless $tab eq 'saved-searches';
+
+ my $save = $args->{'save_changes'};
+ my $handled = $args->{'handled'};
+ my $vars = $args->{'vars'};
+
+ my $dbh = Bugzilla->dbh;
+ my $user = Bugzilla->user;
+ my $params = Bugzilla->input_params;
+
+ if ($save) {
+ my $sth_insert_fp = $dbh->prepare('INSERT INTO mydashboard
+ (namedquery_id, user_id)
+ VALUES (?, ?)');
+ my $sth_delete_fp = $dbh->prepare('DELETE FROM mydashboard
+ WHERE namedquery_id = ?
+ AND user_id = ?');
+ foreach my $q (@{$user->queries}, @{$user->queries_available}) {
+ if (defined $params->{'in_mydashboard_' . $q->id}) {
+ $sth_insert_fp->execute($q->id, $q->user->id) if !$q->in_mydashboard;
+ }
+ else {
+ $sth_delete_fp->execute($q->id, $q->user->id) if $q->in_mydashboard;
+ }
+ }
+ }
+}
+
+sub webservice {
+ my ($self, $args) = @_;
+ my $dispatch = $args->{dispatch};
+ $dispatch->{MyDashboard} = "Bugzilla::Extension::MyDashboard::WebService";
+}
+
+__PACKAGE__->NAME;
diff --git a/extensions/MyDashboard/lib/TimeAgo.pm b/extensions/MyDashboard/lib/TimeAgo.pm
new file mode 100644
index 000000000..f213986d6
--- /dev/null
+++ b/extensions/MyDashboard/lib/TimeAgo.pm
@@ -0,0 +1,182 @@
+package Bugzilla::Extension::MyDashboard::TimeAgo;
+
+use strict;
+use utf8;
+use DateTime;
+use Carp;
+use Exporter qw(import);
+
+use if $ENV{ARCH_64BIT}, 'integer';
+
+our @EXPORT_OK = qw(time_ago);
+
+our $VERSION = '0.06';
+
+my @ranges = (
+ [ -1, 'in the future' ],
+ [ 60, 'just now' ],
+ [ 900, 'a few minutes ago'], # 15*60
+ [ 3000, 'less than an hour ago'], # 50*60
+ [ 4500, 'about an hour ago'], # 75*60
+ [ 7200, 'more than an hour ago'], # 2*60*60
+ [ 21600, 'several hours ago'], # 6*60*60
+ [ 86400, 'today', sub { # 24*60*60
+ my $time = shift;
+ my $now = shift;
+ if ( $time->day < $now->day
+ or $time->month < $now->month
+ or $time->year < $now->year
+ ) {
+ return 'yesterday'
+ }
+ if ($time->hour < 5) {
+ return 'tonight'
+ }
+ if ($time->hour < 10) {
+ return 'this morning'
+ }
+ if ($time->hour < 15) {
+ return 'today'
+ }
+ if ($time->hour < 19) {
+ return 'this afternoon'
+ }
+ return 'this evening'
+ }],
+ [ 172800, 'yesterday'], # 2*24*60*60
+ [ 604800, 'this week'], # 7*24*60*60
+ [ 1209600, 'last week'], # 2*7*24*60*60
+ [ 2678400, 'this month', sub { # 31*24*60*60
+ my $time = shift;
+ my $now = shift;
+ if ($time->year == $now->year and $time->month == $now->month) {
+ return 'this month'
+ }
+ return 'last month'
+ }],
+ [ 5356800, 'last month'], # 2*31*24*60*60
+ [ 24105600, 'several months ago'], # 9*31*24*60*60
+ [ 31536000, 'about a year ago'], # 365*24*60*60
+ [ 34214400, 'last year'], # (365+31)*24*60*60
+ [ 63072000, 'more than a year ago'], # 2*365*24*60*60
+ [ 283824000, 'several years ago'], # 9*365*24*60*60
+ [ 315360000, 'about a decade ago'], # 10*365*24*60*60
+ [ 630720000, 'last decade'], # 20*365*24*60*60
+ [ 2838240000, 'several decades ago'], # 90*365*24*60*60
+ [ 3153600000, 'about a century ago'], # 100*365*24*60*60
+ [ 6307200000, 'last century'], # 200*365*24*60*60
+ [ 6622560000, 'more than a century ago'], # 210*365*24*60*60
+ [ 28382400000, 'several centuries ago'], # 900*365*24*60*60
+ [ 31536000000, 'about a millenium ago'], # 1000*365*24*60*60
+ [ 63072000000, 'more than a millenium ago'], # 2000*365*24*60*60
+);
+
+sub time_ago {
+ my ($time, $now) = @_;
+
+ if (not defined $time or not $time->isa('DateTime')) {
+ croak('DateTime::Duration::Fuzzy::time_ago needs a DateTime object as first parameter')
+ }
+ if (not defined $now) {
+ $now = DateTime->now();
+ }
+ if (not $now->isa('DateTime')) {
+ croak('Invalid second parameter provided to DateTime::Duration::Fuzzy::time_ago; it must be a DateTime object if provided')
+ }
+
+ # Use clones in UTC for safe date calculation
+ my $now_clone = $now->clone->set_time_zone('UTC');
+ my $time_clone = $time->clone->set_time_zone('UTC');
+ my $dur = $now_clone->subtract_datetime_absolute( $time_clone )->in_units('seconds');
+
+ foreach my $range ( @ranges ) {
+ if ( $dur <= $range->[0] ) {
+ if ( $range->[2] ) {
+ return $range->[2]->( $time_clone, $now_clone )
+ }
+ return $range->[1]
+ }
+ }
+
+ return 'millenia ago'
+}
+
+1
+
+__END__
+
+=head1 NAME
+
+DateTime::Duration::Fuzzy -- express dates as fuzzy human-friendly strings
+
+=head1 SYNOPSIS
+
+ use DateTime::Duration::Fuzzy qw(time_ago);
+ use DateTime;
+
+ my $now = DateTime->new(
+ year => 2010, month => 12, day => 12,
+ hour => 19, minute => 59,
+ );
+ my $then = DateTime->new(
+ year => 2010, month => 12, day => 12,
+ hour => 15,
+ );
+ print time_ago($then, $now);
+ # outputs 'several hours ago'
+
+ print time_ago($then);
+ # $now taken from C<time> function
+
+=head1 DESCRIPTION
+
+DateTime::Duration::Fuzzy is inspired from the timeAgo jQuery module
+L<http://timeago.yarp.com/>.
+
+It takes two DateTime objects -- first one representing a moment in the past
+and second optional one representine the present, and returns a human-friendly
+fuzzy expression of the time gone.
+
+=head2 functions
+
+=over 4
+
+=item time_ago($then, $now)
+
+The only exportable function.
+
+First obligatory parameter is a DateTime object.
+
+Second optional parameter is also a DateTime object.
+If it's not provided, then I<now> as the C<time> function returns is
+substituted.
+
+Returns a string expression of the interval between the two DateTime
+objects, like C<several hours ago>, C<yesterday> or <last century>.
+
+=back
+
+=head2 performance
+
+On 64bit machines, it is asvisable to 'use integer', which makes
+the calculations faster. You can turn this on by setting the
+C<ARCH_64BIT> environmental variable to a true value.
+
+If you do this on a 32bit machine, you will get wrong results for
+intervals starting with "several decades ago".
+
+=head1 AUTHOR
+
+Jan Oldrich Kruza, C<< <sixtease at cpan.org> >>
+
+=head1 LICENSE AND COPYRIGHT
+
+Copyright 2010 Jan Oldrich Kruza.
+
+This program is free software; you can redistribute it and/or modify it
+under the terms of either: the GNU General Public License as published
+by the Free Software Foundation; or the Artistic License.
+
+See http://dev.perl.org/licenses/ for more information.
+
+=cut
diff --git a/extensions/MyDashboard/lib/Util.pm b/extensions/MyDashboard/lib/Util.pm
new file mode 100644
index 000000000..ce5db005f
--- /dev/null
+++ b/extensions/MyDashboard/lib/Util.pm
@@ -0,0 +1,48 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::MyDashboard::Util;
+
+use strict;
+
+use base qw(Exporter);
+@Bugzilla::Extension::MyDashboard::Util::EXPORT = qw(
+ open_states
+ closed_states
+ quoted_open_states
+ quoted_closed_states
+);
+
+use Bugzilla::Status;
+
+our $_open_states;
+sub open_states {
+ $_open_states ||= Bugzilla::Status->match({ is_open => 1, isactive => 1 });
+ return wantarray ? @$_open_states : $_open_states;
+}
+
+our $_quoted_open_states;
+sub quoted_open_states {
+ my $dbh = Bugzilla->dbh;
+ $_quoted_open_states ||= [ map { $dbh->quote($_->name) } open_states() ];
+ return wantarray ? @$_quoted_open_states : $_quoted_open_states;
+}
+
+our $_closed_states;
+sub closed_states {
+ $_closed_states ||= Bugzilla::Status->match({ is_open => 0, isactive => 1 });
+ return wantarray ? @$_closed_states : $_closed_states;
+}
+
+our $_quoted_closed_states;
+sub quoted_closed_states {
+ my $dbh = Bugzilla->dbh;
+ $_quoted_closed_states ||= [ map { $dbh->quote($_->name) } closed_states() ];
+ return wantarray ? @$_quoted_closed_states : $_quoted_closed_states;
+}
+
+1;
diff --git a/extensions/MyDashboard/lib/WebService.pm b/extensions/MyDashboard/lib/WebService.pm
new file mode 100644
index 000000000..78285ca06
--- /dev/null
+++ b/extensions/MyDashboard/lib/WebService.pm
@@ -0,0 +1,98 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+package Bugzilla::Extension::MyDashboard::WebService;
+
+use strict;
+use warnings;
+
+use base qw(Bugzilla::WebService);
+
+use Bugzilla::Error;
+use Bugzilla::Util qw(detaint_natural trick_taint);
+
+sub prod_comp_search {
+ my ($self, $params) = @_;
+ my $user = Bugzilla->user;
+ my $dbh = Bugzilla->switch_to_shadow_db();
+
+ my $search = $params->{'search'};
+ $search || ThrowCodeError('param_required',
+ { function => 'Bug.prod_comp_search', param => 'search' });
+
+ my $limit = detaint_natural($params->{'limit'})
+ ? $dbh->sql_limit($params->{'limit'})
+ : '';
+
+ # We do this in the DB directly as we want it to be fast and
+ # not have the overhead of loading full product objects
+
+ # All products which the user has "Entry" access to.
+ my $enterable_ids = $dbh->selectcol_arrayref(
+ 'SELECT products.id FROM products
+ LEFT JOIN group_control_map
+ ON group_control_map.product_id = products.id
+ AND group_control_map.entry != 0
+ AND group_id NOT IN (' . $user->groups_as_string . ')
+ WHERE group_id IS NULL
+ AND products.isactive = 1');
+
+ if (scalar @$enterable_ids) {
+ # And all of these products must have at least one component
+ # and one version.
+ $enterable_ids = $dbh->selectcol_arrayref(
+ 'SELECT DISTINCT products.id FROM products
+ WHERE ' . $dbh->sql_in('products.id', $enterable_ids) .
+ ' AND products.id IN (SELECT DISTINCT components.product_id
+ FROM components
+ WHERE components.isactive = 1)
+ AND products.id IN (SELECT DISTINCT versions.product_id
+ FROM versions
+ WHERE versions.isactive = 1)');
+ }
+
+ return { products => [] } if !scalar @$enterable_ids;
+
+ my @list;
+ foreach my $word (split(/[\s,]+/, $search)) {
+ if ($word ne "") {
+ my $sql_word = $dbh->quote($word);
+ trick_taint($sql_word);
+ # XXX CONCAT_WS is MySQL specific
+ my $field = "CONCAT_WS(' ', products.name, components.name, components.description)";
+ push(@list, $dbh->sql_iposition($sql_word, $field) . " > 0");
+ }
+ }
+
+ my $products = $dbh->selectall_arrayref("
+ SELECT products.name AS product,
+ components.name AS component
+ FROM products
+ INNER JOIN components ON products.id = components.product_id
+ WHERE (" . join(" AND ", @list) . ")
+ AND products.id IN (" . join(",", @$enterable_ids) . ")
+ ORDER BY products.name $limit",
+ { Slice => {} });
+
+ return { products => $products };
+}
+
+1;
+
+__END__
+
+=head1 NAME
+
+Bugzilla::Extension::MyDashboard::Webservice - The MyDashboard WebServices API
+
+=head1 DESCRIPTION
+
+This module contains API methods that are useful to user's of bugzilla.mozilla.org.
+
+=head1 METHODS
+
+See L<Bugzilla::WebService> for a description of how parameters are passed,
+and what B<STABLE>, B<UNSTABLE>, and B<EXPERIMENTAL> mean.
diff --git a/extensions/MyDashboard/template/en/default/hook/account/prefs/saved-searches-saved-header.html.tmpl b/extensions/MyDashboard/template/en/default/hook/account/prefs/saved-searches-saved-header.html.tmpl
new file mode 100644
index 000000000..c822ab040
--- /dev/null
+++ b/extensions/MyDashboard/template/en/default/hook/account/prefs/saved-searches-saved-header.html.tmpl
@@ -0,0 +1,11 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+<th>
+ My Dashboard
+</th>
diff --git a/extensions/MyDashboard/template/en/default/hook/account/prefs/saved-searches-saved-row.html.tmpl b/extensions/MyDashboard/template/en/default/hook/account/prefs/saved-searches-saved-row.html.tmpl
new file mode 100644
index 000000000..cd6a36705
--- /dev/null
+++ b/extensions/MyDashboard/template/en/default/hook/account/prefs/saved-searches-saved-row.html.tmpl
@@ -0,0 +1,15 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+<td align="center">
+ <input type="checkbox"
+ name="in_mydashboard_[% q.id FILTER html %]"
+ value="1"
+ alt="[% q.name FILTER html %]"
+ [% " checked" IF q.in_mydashboard %]>
+</td>
diff --git a/extensions/MyDashboard/template/en/default/hook/global/common-links-action-links.html.tmpl b/extensions/MyDashboard/template/en/default/hook/global/common-links-action-links.html.tmpl
new file mode 100644
index 000000000..518743ccf
--- /dev/null
+++ b/extensions/MyDashboard/template/en/default/hook/global/common-links-action-links.html.tmpl
@@ -0,0 +1,12 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF user.login %]
+ <li><span class="separator"> | </span>
+ <a href="[% urlbase FILTER none %]page.cgi?id=mydashboard.html">My Dashboard</a></li>
+[% END %]
diff --git a/extensions/MyDashboard/template/en/default/mydashboard/prod-comp-search.html.tmpl b/extensions/MyDashboard/template/en/default/mydashboard/prod-comp-search.html.tmpl
new file mode 100644
index 000000000..98daedf1e
--- /dev/null
+++ b/extensions/MyDashboard/template/en/default/mydashboard/prod-comp-search.html.tmpl
@@ -0,0 +1,43 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+<div id="prod_comp_search_main">
+ <div id="prod_comp_search_autocomplete">
+ <div id="prod_comp_search_label">
+ File [% terms.Bug %]:
+ <img id="prod_comp_throbber" src="extensions/BMO/web/images/throbber.gif"
+ class="hidden" width="16" height="11">
+ </div>
+ <input id="prod_comp_search" type="text" size="60">
+ <div id="prod_comp_search_autocomplete_container"></div>
+ </div>
+</div>
+<script type="text/javascript">
+ if(typeof(YAHOO.bugzilla.prodCompSearch) !== 'undefined'
+ && YAHOO.bugzilla.prodCompSearch != null)
+ {
+ YAHOO.bugzilla.prodCompSearch.init(
+ "prod_comp_search",
+ "prod_comp_search_autocomplete_container",
+ "[% format FILTER js %]",
+ "[% cloned_bug_id FILTER js %]");
+ [% IF target == "describecomponents.cgi" %]
+ YAHOO.bugzilla.prodCompSearch.autoComplete.itemSelectEvent.subscribe(function (e, args) {
+ var oData = args[2];
+ var url = "describecomponents.cgi?product=" + encodeURIComponent(oData[0]) +
+ "&component=" + encodeURIComponent(oData[1]) +
+ "#" + encodeURIComponent(oData[1]);
+ var format = YAHOO.bugzilla.prodCompSearch.format;
+ if (format) {
+ url += "&format=" + encodeURIComponent(format);
+ }
+ window.location.href = url;
+ });
+ [% END %]
+ }
+</script>
diff --git a/extensions/MyDashboard/template/en/default/pages/mydashboard.html.tmpl b/extensions/MyDashboard/template/en/default/pages/mydashboard.html.tmpl
new file mode 100644
index 000000000..60c3be668
--- /dev/null
+++ b/extensions/MyDashboard/template/en/default/pages/mydashboard.html.tmpl
@@ -0,0 +1,222 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+
+[% PROCESS global/header.html.tmpl
+ title = "My Dashboard"
+ style_urls = [ "skins/standard/buglist.css",
+ "js/yui/assets/skins/sam/paginator.css",
+ "extensions/MyDashboard/web/styles/mydashboard.css",
+ "extensions/MyDashboard/web/styles/prod_comp_search.css" ]
+ yui = [ "datatable", "paginator", "autocomplete" ]
+ javascript_urls = [ "extensions/MyDashboard/web/js/mydashboard.js",
+ "extensions/MyDashboard/web/js/prod_comp_search.js" ]
+ onload = "MD.showQuerySection();"
+%]
+
+<script type="text/javascript">
+<!--
+ [%# Set up severities list for proper sorting %]
+ MD.severities = new Array();
+ [% sort_count = 0 %]
+ [% FOREACH s = severities %]
+ MD.severities['[% s FILTER js %]'] = [% sort_count FILTER js %];
+ [% sort_count = sort_count + 1 %]
+ [% END %]
+
+ MD.full_query_list = [];
+ [% FOREACH r = results %]
+ MD.full_query_list.push('[% r.name FILTER js %]');
+ [% END %]
+-->
+</script>
+
+[% standard_results = [] %]
+[% saved_results = [] %]
+[% FOREACH r = results %]
+ [% standard_results.push(r) IF !r.saved %]
+ [% saved_results.push(r) IF r.saved %]
+[% END %]
+
+<div id="mydashboard">
+ <div class="yui-skin-sam">
+ <div id="left">
+ <div id="query_list_container">
+ Choose query:
+ <select id="query" name="query" onchange="MD.showQuerySection();">
+ <optgroup id="standard_queries" label="Standard">
+ [% FOREACH r = standard_results %]
+ <option value="[% r.name FILTER html %]">[% r.heading FILTER html %]</option>
+ [% END%]
+ </optgroup>
+ <optgroup id="saved_queries" label="Saved">
+ [% FOREACH r = saved_results %]
+ <option value="[% r.name FILTER html %]">[% r.heading FILTER html %]</option>
+ [% END %]
+ </optgroup>
+ </select>
+ <small>
+ (<a href="userprefs.cgi?tab=saved-searches">add or remove saved searches</a>)
+ </small>
+ </div>
+
+ [% FOREACH r = standard_results %]
+ [% PROCESS query_results r = r %]
+ [% END %]
+
+ [% FOREACH r = saved_results %]
+ [% PROCESS query_results r = r %]
+ [% END %]
+ </div>
+
+ <div id="right">
+ <div id="file_bug_container">
+ [% PROCESS "mydashboard/prod-comp-search.html.tmpl" %]
+ </div>
+
+ <div id="requestee_container">
+ <div class="query_heading">
+ Flags Requested of You
+ </div>
+ <span class="flags_found">
+ [% requestee_list.size FILTER html %]&nbsp;flags found
+ </span>
+ <div id="requestee_table_container">
+ <table id="requestee_table" cellspacing="0" cellpadding="3" width="100%">
+ <thead>
+ <tr bgcolor="#dedede">
+ <th>Requester</th>
+ <th>Flag</th>
+ <th>[% terms.Bug %]</th>
+ <th>Created</th>
+ </tr>
+ </thead>
+ <tbody>
+ [% FOREACH request = requestee_list %]
+ <tr class="bz_bugitem [%+ loop.count() % 2 == 0 ? "bz_row_odd" : "bz_row_even" %]">
+ <td>[% request.requester FILTER html %]</td>
+ <td>[% request.type FILTER html %][% request.status FILTER html %]</td>
+ <td>
+ [% IF request.attach_id %]
+ <a href="[% urlbase FILTER none %]attachment.cgi?action=edit&id=[% request.attach_id FILTER uri %]">
+ [% request.attach_id FILTER html %]: [%+ request.attach_summary FILTER html %]</a>
+ [% ELSE %]
+ <a href="[% urlbase FILTER none %]show_bug.cgi?id=[% request.bug_id FILTER uri %]">
+ [% request.bug_id FILTER html %]: [%+ request.bug_summary FILTER html %]</a>
+ [% END %]
+ </td>
+ <td>[% request.created FILTER time('%Y:%m:%d') FILTER html %]</td>
+ </tr>
+ [% END %]
+ </tbody>
+ </table>
+ </div>
+ </div>
+ <script>
+ <!--
+ MD.addStatListener("requestee_table_container", "requestee_table",
+ MD.requestee_column_defs, MD.requestee_fields,
+ { paginator: new YAHOO.widget.Paginator({ rowsPerPage: 25, alwaysVisible: false }) });
+ -->
+ </script>
+
+ <div id="requester_container">
+ <div class="query_heading">
+ Flags You Have Requested
+ </div>
+ <span class="flags_found">
+ [% requester_list.size FILTER html %]&nbsp;flags found
+ </span>
+ <div id="requester_table_container">
+ <table id="requester_table" cellspacing="0" cellpadding="3" width="100%">
+ <thead bgcolor="#dedede">
+ <tr>
+ <th>Requestee</th>
+ <th>Flag</th>
+ <th>[% terms.Bug %]</th>
+ <th>Created</th>
+ </tr>
+ </thead>
+ <tbody>
+ [% FOREACH request = requester_list %]
+ <tr class="bz_bugitem [%+ loop.count() % 2 == 0 ? "bz_row_odd" : "bz_row_even" %]">
+ <td>[% request.requestee FILTER html %]</td>
+ <td>[% request.type FILTER html %][% request.status FILTER html %]</td>
+ <td>
+ [% IF request.attach_id %]
+ <a href="[% urlbase FILTER none %]attachment.cgi?action=edit&id=[% request.attach_id FILTER uri %]">
+ [% request.attach_id FILTER html %]: [%+ request.attach_summary FILTER html %]</a>
+ [% ELSE %]
+ <a href="[% urlbase FILTER none %]show_bug.cgi?id=[% request.bug_id FILTER uri %]">
+ [% request.bug_id FILTER html %]: [%+ request.bug_summary FILTER html %]</a>
+ [% END %]
+ </td>
+ <td>[% request.created FILTER time('%Y:%m:%d') FILTER html %]</td>
+ </tr>
+ [% END %]
+ </tbody>
+ </table>
+ </div>
+ </div>
+ <script>
+ <!--
+ MD.addStatListener("requester_table_container", "requester_table",
+ MD.requester_column_defs, MD.requester_fields,
+ { paginator: new YAHOO.widget.Paginator({ rowsPerPage: 25, alwaysVisible: false }) });
+ -->
+ </script>
+ </div>
+ <div style="clear:both;"></div>
+ </div>
+</div>
+
+[% PROCESS global/footer.html.tmpl %]
+
+[% BLOCK query_results %]
+ <div id="[% r.name FILTER html %]_container" class="bz_default_hidden">
+ [% IF r.description %]
+ <div class="query_description">
+ [% r.description FILTER html %]
+ </div>
+ [% END %]
+ <span class="bugs_found">
+ <a href="[% urlbase FILTER none %]buglist.cgi?[% r.buffer FILTER none %]">
+ [% r.bugs.size FILTER html %]&nbsp;[% terms.bugs %] found</a>
+ </span>
+ <div id="[% r.name FILTER html %]_table_container">
+ <table id="[% r.name FILTER html %]_table" cellspacing="0" cellpadding="3" width="100%">
+ <thead>
+ <tr>
+ <th>ID</th>
+ <th>Updated</th>
+ <th>Status</th>
+ <th>Summary</th>
+ </tr>
+ </thead>
+ <tbody>
+ [% FOREACH bug = r.bugs %]
+ <tr class="bz_bugitem [%+ loop.count() % 2 == 0 ? "bz_row_odd" : "bz_row_even" %]">
+ <td align="center"><a href="show_bug.cgi?id=[% bug.bug_id FILTER uri %]">[% bug.bug_id FILTER html %]</a></td>
+ <td align="center">[% bug.changeddate FILTER time('%Y:%m:%d') FILTER html %]</td>
+ <td align="center">[% bug.bug_status FILTER html %]</td>
+ <td>[% bug.short_desc FILTER html %]</td>
+ </tr>
+ [% END %]
+ </tbody>
+ </table>
+ </div>
+ <script>
+ <!--
+ MD.addStatListener("[% r.name FILTER js %]_table_container", "[% r.name FILTER js %]_table",
+ MD.query_column_defs, MD.query_fields,
+ { paginator: new YAHOO.widget.Paginator({ rowsPerPage: 25, alwaysVisible: false }) });
+ -->
+ </script>
+ </div>
+[% END %]
diff --git a/extensions/MyDashboard/web/js/mydashboard.js b/extensions/MyDashboard/web/js/mydashboard.js
new file mode 100644
index 000000000..25529d8c8
--- /dev/null
+++ b/extensions/MyDashboard/web/js/mydashboard.js
@@ -0,0 +1,159 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0.
+ */
+
+YAHOO.namespace('MyDashboard');
+
+var MD = YAHOO.MyDashboard;
+
+MD.showQuerySection = function () {
+ var query_select = YAHOO.util.Dom.get('query');
+ var selected_value = '';
+ for (var i = 0, l = query_select.options.length; i < l; i++) {
+ if (query_select.options[i].selected) {
+ selected_value = query_select.options[i].value;
+ }
+ }
+ for (var i = 0, l = MD.full_query_list.length; i < l; i++) {
+ var query = MD.full_query_list[i];
+ if (selected_value == MD.full_query_list[i]) {
+ YAHOO.util.Dom.removeClass(query + '_container', 'bz_default_hidden');
+ }
+ else {
+ YAHOO.util.Dom.addClass(query + '_container', 'bz_default_hidden');
+ }
+ }
+}
+
+MD.query_column_defs = [
+ { key:"id", label:"ID", sortable:true, sortOptions:{ sortFunction: MD.sortBugIdLinks } },
+ { key:"updated", label:"Updated", sortable:true },
+ { key:"bug_status", label:"Status", sortable:true },
+ { key:"summary", label:"Summary", sortable:true },
+];
+
+MD.query_fields = [
+ { key:"id" },
+ { key:"updated" },
+ { key:"bug_status" },
+ { key:"summary" }
+];
+
+MD.requestee_column_defs = [
+ { key:"requester", label:"Requester", sortable:true },
+ { key:"flag", label:"Flag", sortable:true },
+ { key:"bug", label:"Bug", sortable:true },
+ { key:"created", label:"Created", sortable:true }
+];
+
+MD.requestee_fields = [
+ { key:"requester" },
+ { key:"flag" },
+ { key:"bug" },
+ { key:"created" }
+];
+
+MD.requester_column_defs = [
+ { key:"requestee", label:"Requestee", sortable:true },
+ { key:"flag", label:"Flag", sortable:true },
+ { key:"bug", label:"Bug", sortable:true },
+ { key:"created", label:"Created", sortable:true }
+];
+
+MD.requester_fields = [
+ { key:"requestee" },
+ { key:"flag" },
+ { key:"bug" },
+ { key:"created" }
+];
+
+MD.addStatListener = function (div_name, table_name, column_defs, fields, options) {
+ YAHOO.util.Event.addListener(window, "load", function() {
+ YAHOO.example.StatsFromMarkup = new function() {
+ this.myDataSource = new YAHOO.util.DataSource(YAHOO.util.Dom.get(table_name));
+ this.myDataSource.responseType = YAHOO.util.DataSource.TYPE_HTMLTABLE;
+ this.myDataSource.responseSchema = { fields:fields };
+ this.myDataTable = new YAHOO.widget.DataTable(div_name, column_defs, this.myDataSource, options);
+ this.myDataTable.subscribe("rowMouseoverEvent", this.myDataTable.onEventHighlightRow);
+ this.myDataTable.subscribe("rowMouseoutEvent", this.myDataTable.onEventUnhighlightRow);
+ };
+ });
+}
+
+// Custom sort handler to sort by bug id inside an anchor tag
+MD.sortBugIdLinks = function (a, b, desc) {
+ // Deal with empty values
+ if (!YAHOO.lang.isValue(a)) {
+ return (!YAHOO.lang.isValue(b)) ? 0 : 1;
+ }
+ else if(!YAHOO.lang.isValue(b)) {
+ return -1;
+ }
+ // Now we need to pull out the ID text and convert to Numbers
+ // First we do 'a'
+ var container = document.createElement("bug_id_link");
+ container.innerHTML = a.getData("id");
+ var anchors = container.getElementsByTagName("a");
+ var text = anchors[0].textContent;
+ if (text === undefined) text = anchors[0].innerText;
+ var new_a = new Number(text);
+ // Then we do 'b'
+ container.innerHTML = b.getData("id");
+ anchors = container.getElementsByTagName("a");
+ text = anchors[0].textContent;
+ if (text == undefined) text = anchors[0].innerText;
+ var new_b = new Number(text);
+
+ if (!desc) {
+ return YAHOO.util.Sort.compare(new_a, new_b);
+ }
+ else {
+ return YAHOO.util.Sort.compare(new_b, new_a);
+ }
+}
+
+// Custom sort handler for bug severities
+MD.sortBugSeverity = function (a, b, desc) {
+ // Deal with empty values
+ if (!YAHOO.lang.isValue(a)) {
+ return (!YAHOO.lang.isValue(b)) ? 0 : 1;
+ }
+ else if(!YAHOO.lang.isValue(b)) {
+ return -1;
+ }
+
+ var new_a = new Number(MD.severities[YAHOO.lang.trim(a.getData('bug_severity'))]);
+ var new_b = new Number(MD.severities[YAHOO.lang.trim(b.getData('bug_severity'))]);
+
+ if (!desc) {
+ return YAHOO.util.Sort.compare(new_a, new_b);
+ }
+ else {
+ return YAHOO.util.Sort.compare(new_b, new_a);
+ }
+}
+
+// Custom sort handler for bug priorities
+MD.sortBugPriority = function (a, b, desc) {
+ // Deal with empty values
+ if (!YAHOO.lang.isValue(a)) {
+ return (!YAHOO.lang.isValue(b)) ? 0 : 1;
+ }
+ else if(!YAHOO.lang.isValue(b)) {
+ return -1;
+ }
+
+ var new_a = new Number(MD.priorities[YAHOO.lang.trim(a.getData('priority'))]);
+ var new_b = new Number(MD.priorities[YAHOO.lang.trim(b.getData('priority'))]);
+
+ if (!desc) {
+ return YAHOO.util.Sort.compare(new_a, new_b);
+ }
+ else {
+ return YAHOO.util.Sort.compare(new_b, new_a);
+ }
+}
diff --git a/extensions/MyDashboard/web/js/prod_comp_search.js b/extensions/MyDashboard/web/js/prod_comp_search.js
new file mode 100644
index 000000000..06b4c601f
--- /dev/null
+++ b/extensions/MyDashboard/web/js/prod_comp_search.js
@@ -0,0 +1,85 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0.
+ */
+
+YAHOO.bugzilla.prodCompSearch = {
+ counter : 0,
+ format : '',
+ cloned_bug_id : '',
+ dataSource : null,
+ autoComplete: null,
+ generateRequest : function (enteredText) {
+ YAHOO.bugzilla.prodCompSearch.counter = YAHOO.bugzilla.prodCompSearch.counter + 1;
+ YAHOO.util.Connect.setDefaultPostHeader('application/json', true);
+ var json_object = {
+ method : "MyDashboard.prod_comp_search",
+ id : YAHOO.bugzilla.prodCompSearch.counter,
+ params : [ {
+ search : decodeURIComponent(enteredText)
+ } ]
+ };
+ YAHOO.util.Dom.removeClass('prod_comp_throbber', 'hidden');
+ return YAHOO.lang.JSON.stringify(json_object);
+ },
+ resultListFormat : function(oResultData, enteredText, sResultMatch) {
+ return YAHOO.lang.escapeHTML(oResultData[0]) + " :: " +
+ YAHOO.lang.escapeHTML(oResultData[1]);
+ },
+ init_ds : function(){
+ this.dataSource = new YAHOO.util.XHRDataSource("jsonrpc.cgi");
+ this.dataSource.connTimeout = 30000;
+ this.dataSource.connMethodPost = true;
+ this.dataSource.connXhrMode = "cancelStaleRequests";
+ this.dataSource.maxCacheEntries = 5;
+ this.dataSource.responseType = YAHOO.util.DataSource.TYPE_JSON;
+ this.dataSource.responseSchema = {
+ resultsList : "result.products",
+ metaFields : { error: "error", jsonRpcId: "id"},
+ fields : [ "product", "component" ]
+ };
+ },
+ init : function(field, container, format, cloned_bug_id) {
+ if (this.dataSource == null)
+ this.init_ds();
+ this.format = format;
+ this.cloned_bug_id = cloned_bug_id;
+ this.autoComplete = new YAHOO.widget.AutoComplete(field, container, this.dataSource);
+ this.autoComplete.generateRequest = this.generateRequest;
+ this.autoComplete.formatResult = this.resultListFormat;
+ this.autoComplete.minQueryLength = 3;
+ this.autoComplete.autoHighlight = false;
+ this.autoComplete.queryDelay = 0.05;
+ this.autoComplete.useIFrame = true;
+ this.autoComplete.maxResultsDisplayed = 25;
+ this.autoComplete.suppressInputUpdate = true;
+ this.autoComplete.doBeforeLoadData = function(sQuery, oResponse, oPayload) {
+ YAHOO.util.Dom.addClass('prod_comp_throbber', 'hidden');
+ return true;
+ };
+ this.autoComplete.textboxFocusEvent.subscribe(function () {
+ var input = YAHOO.util.Dom.get(field);
+ if (input.value && input.value.length > 3) {
+ this.sendQuery(input.value);
+ }
+ });
+ this.autoComplete.itemSelectEvent.subscribe(function (e, args) {
+ var oData = args[2];
+ var url = "enter_bug.cgi?product=" + encodeURIComponent(oData[0]) +
+ "&component=" + encodeURIComponent(oData[1]);
+ var format = YAHOO.bugzilla.prodCompSearch.format;
+ if (format)
+ url += "&format=" + encodeURIComponent(format);
+ var cloned_bug_id = YAHOO.bugzilla.prodCompSearch.cloned_bug_id;
+ if (cloned_bug_id)
+ url += "&cloned_bug_id=" + encodeURIComponent(cloned_bug_id);
+ window.location.href = url;
+ });
+ this.autoComplete.dataReturnEvent.subscribe(function(type, args) {
+ args[0].autoHighlight = args[2].length == 1;
+ });
+ }
+}
diff --git a/extensions/MyDashboard/web/styles/mydashboard.css b/extensions/MyDashboard/web/styles/mydashboard.css
new file mode 100644
index 000000000..98524e4a0
--- /dev/null
+++ b/extensions/MyDashboard/web/styles/mydashboard.css
@@ -0,0 +1,59 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0. */
+
+#mydashboard .yui-skin-sam .yui-dt table {
+ width:100%;
+}
+
+#mydashboard .query_heading {
+ font-size: 18px;
+ font-weight: strong;
+ padding-bottom: 5px;
+ padding-top: 5px;
+ color: rgb(72, 72, 72);
+}
+
+#mydashboard .query_description {
+ font-size: 90%;
+ font-style: italic;
+ padding-bottom: 5px;
+ color: rgb(109, 117, 129);
+}
+
+#mydashboard .bugs_found,
+#mydashboard .flags_found {
+ font-size: 80%;
+}
+
+#mydashboard_container {
+ margin: 0 auto;
+}
+
+#left {
+ float: left;
+ width: 58%;
+}
+
+#right {
+ float: right;
+ width: 40%;
+}
+
+#file_bug_container {
+ text-align: left;
+}
+
+#query_list_container {
+ text-align:center;
+}
+
+#file_bug_container,
+#query_list_container {
+ margin-bottom: 10px;
+ border: 1px solid rgb(116,126,147);
+ padding: 10px;
+}
diff --git a/extensions/MyDashboard/web/styles/prod_comp_search.css b/extensions/MyDashboard/web/styles/prod_comp_search.css
new file mode 100644
index 000000000..24c0a2cf8
--- /dev/null
+++ b/extensions/MyDashboard/web/styles/prod_comp_search.css
@@ -0,0 +1,22 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0. */
+
+#prod_comp_search_main {
+ width: 400px;
+ margin-right: auto;
+ margin-left: auto;
+}
+
+#prod_comp_search_main .hidden {
+ display: none;
+}
+
+#prod_comp_search_main li.yui-ac-highlight a {
+ text-decoration: none;
+ color: #FFFFFF;
+ display: block;
+}
diff --git a/extensions/Needinfo/Config.pm b/extensions/Needinfo/Config.pm
new file mode 100644
index 000000000..86c99ec59
--- /dev/null
+++ b/extensions/Needinfo/Config.pm
@@ -0,0 +1,18 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+package Bugzilla::Extension::Needinfo;
+use strict;
+
+use constant NAME => 'Needinfo';
+
+use constant REQUIRED_MODULES => [
+];
+
+use constant OPTIONAL_MODULES => [
+];
+
+__PACKAGE__->NAME;
diff --git a/extensions/Needinfo/Extension.pm b/extensions/Needinfo/Extension.pm
new file mode 100644
index 000000000..c48593cff
--- /dev/null
+++ b/extensions/Needinfo/Extension.pm
@@ -0,0 +1,174 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+package Bugzilla::Extension::Needinfo;
+
+use strict;
+
+use base qw(Bugzilla::Extension);
+
+use Bugzilla::Bug;
+use Bugzilla::User;
+use Bugzilla::Flag;
+use Bugzilla::FlagType;
+
+our $VERSION = '0.01';
+
+sub install_update_db {
+ my ($self, $args) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ if (@{ Bugzilla::FlagType::match({ name => 'needinfo' }) }) {
+ return;
+ }
+
+ print "Creating needinfo flag ... " .
+ "enable the Needinfo feature by editing the flag's properties.\n";
+
+ # Initially populate the list of exclusions as __Any__:__Any__ to
+ # allow admin to decide which products to enable the flag for.
+ my $flagtype = Bugzilla::FlagType->create({
+ name => 'needinfo',
+ description => "Set this flag when the bug is in need of additional information",
+ target_type => 'bug',
+ cc_list => '',
+ sortkey => 1,
+ is_active => 1,
+ is_requestable => 1,
+ is_requesteeble => 1,
+ is_multiplicable => 0,
+ request_group => '',
+ grant_group => '',
+ inclusions => [],
+ exclusions => ['0:0'],
+ });
+}
+
+# Clear the needinfo? flag if comment is being given by
+# requestee or someone used the override flag.
+sub bug_end_of_update {
+ my ($self, $args) = @_;
+ my $bug = $args->{bug};
+ my $old_bug = $args->{old_bug};
+ my $timestamp = $args->{timestamp};
+ my $changes = $args->{changes};
+
+ my $user = Bugzilla->user;
+ my $cgi = Bugzilla->cgi;
+ my $params = Bugzilla->input_params;
+
+ # Set needinfo_done param to true so as to not loop back here
+ return if $params->{needinfo_done};
+ $params->{needinfo_done} = 1;
+ Bugzilla->input_params($params);
+
+ # do a match if applicable
+ Bugzilla::User::match_field({
+ 'needinfo_from' => { 'type' => 'single' }
+ });
+
+ my $needinfo = delete $params->{needinfo};
+ my $needinfo_from = delete $params->{needinfo_from};
+ my $needinfo_role = delete $params->{needinfo_role};
+ my $override = delete $params->{needinfo_override};
+ my $is_private = $params->{'comment_is_private'};
+
+ # Set the needinfo flag if user is requesting more information
+ my @new_flags;
+ my $needinfo_requestee;
+
+ if ($user->in_group('canconfirm') && $needinfo) {
+ foreach my $type (@{ $bug->flag_types }) {
+ next if $type->name ne 'needinfo';
+ next if @{ $type->{flags} };
+
+ my $needinfo_flag = { type_id => $type->id, status => '?' };
+
+ # Use assigned_to as requestee
+ if ($needinfo_role eq 'assigned_to') {
+ $needinfo_flag->{requestee} = $bug->assigned_to->login;
+ }
+ # Use reporter as requestee
+ elsif ( $needinfo_role eq 'reporter') {
+ $needinfo_flag->{requestee} = $bug->reporter->login;
+ }
+ # Use qa_contact as requestee
+ elsif ($needinfo_role eq 'qa_contact') {
+ $needinfo_flag->{requestee} = $bug->qa_contact->login;
+ }
+ # Use user specified requestee
+ elsif ($needinfo_role eq 'other' && $needinfo_from) {
+ Bugzilla::User->check($needinfo_from);
+ $needinfo_flag->{requestee} = $needinfo_from;
+ }
+
+ if ($needinfo) {
+ push(@new_flags, $needinfo_flag);
+ last;
+ }
+ }
+ }
+
+ # Clear the flag if bug is being closed or if additional
+ # information was given as requested
+ my @flags;
+ foreach my $flag (@{ $bug->flags }) {
+ next if $flag->type->name ne 'needinfo';
+ my $clear_needinfo = 0;
+
+ # Clear if somehow the flag has been set to +/-
+ $clear_needinfo = 1 if $flag->status ne '?';
+
+ # Clear if current user has selected override
+ $clear_needinfo = 1 if $override;
+
+ # Clear if bug is being closed
+ if (($bug->bug_status ne $old_bug->bug_status)
+ && !$old_bug->status->is_open)
+ {
+ $clear_needinfo = 1;
+ }
+
+ # Clear if comment provided by the proper requestee
+ if ($bug->{added_comments}
+ && (!$flag->requestee || $flag->requestee->login eq Bugzilla->user->login)
+ && (!$is_private || $flag->setter->is_insider))
+ {
+ $clear_needinfo = 1;
+ }
+
+ if ($clear_needinfo) {
+ push(@flags, { id => $flag->id, status => 'X' });
+ }
+ }
+
+ if (@flags || @new_flags) {
+ $bug->set_flags(\@flags, \@new_flags);
+ my ($removed, $added) = Bugzilla::Flag->update_flags($bug, $old_bug, $timestamp);
+ if ($removed || $added) {
+ my $field = 'flagtypes.name';
+ $removed = defined $removed ? $removed : '';
+ $added = defined $added ? $added : '';
+ LogActivityEntry($bug->id, $field, $removed, $added, $user->id, $timestamp);
+
+ # Do not overwrite other flag changes
+ if ($changes->{$field}) {
+ $removed = defined $changes->{$field}->[0]
+ ? $changes->{$field}->[0] . ", $removed"
+ : $removed;
+ $added = defined $changes->{$field}->[1]
+ ? $changes->{$field}->[1] . ", $added"
+ : $added;
+ }
+ $changes->{$field} = [$removed, $added];
+
+ # Adding a flag may result in CC'ing a user, call update to process
+ $bug->update() if $added;
+ }
+ }
+}
+
+__PACKAGE__->NAME;
diff --git a/extensions/Needinfo/template/en/default/bug/needinfo.html.tmpl b/extensions/Needinfo/template/en/default/bug/needinfo.html.tmpl
new file mode 100644
index 000000000..d55f28157
--- /dev/null
+++ b/extensions/Needinfo/template/en/default/bug/needinfo.html.tmpl
@@ -0,0 +1,99 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% show_needinfo = 0 %]
+[% needinfo_requested = 0 %]
+[% needinfo_from = "" %]
+[% needinfo_from_any = 0 %]
+[% can_create_needinfo = 0 %]
+
+[% FOREACH type = bug.flag_types %]
+ [% IF type.name == 'needinfo' %]
+ [% show_needinfo = 1 %]
+ [% FOREACH flag = type.flags %]
+ [% IF flag.status == '?' %]
+ [% needinfo_requested = 1 %]
+ [% IF flag.requestee.login %]
+ [% needinfo_from = flag.requestee.login %]
+ [% ELSE %]
+ [% needinfo_from_any = 1 %]
+ [% END %]
+ [% END %]
+ [% END %]
+ [% END %]
+[% END %]
+
+[% IF user.in_group('canconfirm') && !needinfo_requested %]
+ [% IF bug.status.is_open %]
+ [% can_create_needinfo = 1 %]
+ [% ELSE %]
+ [% FOREACH field = Bugzilla.active_custom_fields(product=>bug.product_obj, component=>bug.component_obj, type=>2) %]
+ [% IF field.description.match('^status-firefox') && bug.${field.name} == 'affected' %]
+ [% can_create_needinfo = 1 %]
+ [% LAST %]
+ [% END %]
+ [% END %]
+ [% END %]
+[% END %]
+
+[% IF show_needinfo %]
+ [%# Displays NEEDINFO tag in bug header %]
+ [% IF needinfo_requested %]
+ <script>
+ var summary_container = document.getElementById('static_bug_status');
+ summary_container.appendChild(document.createTextNode('[NEEDINFO]'));
+ </script>
+ [% END %]
+
+ <div id="needinfo_container">
+ [% IF needinfo_requested %]
+ [% IF needinfo_from == user.login || needinfo_from_any %]
+ Adding comment will automatically clear needinfo request.
+ [% ELSE %]
+ <input type="checkbox" id="needinfo_override" name="needinfo_override" value="1">
+ <label for="needinfo_override">
+ I am providing the requested information for this [% terms.bug %] (this will clear needinfo request).
+ </label>
+ [% END %]
+ [% END %]
+
+ [% IF can_create_needinfo %]
+ <script>
+ function needinfoRole (select) {
+ YAHOO.util.Dom.get('needinfo').checked = true;
+ if (select.value == 'other') {
+ YAHOO.util.Dom.removeClass('needinfo_from_container', 'bz_default_hidden');
+ YAHOO.util.Dom.get('needinfo_from').focus();
+ }
+ else {
+ YAHOO.util.Dom.addClass('needinfo_from_container', 'bz_default_hidden');
+ }
+ }
+ </script>
+ <input type="checkbox" name="needinfo" value="1" id="needinfo">
+ <label for="needinfo">Need additional information from</label>
+ <select name="needinfo_role" id="needinfo_role" onchange="needinfoRole(this);">
+ <option value="">anyone</option>
+ <option value="reporter">reporter</option>
+ <option value="assigned_to">assignee</option>
+ [% IF Param('useqacontact') && bug.qa_contact.login != "" %]
+ <option value="qa_contact">qa contact</option>
+ [% END %]
+ <option value="other">other</option>
+ </select>
+ <span id="needinfo_from_container" class="bz_default_hidden">
+ [%+ INCLUDE global/userselect.html.tmpl
+ id => "needinfo_from"
+ name => "needinfo_from"
+ size => 30
+ value => ""
+ %]
+ </span>
+ [% END %]
+ </div>
+[% END %]
diff --git a/extensions/Needinfo/template/en/default/hook/attachment/create-form_before_submit.html.tmpl b/extensions/Needinfo/template/en/default/hook/attachment/create-form_before_submit.html.tmpl
new file mode 100644
index 000000000..ea9c17bd5
--- /dev/null
+++ b/extensions/Needinfo/template/en/default/hook/attachment/create-form_before_submit.html.tmpl
@@ -0,0 +1,17 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+<tr>
+ <td>&nbsp;</td>
+ <td>
+ [% PROCESS bug/needinfo.html.tmpl
+ bug => bug
+ is_attachment => 1
+ %]
+ </td>
+</tr>
diff --git a/extensions/Needinfo/template/en/default/hook/attachment/edit-after_comment_textarea.html.tmpl b/extensions/Needinfo/template/en/default/hook/attachment/edit-after_comment_textarea.html.tmpl
new file mode 100644
index 000000000..8f03fc752
--- /dev/null
+++ b/extensions/Needinfo/template/en/default/hook/attachment/edit-after_comment_textarea.html.tmpl
@@ -0,0 +1,12 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% PROCESS bug/needinfo.html.tmpl
+ bug => attachment.bug
+ is_attachment => 1
+%]
diff --git a/extensions/Needinfo/template/en/default/hook/bug/edit-after_comment_commit_button.html.tmpl b/extensions/Needinfo/template/en/default/hook/bug/edit-after_comment_commit_button.html.tmpl
new file mode 100644
index 000000000..90f0cc584
--- /dev/null
+++ b/extensions/Needinfo/template/en/default/hook/bug/edit-after_comment_commit_button.html.tmpl
@@ -0,0 +1,11 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% PROCESS bug/needinfo.html.tmpl
+ bug = bug
+%]
diff --git a/extensions/OrangeFactor/Config.pm b/extensions/OrangeFactor/Config.pm
new file mode 100644
index 000000000..9fb0d74ef
--- /dev/null
+++ b/extensions/OrangeFactor/Config.pm
@@ -0,0 +1,13 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::OrangeFactor;
+use strict;
+
+use constant NAME => 'OrangeFactor';
+
+__PACKAGE__->NAME;
diff --git a/extensions/OrangeFactor/Extension.pm b/extensions/OrangeFactor/Extension.pm
new file mode 100644
index 000000000..754663157
--- /dev/null
+++ b/extensions/OrangeFactor/Extension.pm
@@ -0,0 +1,44 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::OrangeFactor;
+use strict;
+use base qw(Bugzilla::Extension);
+
+use Bugzilla::User::Setting;
+use Bugzilla::Constants;
+use Bugzilla::Attachment;
+
+our $VERSION = '1.0';
+
+sub template_before_process {
+ my ($self, $args) = @_;
+ my $file = $args->{'file'};
+ my $vars = $args->{'vars'};
+
+ my $user = Bugzilla->user;
+
+ return unless $user && $user->id && $user->settings;
+ return unless $user->settings->{'orange_factor'}->{'value'} eq 'on';
+
+ # in the header we just need to set the var, to
+ # ensure the css and javascript get included
+ if ($file eq 'bug/show-header.html.tmpl'
+ || $file eq 'bug/edit.html.tmpl') {
+ my $bug = exists $vars->{'bugs'} ? $vars->{'bugs'}[0] : $vars->{'bug'};
+ if ($bug && $bug->status_whiteboard =~ /\[orange\]/) {
+ $vars->{'orange_factor'} = 1;
+ }
+ }
+}
+
+sub install_before_final_checks {
+ my ($self, $args) = @_;
+ add_setting('orange_factor', ['on', 'off'], 'off');
+}
+
+__PACKAGE__->NAME;
diff --git a/extensions/OrangeFactor/template/en/default/hook/bug/edit-after_custom_fields.html.tmpl b/extensions/OrangeFactor/template/en/default/hook/bug/edit-after_custom_fields.html.tmpl
new file mode 100644
index 000000000..a41188a63
--- /dev/null
+++ b/extensions/OrangeFactor/template/en/default/hook/bug/edit-after_custom_fields.html.tmpl
@@ -0,0 +1,26 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% USE Bugzilla %]
+[% cgi = Bugzilla.cgi %]
+
+[% IF orange_factor %]
+ <tr>
+ <th class="field_label" valign="top">
+ Orange Factor:
+ </th>
+ <td>
+ [% IF cgi.user_agent.match('(?i)gecko') %]
+ <canvas id="orange-graph" class="bz_default_hidden"></canvas>
+ <span id="orange-count"></span>
+ [% END %]
+ (<a href="https://brasstacks.mozilla.com/orangefactor/?display=Bug&bugid=[% bug.bug_id FILTER uri %]"
+ title="Click to load Orange Factor page for this [% terms.bug %]">link</a>)
+ </td>
+ </tr>
+[% END %]
diff --git a/extensions/OrangeFactor/template/en/default/hook/bug/show-header-end.html.tmpl b/extensions/OrangeFactor/template/en/default/hook/bug/show-header-end.html.tmpl
new file mode 100644
index 000000000..b41431dcf
--- /dev/null
+++ b/extensions/OrangeFactor/template/en/default/hook/bug/show-header-end.html.tmpl
@@ -0,0 +1,17 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% USE Bugzilla %]
+[% cgi = Bugzilla.cgi %]
+
+[% IF orange_factor && cgi.user_agent.match('(?i)gecko') %]
+ [% style_urls.push('extensions/OrangeFactor/web/style/orangefactor.css') %]
+ [% javascript_urls.push('extensions/OrangeFactor/web/js/sparklines.min.js') %]
+ [% javascript_urls.push('extensions/OrangeFactor/web/js/orange_factor.js') %]
+[% END %]
+
diff --git a/extensions/OrangeFactor/template/en/default/hook/global/setting-descs-settings.none.tmpl b/extensions/OrangeFactor/template/en/default/hook/global/setting-descs-settings.none.tmpl
new file mode 100644
index 000000000..21a525deb
--- /dev/null
+++ b/extensions/OrangeFactor/template/en/default/hook/global/setting-descs-settings.none.tmpl
@@ -0,0 +1,11 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[%
+ setting_descs.orange_factor = "When viewing a $terms.bug, show its corresponding Orange Factor page"
+%]
diff --git a/extensions/OrangeFactor/web/js/AUTHORS.processing.js b/extensions/OrangeFactor/web/js/AUTHORS.processing.js
new file mode 100644
index 000000000..e1244b717
--- /dev/null
+++ b/extensions/OrangeFactor/web/js/AUTHORS.processing.js
@@ -0,0 +1,35 @@
+John Resig
+Alistair MacDonald
+David Humphrey
+Corban Brook
+Anna Sobiepanek
+Andor Salga
+Daniel Hodgin
+Scott Downe
+Yuri Delendik
+Mike Kamermans
+Chris Lonnen
+Mickael Medel
+Matthew Lam
+Jon Buckley
+Dominic Baranski
+Elijah Grey
+Thomas Saunders
+Abel Allison
+Andrew Grimo
+Donghui Liu
+Edward Sin
+Alex Londono
+Robert O'Rourke
+Thanh Dao
+Zhibin Huang
+John Turner
+Tom Brown
+Minoo Ziaei
+Ricard Marxer
+Matt Postill
+Tiago Moreira
+Jonathan Brodsky
+Roger Sodre
+James Boelen
+Michal Ejdys`
diff --git a/extensions/OrangeFactor/web/js/LICENSE.processing.js b/extensions/OrangeFactor/web/js/LICENSE.processing.js
new file mode 100644
index 000000000..404e5d5eb
--- /dev/null
+++ b/extensions/OrangeFactor/web/js/LICENSE.processing.js
@@ -0,0 +1,22 @@
+Copyright (C) 2008 John Resig
+Copyright (C) 2009-2011; see the AUTHORS file for authors and
+copyright holders.
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/extensions/OrangeFactor/web/js/LICENSE.sparklines.js b/extensions/OrangeFactor/web/js/LICENSE.sparklines.js
new file mode 100644
index 000000000..73aaca832
--- /dev/null
+++ b/extensions/OrangeFactor/web/js/LICENSE.sparklines.js
@@ -0,0 +1,20 @@
+Copyright (C) 2008 Will Larson
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/extensions/OrangeFactor/web/js/orange_factor.js b/extensions/OrangeFactor/web/js/orange_factor.js
new file mode 100644
index 000000000..da993580d
--- /dev/null
+++ b/extensions/OrangeFactor/web/js/orange_factor.js
@@ -0,0 +1,91 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0.
+ */
+
+YAHOO.namespace('OrangeFactor');
+
+var OrangeFactor = YAHOO.OrangeFactor;
+
+OrangeFactor.dayMs = 24 * 60 * 60 * 1000,
+OrangeFactor.limit = 7;
+
+OrangeFactor.getOrangeCount = function (data) {
+ data = data.oranges;
+ var total = 0,
+ days = [],
+ date = OrangeFactor.getCurrentDateMs() - OrangeFactor.limit * OrangeFactor.dayMs;
+ for(var i = 0; i < OrangeFactor.limit; i++) {
+ var iso = OrangeFactor.dateString(new Date(date));
+ var count = data[iso] ? data[iso].orangecount : 0;
+ days.push(count);
+ total += count;
+ date += OrangeFactor.dayMs;
+ }
+ OrangeFactor.displayGraph(days);
+ OrangeFactor.displayCount(total);
+}
+
+OrangeFactor.displayGraph = function (dayCounts) {
+ var max = dayCounts.reduce(function(max, count) {
+ return count > max ? count : max;
+ });
+ var graphContainer = YAHOO.util.Dom.get('orange-graph');
+ Dom.removeClass(graphContainer, 'bz_default_hidden');
+ YAHOO.util.Dom.setAttribute(graphContainer, 'title',
+ 'failures over the past week, max in a day: ' + max);
+ var opts = {
+ "percentage_lines":[0.25, 0.5, 0.75],
+ "fill_between_percentage_lines": false,
+ "left_padding": 0,
+ "right_padding": 0,
+ "top_padding": 0,
+ "bottom_padding": 0,
+ "background": "#D0D0D0",
+ "stroke": "#000000",
+ "percentage_fill_color": "#CCCCFF",
+ "scale_from_zero": true,
+ };
+ new Sparkline('orange-graph', dayCounts, opts).draw();
+}
+
+OrangeFactor.displayCount = function (count) {
+ var countContainer = YAHOO.util.Dom.get('orange-count');
+ countContainer.innerHTML = encodeURIComponent(count) +
+ ' failures on trunk in the past week';
+}
+
+OrangeFactor.dateString = function (date) {
+ function norm(part) {
+ return JSON.stringify(part).length == 2 ? part : '0' + part;
+ }
+ return date.getFullYear()
+ + "-" + norm(date.getMonth() + 1)
+ + "-" + norm(date.getDate());
+}
+
+OrangeFactor.getCurrentDateMs = function () {
+ var d = new Date;
+ return d.getTime();
+}
+
+OrangeFactor.orangify = function () {
+ var bugId = document.forms['changeform'].id.value;
+ var url = "https://brasstacks.mozilla.com/orangefactor/api/count?" +
+ "bugid=" + encodeURIComponent(bugId) +
+ "&tree=trunk" +
+ "&callback=OrangeFactor.getOrangeCount";
+ var script = document.createElement('script');
+ Dom.setAttribute(script, 'src', url);
+ Dom.setAttribute(script, 'type', 'text/javascript');
+ var head = document.getElementsByTagName('head')[0];
+ head.appendChild(script);
+ var countContainer = YAHOO.util.Dom.get('orange-count');
+ Dom.removeClass(countContainer, 'bz_default_hidden');
+ countContainer.innerHTML = 'Loading...';a
+}
+
+YAHOO.util.Event.onDOMReady(OrangeFactor.orangify);
diff --git a/extensions/OrangeFactor/web/js/sparklines.min.js b/extensions/OrangeFactor/web/js/sparklines.min.js
new file mode 100644
index 000000000..f1043c55e
--- /dev/null
+++ b/extensions/OrangeFactor/web/js/sparklines.min.js
@@ -0,0 +1,133 @@
+/* Sparklines.js - Will Larson (http://lethain.com)
+ * This code is distributed under the MIT license.
+ * See LICENSE.sparklines.js
+ * More information: https://github.com/lethain/sparklines.js
+ *
+ * Processing.js - John Resig (http://ejohn.org/)
+ * See LICENSE.processing.js and AUTHORS.processing.js
+ * More information: http://processingjs.org/
+ */
+(function(){this.Processing=function Processing(aElement,aCode){if(typeof aElement=="string")
+aElement=document.getElementById(aElement);var p=buildProcessing(aElement);if(aCode)
+p.init(aCode);return p;};function log(){try{console.log.apply(console,arguments);}catch(e){try{opera.postError.apply(opera,arguments);}catch(e){}}}
+var parse=Processing.parse=function parse(aCode,p){aCode=aCode.replace(/\/\/ .*\n/g,"\n");aCode=aCode.replace(/([^\s])%([^\s])/g,"$1 % $2");aCode=aCode.replace(/(?:static )?(\w+ )(\w+)\s*(\([^\)]*\)\s*{)/g,function(all,type,name,args){if(name=="if"||name=="for"||name=="while"){return all;}else{return"Processing."+name+" = function "+name+args;}});aCode=aCode.replace(/\.length\(\)/g,".length");aCode=aCode.replace(/([\(,]\s*)(\w+)((?:\[\])+| )\s*(\w+\s*[\),])/g,"$1$4");aCode=aCode.replace(/([\(,]\s*)(\w+)((?:\[\])+| )\s*(\w+\s*[\),])/g,"$1$4");aCode=aCode.replace(/new (\w+)((?:\[([^\]]*)\])+)/g,function(all,name,args){return"new ArrayList("+args.slice(1,-1).split("][").join(", ")+")";});aCode=aCode.replace(/(?:static )?\w+\[\]\s*(\w+)\[?\]?\s*=\s*{.*?};/g,function(all){return all.replace(/{/g,"[").replace(/}/g,"]");});var intFloat=/(\n\s*(?:int|float)(?:\[\])?(?:\s*|[^\(]*?,\s*))([a-z]\w*)(;|,)/i;while(intFloat.test(aCode)){aCode=aCode.replace(new RegExp(intFloat),function(all,type,name,sep){return type+" "+name+" = 0"+sep;});}
+aCode=aCode.replace(/(?:static )?(\w+)((?:\[\])+| ) *(\w+)\[?\]?(\s*[=,;])/g,function(all,type,arr,name,sep){if(type=="return")
+return all;else
+return"var "+name+sep;});aCode=aCode.replace(/=\s*{((.|\s)*?)};/g,function(all,data){return"= ["+data.replace(/{/g,"[").replace(/}/g,"]")+"]";});aCode=aCode.replace(/static\s*{((.|\n)*?)}/g,function(all,init){return init;});aCode=aCode.replace(/super\(/g,"superMethod(");var classes=["int","float","boolean","string"];function ClassReplace(all,name,extend,vars,last){classes.push(name);var static="";vars=vars.replace(/final\s+var\s+(\w+\s*=\s*.*?;)/g,function(all,set){static+=" "+name+"."+set;return"";});return"function "+name+"() {with(this){\n "+
+(extend?"var __self=this;function superMethod(){extendClass(__self,arguments,"+extend+");}\n":"")+
+vars.replace(/,\s?/g,";\n this.").replace(/\b(var |final |public )+\s*/g,"this.").replace(/this.(\w+);/g,"this.$1 = null;")+
+(extend?"extendClass(this, "+extend+");\n":"")+"<CLASS "+name+" "+static+">"+(typeof last=="string"?last:name+"(");}
+var matchClasses=/(?:public |abstract |static )*class (\w+)\s*(?:extends\s*(\w+)\s*)?{\s*((?:.|\n)*?)\b\1\s*\(/g;var matchNoCon=/(?:public |abstract |static )*class (\w+)\s*(?:extends\s*(\w+)\s*)?{\s*((?:.|\n)*?)(Processing)/g;aCode=aCode.replace(matchClasses,ClassReplace);aCode=aCode.replace(matchNoCon,ClassReplace);var matchClass=/<CLASS (\w+) (.*?)>/,m;while((m=aCode.match(matchClass))){var left=RegExp.leftContext,allRest=RegExp.rightContext,rest=nextBrace(allRest),className=m[1],staticVars=m[2]||"";allRest=allRest.slice(rest.length+1);rest=rest.replace(new RegExp("\\b"+className+"\\(([^\\)]*?)\\)\\s*{","g"),function(all,args){args=args.split(/,\s*?/);if(args[0].match(/^\s*$/))
+args.shift();var fn="if ( arguments.length == "+args.length+" ) {\n";for(var i=0;i<args.length;i++){fn+=" var "+args[i]+" = arguments["+i+"];\n";}
+return fn;});rest=rest.replace(/(?:public )?Processing.\w+ = function (\w+)\((.*?)\)/g,function(all,name,args){return"ADDMETHOD(this, '"+name+"', function("+args+")";});var matchMethod=/ADDMETHOD([\s\S]*?{)/,mc;var methods="";while((mc=rest.match(matchMethod))){var prev=RegExp.leftContext,allNext=RegExp.rightContext,next=nextBrace(allNext);methods+="addMethod"+mc[1]+next+"});"
+rest=prev+allNext.slice(next.length+1);}
+rest=methods+rest;aCode=left+rest+"\n}}"+staticVars+allRest;}
+aCode=aCode.replace(/Processing.\w+ = function addMethod/g,"addMethod");function nextBrace(right){var rest=right;var position=0;var leftCount=1,rightCount=0;while(leftCount!=rightCount){var nextLeft=rest.indexOf("{");var nextRight=rest.indexOf("}");if(nextLeft<nextRight&&nextLeft!=-1){leftCount++;rest=rest.slice(nextLeft+1);position+=nextLeft+1;}else{rightCount++;rest=rest.slice(nextRight+1);position+=nextRight+1;}}
+return right.slice(0,position-1);}
+aCode=aCode.replace(/\(int\)/g,"0|");aCode=aCode.replace(new RegExp("\\(("+classes.join("|")+")(\\[\\])?\\)","g"),"");aCode=aCode.replace(/(\d+)f/g,"$1");aCode=aCode.replace(/('[a-zA-Z0-9]')/g,"$1.charCodeAt(0)");aCode=aCode.replace(/#([a-f0-9]{6})/ig,function(m,hex){var num=toNumbers(hex);return"color("+num[0]+","+num[1]+","+num[2]+")";});function toNumbers(str){var ret=[];str.replace(/(..)/g,function(str){ret.push(parseInt(str,16));});return ret;}
+return aCode;};function buildProcessing(curElement){var p={};p.PI=Math.PI;p.TWO_PI=2*p.PI;p.HALF_PI=p.PI/2;p.P3D=3;p.CORNER=0;p.RADIUS=1;p.CENTER_RADIUS=1;p.CENTER=2;p.POLYGON=2;p.QUADS=5;p.TRIANGLES=6;p.POINTS=7;p.LINES=8;p.TRIANGLE_STRIP=9;p.TRIANGLE_FAN=4;p.QUAD_STRIP=3;p.CORNERS=10;p.CLOSE=true;p.RGB=1;p.HSB=2;p.LEFT=1;p.CENTER=2;p.RIGHT=3;var curContext=curElement.getContext("2d");var doFill=true;var doStroke=true;var loopStarted=false;var hasBackground=false;var doLoop=true;var looping=0;var curRectMode=p.CORNER;var curEllipseMode=p.CENTER;var inSetup=false;var inDraw=false;var curBackground="rgba(204,204,204,1)";var curFrameRate=1000;var curShape=p.POLYGON;var curShapeCount=0;var curvePoints=[];var curTightness=0;var opacityRange=255;var redRange=255;var greenRange=255;var blueRange=255;var pathOpen=false;var mousePressed=false;var keyPressed=false;var firstX,firstY,secondX,secondY,prevX,prevY;var curColorMode=p.RGB;var curTint=-1;var curTextSize=12;var curTextFont="Arial";var getLoaded=false;var start=(new Date).getTime();p.pmouseX=0;p.pmouseY=0;p.mouseX=0;p.mouseY=0;p.mouseButton=0;p.mouseDragged=undefined;p.mouseMoved=undefined;p.mousePressed=undefined;p.mouseReleased=undefined;p.keyPressed=undefined;p.keyReleased=undefined;p.draw=undefined;p.setup=undefined;p.width=curElement.width-0;p.height=curElement.height-0;p.frameCount=0;p.color=function color(aValue1,aValue2,aValue3,aValue4){var aColor="";if(arguments.length==3){aColor=p.color(aValue1,aValue2,aValue3,opacityRange);}else if(arguments.length==4){var a=aValue4/opacityRange;a=isNaN(a)?1:a;if(curColorMode==p.HSB){var rgb=HSBtoRGB(aValue1,aValue2,aValue3);var r=rgb[0],g=rgb[1],b=rgb[2];}else{var r=getColor(aValue1,redRange);var g=getColor(aValue2,greenRange);var b=getColor(aValue3,blueRange);}
+aColor="rgba("+r+","+g+","+b+","+a+")";}else if(typeof aValue1=="string"){aColor=aValue1;if(arguments.length==2){var c=aColor.split(",");c[3]=(aValue2/opacityRange)+")";aColor=c.join(",");}}else if(arguments.length==2){aColor=p.color(aValue1,aValue1,aValue1,aValue2);}else if(typeof aValue1=="number"){aColor=p.color(aValue1,aValue1,aValue1,opacityRange);}else{aColor=p.color(redRange,greenRange,blueRange,opacityRange);}
+function HSBtoRGB(h,s,b){h=(h/redRange)*100;s=(s/greenRange)*100;b=(b/blueRange)*100;if(s==0){return[b,b,b];}else{var hue=h%360;var f=hue%60;var br=Math.round(b/100*255);var p=Math.round((b*(100-s))/10000*255);var q=Math.round((b*(6000-s*f))/600000*255);var t=Math.round((b*(6000-s*(60-f)))/600000*255);switch(Math.floor(hue/60)){case 0:return[br,t,p];case 1:return[q,br,p];case 2:return[p,br,t];case 3:return[p,q,br];case 4:return[t,p,br];case 5:return[br,p,q];}}}
+function getColor(aValue,range){return Math.round(255*(aValue/range));}
+return aColor;}
+p.nf=function(num,pad){var str=""+num;while(pad-str.length)
+str="0"+str;return str;};p.AniSprite=function(prefix,frames){this.images=[];this.pos=0;for(var i=0;i<frames;i++){this.images.push(prefix+p.nf(i,(""+frames).length)+".gif");}
+this.display=function(x,y){p.image(this.images[this.pos],x,y);if(++this.pos>=frames)
+this.pos=0;};this.getWidth=function(){return getImage(this.images[0]).width;};this.getHeight=function(){return getImage(this.images[0]).height;};};function buildImageObject(obj){var pixels=obj.data;var data=p.createImage(obj.width,obj.height);if(data.__defineGetter__&&data.__lookupGetter__&&!data.__lookupGetter__("pixels")){var pixelsDone;data.__defineGetter__("pixels",function(){if(pixelsDone)
+return pixelsDone;pixelsDone=[];for(var i=0;i<pixels.length;i+=4){pixelsDone.push(p.color(pixels[i],pixels[i+1],pixels[i+2],pixels[i+3]));}
+return pixelsDone;});}else{data.pixels=[];for(var i=0;i<pixels.length;i+=4){data.pixels.push(p.color(pixels[i],pixels[i+1],pixels[i+2],pixels[i+3]));}}
+return data;}
+p.createImage=function createImage(w,h,mode){var data={};data.width=w;data.height=h;data.data=[];if(curContext.createImageData){data=curContext.createImageData(w,h);}
+data.pixels=new Array(w*h);data.get=function(x,y){return this.pixels[w*y+x];};data._mask=null;data.mask=function(img){this._mask=img;};data.loadPixels=function(){};data.updatePixels=function(){};return data;};p.createGraphics=function createGraphics(w,h){var canvas=document.createElement("canvas");var ret=buildProcessing(canvas);ret.size(w,h);ret.canvas=canvas;return ret;};p.beginDraw=function beginDraw(){};p.endDraw=function endDraw(){};p.tint=function tint(rgb,a){curTint=a;};function getImage(img){if(typeof img=="string"){return document.getElementById(img);}
+if(img.img||img.canvas){return img.img||img.canvas;}
+for(var i=0,l=img.pixels.length;i<l;i++){var pos=i*4;var c=(img.pixels[i]||"rgba(0,0,0,1)").slice(5,-1).split(",");img.data[pos]=parseInt(c[0]);img.data[pos+1]=parseInt(c[1]);img.data[pos+2]=parseInt(c[2]);img.data[pos+3]=parseFloat(c[3])*100;}
+var canvas=document.createElement("canvas")
+canvas.width=img.width;canvas.height=img.height;var context=canvas.getContext("2d");context.putImageData(img,0,0);img.canvas=canvas;return canvas;}
+p.image=function image(img,x,y,w,h){x=x||0;y=y||0;var obj=getImage(img);if(curTint>=0){var oldAlpha=curContext.globalAlpha;curContext.globalAlpha=curTint/opacityRange;}
+if(arguments.length==3){curContext.drawImage(obj,x,y);}else{curContext.drawImage(obj,x,y,w,h);}
+if(curTint>=0){curContext.globalAlpha=oldAlpha;}
+if(img._mask){var oldComposite=curContext.globalCompositeOperation;curContext.globalCompositeOperation="darker";p.image(img._mask,x,y);curContext.globalCompositeOperation=oldComposite;}};p.exit=function exit(){clearInterval(looping);};p.save=function save(file){};p.loadImage=function loadImage(file){var img=document.getElementById(file);if(!img)
+return;var h=img.height,w=img.width;var canvas=document.createElement("canvas");canvas.width=w;canvas.height=h;var context=canvas.getContext("2d");context.drawImage(img,0,0);var data=buildImageObject(context.getImageData(0,0,w,h));data.img=img;return data;};p.loadFont=function loadFont(name){return{name:name,width:function(str){if(curContext.mozMeasureText)
+return curContext.mozMeasureText(typeof str=="number"?String.fromCharCode(str):str)/curTextSize;else
+return 0;}};};p.textFont=function textFont(name,size){curTextFont=name;p.textSize(size);};p.textSize=function textSize(size){if(size){curTextSize=size;}};p.textAlign=function textAlign(){};p.text=function text(str,x,y){if(str&&curContext.mozDrawText){curContext.save();curContext.mozTextStyle=curTextSize+"px "+curTextFont.name;curContext.translate(x,y);curContext.mozDrawText(typeof str=="number"?String.fromCharCode(str):str);curContext.restore();}};p.char=function char(key){return key;};p.println=function println(){};p.map=function map(value,istart,istop,ostart,ostop){return ostart+(ostop-ostart)*((value-istart)/(istop-istart));};String.prototype.replaceAll=function(re,replace){return this.replace(new RegExp(re,"g"),replace);};p.Point=function Point(x,y){this.x=x;this.y=y;this.copy=function(){return new Point(x,y);}};p.Random=function(){var haveNextNextGaussian=false;var nextNextGaussian;this.nextGaussian=function(){if(haveNextNextGaussian){haveNextNextGaussian=false;return nextNextGaussian;}else{var v1,v2,s;do{v1=2*p.random(1)-1;v2=2*p.random(1)-1;s=v1*v1+v2*v2;}while(s>=1||s==0);var multiplier=Math.sqrt(-2*Math.log(s)/s);nextNextGaussian=v2*multiplier;haveNextNextGaussian=true;return v1*multiplier;}};};p.ArrayList=function ArrayList(size,size2,size3){var array=new Array(0|size);if(size2){for(var i=0;i<size;i++){array[i]=[];for(var j=0;j<size2;j++){var a=array[i][j]=size3?new Array(size3):0;for(var k=0;k<size3;k++){a[k]=0;}}}}else{for(var i=0;i<size;i++){array[i]=0;}}
+array.size=function(){return this.length;};array.get=function(i){return this[i];};array.remove=function(i){return this.splice(i,1);};array.add=function(item){return this.push(item);};array.clone=function(){var a=new ArrayList(size);for(var i=0;i<size;i++){a[i]=this[i];}
+return a;};array.isEmpty=function(){return!this.length;};array.clear=function(){this.length=0;};return array;};p.colorMode=function colorMode(mode,range1,range2,range3,range4){curColorMode=mode;if(arguments.length>=4){redRange=range1;greenRange=range2;blueRange=range3;}
+if(arguments.length==5){opacityRange=range4;}
+if(arguments.length==2){p.colorMode(mode,range1,range1,range1,range1);}};p.beginShape=function beginShape(type){curShape=type;curShapeCount=0;curvePoints=[];};p.endShape=function endShape(close){if(curShapeCount!=0){if(close||doFill)
+curContext.lineTo(firstX,firstY);if(doFill)
+curContext.fill();if(doStroke)
+curContext.stroke();curContext.closePath();curShapeCount=0;pathOpen=false;}
+if(pathOpen){if(doFill)
+curContext.fill();if(doStroke)
+curContext.stroke();curContext.closePath();curShapeCount=0;pathOpen=false;}};p.vertex=function vertex(x,y,x2,y2,x3,y3){if(curShapeCount==0&&curShape!=p.POINTS){pathOpen=true;curContext.beginPath();curContext.moveTo(x,y);firstX=x;firstY=y;}else{if(curShape==p.POINTS){p.point(x,y);}else if(arguments.length==2){if(curShape!=p.QUAD_STRIP||curShapeCount!=2)
+curContext.lineTo(x,y);if(curShape==p.TRIANGLE_STRIP){if(curShapeCount==2){p.endShape(p.CLOSE);pathOpen=true;curContext.beginPath();curContext.moveTo(prevX,prevY);curContext.lineTo(x,y);curShapeCount=1;}
+firstX=prevX;firstY=prevY;}
+if(curShape==p.TRIANGLE_FAN&&curShapeCount==2){p.endShape(p.CLOSE);pathOpen=true;curContext.beginPath();curContext.moveTo(firstX,firstY);curContext.lineTo(x,y);curShapeCount=1;}
+if(curShape==p.QUAD_STRIP&&curShapeCount==3){curContext.lineTo(prevX,prevY);p.endShape(p.CLOSE);pathOpen=true;curContext.beginPath();curContext.moveTo(prevX,prevY);curContext.lineTo(x,y);curShapeCount=1;}
+if(curShape==p.QUAD_STRIP){firstX=secondX;firstY=secondY;secondX=prevX;secondY=prevY;}}else if(arguments.length==4){if(curShapeCount>1){curContext.moveTo(prevX,prevY);curContext.quadraticCurveTo(firstX,firstY,x,y);curShapeCount=1;}}else if(arguments.length==6){curContext.bezierCurveTo(x,y,x2,y2,x3,y3);curShapeCount=-1;}}
+prevX=x;prevY=y;curShapeCount++;if(curShape==p.LINES&&curShapeCount==2||(curShape==p.TRIANGLES)&&curShapeCount==3||(curShape==p.QUADS)&&curShapeCount==4){p.endShape(p.CLOSE);}};p.curveVertex=function(x,y,x2,y2){if(curvePoints.length<3){curvePoints.push([x,y]);}else{var b=[],s=1-curTightness;curvePoints.push([x,y]);b[0]=[curvePoints[1][0],curvePoints[1][1]];b[1]=[curvePoints[1][0]+(s*curvePoints[2][0]-s*curvePoints[0][0])/6,curvePoints[1][1]+(s*curvePoints[2][1]-s*curvePoints[0][1])/6];b[2]=[curvePoints[2][0]+(s*curvePoints[1][0]-s*curvePoints[3][0])/6,curvePoints[2][1]+(s*curvePoints[1][1]-s*curvePoints[3][1])/6];b[3]=[curvePoints[2][0],curvePoints[2][1]];if(!pathOpen){p.vertex(b[0][0],b[0][1]);}else{curShapeCount=1;}
+p.vertex(b[1][0],b[1][1],b[2][0],b[2][1],b[3][0],b[3][1]);curvePoints.shift();}};p.curveTightness=function(tightness){curTightness=tightness;};p.bezierVertex=p.vertex;p.rectMode=function rectMode(aRectMode){curRectMode=aRectMode;};p.imageMode=function(){};p.ellipseMode=function ellipseMode(aEllipseMode){curEllipseMode=aEllipseMode;};p.dist=function dist(x1,y1,x2,y2){return Math.sqrt(Math.pow(x2-x1,2)+Math.pow(y2-y1,2));};p.year=function year(){return(new Date).getYear()+1900;};p.month=function month(){return(new Date).getMonth();};p.day=function day(){return(new Date).getDay();};p.hour=function hour(){return(new Date).getHours();};p.minute=function minute(){return(new Date).getMinutes();};p.second=function second(){return(new Date).getSeconds();};p.millis=function millis(){return(new Date).getTime()-start;};p.ortho=function ortho(){};p.translate=function translate(x,y){curContext.translate(x,y);};p.scale=function scale(x,y){curContext.scale(x,y||x);};p.rotate=function rotate(aAngle){curContext.rotate(aAngle);};p.pushMatrix=function pushMatrix(){curContext.save();};p.popMatrix=function popMatrix(){curContext.restore();};p.redraw=function redraw(){if(hasBackground){p.background();}
+p.frameCount++;inDraw=true;p.pushMatrix();p.draw();p.popMatrix();inDraw=false;};p.loop=function loop(){if(loopStarted)
+return;looping=setInterval(function(){try{p.redraw();}
+catch(e){clearInterval(looping);throw e;}},1000/curFrameRate);loopStarted=true;};p.frameRate=function frameRate(aRate){curFrameRate=aRate;};p.background=function background(img){if(arguments.length){if(img&&img.img){curBackground=img;}else{curBackground=p.color.apply(this,arguments);}}
+if(curBackground.img){p.image(curBackground,0,0);}else{var oldFill=curContext.fillStyle;curContext.fillStyle=curBackground+"";curContext.fillRect(0,0,p.width,p.height);curContext.fillStyle=oldFill;}};p.sq=function sq(aNumber){return aNumber*aNumber;};p.sqrt=function sqrt(aNumber){return Math.sqrt(aNumber);};p.int=function int(aNumber){return Math.floor(aNumber);};p.min=function min(aNumber,aNumber2){return Math.min(aNumber,aNumber2);};p.max=function max(aNumber,aNumber2){return Math.max(aNumber,aNumber2);};p.ceil=function ceil(aNumber){return Math.ceil(aNumber);};p.floor=function floor(aNumber){return Math.floor(aNumber);};p.float=function float(aNumber){return typeof aNumber=="string"?p.float(aNumber.charCodeAt(0)):parseFloat(aNumber);};p.byte=function byte(aNumber){return aNumber||0;};p.random=function random(aMin,aMax){return arguments.length==2?aMin+(Math.random()*(aMax-aMin)):Math.random()*aMin;};p.noise=function(x,y,z){return arguments.length>=2?PerlinNoise_2D(x,y):PerlinNoise_2D(x,x);};function Noise(x,y){var n=x+y*57;n=(n<<13)^n;return Math.abs(1.0-(((n*((n*n*15731)+789221)+1376312589)&0x7fffffff)/1073741824.0));};function SmoothedNoise(x,y){var corners=(Noise(x-1,y-1)+Noise(x+1,y-1)+Noise(x-1,y+1)+Noise(x+1,y+1))/16;var sides=(Noise(x-1,y)+Noise(x+1,y)+Noise(x,y-1)+Noise(x,y+1))/8;var center=Noise(x,y)/4;return corners+sides+center;};function InterpolatedNoise(x,y){var integer_X=Math.floor(x);var fractional_X=x-integer_X;var integer_Y=Math.floor(y);var fractional_Y=y-integer_Y;var v1=SmoothedNoise(integer_X,integer_Y);var v2=SmoothedNoise(integer_X+1,integer_Y);var v3=SmoothedNoise(integer_X,integer_Y+1);var v4=SmoothedNoise(integer_X+1,integer_Y+1);var i1=Interpolate(v1,v2,fractional_X);var i2=Interpolate(v3,v4,fractional_X);return Interpolate(i1,i2,fractional_Y);}
+function PerlinNoise_2D(x,y){var total=0;var p=0.25;var n=3;for(var i=0;i<=n;i++){var frequency=Math.pow(2,i);var amplitude=Math.pow(p,i);total=total+InterpolatedNoise(x*frequency,y*frequency)*amplitude;}
+return total;}
+function Interpolate(a,b,x){var ft=x*p.PI;var f=(1-p.cos(ft))*.5;return a*(1-f)+b*f;}
+p.red=function(aColor){return parseInt(aColor.slice(5));};p.green=function(aColor){return parseInt(aColor.split(",")[1]);};p.blue=function(aColor){return parseInt(aColor.split(",")[2]);};p.alpha=function(aColor){return parseInt(aColor.split(",")[3]);};p.abs=function abs(aNumber){return Math.abs(aNumber);};p.cos=function cos(aNumber){return Math.cos(aNumber);};p.sin=function sin(aNumber){return Math.sin(aNumber);};p.pow=function pow(aNumber,aExponent){return Math.pow(aNumber,aExponent);};p.constrain=function constrain(aNumber,aMin,aMax){return Math.min(Math.max(aNumber,aMin),aMax);};p.sqrt=function sqrt(aNumber){return Math.sqrt(aNumber);};p.atan2=function atan2(aNumber,aNumber2){return Math.atan2(aNumber,aNumber2);};p.radians=function radians(aAngle){return(aAngle/180)*p.PI;};p.size=function size(aWidth,aHeight){var fillStyle=curContext.fillStyle;var strokeStyle=curContext.strokeStyle;curElement.width=p.width=aWidth;curElement.height=p.height=aHeight;curContext.fillStyle=fillStyle;curContext.strokeStyle=strokeStyle;};p.noStroke=function noStroke(){doStroke=false;};p.noFill=function noFill(){doFill=false;};p.smooth=function smooth(){};p.noLoop=function noLoop(){doLoop=false;};p.fill=function fill(){doFill=true;curContext.fillStyle=p.color.apply(this,arguments);};p.stroke=function stroke(){doStroke=true;curContext.strokeStyle=p.color.apply(this,arguments);};p.strokeWeight=function strokeWeight(w){curContext.lineWidth=w;};p.point=function point(x,y){var oldFill=curContext.fillStyle;curContext.fillStyle=curContext.strokeStyle;curContext.fillRect(Math.round(x),Math.round(y),1,1);curContext.fillStyle=oldFill;};p.get=function get(x,y){if(arguments.length==0){var c=p.createGraphics(p.width,p.height);c.image(curContext,0,0);return c;}
+if(!getLoaded){getLoaded=buildImageObject(curContext.getImageData(0,0,p.width,p.height));}
+return getLoaded.get(x,y);};p.set=function set(x,y,obj){if(obj&&obj.img){p.image(obj,x,y);}else{var oldFill=curContext.fillStyle;var color=obj;curContext.fillStyle=color;curContext.fillRect(Math.round(x),Math.round(y),1,1);curContext.fillStyle=oldFill;}};p.arc=function arc(x,y,width,height,start,stop){if(width<=0)
+return;if(curEllipseMode==p.CORNER){x+=width/2;y+=height/2;}
+curContext.beginPath();curContext.moveTo(x,y);curContext.arc(x,y,curEllipseMode==p.CENTER_RADIUS?width:width/2,start,stop,false);if(doFill)
+curContext.fill();if(doStroke)
+curContext.stroke();curContext.closePath();};p.line=function line(x1,y1,x2,y2){curContext.lineCap="round";curContext.beginPath();curContext.moveTo(x1||0,y1||0);curContext.lineTo(x2||0,y2||0);curContext.stroke();curContext.closePath();};p.bezier=function bezier(x1,y1,x2,y2,x3,y3,x4,y4){curContext.lineCap="butt";curContext.beginPath();curContext.moveTo(x1,y1);curContext.bezierCurveTo(x2,y2,x3,y3,x4,y4);curContext.stroke();curContext.closePath();};p.triangle=function triangle(x1,y1,x2,y2,x3,y3){p.beginShape();p.vertex(x1,y1);p.vertex(x2,y2);p.vertex(x3,y3);p.endShape();};p.quad=function quad(x1,y1,x2,y2,x3,y3,x4,y4){p.beginShape();p.vertex(x1,y1);p.vertex(x2,y2);p.vertex(x3,y3);p.vertex(x4,y4);p.endShape();};p.rect=function rect(x,y,width,height){if(width==0&&height==0)
+return;curContext.beginPath();var offsetStart=0;var offsetEnd=0;if(curRectMode==p.CORNERS){width-=x;height-=y;}
+if(curRectMode==p.RADIUS){width*=2;height*=2;}
+if(curRectMode==p.CENTER||curRectMode==p.RADIUS){x-=width/2;y-=height/2;}
+curContext.rect(Math.round(x)-offsetStart,Math.round(y)-offsetStart,Math.round(width)+offsetEnd,Math.round(height)+offsetEnd);if(doFill)
+curContext.fill();if(doStroke)
+curContext.stroke();curContext.closePath();};p.ellipse=function ellipse(x,y,width,height){x=x||0;y=y||0;if(width<=0&&height<=0)
+return;curContext.beginPath();if(curEllipseMode==p.RADIUS){width*=2;height*=2;}
+var offsetStart=0;if(width==height)
+curContext.arc(x-offsetStart,y-offsetStart,width/2,0,Math.PI*2,false);if(doFill)
+curContext.fill();if(doStroke)
+curContext.stroke();curContext.closePath();};p.link=function(href,target){window.location=href;};p.loadPixels=function(){p.pixels=buildImageObject(curContext.getImageData(0,0,p.width,p.height)).pixels;};p.updatePixels=function(){var colors=/(\d+),(\d+),(\d+),(\d+)/;var pixels={};pixels.width=p.width;pixels.height=p.height;pixels.data=[];if(curContext.createImageData){pixels=curContext.createImageData(p.width,p.height);}
+var data=pixels.data;var pos=0;for(var i=0,l=p.pixels.length;i<l;i++){var c=(p.pixels[i]||"rgba(0,0,0,1)").match(colors);data[pos]=parseInt(c[1]);data[pos+1]=parseInt(c[2]);data[pos+2]=parseInt(c[3]);data[pos+3]=parseFloat(c[4])*100;pos+=4;}
+curContext.putImageData(pixels,0,0);};p.extendClass=function extendClass(obj,args,fn){if(arguments.length==3){fn.apply(obj,args);}else{args.call(obj);}};p.addMethod=function addMethod(object,name,fn){if(object[name]){var args=fn.length;var oldfn=object[name];object[name]=function(){if(arguments.length==args)
+return fn.apply(this,arguments);else
+return oldfn.apply(this,arguments);};}else{object[name]=fn;}};p.init=function init(code){p.stroke(0);p.fill(255);curContext.translate(0.5,0.5);if(code){(function(Processing){with(p){eval(parse(code,p));}})(p);}
+if(p.setup){inSetup=true;p.setup();}
+inSetup=false;if(p.draw){if(!doLoop){p.redraw();}else{p.loop();}}
+attach(curElement,"mousemove",function(e){var scrollX=window.scrollX!=null?window.scrollX:window.pageXOffset;var scrollY=window.scrollY!=null?window.scrollY:window.pageYOffset;p.pmouseX=p.mouseX;p.pmouseY=p.mouseY;p.mouseX=e.clientX-curElement.offsetLeft+scrollX;p.mouseY=e.clientY-curElement.offsetTop+scrollY;if(p.mouseMoved){p.mouseMoved();}
+if(mousePressed&&p.mouseDragged){p.mouseDragged();}});attach(curElement,"mousedown",function(e){mousePressed=true;p.mouseButton=e.which;if(typeof p.mousePressed=="function"){p.mousePressed();}else{p.mousePressed=true;}});attach(curElement,"contextmenu",function(e){e.preventDefault();e.stopPropagation();});attach(curElement,"mouseup",function(e){mousePressed=false;if(typeof p.mousePressed!="function"){p.mousePressed=false;}
+if(p.mouseReleased){p.mouseReleased();}});attach(document,"keydown",function(e){keyPressed=true;p.key=e.keyCode+32;if(e.shiftKey){p.key=String.fromCharCode(p.key).toUpperCase().charCodeAt(0);}
+if(typeof p.keyPressed=="function"){p.keyPressed();}else{p.keyPressed=true;}});attach(document,"keyup",function(e){keyPressed=false;if(typeof p.keyPressed!="function"){p.keyPressed=false;}
+if(p.keyReleased){p.keyReleased();}});function attach(elem,type,fn){if(elem.addEventListener)
+elem.addEventListener(type,fn,false);else
+elem.attachEvent("on"+type,fn);}};return p;}})();if(!Array.prototype.map)
+{Array.prototype.map=function(fun)
+{var len=this.length;if(typeof fun!="function")
+throw new TypeError();var res=new Array(len);var thisp=arguments[1];for(var i=0;i<len;i++)
+{if(i in this)
+res[i]=fun.call(thisp,this[i],i,this);}
+return res;};}
+var BaseSparkline=function(){this.init=function(id,data,mixins){this.background=50;this.stroke="rgba(230,230,230,0.70);";this.percentage_color="#5555FF";this.percentage_fill_color=75;this.value_line_color="#7777FF";this.value_line_fill_color=85;this.canvas=document.getElementById(id);this.data=data;this.scale_from=undefined;this.scale_to=undefined;this.top_padding=10;this.bottom_padding=10;this.left_padding=10;this.right_padding=10;this.percentage_lines=[];this.fill_between_percentage_lines=false;this.value_lines=[];this.fill_between_value_lines=false;for(var property in mixins)this[property]=mixins[property];};this.parse_height=function(x){return x;};this.heights=function(){return this.data.map(this.parse_height);};this.max=function(){var vals=this.heights();var max=vals[0];var l=vals.length;for(var i=1;i<l;i++)max=Math.max(max,vals[i]);return max;};this.min=function(){var vals=this.heights();var min=vals[0];var l=vals.length;for(var i=1;i<l;i++)min=Math.min(min,vals[i]);return min;};this.height=function(){return this.canvas.height-this.top_padding-this.bottom_padding;};this.width=function(){return this.canvas.width-this.left_padding-this.right_padding;};this.scale_values=function(values,max){if(!max)max=this.max();var p=this.top_padding;var h=this.height();var top=(this.scale_to!=undefined)?this.scale_to:max;var bottom=(this.scale_from!=undefined)?this.scale_from:this.min();var range=Math.abs(top-bottom);var scale=function(x){var percentage=((x-bottom)*1.0)/range;return h-(h*percentage)+p;};return values.map(scale,this);};this.calc_value_lines=function(){var scaled=this.scale_values(this.value_lines);scaled.sort(function(a,b){return a-b;});return scaled;};this.calc_percentages=function(){var sorted=this.heights();sorted.sort(function(a,b){return a-b;});var points=[];var n=sorted.length;var l=this.percentage_lines.length;for(var i=0;i<l;i++){var percentage=this.percentage_lines[i];var position=Math.round(percentage*(n+1));points.push(sorted[position]);}
+var max=sorted[n-1];var raws=this.scale_values(points,max);raws.sort(function(a,b){return a-b;});return raws;};this.scale_height=function(){return this.scale_values(this.heights());};this.segment_width=function(){var w=this.width();var l=this.data.length;return(w*1.0)/(l-1);};this.scale_width=function(){var widths=[];var l=this.data.length;var segment_width=this.segment_width();for(var i=0;i<l;i++){widths.push((i*segment_width)+this.left_padding);}
+return widths;};this.scale_data=function(){var heights=this.scale_height();var widths=this.scale_width();var l=heights.length;var data=[];for(var i=0;i<l;i++)
+data.push({'y':heights[i],'x':widths[i]});return data;};this.draw=function(){var sl=this;with(Processing(sl.canvas)){setup=function(){};draw=function(){background(sl.background);scaled=sl.scale_data();var l=scaled.length;var percentages=sl.calc_percentages();if(sl.fill_between_percentage_lines&&percentages.length>1){noStroke();fill(sl.percentage_fill_color);var height=percentages[percentages.length-1]-percentages[0];var width=scaled[l-1].x-scaled[0].x;rect(scaled[0].x,percentages[0],width,height);}
+var value_lines=sl.calc_value_lines();if(sl.fill_between_value_lines&&value_lines.length>1){noStroke();fill(sl.value_line_fill_color);var height=value_lines[value_lines.length-1]-value_lines[0];var width=scaled[l-1].x-scaled[0].x;rect(scaled[0].x,value_lines[0],width,height);}
+stroke(sl.value_line_color);for(var h=0;h<value_lines.length;h++){var y=value_lines[h];line(scaled[0].x,y,scaled[l-1].x,y);}
+stroke(sl.percentage_color);for(var j=0;j<percentages.length;j++){var y=percentages[j];line(scaled[0].x,y,scaled[l-1].x,y);}
+stroke(sl.stroke);for(var i=1;i<l;i++){var curr=scaled[i];var previous=scaled[i-1];line(previous.x,previous.y,curr.x,curr.y);}
+this.exit();};init();};};};var Sparkline=function(id,data,mixins){this.init(id,data,mixins);}
+Sparkline.prototype=new BaseSparkline();var BarSparkline=function(id,data,mixins){if(!mixins)mixins={};this.marking_padding=5;this.padding_between_bars=5;this.extend_markings=true;if(!mixins.hasOwnProperty('scale_from'))mixins.scale_from=0;this.init(id,data,mixins);this.segment_width=function(){var l=this.data.length;var w=this.width();return((w*1.0)-((l-1)*this.padding_between_bars))/l;};this.scale_width=function(){var widths=[];var l=this.data.length;var segment_width=this.segment_width();for(var i=0;i<l;i++){widths.push((i*segment_width)+(this.padding_between_bars*i)+this.left_padding);}
+return widths;};this.draw=function(){var sl=this;with(Processing(sl.canvas)){draw=function(){background(sl.background);var scaled=sl.scale_data();var l=scaled.length;var sw=sl.segment_width();var gap=sl.padding_between_bars;var mp=sl.marking_padding;var value_lines=sl.calc_value_lines();if(sl.fill_between_value_lines&&value_lines.length>1){noStroke();fill(sl.percentage_fill_color);var height=value_lines[value_lines.length-1]-value_lines[0];var width=scaled[l-1].x-scaled[0].x+sw;if(sl.extend_markings){width+=2*mp;rect(scaled[0].x-mp,value_lines[0],width,height);}
+else rect(scaled[0].x,value_lines[0],width,height);}
+stroke(sl.value_line_color);for(var h=0;h<value_lines.length;h++){var y=value_lines[h];if(sl.extend_markings){line(scaled[0].x-mp,y,scaled[l-1].x+mp+sw,y);}
+else line(scaled[0].x,y,scaled[l-1].x+sw,y);}
+var percentages=sl.calc_percentages();if(sl.fill_between_percentage_lines&&percentages.length>1){noStroke();fill(sl.percentage_fill_color);var height=percentages[percentages.length-1]-percentages[0];var width=scaled[l-1].x-scaled[0].x+sw;if(sl.extend_markings){width+=2*mp;rect(scaled[0].x-mp,percentages[0],width,height);}
+else rect(scaled[0].x,percentages[0],width,height);}
+stroke(sl.percentage_color);for(var j=0;j<percentages.length;j++){var y=percentages[j];if(sl.extend_markings){line(scaled[0].x-mp,y,scaled[l-1].x+mp+sw,y);}
+else line(scaled[0].x,y,scaled[l-1].x+sw,y);}
+stroke(sl.stroke);fill(sl.stroke);var width=sl.segment_width();var height=sl.height();for(var i=0;i<l;i++){var d=scaled[i];rect(d.x,d.y,width,height-d.y);};this.exit();};init();};};}
+BarSparkline.prototype=new BaseSparkline();
diff --git a/extensions/OrangeFactor/web/style/orangefactor.css b/extensions/OrangeFactor/web/style/orangefactor.css
new file mode 100644
index 000000000..211ad575e
--- /dev/null
+++ b/extensions/OrangeFactor/web/style/orangefactor.css
@@ -0,0 +1,13 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0. */
+
+#orange-graph {
+ display: block;
+ width: 180px;
+ height: 38px;
+ margin: 0 .5em .5em 0;
+}
diff --git a/extensions/ProductDashboard/Config.pm b/extensions/ProductDashboard/Config.pm
new file mode 100644
index 000000000..3a4654974
--- /dev/null
+++ b/extensions/ProductDashboard/Config.pm
@@ -0,0 +1,14 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::ProductDashboard;
+
+use strict;
+
+use constant NAME => 'ProductDashboard';
+
+__PACKAGE__->NAME;
diff --git a/extensions/ProductDashboard/Extension.pm b/extensions/ProductDashboard/Extension.pm
new file mode 100644
index 000000000..8ccc897ed
--- /dev/null
+++ b/extensions/ProductDashboard/Extension.pm
@@ -0,0 +1,186 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::ProductDashboard;
+
+use strict;
+
+use base qw(Bugzilla::Extension);
+
+use Bugzilla;
+use Bugzilla::Constants;
+use Bugzilla::Util;
+use Bugzilla::Error;
+use Bugzilla::Product;
+use Bugzilla::Field;
+
+use Bugzilla::Extension::ProductDashboard::Queries;
+use Bugzilla::Extension::ProductDashboard::Util;
+
+our $VERSION = BUGZILLA_VERSION;
+
+sub page_before_template {
+ my ($self, $args) = @_;
+
+ my $page = $args->{page_id};
+ my $vars = $args->{vars};
+
+ if ($page =~ m{^productdashboard\.}) {
+ _page_dashboard($vars);
+ }
+}
+
+sub _page_dashboard {
+ my $vars = shift;
+
+ my $cgi = Bugzilla->cgi;
+ my $input = Bugzilla->input_params;
+ my $user = Bugzilla->user;
+
+ # Switch to shadow db since we are just reading information
+ Bugzilla->switch_to_shadow_db();
+
+ # All pages point to the same part of the documentation.
+ $vars->{'doc_section'} = 'bugreports.html';
+
+ # Forget any previously selected product
+ $cgi->send_cookie(-name => 'PRODUCT_DASHBOARD',
+ -value => 'X',
+ -expires => "Fri, 01-Jan-1970 00:00:00 GMT");
+
+ # If the user cannot enter bugs in any product, stop here.
+ scalar @{$user->get_selectable_products}
+ || ThrowUserError('no_products');
+
+ # Create data structures representing each classification
+ my @classifications = ();
+ foreach my $c (@{$user->get_selectable_classifications}) {
+ # Create hash to hold attributes for each classification.
+ my %classification = (
+ 'name' => $c->name,
+ 'products' => [ @{$user->get_selectable_products($c->id)} ]
+ );
+ # Assign hash back to classification array.
+ push @classifications, \%classification;
+ }
+ $vars->{'classifications'} = \@classifications;
+
+ my $product_name = trim($input->{'product'} || '');
+
+ if (!$product_name && $cgi->cookie('PRODUCT_DASHBOARD')) {
+ $product_name = $cgi->cookie('PRODUCT_DASHBOARD');
+ }
+
+ return if !$product_name;
+
+ # Do not use Bugzilla::Product::check_product() here, else the user
+ # could know whether the product doesn't exist or is not accessible.
+ my $product = new Bugzilla::Product({'name' => $product_name});
+
+ # We need to check and make sure that the user has permission
+ # to enter a bug against this product.
+ if (!$product || !$user->can_enter_product($product->name)) {
+ return;
+ }
+
+ # Remember selected product
+ $cgi->send_cookie(-name => 'PRODUCT_DASHBOARD',
+ -value => $product->name,
+ -expires => "Fri, 01-Jan-2038 00:00:00 GMT");
+
+ my $current_tab_name = $input->{'tab'} || "summary";
+ trick_taint($current_tab_name);
+ $vars->{'current_tab_name'} = $current_tab_name;
+
+ my $bug_status = trim($input->{'bug_status'} || 'open');
+
+ $vars->{'bug_status'} = $bug_status;
+ $vars->{'product'} = $product;
+ $vars->{'bug_link_all'} = bug_link_all($product);
+ $vars->{'bug_link_open'} = bug_link_open($product);
+ $vars->{'bug_link_closed'} = bug_link_closed($product);
+ $vars->{'total_bugs'} = total_bugs($product);
+ $vars->{'total_open_bugs'} = total_open_bugs($product);
+ $vars->{'total_closed_bugs'} = total_closed_bugs($product);
+ $vars->{'severities'} = get_legal_field_values('bug_severity');
+
+ if ($current_tab_name eq 'summary') {
+ $vars->{'by_priority'} = by_priority($product, $bug_status);
+ $vars->{'by_severity'} = by_severity($product, $bug_status);
+ $vars->{'by_assignee'} = by_assignee($product, $bug_status);
+ $vars->{'by_status'} = by_status($product, $bug_status);
+ }
+
+ if ($current_tab_name eq 'recents') {
+ my $recent_days = $input->{'recent_days'} || 7;
+ (detaint_natural($recent_days) && $recent_days > 0 && $recent_days < 101)
+ || ThrowUserError('product_dashboard_invalid_recent_days');
+
+ my $params = {
+ product => $product,
+ days => $recent_days,
+ date_from => $input->{'date_from'} || '',
+ date_to => $input->{'date_to'} || '',
+ };
+
+ $vars->{'recently_opened'} = recently_opened($params);
+ $vars->{'recently_closed'} = recently_closed($params);
+ $vars->{'recent_days'} = $recent_days;
+ $vars->{'date_from'} = $input->{'date_from'};
+ $vars->{'date_to'} = $input->{'date_to'};
+ }
+
+ if ($current_tab_name eq 'components') {
+ if ($input->{'component'}) {
+ $vars->{'summary'} = by_value_summary($product, 'component', $input->{'component'}, $bug_status);
+ $vars->{'summary'}{'type'} = 'component';
+ $vars->{'summary'}{'value'} = $input->{'component'};
+ }
+ elsif ($input->{'version'}) {
+ $vars->{'summary'} = by_value_summary($product, 'version', $input->{'version'}, $bug_status);
+ $vars->{'summary'}{'type'} = 'version';
+ $vars->{'summary'}{'value'} = $input->{'version'};
+ }
+ elsif ($input->{'target_milestone'} && Bugzilla->params->{'usetargetmilestone'}) {
+ $vars->{'summary'} = by_value_summary($product, 'target_milestone', $input->{'target_milestone'}, $bug_status);
+ $vars->{'summary'}{'type'} = 'target_milestone';
+ $vars->{'summary'}{'value'} = $input->{'target_milestone'};
+ }
+ else {
+ $vars->{'by_component'} = by_component($product, $bug_status);
+ $vars->{'by_version'} = by_version($product, $bug_status);
+ if (Bugzilla->params->{'usetargetmilestone'}) {
+ $vars->{'by_milestone'} = by_milestone($product, $bug_status);
+ }
+ }
+ }
+
+ if ($current_tab_name eq 'duplicates') {
+ $vars->{'by_duplicate'} = by_duplicate($product, $bug_status);
+ }
+
+ if ($current_tab_name eq 'popularity') {
+ $vars->{'by_popularity'} = by_popularity($product, $bug_status);
+ }
+
+ if ($current_tab_name eq 'roadmap') {
+ foreach my $milestone (@{$product->milestones}){
+ my %milestone_stats;
+ $milestone_stats{'name'} = $milestone->name;
+ $milestone_stats{'total_bugs'} = total_bug_milestone($product, $milestone);
+ $milestone_stats{'open_bugs'} = bug_milestone_by_status($product, $milestone, 'open');
+ $milestone_stats{'closed_bugs'} = bug_milestone_by_status($product, $milestone, 'closed');
+ $milestone_stats{'link_total'} = bug_milestone_link_total($product, $milestone);
+ $milestone_stats{'link_open'} = bug_milestone_link_open($product, $milestone);
+ $milestone_stats{'link_closed'} = bug_milestone_link_closed($product, $milestone);
+ push (@{$vars->{by_roadmap}}, \%milestone_stats);
+ }
+ }
+}
+
+__PACKAGE__->NAME;
+
diff --git a/extensions/ProductDashboard/lib/Queries.pm b/extensions/ProductDashboard/lib/Queries.pm
new file mode 100644
index 000000000..9c3d91539
--- /dev/null
+++ b/extensions/ProductDashboard/lib/Queries.pm
@@ -0,0 +1,467 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+package Bugzilla::Extension::ProductDashboard::Queries;
+
+use strict;
+
+use base qw(Exporter);
+@Bugzilla::Extension::ProductDashboard::Queries::EXPORT = qw(
+ total_bugs
+ total_open_bugs
+ total_closed_bugs
+ by_version
+ by_value_summary
+ by_milestone
+ by_priority
+ by_severity
+ by_component
+ by_assignee
+ by_status
+ by_duplicate
+ by_popularity
+ recently_opened
+ recently_closed
+ total_bug_milestone
+ bug_milestone_by_status
+);
+
+use Bugzilla::CGI;
+use Bugzilla::User;
+use Bugzilla::Search;
+use Bugzilla::Util;
+use Bugzilla::Component;
+use Bugzilla::Version;
+use Bugzilla::Milestone;
+
+use Bugzilla::Extension::ProductDashboard::Util qw(open_states closed_states
+ quoted_open_states quoted_closed_states);
+
+sub total_bugs {
+ my $product = shift;
+ my $dbh = Bugzilla->dbh;
+
+ return $dbh->selectrow_array("SELECT COUNT(bug_id)
+ FROM bugs
+ WHERE product_id = ?", undef, $product->id);
+}
+
+sub total_open_bugs {
+ my $product = shift;
+ my $bug_status = shift;
+ my $dbh = Bugzilla->dbh;
+
+ return $dbh->selectrow_array("SELECT COUNT(bug_id)
+ FROM bugs
+ WHERE bug_status IN (" . join(',', quoted_open_states()) . ")
+ AND product_id = ?", undef, $product->id);
+}
+
+sub total_closed_bugs {
+ my $product = shift;
+ my $dbh = Bugzilla->dbh;
+
+ return $dbh->selectrow_array("SELECT COUNT(bug_id)
+ FROM bugs
+ WHERE bug_status IN ('CLOSED')
+ AND product_id = ?", undef, $product->id);
+}
+
+sub bug_link_all {
+ my $product = shift;
+
+ return correct_urlbase() . 'buglist.cgi?product=' . url_quote($product->name);
+}
+
+sub bug_link_open {
+ my $product = shift;
+
+ return correct_urlbase() . 'buglist.cgi?product=' . url_quote($product->name) . "&bug_status=__open__";
+}
+
+sub bug_link_closed {
+ my $product = shift;
+
+ return correct_urlbase() . 'buglist.cgi?product=' . url_quote($product->name) . "&bug_status=__closed__";
+}
+
+sub by_version {
+ my ($product, $bug_status) = @_;
+ my $dbh = Bugzilla->dbh;
+ my $extra;
+
+ $extra = "AND bugs.bug_status IN (" . join(',', quoted_open_states()) . ")" if $bug_status eq 'open';
+ $extra = "AND bugs.bug_status IN (" . join(',', quoted_closed_states()) . ")" if $bug_status eq 'closed';
+
+ return $dbh->selectall_arrayref("SELECT version, COUNT(bug_id)
+ FROM bugs
+ WHERE product_id = ?
+ $extra
+ GROUP BY version
+ ORDER BY COUNT(bug_id) DESC", undef, $product->id);
+}
+
+sub by_milestone {
+ my ($product, $bug_status) = @_;
+ my $dbh = Bugzilla->dbh;
+ my $extra;
+
+ $extra = "AND bugs.bug_status IN (" . join(',', quoted_open_states()) . ")" if $bug_status eq 'open';
+ $extra = "AND bugs.bug_status IN (" . join(',', quoted_closed_states()) . ")" if $bug_status eq 'closed';
+
+ return $dbh->selectall_arrayref("SELECT target_milestone, COUNT(bug_id)
+ FROM bugs
+ WHERE product_id = ?
+ $extra
+ GROUP BY target_milestone
+ ORDER BY COUNT(bug_id) DESC", undef, $product->id);
+}
+
+sub by_priority {
+ my ($product, $bug_status) = @_;
+ my $dbh = Bugzilla->dbh;
+ my $extra;
+
+ $extra = "AND bugs.bug_status IN (" . join(',', quoted_open_states()) . ")" if $bug_status eq 'open';
+ $extra = "AND bugs.bug_status IN (" . join(',', quoted_closed_states()) . ")" if $bug_status eq 'closed';
+
+ return $dbh->selectall_arrayref("SELECT priority, COUNT(bug_id)
+ FROM bugs
+ WHERE product_id = ?
+ $extra
+ GROUP BY priority
+ ORDER BY COUNT(bug_id) DESC", undef, $product->id);
+}
+
+sub by_severity {
+ my ($product, $bug_status) = @_;
+ my $dbh = Bugzilla->dbh;
+ my $extra;
+
+ $extra = "AND bugs.bug_status IN (" . join(',', quoted_open_states()) . ")" if $bug_status eq 'open';
+ $extra = "AND bugs.bug_status IN (" . join(',', quoted_closed_states()) . ")" if $bug_status eq 'closed';
+
+ return $dbh->selectall_arrayref("SELECT bug_severity, COUNT(bug_id)
+ FROM bugs
+ WHERE product_id = ?
+ $extra
+ GROUP BY bug_severity
+ ORDER BY COUNT(bug_id) DESC", undef, $product->id);
+}
+
+sub by_component {
+ my ($product, $bug_status) = @_;
+ my $dbh = Bugzilla->dbh;
+ my $extra;
+
+ $extra = "AND bugs.bug_status IN (" . join(',', quoted_open_states()) . ")" if $bug_status eq 'open';
+ $extra = "AND bugs.bug_status IN (" . join(',', quoted_closed_states()) . ")" if $bug_status eq 'closed';
+
+ return $dbh->selectall_arrayref("SELECT components.name, COUNT(bugs.bug_id)
+ FROM bugs INNER JOIN components ON bugs.component_id = components.id
+ WHERE bugs.product_id = ?
+ $extra
+ GROUP BY components.name
+ ORDER BY COUNT(bugs.bug_id) DESC", undef, $product->id);
+}
+
+sub by_value_summary {
+ my ($product, $type, $value, $bug_status) = @_;
+ my $dbh = Bugzilla->dbh;
+ my $extra;
+
+ my $query = "SELECT bugs.bug_id AS id,
+ bugs.bug_status AS status,
+ bugs.version AS version,
+ components.name AS component,
+ bugs.bug_severity AS severity,
+ bugs.short_desc AS summary
+ FROM bugs, components
+ WHERE bugs.product_id = ?
+ AND bugs.component_id = components.id ";
+
+ if ($type eq 'component') {
+ Bugzilla::Component->check({ product => $product, name => $value });
+ $query .= "AND components.name = ? " if $type eq 'component';
+ }
+ elsif ($type eq 'version') {
+ Bugzilla::Version->check({ product => $product, name => $value });
+ $query .= "AND bugs.version = ? " if $type eq 'version';
+ }
+ elsif ($type eq 'target_milestone') {
+ Bugzilla::Milestone->check({ product => $product, name => $value });
+ $query .= "AND bugs.target_milestone = ? " if $type eq 'target_milestone';
+ }
+
+ $query .= "AND bugs.bug_status IN (" . join(',', quoted_open_states()) . ") " if $bug_status eq 'open';
+ $query .= "AND bugs.bug_status IN (" . join(',', quoted_closed_states()) . ") " if $bug_status eq 'closed';
+
+ trick_taint($value);
+
+ my $past_due_bugs = $dbh->selectall_arrayref($query .
+ "AND (bugs.deadline IS NOT NULL AND bugs.deadline != '')
+ AND bugs.deadline < now() ORDER BY bugs.deadline LIMIT 10",
+ {'Slice' => {}}, $product->id, $value);
+
+ my $updated_recently_bugs = $dbh->selectall_arrayref($query .
+ "AND bugs.delta_ts != bugs.creation_ts " .
+ "ORDER BY bugs.delta_ts DESC LIMIT 10",
+ {'Slice' => {}}, $product->id, $value);
+
+ my $timestamp = $dbh->selectrow_array("SELECT " . $dbh->sql_date_format("LOCALTIMESTAMP(0)", "%Y-%m-%d"));
+
+ return {
+ timestamp => $timestamp,
+ past_due => _filter_bugs($past_due_bugs),
+ updated_recently => _filter_bugs($updated_recently_bugs),
+ };
+}
+
+sub by_assignee {
+ my ($product, $bug_status, $limit) = @_;
+ my $dbh = Bugzilla->dbh;
+ my $extra;
+
+ $limit = detaint_natural($limit) ? $dbh->sql_limit($limit) : "";
+
+ $extra = "AND bugs.bug_status IN (" . join(',', quoted_open_states()) . ")" if $bug_status eq 'open';
+ $extra = "AND bugs.bug_status IN (" . join(',', quoted_closed_states()) . ")" if $bug_status eq 'closed';
+
+ my @result = map { [ Bugzilla::User->new($_->[0]), $_->[1] ] }
+ @{$dbh->selectall_arrayref("SELECT bugs.assigned_to AS userid, COUNT(bugs.bug_id)
+ FROM bugs, profiles
+ WHERE bugs.product_id = ?
+ AND bugs.assigned_to = profiles.userid
+ $extra
+ GROUP BY profiles.login_name
+ ORDER BY COUNT(bugs.bug_id) DESC $limit",
+ undef, $product->id)};
+
+ return \@result;
+}
+
+sub by_status {
+ my ($product, $bug_status) = @_;
+ my $dbh = Bugzilla->dbh;
+ my $extra;
+
+ $extra = "AND bugs.bug_status IN (" . join(',', quoted_open_states()) . ")" if $bug_status eq 'open';
+ $extra = "AND bugs.bug_status IN (" . join(',', quoted_closed_states()) . ")" if $bug_status eq 'closed';
+
+ return $dbh->selectall_arrayref("SELECT bugs.bug_status, COUNT(bugs.bug_id)
+ FROM bugs
+ WHERE bugs.product_id = ?
+ $extra
+ GROUP BY bugs.bug_status
+ ORDER BY COUNT(bugs.bug_id) DESC", undef, $product->id);
+}
+
+sub total_bug_milestone {
+ my ($product, $milestone) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ return $dbh->selectrow_array("SELECT COUNT(bug_id)
+ FROM bugs
+ WHERE target_milestone = ?
+ AND product_id = ?",
+ undef,
+ $milestone->name,
+ $product->id);
+
+}
+
+sub bug_milestone_by_status {
+ my ($product, $milestone, $bug_status) = @_;
+ my $dbh = Bugzilla->dbh;
+ my $extra;
+
+ $extra = "AND bugs.bug_status IN (" . join(',', quoted_open_states()) . ")" if $bug_status eq 'open';
+ $extra = "AND bugs.bug_status IN (" . join(',', quoted_closed_states()) . ")" if $bug_status eq 'closed';
+
+ return $dbh->selectrow_array("SELECT COUNT(bug_id)
+ FROM bugs
+ WHERE target_milestone = ?
+ AND product_id = ? $extra",
+ undef,
+ $milestone->name,
+ $product->id);
+
+}
+
+sub by_duplicate {
+ my ($product, $bug_status, $limit) = @_;
+ my $dbh = Bugzilla->dbh;
+ $limit = detaint_natural($limit) ? $dbh->sql_limit($limit) : "";
+
+ my $extra;
+ $extra = "AND bugs.bug_status IN (" . join(',', quoted_open_states()) . ")" if $bug_status eq 'open';
+ $extra = "AND bugs.bug_status IN (" . join(',', quoted_closed_states()) . ")" if $bug_status eq 'closed';
+
+ my $unfiltered_bugs = $dbh->selectall_arrayref("SELECT bugs.bug_id AS id,
+ bugs.bug_status AS status,
+ bugs.version AS version,
+ components.name AS component,
+ bugs.bug_severity AS severity,
+ bugs.short_desc AS summary,
+ COUNT(duplicates.dupe) AS dupe_count
+ FROM bugs, duplicates, components
+ WHERE bugs.product_id = ?
+ AND bugs.component_id = components.id
+ AND bugs.bug_id = duplicates.dupe_of
+ $extra
+ GROUP BY bugs.bug_id, bugs.bug_status, components.name,
+ bugs.bug_severity, bugs.short_desc
+ HAVING COUNT(duplicates.dupe) > 1
+ ORDER BY COUNT(duplicates.dupe) DESC $limit",
+ {'Slice' => {}}, $product->id);
+
+ return _filter_bugs($unfiltered_bugs);
+}
+
+sub by_popularity {
+ my ($product, $bug_status, $limit) = @_;
+ my $dbh = Bugzilla->dbh;
+ $limit = detaint_natural($limit) ? $dbh->sql_limit($limit) : "";
+
+ my $extra;
+ $extra = "AND bugs.bug_status IN (" . join(',', quoted_open_states()) . ")" if $bug_status eq 'open';
+ $extra = "AND bugs.bug_status IN (" . join(',', quoted_closed_states()) . ")" if $bug_status eq 'closed';
+
+ my $unfiltered_bugs = $dbh->selectall_arrayref("SELECT bugs.bug_id AS id,
+ bugs.bug_status AS status,
+ bugs.version AS version,
+ components.name AS component,
+ bugs.bug_severity AS severity,
+ bugs.short_desc AS summary,
+ bugs.votes AS votes
+ FROM bugs, components
+ WHERE bugs.product_id = ?
+ AND bugs.component_id = components.id
+ AND bugs.votes > 1
+ $extra
+ ORDER BY bugs.votes DESC $limit",
+ {'Slice' => {}}, $product->id);
+
+ return _filter_bugs($unfiltered_bugs);
+}
+
+sub recently_opened {
+ my ($params) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ my $product = $params->{'product'};
+ my $days = $params->{'days'};
+ my $limit = $params->{'limit'};
+ my $date_from = $params->{'date_from'};
+ my $date_to = $params->{'date_to'};
+
+ $days ||= 7;
+ $limit = detaint_natural($limit) ? $dbh->sql_limit($limit) : "";
+
+ my @values = ($product->id);
+
+ my $date_part;
+ if ($date_from && $date_to) {
+ validate_date($date_from)
+ || ThrowUserError('illegal_date', { date => $date_from,
+ format => 'YYYY-MM-DD' });
+ validate_date($date_to)
+ || ThrowUserError('illegal_date', { date => $date_to,
+ format => 'YYYY-MM-DD' });
+ $date_part = "AND bugs.creation_ts >= ? AND bugs.creation_ts <= ?";
+ push(@values, trick_taint($date_from), trick_taint($date_to));
+ }
+ else {
+ $date_part = "AND bugs.creation_ts >= NOW() - " . $dbh->sql_to_days('?');
+ push(@values, $days);
+ }
+
+ my $unfiltered_bugs = $dbh->selectall_arrayref("SELECT bugs.bug_id AS id,
+ bugs.bug_status AS status,
+ bugs.version AS version,
+ components.name AS component,
+ bugs.bug_severity AS severity,
+ bugs.short_desc AS summary
+ FROM bugs, components
+ WHERE bugs.product_id = ?
+ AND bugs.component_id = components.id
+ AND bugs.bug_status IN (" . join(',', quoted_open_states()) . ")
+ $date_part
+ ORDER BY bugs.bug_id DESC $limit",
+ {'Slice' => {}}, @values);
+
+ return _filter_bugs($unfiltered_bugs);
+}
+
+sub recently_closed {
+ my ($params) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ my $product = $params->{'product'};
+ my $days = $params->{'days'};
+ my $limit = $params->{'limit'};
+ my $date_from = $params->{'date_from'};
+ my $date_to = $params->{'date_to'};
+
+ $days ||= 7;
+ $limit = detaint_natural($limit) ? $dbh->sql_limit($limit) : "";
+
+ my @values = ($product->id);
+
+ my $date_part;
+ if ($date_from && $date_to) {
+ validate_date($date_from)
+ || ThrowUserError('illegal_date', { date => $date_from,
+ format => 'YYYY-MM-DD' });
+ validate_date($date_to)
+ || ThrowUserError('illegal_date', { date => $date_to,
+ format => 'YYYY-MM-DD' });
+ $date_part = "AND bugs.creation_ts >= ? AND bugs.creation_ts <= ?";
+ push(@values, trick_taint($date_from), trick_taint($date_to));
+ }
+ else {
+ $date_part = "AND bugs.creation_ts >= NOW() - " . $dbh->sql_to_days('?');
+ push(@values, $days);
+ }
+
+ my $unfiltered_bugs = $dbh->selectall_arrayref("SELECT DISTINCT bugs.bug_id AS id,
+ bugs.bug_status AS status,
+ bugs.version AS version,
+ components.name AS component,
+ bugs.bug_severity AS severity,
+ bugs.short_desc AS summary
+ FROM bugs, components, bugs_activity
+ WHERE bugs.product_id = ?
+ AND bugs.component_id = components.id
+ AND bugs.bug_status IN (" . join(',', quoted_closed_states()) . ")
+ AND bugs.bug_id = bugs_activity.bug_id
+ AND bugs_activity.added IN (" . join(',', quoted_closed_states()) . ")
+ $date_part
+ ORDER BY bugs.bug_id DESC $limit",
+ {'Slice' => {}}, @values);
+
+ return _filter_bugs($unfiltered_bugs);
+}
+
+sub _filter_bugs {
+ my ($unfiltered_bugs) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ return [] if !$unfiltered_bugs;
+
+ my @unfiltered_bug_ids = map { $_->{'id'} } @$unfiltered_bugs;
+ my %filtered_bug_ids = map { $_ => 1 } @{ Bugzilla->user->visible_bugs(\@unfiltered_bug_ids) };
+
+ my @filtered_bugs;
+ foreach my $bug (@$unfiltered_bugs) {
+ next if !$filtered_bug_ids{$bug->{'id'}};
+ push(@filtered_bugs, $bug);
+ }
+
+ return \@filtered_bugs;
+}
+
+1;
diff --git a/extensions/ProductDashboard/lib/Util.pm b/extensions/ProductDashboard/lib/Util.pm
new file mode 100644
index 000000000..d83ddf187
--- /dev/null
+++ b/extensions/ProductDashboard/lib/Util.pm
@@ -0,0 +1,116 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+package Bugzilla::Extension::ProductDashboard::Util;
+
+use strict;
+
+use base qw(Exporter);
+@Bugzilla::Extension::ProductDashboard::Util::EXPORT = qw(
+ bug_link_all
+ bug_link_open
+ bug_link_closed
+ open_states
+ closed_states
+ quoted_open_states
+ quoted_closed_states
+ filter_bugs
+ bug_milestone_link_total
+ bug_milestone_link_open
+ bug_milestone_link_closed
+);
+
+use Bugzilla::Status;
+use Bugzilla::Util;
+
+use Bugzilla::Status;
+
+our $_open_states;
+sub open_states {
+ $_open_states ||= Bugzilla::Status->match({ is_open => 1, isactive => 1 });
+ return wantarray ? @$_open_states : $_open_states;
+}
+
+our $_quoted_open_states;
+sub quoted_open_states {
+ my $dbh = Bugzilla->dbh;
+ $_quoted_open_states ||= [ map { $dbh->quote($_->name) } open_states() ];
+ return wantarray ? @$_quoted_open_states : $_quoted_open_states;
+}
+
+our $_closed_states;
+sub closed_states {
+ $_closed_states ||= Bugzilla::Status->match({ is_open => 0, isactive => 1 });
+ return wantarray ? @$_closed_states : $_closed_states;
+}
+
+our $_quoted_closed_states;
+sub quoted_closed_states {
+ my $dbh = Bugzilla->dbh;
+ $_quoted_closed_states ||= [ map { $dbh->quote($_->name) } closed_states() ];
+ return wantarray ? @$_quoted_closed_states : $_quoted_closed_states;
+}
+
+sub bug_link_all {
+ my $product = shift;
+
+ return correct_urlbase() . 'buglist.cgi?product=' . url_quote($product->name);
+}
+
+sub bug_link_open {
+ my $product = shift;
+
+ return correct_urlbase() . 'buglist.cgi?product=' . url_quote($product->name) .
+ "&bug_status=__open__";
+}
+
+sub bug_link_closed {
+ my $product = shift;
+
+ return correct_urlbase() . 'buglist.cgi?product=' . url_quote($product->name) .
+ "&bug_status=__closed__";
+}
+
+sub bug_milestone_link_total {
+ my ($product, $milestone) = @_;
+
+ return correct_urlbase() . 'buglist.cgi?product=' . url_quote($product->name) .
+ "&target_milestone=" . url_quote($milestone->name);
+}
+
+sub bug_milestone_link_open {
+ my ($product, $milestone) = @_;
+
+ return correct_urlbase() . 'buglist.cgi?product=' . url_quote($product->name) .
+ "&target_milestone=" . url_quote($milestone->name) . "&bug_status=__open__";
+}
+
+sub bug_milestone_link_closed {
+ my ($product, $milestone) = @_;
+
+ return correct_urlbase() . 'buglist.cgi?product=' . url_quote($product->name) .
+ "&target_milestone=" . url_quote($milestone->name) . "&bug_status=__closed__";
+}
+
+sub filter_bugs {
+ my ($unfiltered_bugs) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ # Filter out which bugs that cannot be viewed
+ my $params = Bugzilla::CGI->new({ bug_id => [ map { $_->{'id'} } @$unfiltered_bugs ] });
+ my $search = Bugzilla::Search->new(fields => ['bug_id' ], params => $params );
+ my %filtered_bug_ids = map { $_ => 1 } @{$dbh->selectcol_arrayref($search->getSQL())};
+
+ my @filtered_bugs;
+ foreach my $bug (@$unfiltered_bugs) {
+ next if !$filtered_bug_ids{$bug->{'id'}};
+ push(@filtered_bugs, $bug);
+ }
+
+ return \@filtered_bugs;
+}
+
+1;
diff --git a/extensions/ProductDashboard/template/en/default/hook/global/common-links-action-links.html.tmpl b/extensions/ProductDashboard/template/en/default/hook/global/common-links-action-links.html.tmpl
new file mode 100644
index 000000000..e9be8a13d
--- /dev/null
+++ b/extensions/ProductDashboard/template/en/default/hook/global/common-links-action-links.html.tmpl
@@ -0,0 +1,9 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+ <li><span class="separator"> | </span><a href="page.cgi?id=productdashboard.html">Product Dashboard</a></li>
diff --git a/extensions/ProductDashboard/template/en/default/hook/global/user-error-errors.html.tmpl b/extensions/ProductDashboard/template/en/default/hook/global/user-error-errors.html.tmpl
new file mode 100644
index 000000000..d8af64d31
--- /dev/null
+++ b/extensions/ProductDashboard/template/en/default/hook/global/user-error-errors.html.tmpl
@@ -0,0 +1,12 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF error == "product_dashboard_invalid_recent_days" %]
+ [% title = "Invalid Recent Days" %]
+ Invalid value for recent days.
+[% END %]
diff --git a/extensions/ProductDashboard/template/en/default/pages/productdashboard.html.tmpl b/extensions/ProductDashboard/template/en/default/pages/productdashboard.html.tmpl
new file mode 100644
index 000000000..daf0ea6df
--- /dev/null
+++ b/extensions/ProductDashboard/template/en/default/pages/productdashboard.html.tmpl
@@ -0,0 +1,217 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+
+[% filtered_product = product.name FILTER html %]
+[% PROCESS global/header.html.tmpl
+ title = "Product Dashboard: $filtered_product"
+ style_urls = [ "skins/standard/buglist.css",
+ "js/yui/assets/skins/sam/paginator.css",
+ "extensions/ProductDashboard/web/styles/productdashboard.css" ]
+ yui = [ "datatable", "paginator", "calendar" ]
+ javascript_urls = [ "js/util.js", "js/field.js",
+ "extensions/ProductDashboard/web/js/productdashboard.js" ]
+%]
+
+<script type="text/javascript">
+<!--
+ [%# Set up severities list for proper sorting %]
+ PD.severities = new Array();
+ [% sort_count = 0 %]
+ [% FOREACH s = severities %]
+ PD.severities['[% s FILTER js %]'] = [% sort_count FILTER js %];
+ [% sort_count = sort_count + 1 %]
+ [% END %]
+-->
+</script>
+
+[% url_filtered_product = product.name FILTER uri %]
+[% url_filtered_status = bug_status FILTER uri %]
+
+[% tabs = [
+ {
+ name => "summary",
+ label => "Summary",
+ link => "page.cgi?id=productdashboard.html&amp;product=$url_filtered_product&bug_status=$url_filtered_status&tab=summary"
+ },
+ {
+ name => "recents",
+ label => "Recents",
+ link => "page.cgi?id=productdashboard.html&amp;product=$url_filtered_product&bug_status=$url_filtered_status&tab=recents"
+ },
+ {
+ name => "components",
+ label => "Components/Versions",
+ link => "page.cgi?id=productdashboard.html&amp;product=$url_filtered_product&bug_status=$url_filtered_status&tab=components"
+ },
+ {
+ name => "duplicates",
+ label => "Duplicates",
+ link => "page.cgi?id=productdashboard.html&amp;product=$url_filtered_product&bug_status=$url_filtered_status&tab=duplicates"
+ },
+ {
+ name => "roadmap",
+ label => "Road Map",
+ link => "page.cgi?id=productdashboard.html&amp;product=$url_filtered_product&bug_status=$url_filtered_status&tab=roadmap"
+ },
+ ]
+%]
+
+[% IF product.votesperuser %]
+ [%
+ tabs.push({
+ name => "popularity",
+ label => "Popularity",
+ link => "page.cgi?id=productdashboard.html&amp;product=$url_filtered_product&bug_status=$url_filtered_status&tab=popularity"
+ })
+ %]
+[% END %]
+
+[% FOREACH tab IN tabs %]
+ [% IF tab.name == current_tab_name %]
+ [% current_tab = tab %]
+ [% LAST %]
+ [% END %]
+[% END %]
+
+[% full_bug_count = 0 %]
+[% IF bug_status == 'open' %]
+ [% full_bug_count = total_open_bugs %]
+[% ELSIF bug_status == 'closed' %]
+ [% full_bug_count = total_closed_bugs %]
+[% ELSE %]
+ [% full_bug_count = total_bugs %]
+[% END %]
+
+[% bug_link = bug_link_all %]
+[% IF bug_status == 'open' %]
+ [% bug_link = bug_link_open %]
+[% ELSIF bug_status == 'closed' %]
+ [% bug_link = bug_link_closed %]
+[% END %]
+
+<div class="yui-skin-sam">
+ <a name="top"></a>
+
+ <form action="page.cgi" method="get">
+ <input type="hidden" name="id" value="productdashboard.html">
+ <input type="hidden" name="tab" value="[% current_tab.name FILTER html %]">
+
+ [% IF summary.keys %]
+ <input type="hidden" name="[% summary.type FILTER html %]" value="[% summary.value FILTER html %]">
+ [% END %]
+
+ [% IF product %]
+ <span id="product_dashboard_links">
+ <ul>
+ <li><a href="[% urlbase FILTER none %]enter_bug.cgi?product=[% product.name FILTER uri %]">
+ Create a new [% terms.bug %] in this product</a></li>
+ <li><a href="[% urlbase FILTER none %]describecomponents.cgi?product=[% product.name FILTER uri %]">
+ Show full component descriptions for this product</a></li>
+ </ul>
+ </span>
+ [% END %]
+
+ <strong>Choose product:</strong>
+ <select name="product">
+ [% FOREACH c = classifications %]
+ <optgroup label="[% c.name FILTER html %]">
+ [% FOREACH p = c.products %]
+ <option value="[% p.name FILTER html %]"
+ [% IF p.name == product.name %]selected="selected"[% END %]>
+ [% p.name FILTER html %]</option>
+ [% END %]</optgroup>
+ [% END %]
+ </select>
+ <select name="bug_status" id="bug_status">
+ [% statuses = [ { name = 'open', label = "Open $terms.Bugs" },
+ { name = 'closed', label = "Closed $terms.Bugs" },
+ { name = 'all', label = "All $terms.Bugs" } ] %]
+ [% FOREACH status = statuses %]
+ <option value="[% status.name FILTER html %]"
+ [% " selected" IF bug_status == "${status.name}" %]>
+ [% status.label FILTER html %]
+ </option>
+ [% END %]
+ </select>
+
+ <input type="submit" value="[% IF product %]Change[% ELSE %]Submit[% END %]">
+
+ [% IF product %]
+ <div class="product_name">
+ [% product.name FILTER html %]
+ </div>
+
+ <div class="product_description">
+ [% product.description FILTER none %]
+ </div>
+
+ [% WRAPPER global/tabs.html.tmpl
+ tabs = tabs
+ current_tab = current_tab
+ %]
+
+ [% IF current_tab.name == 'summary' %]
+ [% PROCESS pages/productdashboard/summary.html.tmpl %]
+ [% END %]
+
+ [% IF current_tab.name == 'recents' %]
+ [% PROCESS pages/productdashboard/recents.html.tmpl %]
+ [% END %]
+
+ [% IF current_tab.name == 'components' %]
+ [% PROCESS pages/productdashboard/components.html.tmpl %]
+ [% END %]
+
+ [% IF current_tab.name == 'duplicates' %]
+ [% PROCESS pages/productdashboard/duplicates.html.tmpl %]
+ [% END %]
+
+ [% IF current_tab.name == 'popularity' %]
+ [% PROCESS pages/productdashboard/popularity.html.tmpl %]
+ [% END %]
+
+ [% IF current_tab.name == 'roadmap' && Param('usetargetmilestone') %]
+ [% PROCESS pages/productdashboard/roadmap.html.tmpl %]
+ [% END %]
+
+ [% END %][%# END WRAPPER %]
+ [% END %]
+
+ </form>
+</div>
+
+[% PROCESS global/footer.html.tmpl %]
+
+[% BLOCK bar_graph %]
+ [% IF full_bug_count > 0 %][%# No divide by zero %]
+ [% percentage_bugs = (count / full_bug_count) * 100 FILTER format('%02.2f') %]
+ [% ELSE %]
+ [% percentage_bugs = 0 %]
+ [% END %]
+ <div class="bar_graph">
+ <table cellpadding="0" cellspacing="0" width="300px">
+ <tr>
+ <td width="[% percentage_bugs FILTER html %]%">
+ <table cellpadding="0" cellspacing="0" width="100%">
+ <tr>
+ <td bgcolor="#3c78b5">
+ <a title="[% percentage_bugs FILTER html %]%">
+ <img src="extensions/ProductDashboard/web/images/spacer.gif" height=10 width="100%" title="[% percentage_bugs FILTER html %]%">
+ </a>
+ </td>
+ </tr>
+ </table>
+ </td>
+ <td width="[% 100 - percentage_bugs FILTER html %]%">&nbsp;&nbsp;&nbsp;[% percentage_bugs FILTER html %]%</td>
+ </tr>
+ </table>
+ </div>
+[% END %]
+
diff --git a/extensions/ProductDashboard/template/en/default/pages/productdashboard/components.html.tmpl b/extensions/ProductDashboard/template/en/default/pages/productdashboard/components.html.tmpl
new file mode 100644
index 000000000..0d2ac5e6f
--- /dev/null
+++ b/extensions/ProductDashboard/template/en/default/pages/productdashboard/components.html.tmpl
@@ -0,0 +1,266 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF summary.keys %]
+
+ <h3>Summary for [% summary.type FILTER html %]: [% summary.value FILTER html %]</h3>
+
+ <style>
+ .yui-skin-sam .yui-dt table {width:100%;}
+ </style>
+
+ <script type="text/javascript">
+ <!--
+ PD.options = {
+ paginator: new YAHOO.widget.Paginator({ rowsPerPage: 25, alwaysVisible: false })
+ };
+ PD.column_defs = [
+ { key:"id", label:"ID", sortable:true, sortOptions:{ sortFunction: PD.sortBugIdLinks } },
+ { key:"bug_status", label:"Status", sortable:true },
+ { key:"version", label:"Version", sortable:true },
+ { key:"component", label:"Component", sortable:true },
+ { key:"bug_severity", label:"Severity", sortable:true, sortOptions:{ sortFunction: PD.sortBugSeverity } },
+ { key:"Summary", label:"Summary", sortable:false },
+ ];
+ PD.fields = [
+ { key:"id" },
+ { key:"bug_status" },
+ { key:"version" },
+ { key:"component" },
+ { key:"bug_severity" },
+ { key:"Summary" }
+ ];
+ [% IF user.is_timetracker %]
+ PD.addStatListener("past_due", "past_due_table", PD.column_defs, PD.fields, PD.options);
+ [% END %]
+ PD.addStatListener("updated_recently", "updated_recently_table", PD.column_defs, PD.fields, PD.options);
+ -->
+ </script>
+
+ [% IF user.is_timetracker %]
+ <p>
+ <a href="#past_due">Past Due</a> |
+ <a href="#updated_recently">Updated Recently</a>
+ </p>
+ [% END %]
+
+ <div class="yui-skin-sam">
+
+ [% IF user.is_timetracker %]
+ <a name="past_due"></a>
+ <b>[% summary.past_due.size FILTER html %] Past Due [% terms.Bugs %]</b> (deadline is before today's date)
+ (<a href="[% bug_link FILTER html %]&amp;[% summary.type FILTER uri %]=[% summary.value FILTER uri %]&field0-0-0=deadline&type0-0-0=lessthan&value0-0-0=[% summary.timestamp FILTER uri %]&order=deadline">full list</a>)
+ <div id="past_due">
+ <table id="past_due_table" cellspacing="3" cellpadding="0" border="0" width="100%">
+ <thead>
+ <tr bgcolor="#CCCCCC">
+ [% FOREACH column = [ "ID", "Status", "Version", "Component", "Severity" "Summary" ] %]
+ <th>[% column FILTER html %]</th>
+ [% END %]
+ </tr>
+ </thead>
+ <tbody>
+ [% FOREACH bug = summary.past_due %]
+ [% count = loop.count() %]
+ <tr class="[%+ count % 2 == 1 ? "bz_row_odd" : "bz_row_even" -%]">
+ <td align="center"><a href="[% urlbase FILTER none %]show_bug.cgi?id=[% bug.id FILTER uri %]">
+ [% bug.id FILTER html %]</a></td>
+ <td align="center">[% bug.status FILTER html %]</td>
+ <td align="center">[% bug.version FILTER html %]</td>
+ <td align="center">[% bug.component FILTER html %]</td>
+ <td align="center">[% bug.severity FILTER html %]</td>
+ <td>[% bug.summary FILTER html %]</td>
+ </tr>
+ [% END %]
+ </tbody>
+ </table>
+ </div>
+ <br>
+ [% END %]
+
+ <a name="updated_recently"></a>
+ <b>[% summary.updated_recently.size FILTER html %] Most Recently Updated [% terms.Bugs %]</b>
+ [% IF user.is_timetracker %](<a href="#top">back to top</a>)[% END %]
+ (<a href="[% bug_link FILTER html %]&amp;[% summary.type FILTER uri %]=[% summary.value FILTER uri %]&order=changeddate DESC">full list</a>)
+ <div id="updated_recently">
+ <table id="updated_recently_table" cellspacing="3" cellpadding="0" border="0" width="100%">
+ <thead>
+ <tr bgcolor="#CCCCCC">
+ [% FOREACH column = [ "ID", "Status", "Version", "Component", "Severity" "Summary" ] %]
+ <th>[% column FILTER html %]</th>
+ [% END %]
+ </tr>
+ </thead>
+ <tbody>
+ [% FOREACH bug = summary.updated_recently %]
+ [% count = loop.count() %]
+ <tr class="[%+ count % 2 == 1 ? "bz_row_odd" : "bz_row_even" -%]">
+ <td align="center"><a href="[% urlbase FILTER none %]show_bug.cgi?id=[% bug.id FILTER uri %]">
+ [% bug.id FILTER html %]</a></td>
+ <td align="center">[% bug.status FILTER html %]</td>
+ <td align="center">[% bug.version FILTER html %]</td>
+ <td align="center">[% bug.component FILTER html %]</td>
+ <td align="center">[% bug.severity FILTER html %]</td>
+ <td>[% bug.summary FILTER html %]</td>
+ </tr>
+ [% END %]
+ </tbody>
+ </table>
+ </div>
+ </div>
+
+[% ELSE %]
+
+ <script type="text/javascript">
+ <!--
+ PD.column_defs = [
+ { key:"name", label:"Name", sortable:true },
+ { key:"count", label:"Count", sortable:true },
+ { key:"percentage", label:"Percentage", sortable:false },
+ { key:"bug_list", label:"[% terms.Bug %] List", sortable:false }
+ ];
+ PD.fields = [
+ { key:"name" },
+ { key:"count", parser:"number" },
+ { key:"percentage" },
+ { key:"bug_list" }
+ ];
+ PD.addStatListener("component_counts", "component_counts_table",
+ PD.column_defs, PD.fields,
+ { paginator: new YAHOO.widget.Paginator({ rowsPerPage: 25, alwaysVisible: false }) });
+ PD.addStatListener("version_counts", "version_counts_table",
+ PD.column_defs, PD.fields,
+ { paginator: new YAHOO.widget.Paginator({ rowsPerPage: 25, alwaysVisible: false }) });
+ PD.addStatListener("milestone_counts", "milestone_counts_table",
+ PD.column_defs, PD.fields,
+ { paginator: new YAHOO.widget.Paginator({ rowsPerPage: 25, alwaysVisible: false }) });
+ -->
+ </script>
+
+ [% summary_url = "page.cgi?id=productdashboard.html&amp;product=$url_filtered_product&bug_status=$url_filtered_status&tab=components" %]
+
+ <h3>[% terms.Bug %] counts per component, version and milestone.</h3>
+
+ <p>
+ <a href="#component">Component</a> |
+ <a href="#version">Version</a> |
+ <a href="#milestone">Milestone</a>
+ </p>
+
+ <p>Click on a value to show a list of most recently updated [% terms.bugs %].</p>
+
+ <div class="yui-skin-sam">
+ <a name="component"></a>
+ <b>Component</b>
+ <div id="component_counts">
+ <table id="component_counts_table" border="0" cellspacing="3" cellpadding="0">
+ <thead>
+ <tr>
+ <th>Name</th>
+ <th>Count</th>
+ <th>Percentage</th>
+ <th>[% terms.Bug %] List</th>
+ </tr>
+ </thead>
+ <tbody>
+ [% FOREACH col = by_component %]
+ <tr>
+ <td>
+ <a href="[% summary_url FILTER none %]&component=[% col.0 FILTER uri %]">
+ [% col.0 FILTER html %]</a>
+ </td>
+ <td align="right">
+ [% col.1 FILTER html %]
+ </td>
+ <td width="70%">
+ [% INCLUDE bar_graph count = col.1 %]
+ </td>
+ <td>
+ <a href="[% bug_link FILTER html %]&amp;component=[% col.0 FILTER uri %]">View</a>
+ </td>
+ </tr>
+ [% END %]
+ </tbody>
+ </table>
+ </div>
+ <br>
+ <a name="version"></a>
+ <b>Version</b>
+ (<a href="#top">back to top</a>)
+ <div id="version_counts">
+ <table id="version_counts_table" border="0" cellspacing="3" cellpadding="0">
+ <thead>
+ <tr>
+ <th>Name</th>
+ <th>Count</th>
+ <th>Percentage</th>
+ <th>[% terms.Bug %] List</th>
+ </tr>
+ </thead>
+ <tbody>
+ [% FOREACH col = by_version %]
+ <tr>
+ <td>
+ <a href="[% summary_url FILTER none %]&version=[% col.0 FILTER uri %]">
+ [% col.0 FILTER html %]</a>
+ </td>
+ <td align="right">
+ [% col.1 FILTER html %]
+ </td>
+ <td width="70%">
+ [% INCLUDE bar_graph count = col.1 %]
+ </td>
+ <td>
+ <a href="[% bug_link FILTER html %]&amp;version=[% col.0 FILTER uri %]">View</a>
+ </td>
+ </tr>
+ [% END %]
+ </tbody>
+ </table>
+ </div>
+
+ [% IF Param('usetargetmilestone') %]
+ <br>
+ <a name="milestone"></a>
+ <b>Milestone</b>
+ (<a href="#top">back to top</a>)
+ <div id="milestone_counts">
+ <table id="milestone_counts_table" border="0" cellspacing="3" cellpadding="0">
+ <thead>
+ <tr>
+ <th>Name</th>
+ <th>Count</th>
+ <th>Percentage</th>
+ <th>[% terms.Bug %] List</th>
+ </tr>
+ </thead>
+ <tbody>
+ [% FOREACH col = by_milestone %]
+ <tr>
+ <td>
+ <a href="[% summary_url FILTER none %]&target_milestone=[% col.0 FILTER uri %]">
+ [% col.0 FILTER html %]</a>
+ </td>
+ <td align="right">
+ [% col.1 FILTER html %]
+ </td>
+ <td width="70%">
+ [% INCLUDE bar_graph count = col.1 %]
+ </td>
+ <td>
+ <a href="[% bug_link FILTER html %]&amp;target_milestone=[% col.0 FILTER uri %]">View</a>
+ </td>
+ </tr>
+ [% END %]
+ </tbody>
+ </table>
+ </div>
+ [% END %]
+ </div>
+
+[% END %]
diff --git a/extensions/ProductDashboard/template/en/default/pages/productdashboard/duplicates.html.tmpl b/extensions/ProductDashboard/template/en/default/pages/productdashboard/duplicates.html.tmpl
new file mode 100644
index 000000000..bf1cdaeb1
--- /dev/null
+++ b/extensions/ProductDashboard/template/en/default/pages/productdashboard/duplicates.html.tmpl
@@ -0,0 +1,75 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+<style>
+ .yui-skin-sam .yui-dt table {width:100%;}
+</style>
+
+<script type="text/javascript">
+<!--
+PD.column_defs = [
+ { key:"id", label:"ID", sortable:true, sortOptions:{ sortFunction: PD.sortBugIdLinks } },
+ { key:"count", label:"Count", sortable:true },
+ { key:"bug_status", label:"Status", sortable:true },
+ { key:"version", label:"Version", sortable:true },
+ { key:"component", label:"Component", sortable:true },
+ { key:"bug_severity", label:"Severity", sortable:true, sortOptions:{ sortFunction: PD.sortBugSeverity } },
+ { key:"Summary", label:"Summary", sortable:false },
+];
+PD.fields = [
+ { key:"id" },
+ { key:"count", parser:"number" },
+ { key:"bug_status" },
+ { key:"version" },
+ { key:"component" },
+ { key:"bug_severity" },
+ { key:"Summary" }
+];
+PD.addStatListener("duplicate_counts", "duplicate_counts_table",
+ PD.column_defs, PD.fields,
+ { paginator: new YAHOO.widget.Paginator({ rowsPerPage: 25, alwaysVisible: false }) });
+-->
+</script>
+
+<h3>Most duplicated [% terms.bugs %]</h3>
+
+[% IF by_duplicate.size %]
+ <b>[% by_duplicate.size FILTER html %]&nbsp;[% terms.Bugs %] Found</b>
+ <div class="yui-skin-sam">
+ <div id="duplicate_counts">
+ <table id="duplicate_counts_table" cellspacing="3" cellpadding="0" border="0" width="100%">
+ <thead>
+ <tr bgcolor="#CCCCCC">
+ [% FOREACH column = [ "ID", "Dupe Count", "Status", "Version"
+ "Component", "Severity" "Summary" ] %]
+
+ <th>[% column FILTER html %]</th>
+ [% END %]
+ </tr>
+ </thead>
+ <tbody>
+ [% FOREACH bug = by_duplicate %]
+ [% count = loop.count() %]
+ <tr class="[%+ count % 2 == 1 ? "bz_row_odd" : "bz_row_even" -%]">
+ <td align="center"><a href="[% urlbase FILTER none %]show_bug.cgi?id=[% bug.id FILTER uri %]">
+ [% bug.id FILTER html %]</a></td>
+ <td align="center">[% bug.dupe_count FILTER html %]</td>
+ <td align="center">[% bug.status FILTER html %]</td>
+ <td align="center">[% bug.version FILTER html %]</td>
+ <td align="center">[% bug.component FILTER html %]</td>
+ <td align="center">[% bug.severity FILTER html %]</td>
+ <td>[% bug.summary FILTER html %]</td>
+ </tr>
+ [% END %]
+ </tbody>
+ </table>
+ </div>
+ </div>
+[% ELSE %]
+ <b>No duplicate [% terms.bugs %] found.</b>
+[% END %]
diff --git a/extensions/ProductDashboard/template/en/default/pages/productdashboard/popularity.html.tmpl b/extensions/ProductDashboard/template/en/default/pages/productdashboard/popularity.html.tmpl
new file mode 100644
index 000000000..5ecad3426
--- /dev/null
+++ b/extensions/ProductDashboard/template/en/default/pages/productdashboard/popularity.html.tmpl
@@ -0,0 +1,75 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+<style>
+ .yui-skin-sam .yui-dt table {width:100%;}
+</style>
+
+<script type="text/javascript">
+<!--
+PD.column_defs = [
+ { key:"id", label:"ID", sortable:true, sortOptions:{ sortFunction: PD.sortBugIdLinks } },
+ { key:"count", label:"Count", sortable:true },
+ { key:"bug_status", label:"Status", sortable:true },
+ { key:"version", label:"Version", sortable:true },
+ { key:"component", label:"Component", sortable:true },
+ { key:"bug_severity", label:"Severity", sortable:true, sortOptions:{ sortFunction: PD.sortBugSeverity } },
+ { key:"Summary", label:"Summary", sortable:false },
+];
+PD.fields = [
+ { key:"id" },
+ { key:"count", parser:"number" },
+ { key:"bug_status" },
+ { key:"version" },
+ { key:"component" },
+ { key:"bug_severity" },
+ { key:"Summary" }
+];
+PD.addStatListener("popularity_counts", "popularity_counts_table",
+ PD.column_defs, PD.fields,
+ { paginator: new YAHOO.widget.Paginator({ rowsPerPage: 25, alwaysVisible: false }) });
+-->
+</script>
+
+<h3>Most voted on [% terms.bugs %]</h3>
+
+[% IF by_popularity.size %]
+ <b>[% by_popularity.size FILTER html %]&nbsp;[% terms.Bugs %] Found</b>
+ <div class="yui-skin-sam">
+ <div id="popularity_counts">
+ <table id="popularity_counts_table" cellspacing="3" cellpadding="0" border="0" width="100%">
+ <thead>
+ <tr bgcolor="#CCCCCC">
+ [% FOREACH column = [ "ID", "Count", "Status", "Version"
+ "Component", "Severity" "Summary" ] %]
+
+ <th>[% column FILTER html %]</th>
+ [% END %]
+ </tr>
+ </thead>
+ <tbody>
+ [% FOREACH bug = by_popularity %]
+ [% count = loop.count() %]
+ <tr class="[%+ count % 2 == 1 ? "bz_row_odd" : "bz_row_even" -%]">
+ <td align="center"><a href="[% urlbase FILTER none %]show_bug.cgi?id=[% bug.id FILTER uri %]">
+ [% bug.id FILTER html %]</a></td>
+ <td align="center">[% bug.votes FILTER html %]</td>
+ <td align="center">[% bug.status FILTER html %]</td>
+ <td align="center">[% bug.version FILTER html %]</td>
+ <td align="center">[% bug.component FILTER html %]</td>
+ <td align="center">[% bug.severity FILTER html %]</td>
+ <td>[% bug.summary FILTER html %]</td>
+ </tr>
+ [% END %]
+ </tbody>
+ </table>
+ </div>
+ </div>
+[% ELSE %]
+ <b>No [% terms.bugs %] found.</b>
+[% END %]
diff --git a/extensions/ProductDashboard/template/en/default/pages/productdashboard/recents.html.tmpl b/extensions/ProductDashboard/template/en/default/pages/productdashboard/recents.html.tmpl
new file mode 100644
index 000000000..919a7da97
--- /dev/null
+++ b/extensions/ProductDashboard/template/en/default/pages/productdashboard/recents.html.tmpl
@@ -0,0 +1,135 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+<style>
+ .yui-skin-sam .yui-dt table {width:100%;}
+</style>
+
+<script type="text/javascript">
+<!--
+PD.column_defs = [
+ { key:"id", label:"ID", sortable:true, sortOptions:{ sortFunction: PD.sortBugIdLinks } },
+ { key:"bug_status", label:"Status", sortable:true },
+ { key:"version", label:"Version", sortable:true },
+ { key:"component", label:"Component", sortable:true },
+ { key:"bug_severity", label:"Severity", sortable:true, sortOptions:{ sortFunction: PD.sortBugSeverity } },
+ { key:"Summary", label:"Summary", sortable:false },
+];
+PD.fields = [
+ { key:"id" },
+ { key:"bug_status" },
+ { key:"version" },
+ { key:"component" },
+ { key:"bug_severity" },
+ { key:"Summary" }
+];
+PD.addStatListener("recently_opened", "recently_opened_table",
+ PD.column_defs, PD.fields,
+ { paginator: new YAHOO.widget.Paginator({ rowsPerPage: 25, alwaysVisible: false }) });
+
+PD.addStatListener("recently_closed", "recently_closed_table",
+ PD.column_defs, PD.fields,
+ { paginator: new YAHOO.widget.Paginator({ rowsPerPage: 25, alwaysVisible: false }) });
+-->
+</script>
+
+<h3>Most recently opened and closed [% terms.bugs %]</h3>
+
+<p>
+ Activity within the last <input type="text" size="4" name="recent_days"
+ value="[% recent_days FILTER html %]">
+ days (between 1 and 100) or from
+ <input name="date_from" size="10" id="date_from"
+ value="[% date_from FILTER html %]"
+ onchange="updateCalendarFromField(this)">
+ <button type="button" class="calendar_button"
+ id="button_calendar_date_from"
+ onclick="showCalendar('date_from')">
+ <span>Calendar</span>
+ </button>
+ <span id="con_calendar_date_from"></span>
+ to
+ <input name="date_to" size="10" id="date_to"
+ value="[% date_to FILTER html %]"
+ onchange="updateCalendarFromField(this)">
+ <button type="button" class="calendar_button"
+ id="button_calendar_date_to"
+ onclick="showCalendar('date_to')">
+ <span>Calendar</span>
+ </button>
+ <span id="con_calendar_date_to"></span>
+ <script type="text/javascript">
+ createCalendar('date_from')
+ createCalendar('date_to')
+ </script>
+ <input type="submit" name="change" value="Change">
+</p>
+<p>
+ <a href="#recently_opened">Recently Opened</a>
+ <span class="separator"> | </span>
+ <a href="#recently_closed">Recently Closed</a>
+</p>
+
+<div class="yui-skin-sam">
+ <a name="recently_opened"></a>
+ <b>[% recently_opened.size FILTER html %] Recently Opened [% terms.Bugs %]</b>
+ <div id="recently_opened">
+ <table id="recently_opened_table" cellspacing="3" cellpadding="0" border="0" width="100%">
+ <thead>
+ <tr bgcolor="#CCCCCC">
+ [% FOREACH column = [ "ID", "Status", "Version", "Component", "Severity" "Summary" ] %]
+ <th>[% column FILTER html %]</th>
+ [% END %]
+ </tr>
+ </thead>
+ <tbody>
+ [% FOREACH bug = recently_opened %]
+ [% count = loop.count() %]
+ <tr class="[%+ count % 2 == 1 ? "bz_row_odd" : "bz_row_even" -%]">
+ <td align="center"><a href="[% urlbase FILTER none %]show_bug.cgi?id=[% bug.id FILTER uri %]">
+ [% bug.id FILTER html %]</a></td>
+ <td align="center">[% bug.status FILTER html %]</td>
+ <td align="center">[% bug.version FILTER html %]</td>
+ <td align="center">[% bug.component FILTER html %]</td>
+ <td align="center">[% bug.severity FILTER html %]</td>
+ <td>[% bug.summary FILTER html %]</td>
+ </tr>
+ [% END %]
+ </tbody>
+ </table>
+ </div>
+ <br>
+ <a name="recently_closed"></a>
+ <b>[% recently_closed.size FILTER html %] Recently Closed [% terms.Bugs %]</b>
+ (<a href="#top">back to top</a>)
+ <div id="recently_closed">
+ <table id="recently_closed_table" cellspacing="3" cellpadding="0" border="0" width="100%">
+ <thead>
+ <tr bgcolor="#CCCCCC">
+ [% FOREACH column = [ "ID", "Status", "Version", "Component", "Severity" "Summary" ] %]
+ <th>[% column FILTER html %]</th>
+ [% END %]
+ </tr>
+ </thead>
+ <tbody>
+ [% FOREACH bug = recently_closed %]
+ [% count = loop.count() %]
+ <tr class="[%+ count % 2 == 1 ? "bz_row_odd" : "bz_row_even" -%]">
+ <td align="center"><a href="[% urlbase FILTER none %]show_bug.cgi?id=[% bug.id FILTER uri %]">
+ [% bug.id FILTER html %]</a></td>
+ <td align="center">[% bug.status FILTER html %]</td>
+ <td align="center">[% bug.version FILTER html %]</td>
+ <td align="center">[% bug.component FILTER html %]</td>
+ <td align="center">[% bug.severity FILTER html %]</td>
+ <td>[% bug.summary FILTER html %]</td>
+ </tr>
+ [% END %]
+ </tbody>
+ </table>
+ </div>
+</div>
diff --git a/extensions/ProductDashboard/template/en/default/pages/productdashboard/roadmap.html.tmpl b/extensions/ProductDashboard/template/en/default/pages/productdashboard/roadmap.html.tmpl
new file mode 100644
index 000000000..1597b7a36
--- /dev/null
+++ b/extensions/ProductDashboard/template/en/default/pages/productdashboard/roadmap.html.tmpl
@@ -0,0 +1,57 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+<script type="text/javascript">
+<!--
+PD.column_defs = [
+ { key:"milestone", label:"Milestone", sortable:true },
+ { key:"percentage complete", label:"Percentage Complete", sortable:false },
+ { key:"links", label:"Links", sortable:false },
+];
+PD.fields = [
+ { key:"milestone" },
+ { key:"percentage complete" },
+ { key:"links" }
+];
+PD.addStatListener("bug_milestones", "bug_milestones_table",
+ PD.column_defs, PD.fields,
+ { paginator: new YAHOO.widget.Paginator({ rowsPerPage: 25, alwaysVisible: false }) });
+-->
+</script>
+
+<h3>Percentage of [% terms.bug %] closure per milestone</h3>
+
+<div class="yui-skin-sam">
+<div id="bug_milestones">
+ <table id="bug_milestones_table" border="0" cellspacing="3" cellpadding="0">
+ <thead>
+ <tr>
+ <th>Milestone</th>
+ <th>Percentage Complete</th>
+ <th>Links</th>
+ </tr>
+ </thead>
+ <tbody>
+ [% FOREACH milestone = by_roadmap %]
+ <tr>
+ <td>[% milestone.name FILTER html %]</td>
+ <td width="70%">
+ [% INCLUDE bar_graph count = milestone.closed_bugs full_bug_count = milestone.total_bugs %]
+ </td>
+ <td>
+ <a href="[% milestone.link_closed FILTER html %]">
+ [% milestone.closed_bugs FILTER html %]</a>&nbsp;of&nbsp;
+ <a href="[% milestone.link_total FILTER html %]">
+ [% milestone.total_bugs FILTER html %]</a>&nbsp;bugs have been closed
+ </td>
+ </tr>
+ [% END %]
+ </tbody>
+ </table>
+ </div>
+</div>
diff --git a/extensions/ProductDashboard/template/en/default/pages/productdashboard/summary.html.tmpl b/extensions/ProductDashboard/template/en/default/pages/productdashboard/summary.html.tmpl
new file mode 100644
index 000000000..a7398c823
--- /dev/null
+++ b/extensions/ProductDashboard/template/en/default/pages/productdashboard/summary.html.tmpl
@@ -0,0 +1,217 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+<script type="text/javascript">
+<!--
+PD.column_defs = [
+ { key:"name", label:"Name", sortable:true },
+ { key:"count", label:"Count", sortable:true },
+ { key:"percentage", label:"Percentage", sortable:false }
+];
+PD.fields = [
+ { key:"name" },
+ { key:"count", parser:"number" },
+ { key:"percentage" }
+];
+PD.addStatListener("bug_counts", "bug_counts_table",
+ PD.column_defs, PD.fields,
+ { paginator: new YAHOO.widget.Paginator({ rowsPerPage: 25, alwaysVisible: false }) });
+PD.addStatListener("status_counts", "status_counts_table",
+ PD.column_defs, PD.fields,
+ { paginator: new YAHOO.widget.Paginator({ rowsPerPage: 25, alwaysVisible: false }) });
+PD.addStatListener("priority_counts", "priority_counts_table",
+ PD.column_defs, PD.fields,
+ { paginator: new YAHOO.widget.Paginator({ rowsPerPage: 25, alwaysVisible: false }) });
+PD.addStatListener("severity_counts", "severity_counts_table",
+ PD.column_defs, PD.fields,
+ { paginator: new YAHOO.widget.Paginator({ rowsPerPage: 25, alwaysVisible: false }) });
+PD.addStatListener("assignee_counts", "assignee_counts_table",
+ PD.column_defs, PD.fields,
+ { paginator: new YAHOO.widget.Paginator({ rowsPerPage: 25, alwaysVisible: false }) });
+-->
+</script>
+
+<h3>Summary of [% terms.bug %] counts</h3>
+
+<p>
+ <a href="#counts">Counts</a>
+ <span class="separator"> | </span>
+ <a href="#status">Status</a>
+ <span class="separator"> | </span>
+ <a href="#priority">Priority</a>
+ <span class="separator"> | </span>
+ <a href="#severity">Severity</a>
+ <span class="separator"> | </span>
+ <a href="#assignee">Assignee</a>
+</p>
+
+<div class="yui-skin-sam">
+ <a name="counts"></a>
+ <b>[% terms.Bug %] Counts</b>
+ <div id="bug_counts">
+ <table id="bug_counts_table" border="0" cellspacing="3" cellpadding="0">
+ <thead>
+ <tr>
+ <th>Name</th>
+ <th>Count</th>
+ <th>Percentage</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td><a href="[% bug_link_all FILTER html %]">Total [% terms.Bugs %]</a></td>
+ <td>[% total_bugs FILTER html %]</td>
+ <td>&nbsp;</td>
+ </tr>
+ <tr>
+ <td><a href="[% bug_link_open FILTER html %]">Open [% terms.Bugs %]</a></td>
+ <td>[% total_open_bugs FILTER html %]</td>
+ <td width="70%">
+ [% INCLUDE bar_graph count = total_open_bugs full_bug_count = total_bugs %]
+ </td>
+ </tr>
+ <tr>
+ <td><a href="[% bug_link_closed FILTER html %]">Closed [% terms.Bugs %]</a></td>
+ <td>[% total_closed_bugs FILTER html %]</td>
+ <td width="70%">
+ [% INCLUDE bar_graph count = total_closed_bugs full_bug_count = total_bugs %]
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ <br>
+ <a name="status"></a>
+ <b>Status</b>
+ (<a href="#top">back to top</a>)
+ <div id="status_counts">
+ <table id="status_counts_table" border="0" cellspacing="3" cellpadding="0">
+ <thead>
+ <tr>
+ <th>Name</th>
+ <th>Count</th>
+ <th>Percentage</th>
+ </tr>
+ </thead>
+ <tbody>
+ [% FOREACH col = by_status %]
+ [% NEXT IF col.0 == 'CLOSED' %]
+ <tr>
+ <td>
+ <a href="[% bug_link_all FILTER html %]&amp;bug_status=[% col.0 FILTER uri %]">
+ [% col.0 FILTER html %]</a>
+ </td>
+ <td>
+ [% col.1 FILTER html %]
+ </td>
+ <td width="70%">
+ [% INCLUDE bar_graph count = col.1 %]
+ </td>
+ </tr>
+ [% END %]
+ </tbody>
+ </table>
+ </div>
+ <br>
+ <a name="priority"></a>
+ <b>Priority</b>
+ (<a href="#top">back to top</a>)
+ <div id="priority_counts">
+ <table id="priority_counts_table" border="0" cellspacing="3" cellpadding="0">
+ <thead>
+ <tr>
+ <th>Name</th>
+ <th>Count</th>
+ <th>Percentage</th>
+ </tr>
+ </thead>
+ </tbody>
+ [% FOREACH col = by_priority %]
+ <tr>
+ <td>
+ <a href="[% bug_link FILTER html %]&amp;priority=[% col.0 FILTER uri %]">
+ [% col.0 FILTER html %]</a>
+ </td>
+ <td>
+ [% col.1 FILTER html %]
+ </td>
+ <td width="70%">
+ [% INCLUDE bar_graph count = col.1 %]
+ </td>
+ </tr>
+ [% END %]
+ </tbody>
+ </table>
+ </div>
+ <br>
+ <a name="severity"></a>
+ <b>Severity</b>
+ (<a href="#top">back to top</a>)
+ <div id="severity_counts">
+ <table id="severity_counts_table" border="0" cellspacing="3" cellpadding="0">
+ <thead>
+ <tr>
+ <th>Name</th>
+ <th>Count</th>
+ <th>Percentage</th>
+ </tr>
+ </thead>
+ <tbody>
+ [% FOREACH col = by_severity %]
+ <tr>
+ <td>
+ <a href="[% bug_link FILTER html %]&amp;bug_severity=[% col.0 FILTER uri %]">
+ [% col.0 FILTER html %]</a>
+ </td>
+ <td align="right">
+ [% col.1 FILTER html %]
+ </td>
+ <td width="70%">
+ [% INCLUDE bar_graph count = col.1 %]
+ </td>
+ </tr>
+ [% END %]
+ </tbody>
+ </table>
+ </div>
+ <br>
+ <a name="assignee"></a>
+ <b>Assignee</b>
+ (<a href="#top">back to top</a>)
+ <div id="assignee_counts">
+ <table id="assignee_counts_table" border="0" cellspacing="3" cellpadding="0">
+ <thead>
+ <tr>
+ <th>Name</th>
+ <th>Count</th>
+ <th>Percentage</th>
+ </tr>
+ </thead>
+ <tbody>
+ [% FOREACH col = by_assignee %]
+ <tr>
+ <td>
+ [% IF user.id %]
+ <a href="[% bug_link FILTER html %]&amp;emailassigned_to1=1&amp;emailtype1=exact&amp;email1=[% col.0.email FILTER uri %]">
+ [% col.0.email FILTER html %]</a>
+ [% ELSE %]
+ [% col.0.realname || "No Name" FILTER html %]
+ [% END %]
+ </td>
+ <td>
+ [% col.1 FILTER html %]
+ </td>
+ <td width="70%">
+ [% INCLUDE bar_graph count = col.1 %]
+ </td>
+ </tr>
+ [% END %]
+ </tbody>
+ </table>
+ </div>
+</div>
diff --git a/extensions/ProductDashboard/web/images/spacer.gif b/extensions/ProductDashboard/web/images/spacer.gif
new file mode 100644
index 000000000..fc2560981
--- /dev/null
+++ b/extensions/ProductDashboard/web/images/spacer.gif
Binary files differ
diff --git a/extensions/ProductDashboard/web/js/productdashboard.js b/extensions/ProductDashboard/web/js/productdashboard.js
new file mode 100644
index 000000000..56bc451ff
--- /dev/null
+++ b/extensions/ProductDashboard/web/js/productdashboard.js
@@ -0,0 +1,98 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0.
+ */
+
+YAHOO.namespace('ProductDashboard');
+
+var PD = YAHOO.ProductDashboard;
+
+PD.addStatListener = function (div_name, table_name, column_defs, fields, options) {
+ YAHOO.util.Event.addListener(window, "load", function() {
+ YAHOO.example.StatsFromMarkup = new function() {
+ this.myDataSource = new YAHOO.util.DataSource(YAHOO.util.Dom.get(table_name));
+ this.myDataSource.responseType = YAHOO.util.DataSource.TYPE_HTMLTABLE;
+ this.myDataSource.responseSchema = { fields:fields };
+ this.myDataTable = new YAHOO.widget.DataTable(div_name, column_defs, this.myDataSource, options);
+ this.myDataTable.subscribe("rowMouseoverEvent", this.myDataTable.onEventHighlightRow);
+ this.myDataTable.subscribe("rowMouseoutEvent", this.myDataTable.onEventUnhighlightRow);
+ };
+ });
+}
+
+// Custom sort handler to sort by bug id inside an anchor tag
+PD.sortBugIdLinks = function (a, b, desc) {
+ // Deal with empty values
+ if (!YAHOO.lang.isValue(a)) {
+ return (!YAHOO.lang.isValue(b)) ? 0 : 1;
+ }
+ else if(!YAHOO.lang.isValue(b)) {
+ return -1;
+ }
+ // Now we need to pull out the ID text and convert to Numbers
+ // First we do 'a'
+ var container = document.createElement("bug_id_link");
+ container.innerHTML = a.getData("id");
+ var anchors = container.getElementsByTagName("a");
+ var text = anchors[0].textContent;
+ if (text === undefined) text = anchors[0].innerText;
+ var new_a = new Number(text);
+ // Then we do 'b'
+ container.innerHTML = b.getData("id");
+ anchors = container.getElementsByTagName("a");
+ text = anchors[0].textContent;
+ if (text == undefined) text = anchors[0].innerText;
+ var new_b = new Number(text);
+
+ if (!desc) {
+ return YAHOO.util.Sort.compare(new_a, new_b);
+ }
+ else {
+ return YAHOO.util.Sort.compare(new_b, new_a);
+ }
+}
+
+// Custom sort handler for bug severities
+PD.sortBugSeverity = function (a, b, desc) {
+ // Deal with empty values
+ if (!YAHOO.lang.isValue(a)) {
+ return (!YAHOO.lang.isValue(b)) ? 0 : 1;
+ }
+ else if(!YAHOO.lang.isValue(b)) {
+ return -1;
+ }
+
+ var new_a = new Number(severities[YAHOO.lang.trim(a.getData('bug_severity'))]);
+ var new_b = new Number(severities[YAHOO.lang.trim(b.getData('bug_severity'))]);
+
+ if (!desc) {
+ return YAHOO.util.Sort.compare(new_a, new_b);
+ }
+ else {
+ return YAHOO.util.Sort.compare(new_b, new_a);
+ }
+}
+
+// Custom sort handler for bug priorities
+PD.sortBugPriority = function (a, b, desc) {
+ // Deal with empty values
+ if (!YAHOO.lang.isValue(a)) {
+ return (!YAHOO.lang.isValue(b)) ? 0 : 1;
+ }
+ else if(!YAHOO.lang.isValue(b)) {
+ return -1;
+ }
+
+ var new_a = new Number(priorities[YAHOO.lang.trim(a.getData('priority'))]);
+ var new_b = new Number(priorities[YAHOO.lang.trim(b.getData('priority'))]);
+
+ if (!desc) {
+ return YAHOO.util.Sort.compare(new_a, new_b);
+ }
+ else {
+ return YAHOO.util.Sort.compare(new_b, new_a);
+ }
+}
diff --git a/extensions/ProductDashboard/web/styles/productdashboard.css b/extensions/ProductDashboard/web/styles/productdashboard.css
new file mode 100644
index 000000000..1e821fa11
--- /dev/null
+++ b/extensions/ProductDashboard/web/styles/productdashboard.css
@@ -0,0 +1,25 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0. */
+
+#product_dashboard_links {
+ float: right;
+ padding-right: 25px;
+ border: 1px solid rgb(116, 126, 147);
+}
+
+.product_name {
+ font-size: 2em;
+ margin: 10px 0 10px 0;
+ color: rgb(109, 117, 129);
+}
+
+.product_description {
+ font-size: 90%;
+ font-style: italic;
+ padding-bottom: 5px;
+ margin-bottom: 10px;
+}
diff --git a/extensions/Profanivore/Config.pm b/extensions/Profanivore/Config.pm
new file mode 100644
index 000000000..354325c58
--- /dev/null
+++ b/extensions/Profanivore/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 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 <gerv@gerv.net>
+
+package Bugzilla::Extension::Profanivore;
+use strict;
+
+use constant NAME => 'Profanivore';
+
+use constant REQUIRED_MODULES => [
+ {
+ package => 'Regexp-Common',
+ module => 'Regexp::Common',
+ version => 0
+ },
+ {
+ package => 'HTML-Tree',
+ module => 'HTML::Tree',
+ version => 0,
+ }
+];
+
+__PACKAGE__->NAME;
diff --git a/extensions/Profanivore/Extension.pm b/extensions/Profanivore/Extension.pm
new file mode 100644
index 000000000..cdec6e1c6
--- /dev/null
+++ b/extensions/Profanivore/Extension.pm
@@ -0,0 +1,169 @@
+# -*- 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 <gerv@gerv.net>
+
+package Bugzilla::Extension::Profanivore;
+use strict;
+use base qw(Bugzilla::Extension);
+
+use Regexp::Common 'RE_ALL';
+
+use Bugzilla::Util qw(is_7bit_clean);
+
+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('-i'),
+ 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 && lc($author) ne lc($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 });
+
+ if ($author &&
+ $author->id &&
+ !$author->in_group('editbugs'))
+ {
+ # Multipart emails
+ if (scalar $email->parts > 1) {
+ $email->walk_parts(sub {
+ my ($part) = @_;
+ return if $part->parts > 1; # Top-level
+ # do not filter attachments such as patches, etc.
+ if ($part->header('Content-Disposition')
+ && $part->header('Content-Disposition') =~ /attachment/)
+ {
+ return;
+ }
+ _fix_encoding($part);
+ my $body = $part->body_str;
+ my $new_body;
+ if ($part->content_type =~ /^text\/html/) {
+ $new_body = _filter_html($body);
+ if ($new_body ne $body) {
+ # HTML::Tree removes unnecessary whitespace,
+ # resulting in very long lines. We need to use
+ # quoted-printable encoding to avoid exceeding
+ # email's maximum line length.
+ $part->encoding_set('quoted-printable');
+ }
+ }
+ elsif ($part->content_type =~ /^text\/plain/) {
+ $new_body = _filter_text($body);
+ }
+ if ($new_body && $new_body ne $body) {
+ $part->body_str_set($new_body);
+ }
+ });
+ }
+ # Single part email
+ else {
+ _fix_encoding($email);
+ $email->body_str_set(_filter_text($email->body_str));
+ }
+ }
+ }
+}
+
+sub _fix_encoding {
+ my $part = shift;
+ my $body = $part->body;
+ if (Bugzilla->params->{'utf8'}) {
+ $part->charset_set('UTF-8');
+ # encoding_set works only with bytes, not with utf8 strings.
+ my $raw = $part->body_raw;
+ if (utf8::is_utf8($raw)) {
+ utf8::encode($raw);
+ $part->body_set($raw);
+ }
+ }
+ $part->encoding_set('quoted-printable') if !is_7bit_clean($body);
+}
+
+sub _filter_text {
+ my $text = shift;
+ my $offensive = RE_profanity('-i');
+ $text =~ s/$offensive/****/g;
+ return $text;
+}
+
+sub _filter_html {
+ my $html = shift;
+ my $tree = HTML::Tree->new->parse_content($html);
+ my $comments_div = $tree->look_down( _tag => 'div', id => 'comments' );
+ return $html if !$comments_div;
+ my @comments = $comments_div->look_down( _tag => 'pre' );
+ my $dirty = 0;
+ foreach my $comment (@comments) {
+ _filter_html_node($comment, \$dirty);
+ }
+ return $dirty ? $tree->as_HTML : $html;
+}
+
+sub _filter_html_node {
+ my ($node, $dirty) = @_;
+ my $content = [ $node->content_list ];
+ foreach my $item_r ($node->content_refs_list) {
+ if (ref $$item_r) {
+ _filter_html_node($$item_r);
+ } else {
+ my $new_text = _filter_text($$item_r);
+ if ($new_text ne $$item_r) {
+ $$item_r = $new_text;
+ $$dirty = 1;
+ }
+ }
+ }
+}
+
+__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/Push/Config.pm b/extensions/Push/Config.pm
new file mode 100644
index 000000000..45cae9183
--- /dev/null
+++ b/extensions/Push/Config.pm
@@ -0,0 +1,61 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::Push;
+
+use strict;
+
+use constant NAME => 'Push';
+
+use constant REQUIRED_MODULES => [
+ {
+ package => 'Daemon-Generic',
+ module => 'Daemon::Generic',
+ version => '0'
+ },
+ {
+ package => 'JSON-XS',
+ module => 'JSON::XS',
+ version => '2.0'
+ },
+ {
+ package => 'Crypt-CBC',
+ module => 'Crypt::CBC',
+ version => '0'
+ },
+ {
+ package => 'Crypt-DES',
+ module => 'Crypt::DES',
+ version => '0'
+ },
+ {
+ package => 'Crypt-DES_EDE3',
+ module => 'Crypt::DES_EDE3',
+ version => '0'
+ },
+];
+
+use constant OPTIONAL_MODULES => [
+ # connectors need the ability to extend this
+ {
+ package => 'Net--RabbitMQ',
+ module => 'Net::RabbitMQ',
+ version => '0'
+ },
+ {
+ package => 'Net-SFTP',
+ module => 'Net::SFTP',
+ version => '0'
+ },
+ {
+ package => 'XML-Simple',
+ module => 'XML::Simple',
+ version => '0'
+ },
+];
+
+__PACKAGE__->NAME;
diff --git a/extensions/Push/Extension.pm b/extensions/Push/Extension.pm
new file mode 100644
index 000000000..f48a60210
--- /dev/null
+++ b/extensions/Push/Extension.pm
@@ -0,0 +1,637 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::Push;
+
+use strict;
+use warnings;
+
+use base qw(Bugzilla::Extension);
+
+use Bugzilla::Constants;
+use Bugzilla::Comment;
+use Bugzilla::Error;
+use Bugzilla::Extension::Push::Admin;
+use Bugzilla::Extension::Push::Connectors;
+use Bugzilla::Extension::Push::Logger;
+use Bugzilla::Extension::Push::Message;
+use Bugzilla::Extension::Push::Push;
+use Bugzilla::Extension::Push::Serialise;
+use Bugzilla::Extension::Push::Util;
+use Bugzilla::Install::Filesystem;
+
+use Encode;
+use Scalar::Util 'blessed';
+use Storable 'dclone';
+
+our $VERSION = '1';
+
+$Carp::CarpInternal{'CGI::Carp'} = 1;
+
+#
+# monkey patch for convience
+#
+
+BEGIN {
+ *Bugzilla::push_ext = \&_get_instance;
+}
+
+my $_instance;
+sub _get_instance {
+ if (!$_instance) {
+ $_instance = Bugzilla::Extension::Push::Push->new();
+ $_instance->logger(Bugzilla::Extension::Push::Logger->new());
+ $_instance->connectors(Bugzilla::Extension::Push::Connectors->new());
+ }
+ return $_instance;
+}
+
+#
+# enabled
+#
+
+sub _enabled {
+ my ($self) = @_;
+ if (!exists $self->{'enabled'}) {
+ my $push = Bugzilla->push_ext;
+ $self->{'enabled'} = $push->config->{enabled} eq 'Enabled';
+ if ($self->{'enabled'}) {
+ # if no connectors are enabled, no need to push anything
+ $self->{'enabled'} = 0;
+ foreach my $connector (Bugzilla->push_ext->connectors->list) {
+ if ($connector->enabled) {
+ $self->{'enabled'} = 1;
+ last;
+ }
+ }
+ }
+ }
+ return $self->{'enabled'};
+}
+
+#
+# deal with creation and updated events
+#
+
+sub _object_created {
+ my ($self, $args) = @_;
+
+ my $object = _get_object_from_args($args);
+ return unless $object;
+ return unless _should_push($object);
+
+ $self->_push_object('create', $object, change_set_id(), { timestamp => $args->{'timestamp'} });
+}
+
+sub _object_modified {
+ my ($self, $args) = @_;
+
+ my $object = _get_object_from_args($args);
+ return unless $object;
+ return unless _should_push($object);
+
+ my $changes = $args->{'changes'} || {};
+ return unless scalar keys %$changes;
+
+ my $change_set = change_set_id();
+
+ # detect when a bug changes from public to private (or back), so connectors
+ # can remove now-private bugs if required.
+ if ($object->isa('Bugzilla::Bug')) {
+ # we can't use user->can_see_bug(old_bug) as that works on IDs, and the
+ # bug has already been updated, so for now assume that a bug without
+ # groups is public.
+ my $old_bug = $args->{'old_bug'};
+ my $is_public = is_public($object);
+ my $was_public = $old_bug ? !@{$old_bug->groups_in} : $is_public;
+
+ if (!$is_public && $was_public) {
+ # bug is changing from public to private
+ # push a fake update with the just is_private change
+ my $private_changes = {
+ timestamp => $args->{'timestamp'},
+ changes => [
+ {
+ field => 'is_private',
+ removed => '0',
+ added => '1',
+ },
+ ],
+ };
+ # note we're sending the old bug object so we don't leak any
+ # security sensitive information.
+ $self->_push_object('modify', $old_bug, $change_set, $private_changes);
+ } elsif ($is_public && !$was_public) {
+ # bug is changing from private to public
+ # push a fake update with the just is_private change
+ my $private_changes = {
+ timestamp => $args->{'timestamp'},
+ changes => [
+ {
+ field => 'is_private',
+ removed => '1',
+ added => '0',
+ },
+ ],
+ };
+ # it's ok to send the new bug state here
+ $self->_push_object('modify', $object, $change_set, $private_changes);
+ }
+ }
+
+ # make flagtypes changes easier to process
+ if (exists $changes->{'flagtypes.name'}) {
+ _split_flagtypes($changes);
+ }
+
+ # TODO split group changes?
+
+ # restructure the changes hash
+ my $changes_data = {
+ timestamp => $args->{'timestamp'},
+ changes => [],
+ };
+ foreach my $field_name (sort keys %$changes) {
+ push @{$changes_data->{'changes'}}, {
+ field => $field_name,
+ removed => $changes->{$field_name}[0],
+ added => $changes->{$field_name}[1],
+ };
+ }
+
+ $self->_push_object('modify', $object, $change_set, $changes_data);
+}
+
+sub _get_object_from_args {
+ my ($args) = @_;
+ return get_first_value($args, qw(object bug flag group));
+}
+
+sub _should_push {
+ my ($object_or_class) = @_;
+ my $class = blessed($object_or_class) || $object_or_class;
+ return grep { $_ eq $class } qw(Bugzilla::Bug Bugzilla::Attachment Bugzilla::Comment);
+}
+
+# changes to bug flags are presented in a single field 'flagtypes.name' split
+# into individual fields
+sub _split_flagtypes {
+ my ($changes) = @_;
+
+ my @removed = _split_flagtype($changes->{'flagtypes.name'}->[0]);
+ my @added = _split_flagtype($changes->{'flagtypes.name'}->[1]);
+ delete $changes->{'flagtypes.name'};
+
+ foreach my $ra (@removed, @added) {
+ $changes->{$ra->[0]} = ['', ''];
+ }
+ foreach my $ra (@removed) {
+ my ($name, $value) = @$ra;
+ $changes->{$name}->[0] = $value;
+ }
+ foreach my $ra (@added) {
+ my ($name, $value) = @$ra;
+ $changes->{$name}->[1] = $value;
+ }
+}
+
+sub _split_flagtype {
+ my ($value) = @_;
+ my @result;
+ foreach my $change (split(/, /, $value)) {
+ my $requestee = '';
+ if ($change =~ s/\(([^\)]+)\)$//) {
+ $requestee = $1;
+ }
+ my ($name, $value) = $change =~ /^(.+)(.)$/;
+ $value .= " ($requestee)" if $requestee;
+ push @result, [ "flag.$name", $value ];
+ }
+ return @result;
+}
+
+# changes to attachment flags come in via flag_end_of_update which has a
+# completely different structure for reporting changes than
+# object_end_of_update. this morphs flag to object updates.
+sub _morph_flag_updates {
+ my ($args) = @_;
+
+ my @removed = _morph_flag_update($args->{'old_flags'});
+ my @added = _morph_flag_update($args->{'new_flags'});
+ delete $args->{'old_flags'};
+ delete $args->{'new_flags'};
+
+ my $changes = {};
+ foreach my $ra (@removed, @added) {
+ $changes->{$ra->[0]} = ['', ''];
+ }
+ foreach my $ra (@removed) {
+ my ($name, $value) = @$ra;
+ $changes->{$name}->[0] = $value;
+ }
+ foreach my $ra (@added) {
+ my ($name, $value) = @$ra;
+ $changes->{$name}->[1] = $value;
+ }
+
+ foreach my $flag (keys %$changes) {
+ if ($changes->{$flag}->[0] eq $changes->{$flag}->[1]) {
+ delete $changes->{$flag};
+ }
+ }
+
+ $args->{'changes'} = $changes;
+}
+
+sub _morph_flag_update {
+ my ($values) = @_;
+ my @result;
+ foreach my $change (@$values) {
+ $change =~ s/^[^:]+://;
+ my $requestee = '';
+ if ($change =~ s/\(([^\)]+)\)$//) {
+ $requestee = $1;
+ }
+ my ($name, $value) = $change =~ /^(.+)(.)$/;
+ $value .= " ($requestee)" if $requestee;
+ push @result, [ "flag.$name", $value ];
+ }
+ return @result;
+}
+
+#
+# serialise and insert into the table
+#
+
+sub _push_object {
+ my ($self, $message_type, $object, $change_set, $changes) = @_;
+ my $rh;
+
+ # serialise the object
+ my ($rh_object, $name) = Bugzilla::Extension::Push::Serialise->instance->object_to_hash($object);
+
+ if (!$rh_object) {
+ warn "empty hash from serialiser ($message_type $object)\n";
+ return;
+ }
+ $rh->{$name} = $rh_object;
+
+ # add in the events hash
+ my $rh_event = Bugzilla::Extension::Push::Serialise->instance->changes_to_event($changes);
+ return unless $rh_event;
+ $rh_event->{'action'} = $message_type;
+ $rh_event->{'target'} = $name;
+ $rh_event->{'change_set'} = $change_set;
+ $rh_event->{'routing_key'} = "$name.$message_type";
+ if (exists $rh_event->{'changes'}) {
+ $rh_event->{'routing_key'} .= ':' . join(',', map { $_->{'field'} } @{$rh_event->{'changes'}});
+ }
+ $rh->{'event'} = $rh_event;
+
+ # create message object
+ my $message = Bugzilla::Extension::Push::Message->new_transient({
+ payload => to_json($rh),
+ change_set => $change_set,
+ routing_key => $rh_event->{'routing_key'},
+ });
+
+ # don't hit the database unless there are interested connectors
+ my $should_push = 0;
+ foreach my $connector (Bugzilla->push_ext->connectors->list) {
+ next unless $connector->enabled;
+ next unless $connector->should_send($message);
+ $should_push = 1;
+ last;
+ }
+ return unless $should_push;
+
+ # insert into push table
+ $message->create_from_transient();
+}
+
+#
+# update/create hooks
+#
+
+sub object_end_of_create {
+ my ($self, $args) = @_;
+ return unless $self->_enabled;
+
+ # it's better to process objects from a non-generic end_of_create where
+ # possible; don't process them here to avoid duplicate messages
+ my $object = _get_object_from_args($args);
+ return if !$object ||
+ $object->isa('Bugzilla::Bug');
+
+ $self->_object_created($args);
+}
+
+sub object_end_of_update {
+ my ($self, $args) = @_;
+ return unless $self->_enabled;
+
+ # it's better to process objects from a non-generic end_of_update where
+ # possible; don't process them here to avoid duplicate messages
+ my $object = _get_object_from_args($args);
+ return if !$object ||
+ $object->isa('Bugzilla::Bug') ||
+ $object->isa('Bugzilla::Flag');
+
+ $self->_object_modified($args);
+}
+
+# process bugs once they are fully formed
+# object_end_of_update is triggered while a bug is being created
+sub bug_end_of_create {
+ my ($self, $args) = @_;
+ return unless $self->_enabled;
+ $self->_object_created($args);
+}
+
+sub bug_end_of_update {
+ my ($self, $args) = @_;
+ return unless $self->_enabled;
+ $self->_object_modified($args);
+}
+
+sub flag_end_of_update {
+ my ($self, $args) = @_;
+ return unless $self->_enabled;
+ _morph_flag_updates($args);
+ $self->_object_modified($args);
+}
+
+# comments in bugzilla 4.0 doesn't aren't included in the bug_end_of_* hooks,
+# this code uses custom hooks to trigger
+sub bug_comment_create {
+ my ($self, $args) = @_;
+ return unless $self->_enabled;
+
+ return unless _should_push('Bugzilla::Comment');
+ my $bug = $args->{'bug'} or return;
+ my $timestamp = $args->{'timestamp'} or return;
+
+ my $comments = Bugzilla::Comment->match({ bug_id => $bug->id, bug_when => $timestamp });
+
+ foreach my $comment (@$comments) {
+ if ($comment->body ne '') {
+ $self->_push_object('create', $comment, change_set_id(), { timestamp => $timestamp });
+ }
+ }
+}
+
+sub bug_comment_update {
+ my ($self, $args) = @_;
+ return unless $self->_enabled;
+
+ return unless _should_push('Bugzilla::Comment');
+ my $bug = $args->{'bug'} or return;
+ my $timestamp = $args->{'timestamp'} or return;
+
+ my $comment_id = $args->{'comment_id'};
+ if ($comment_id) {
+ # XXX this should set changes. only is_private changes will trigger this event
+ my $comment = Bugzilla::Comment->new($comment_id);
+ $self->_push_object('update', $comment, change_set_id(), { timestamp => $timestamp });
+
+ } else {
+ # when a bug is created, an update is also triggered; we don't want to sent
+ # update messages for the initial comment, or for empty comments
+ my $comments = Bugzilla::Comment->match({ bug_id => $bug->id, bug_when => $timestamp });
+ foreach my $comment (@$comments) {
+ if ($comment->body ne '' && $comment->count) {
+ $self->_push_object('create', $comment, change_set_id(), { timestamp => $timestamp });
+ }
+ }
+ }
+}
+
+#
+# admin hooks
+#
+
+sub page_before_template {
+ my ($self, $args) = @_;
+ my $page = $args->{'page_id'};
+ my $vars = $args->{'vars'};
+
+ if ($page eq 'push_config.html') {
+ Bugzilla->user->in_group('admin')
+ || ThrowUserError('auth_failure',
+ { group => 'admin',
+ action => 'access',
+ object => 'administrative_pages' });
+ admin_config($vars);
+
+ } elsif ($page eq 'push_queues.html'
+ || $page eq 'push_queues_view.html'
+ ) {
+ Bugzilla->user->in_group('admin')
+ || ThrowUserError('auth_failure',
+ { group => 'admin',
+ action => 'access',
+ object => 'administrative_pages' });
+ admin_queues($vars, $page);
+
+ } elsif ($page eq 'push_log.html') {
+ Bugzilla->user->in_group('admin')
+ || ThrowUserError('auth_failure',
+ { group => 'admin',
+ action => 'access',
+ object => 'administrative_pages' });
+ admin_log($vars);
+ }
+}
+
+#
+# installation/config hooks
+#
+
+sub db_schema_abstract_schema {
+ my ($self, $args) = @_;
+ $args->{'schema'}->{'push'} = {
+ FIELDS => [
+ id => {
+ TYPE => 'MEDIUMSERIAL',
+ NOTNULL => 1,
+ PRIMARYKEY => 1,
+ },
+ push_ts => {
+ TYPE => 'DATETIME',
+ NOTNULL => 1,
+ },
+ payload => {
+ TYPE => 'LONGTEXT',
+ NOTNULL => 1,
+ },
+ change_set => {
+ TYPE => 'VARCHAR(32)',
+ NOTNULL => 1,
+ },
+ routing_key => {
+ TYPE => 'VARCHAR(64)',
+ NOTNULL => 1,
+ },
+ ],
+ };
+ $args->{'schema'}->{'push_backlog'} = {
+ FIELDS => [
+ id => {
+ TYPE => 'MEDIUMSERIAL',
+ NOTNULL => 1,
+ PRIMARYKEY => 1,
+ },
+ message_id => {
+ TYPE => 'INT3',
+ NOTNULL => 1,
+ },
+ push_ts => {
+ TYPE => 'DATETIME',
+ NOTNULL => 1,
+ },
+ payload => {
+ TYPE => 'LONGTEXT',
+ NOTNULL => 1,
+ },
+ change_set => {
+ TYPE => 'VARCHAR(32)',
+ NOTNULL => 1,
+ },
+ routing_key => {
+ TYPE => 'VARCHAR(64)',
+ NOTNULL => 1,
+ },
+ connector => {
+ TYPE => 'VARCHAR(32)',
+ NOTNULL => 1,
+ },
+ attempt_ts => {
+ TYPE => 'DATETIME',
+ },
+ attempts => {
+ TYPE => 'INT2',
+ NOTNULL => 1,
+ },
+ last_error => {
+ TYPE => 'MEDIUMTEXT',
+ },
+ ],
+ INDEXES => [
+ push_backlog_idx => {
+ FIELDS => ['message_id', 'connector'],
+ TYPE => 'UNIQUE',
+ },
+ ],
+ };
+ $args->{'schema'}->{'push_backoff'} = {
+ FIELDS => [
+ id => {
+ TYPE => 'MEDIUMSERIAL',
+ NOTNULL => 1,
+ PRIMARYKEY => 1,
+ },
+ connector => {
+ TYPE => 'VARCHAR(32)',
+ NOTNULL => 1,
+ },
+ next_attempt_ts => {
+ TYPE => 'DATETIME',
+ },
+ attempts => {
+ TYPE => 'INT2',
+ NOTNULL => 1,
+ },
+ ],
+ INDEXES => [
+ push_backoff_idx => {
+ FIELDS => ['connector'],
+ TYPE => 'UNIQUE',
+ },
+ ],
+ };
+ $args->{'schema'}->{'push_options'} = {
+ FIELDS => [
+ id => {
+ TYPE => 'MEDIUMSERIAL',
+ NOTNULL => 1,
+ PRIMARYKEY => 1,
+ },
+ connector => {
+ TYPE => 'VARCHAR(32)',
+ NOTNULL => 1,
+ },
+ option_name => {
+ TYPE => 'VARCHAR(32)',
+ NOTNULL => 1,
+ },
+ option_value => {
+ TYPE => 'VARCHAR(255)',
+ NOTNULL => 1,
+ },
+ ],
+ INDEXES => [
+ push_options_idx => {
+ FIELDS => ['connector', 'option_name'],
+ TYPE => 'UNIQUE',
+ },
+ ],
+ };
+ $args->{'schema'}->{'push_log'} = {
+ FIELDS => [
+ id => {
+ TYPE => 'MEDIUMSERIAL',
+ NOTNULL => 1,
+ PRIMARYKEY => 1,
+ },
+ message_id => {
+ TYPE => 'INT3',
+ NOTNULL => 1,
+ },
+ change_set => {
+ TYPE => 'VARCHAR(32)',
+ NOTNULL => 1,
+ },
+ routing_key => {
+ TYPE => 'VARCHAR(64)',
+ NOTNULL => 1,
+ },
+ connector => {
+ TYPE => 'VARCHAR(32)',
+ NOTNULL => 1,
+ },
+ push_ts => {
+ TYPE => 'DATETIME',
+ NOTNULL => 1,
+ },
+ processed_ts => {
+ TYPE => 'DATETIME',
+ NOTNULL => 1,
+ },
+ result => {
+ TYPE => 'INT1',
+ NOTNULL => 1,
+ },
+ data => {
+ TYPE => 'MEDIUMTEXT',
+ },
+ ],
+ };
+}
+
+sub install_filesystem {
+ my ($self, $args) = @_;
+ my $files = $args->{'files'};
+
+ my $extensionsdir = bz_locations()->{'extensionsdir'};
+ my $scriptname = $extensionsdir . "/Push/bin/bugzilla-pushd.pl";
+
+ $files->{$scriptname} = {
+ perms => Bugzilla::Install::Filesystem::WS_EXECUTE
+ };
+}
+
+__PACKAGE__->NAME;
diff --git a/extensions/Push/bin/bugzilla-pushd.pl b/extensions/Push/bin/bugzilla-pushd.pl
new file mode 100755
index 000000000..f048df157
--- /dev/null
+++ b/extensions/Push/bin/bugzilla-pushd.pl
@@ -0,0 +1,54 @@
+#!/usr/bin/perl
+
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+use strict;
+use warnings;
+
+use FindBin '$RealBin';
+use lib "$RealBin/../../..";
+use lib "$RealBin/../../../lib";
+use lib "$RealBin/../lib";
+
+BEGIN {
+ use Bugzilla;
+ Bugzilla->extensions;
+}
+
+use Bugzilla::Extension::Push::Daemon;
+Bugzilla::Extension::Push::Daemon->start();
+
+=head1 NAME
+
+bugzilla-push.pl - Pushes changes queued by the Push extension to connectors.
+
+=head1 SYNOPSIS
+
+ bugzilla-push.pl [OPTIONS] COMMAND
+
+ OPTIONS:
+ -f Run in the foreground (don't detach)
+ -d Output a lot of debugging information
+ -p file Specify the file where bugzilla-push.pl should store its current
+ process id. Defaults to F<data/bugzilla-push.pl.pid>.
+ -n name What should this process call itself in the system log?
+ Defaults to the full path you used to invoke the script.
+
+ COMMANDS:
+ start Starts a new bugzilla-push daemon if there isn't one running already
+ stop Stops a running bugzilla-push daemon
+ restart Stops a running bugzilla-push if one is running, and then
+ starts a new one.
+ check Report the current status of the daemon.
+ install On some *nix systems, this automatically installs and
+ configures bugzilla-push.pl as a system service so that it will
+ start every time the machine boots.
+ uninstall Removes the system service for bugzilla-push.pl.
+ help Display this usage info
+
+
diff --git a/extensions/Push/lib/Admin.pm b/extensions/Push/lib/Admin.pm
new file mode 100644
index 000000000..d7df25c09
--- /dev/null
+++ b/extensions/Push/lib/Admin.pm
@@ -0,0 +1,121 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::Push::Admin;
+
+use strict;
+use warnings;
+
+use Bugzilla;
+use Bugzilla::Error;
+use Bugzilla::Extension::Push::Util;
+use Bugzilla::Util qw(trim detaint_natural);
+
+use base qw(Exporter);
+our @EXPORT = qw(
+ admin_config
+ admin_queues
+ admin_log
+);
+
+sub admin_config {
+ my ($vars) = @_;
+ my $push = Bugzilla->push_ext;
+ my $input = Bugzilla->input_params;
+
+ if ($input->{save}) {
+ my $dbh = Bugzilla->dbh;
+ $dbh->bz_start_transaction();
+ _update_config_from_form('global', $push->config);
+ foreach my $connector ($push->connectors->list) {
+ _update_config_from_form($connector->name, $connector->config);
+ }
+ $push->set_config_last_modified();
+ $dbh->bz_commit_transaction();
+ $vars->{message} = 'push_config_updated';
+ }
+
+ $vars->{push} = $push;
+ $vars->{connectors} = $push->connectors;
+}
+
+sub _update_config_from_form {
+ my ($name, $config) = @_;
+ my $input = Bugzilla->input_params;
+
+ # read values from form
+ my $values = {};
+ foreach my $option ($config->options) {
+ my $option_name = $option->{name};
+ $values->{$option_name} = trim($input->{$name . ".$option_name"});
+ }
+
+ # validate
+ if ($values->{enabled} eq 'Enabled') {
+ eval {
+ $config->validate($values);
+ };
+ if ($@) {
+ ThrowUserError('push_error', { error_message => clean_error($@) });
+ }
+ }
+
+ # update
+ foreach my $option ($config->options) {
+ my $option_name = $option->{name};
+ $config->{$option_name} = $values->{$option_name};
+ }
+ $config->update();
+}
+
+sub admin_queues {
+ my ($vars, $page) = @_;
+ my $push = Bugzilla->push_ext;
+ my $input = Bugzilla->input_params;
+
+ if ($page eq 'push_queues.html') {
+ $vars->{push} = $push;
+
+ } elsif ($page eq 'push_queues_view.html') {
+ my $queue;
+ if ($input->{connector}) {
+ my $connector = $push->connectors->by_name($input->{connector})
+ || ThrowUserError('push_error', { error_message => 'Invalid connector' });
+ $queue = $connector->backlog;
+ } else {
+ $queue = $push->queue;
+ }
+ $vars->{queue} = $queue;
+
+ my $id = $input->{message} || 0;
+ detaint_natural($id)
+ || ThrowUserError('push_error', { error_message => 'Invalid message ID' });
+ my $message = $queue->by_id($id)
+ || ThrowUserError('push_error', { error_message => 'Invalid message ID' });
+
+ if ($input->{delete}) {
+ $message->remove_from_db();
+ $vars->{message} = 'push_message_deleted';
+
+ } else {
+ $vars->{message_obj} = $message;
+ eval {
+ $vars->{json} = to_json($message->payload_decoded, 1);
+ };
+ }
+ }
+}
+
+sub admin_log {
+ my ($vars) = @_;
+ my $push = Bugzilla->push_ext;
+ my $input = Bugzilla->input_params;
+
+ $vars->{push} = $push;
+}
+
+1;
diff --git a/extensions/Push/lib/BacklogMessage.pm b/extensions/Push/lib/BacklogMessage.pm
new file mode 100644
index 000000000..f9496fa24
--- /dev/null
+++ b/extensions/Push/lib/BacklogMessage.pm
@@ -0,0 +1,145 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::Push::BacklogMessage;
+
+use strict;
+use warnings;
+
+use base 'Bugzilla::Object';
+
+use Bugzilla;
+use Bugzilla::Error;
+use Bugzilla::Extension::Push::Util;
+use Bugzilla::Util;
+use Encode;
+
+#
+# initialisation
+#
+
+use constant DB_TABLE => 'push_backlog';
+use constant DB_COLUMNS => qw(
+ id
+ message_id
+ push_ts
+ payload
+ change_set
+ routing_key
+ connector
+ attempt_ts
+ attempts
+ last_error
+);
+use constant UPDATE_COLUMNS => qw(
+ attempt_ts
+ attempts
+ last_error
+);
+use constant LIST_ORDER => 'push_ts';
+use constant VALIDATORS => {
+ payload => \&_check_payload,
+ change_set => \&_check_change_set,
+ routing_key => \&_check_routing_key,
+ connector => \&_check_connector,
+ attempts => \&_check_attempts,
+};
+
+#
+# constructors
+#
+
+sub create_from_message {
+ my ($class, $message, $connector) = @_;
+ my $self = $class->create({
+ message_id => $message->id,
+ push_ts => $message->push_ts,
+ payload => $message->payload,
+ change_set => $message->change_set,
+ routing_key => $message->routing_key,
+ connector => $connector->name,
+ attempt_ts => undef,
+ attempts => 0,
+ last_error => undef,
+ });
+ return $self;
+}
+
+#
+# accessors
+#
+
+sub message_id { return $_[0]->{'message_id'} }
+sub push_ts { return $_[0]->{'push_ts'}; }
+sub payload { return $_[0]->{'payload'}; }
+sub change_set { return $_[0]->{'change_set'}; }
+sub routing_key { return $_[0]->{'routing_key'}; }
+sub connector { return $_[0]->{'connector'}; }
+sub attempt_ts { return $_[0]->{'attempt_ts'}; }
+sub attempts { return $_[0]->{'attempts'}; }
+sub last_error { return $_[0]->{'last_error'}; }
+
+sub payload_decoded {
+ my ($self) = @_;
+ return from_json($self->{'payload'});
+}
+
+sub attempt_time {
+ my ($self) = @_;
+ if (!exists $self->{'attempt_time'}) {
+ $self->{'attempt_time'} = datetime_from($self->attempt_ts)->epoch;
+ }
+ return $self->{'attempt_time'};
+}
+
+#
+# mutators
+#
+
+sub inc_attempts {
+ my ($self, $error) = @_;
+ $self->{attempt_ts} = Bugzilla->dbh->selectrow_array('SELECT NOW()');
+ $self->{attempts} = $self->{attempts} + 1;
+ $self->{last_error} = $error;
+ $self->update;
+}
+
+#
+# validators
+#
+
+sub _check_payload {
+ my ($invocant, $value) = @_;
+ length($value) || ThrowCodeError('push_invalid_payload');
+ return $value;
+}
+
+sub _check_change_set {
+ my ($invocant, $value) = @_;
+ (defined($value) && length($value)) || ThrowCodeError('push_invalid_change_set');
+ return $value;
+}
+
+sub _check_routing_key {
+ my ($invocant, $value) = @_;
+ (defined($value) && length($value)) || ThrowCodeError('push_invalid_routing_key');
+ return $value;
+}
+
+sub _check_connector {
+ my ($invocant, $value) = @_;
+ Bugzilla->push_ext->connectors->exists($value) || ThrowCodeError('push_invalid_connector');
+ return $value;
+}
+
+sub _check_attempts {
+ my ($invocant, $value) = @_;
+ return $value || 0;
+}
+
+1;
+
diff --git a/extensions/Push/lib/BacklogQueue.pm b/extensions/Push/lib/BacklogQueue.pm
new file mode 100644
index 000000000..79b9b72ee
--- /dev/null
+++ b/extensions/Push/lib/BacklogQueue.pm
@@ -0,0 +1,127 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::Push::BacklogQueue;
+
+use strict;
+use warnings;
+
+use Bugzilla;
+use Bugzilla::Extension::Push::BacklogMessage;
+
+sub new {
+ my ($class, $connector) = @_;
+ my $self = {};
+ bless($self, $class);
+ $self->{connector} = $connector;
+ return $self;
+}
+
+sub count {
+ my ($self) = @_;
+ my $dbh = Bugzilla->dbh;
+ return $dbh->selectrow_array("
+ SELECT COUNT(*)
+ FROM push_backlog
+ WHERE connector = ?",
+ undef,
+ $self->{connector});
+}
+
+sub oldest {
+ my ($self) = @_;
+ my @messages = $self->list(
+ limit => 1,
+ filter => 'AND ((next_attempt_ts IS NULL) OR (next_attempt_ts <= NOW()))',
+ );
+ return scalar(@messages) ? $messages[0] : undef;
+}
+
+sub by_id {
+ my ($self, $id) = @_;
+ my @messages = $self->list(
+ limit => 1,
+ filter => "AND (log.id = $id)",
+ );
+ return scalar(@messages) ? $messages[0] : undef;
+}
+
+sub list {
+ my ($self, %args) = @_;
+ $args{limit} ||= 10;
+ $args{filter} ||= '';
+ my @result;
+ my $dbh = Bugzilla->dbh;
+
+ my $filter_sql = $args{filter} || '';
+ my $sth = $dbh->prepare("
+ SELECT log.id, message_id, push_ts, payload, change_set, routing_key, attempt_ts, log.attempts
+ FROM push_backlog log
+ LEFT JOIN push_backoff off ON off.connector = log.connector
+ WHERE log.connector = ? ".
+ $args{filter} . "
+ ORDER BY push_ts " .
+ $dbh->sql_limit($args{limit})
+ );
+ $sth->execute($self->{connector});
+ while (my $row = $sth->fetchrow_hashref()) {
+ push @result, Bugzilla::Extension::Push::BacklogMessage->new({
+ id => $row->{id},
+ message_id => $row->{message_id},
+ push_ts => $row->{push_ts},
+ payload => $row->{payload},
+ change_set => $row->{change_set},
+ routing_key => $row->{routing_key},
+ connector => $self->{connector},
+ attempt_ts => $row->{attempt_ts},
+ attempts => $row->{attempts},
+ });
+ }
+ return @result;
+}
+
+#
+# backoff
+#
+
+sub backoff {
+ my ($self) = @_;
+ if (!$self->{backoff}) {
+ my $ra = Bugzilla::Extension::Push::Backoff->match({
+ connector => $self->{connector}
+ });
+ if (@$ra) {
+ $self->{backoff} = $ra->[0];
+ } else {
+ $self->{backoff} = Bugzilla::Extension::Push::Backoff->create({
+ connector => $self->{connector}
+ });
+ }
+ }
+ return $self->{backoff};
+}
+
+sub reset_backoff {
+ my ($self) = @_;
+ my $backoff = $self->backoff;
+ $backoff->reset();
+ $backoff->update();
+}
+
+sub inc_backoff {
+ my ($self) = @_;
+ my $backoff = $self->backoff;
+ $backoff->inc();
+ $backoff->update();
+}
+
+sub connector {
+ my ($self) = @_;
+ return $self->{connector};
+}
+
+1;
diff --git a/extensions/Push/lib/Backoff.pm b/extensions/Push/lib/Backoff.pm
new file mode 100644
index 000000000..bc302a2a9
--- /dev/null
+++ b/extensions/Push/lib/Backoff.pm
@@ -0,0 +1,105 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::Push::Backoff;
+
+use strict;
+use warnings;
+
+use base 'Bugzilla::Object';
+
+use Bugzilla;
+use Bugzilla::Util;
+
+#
+# initialisation
+#
+
+use constant DB_TABLE => 'push_backoff';
+use constant DB_COLUMNS => qw(
+ id
+ connector
+ next_attempt_ts
+ attempts
+);
+use constant UPDATE_COLUMNS => qw(
+ next_attempt_ts
+ attempts
+);
+use constant VALIDATORS => {
+ connector => \&_check_connector,
+ next_attempt_ts => \&_check_next_attempt_ts,
+ attempts => \&_check_attempts,
+};
+use constant LIST_ORDER => 'next_attempt_ts';
+
+#
+# accessors
+#
+
+sub connector { return $_[0]->{'connector'}; }
+sub next_attempt_ts { return $_[0]->{'next_attempt_ts'}; }
+sub attempts { return $_[0]->{'attempts'}; }
+
+sub next_attempt_time {
+ my ($self) = @_;
+ if (!exists $self->{'next_attempt_time'}) {
+ $self->{'next_attempt_time'} = datetime_from($self->next_attempt_ts)->epoch;
+ }
+ return $self->{'next_attempt_time'};
+}
+
+#
+# mutators
+#
+
+sub reset {
+ my ($self) = @_;
+ $self->{next_attempt_ts} = Bugzilla->dbh->selectrow_array('SELECT NOW()');
+ $self->{attempts} = 0;
+ Bugzilla->push_ext->logger->debug(
+ sprintf("resetting backoff for %s", $self->connector)
+ );
+}
+
+sub inc {
+ my ($self) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ my $attempts = $self->attempts + 1;
+ my $seconds = $attempts <= 4 ? 5 ** $attempts : 15 * 60;
+ my ($date) = $dbh->selectrow_array("SELECT NOW() + " . $dbh->sql_interval($seconds, 'SECOND'));
+
+ $self->{next_attempt_ts} = $date;
+ $self->{attempts} = $attempts;
+ Bugzilla->push_ext->logger->debug(
+ sprintf("setting next attempt for %s to %s (attempt %s)", $self->connector, $date, $attempts)
+ );
+}
+
+#
+# validators
+#
+
+sub _check_connector {
+ my ($invocant, $value) = @_;
+ Bugzilla->push_ext->connectors->exists($value) || ThrowCodeError('push_invalid_connector');
+ return $value;
+}
+
+sub _check_next_attempt_ts {
+ my ($invocant, $value) = @_;
+ return $value || Bugzilla->dbh->selectrow_array('SELECT NOW()');
+}
+
+sub _check_attempts {
+ my ($invocant, $value) = @_;
+ return $value || 0;
+}
+
+1;
+
diff --git a/extensions/Push/lib/Config.pm b/extensions/Push/lib/Config.pm
new file mode 100644
index 000000000..31fa6af36
--- /dev/null
+++ b/extensions/Push/lib/Config.pm
@@ -0,0 +1,215 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::Push::Config;
+
+use strict;
+use warnings;
+
+use Bugzilla;
+use Bugzilla::Constants;
+use Bugzilla::Extension::Push::Option;
+use Crypt::CBC;
+
+sub new {
+ my ($class, $name, @options) = @_;
+ my $self = {
+ _name => $name
+ };
+ bless($self, $class);
+
+ $self->{_options} = [@options];
+ unshift @{$self->{_options}}, {
+ name => 'enabled',
+ label => 'Status',
+ help => '',
+ type => 'select',
+ values => [ 'Enabled', 'Disabled' ],
+ default => 'Disabled',
+ };
+
+ return $self;
+}
+
+sub options {
+ my ($self) = @_;
+ return @{$self->{_options}};
+}
+
+sub option {
+ my ($self, $name) = @_;
+ foreach my $option ($self->options) {
+ return $option if $option->{name} eq $name;
+ }
+ return undef;
+}
+
+sub load {
+ my ($self) = @_;
+ my $config = {};
+ my $logger = Bugzilla->push_ext->logger;
+
+ # prime $config with defaults
+ foreach my $rh ($self->options) {
+ $config->{$rh->{name}} = $rh->{default};
+ }
+
+ # override defaults with values from database
+ my $options = Bugzilla::Extension::Push::Option->match({
+ connector => $self->{_name},
+ });
+ foreach my $option (@$options) {
+ my $option_config = $self->option($option->name)
+ || next;
+ if ($option_config->{type} eq 'password') {
+ $config->{$option->name} = $self->_decrypt($option->value);
+ } else {
+ $config->{$option->name} = $option->value;
+ }
+ }
+
+ # validate when running from the daemon
+ if (Bugzilla->push_ext->is_daemon) {
+ $self->_validate_config($config);
+ }
+
+ # done, update self
+ foreach my $name (keys %$config) {
+ my $value = $self->option($name)->{type} eq 'password' ? '********' : $config->{$name};
+ $logger->debug(sprintf("%s: set %s=%s\n", $self->{_name}, $name, $value));
+ $self->{$name} = $config->{$name};
+ }
+}
+
+sub validate {
+ my ($self, $config) = @_;
+ $self->_validate_mandatory($config);
+ $self->_validate_config($config);
+}
+
+sub update {
+ my ($self) = @_;
+
+ my @valid_options = map { $_->{name} } $self->options;
+
+ my %options;
+ my $options_list = Bugzilla::Extension::Push::Option->match({
+ connector => $self->{_name},
+ });
+ foreach my $option (@$options_list) {
+ $options{$option->name} = $option;
+ }
+
+ # delete options which are no longer valid
+ foreach my $name (keys %options) {
+ if (!grep { $_ eq $name } @valid_options) {
+ $options{$name}->remove_from_db();
+ delete $options{$name};
+ }
+ }
+
+ # update options
+ foreach my $name (keys %options) {
+ my $option = $options{$name};
+ if ($self->option($name)->{type} eq 'password') {
+ $option->set_value($self->_encrypt($self->{$name}));
+ } else {
+ $option->set_value($self->{$name});
+ }
+ $option->update();
+ }
+
+ # add missing options
+ foreach my $name (@valid_options) {
+ next if exists $options{$name};
+ Bugzilla::Extension::Push::Option->create({
+ connector => $self->{_name},
+ option_name => $name,
+ option_value => $self->{$name},
+ });
+ }
+}
+
+sub _remove_invalid_options {
+ my ($self, $config) = @_;
+ my @names;
+ foreach my $rh ($self->options) {
+ push @names, $rh->{name};
+ }
+ foreach my $name (keys %$config) {
+ if ($name =~ /^_/ || !grep { $_ eq $name } @names) {
+ delete $config->{$name};
+ }
+ }
+}
+
+sub _validate_mandatory {
+ my ($self, $config) = @_;
+ $self->_remove_invalid_options($config);
+
+ my @missing;
+ foreach my $option ($self->options) {
+ next unless $option->{required};
+ my $name = $option->{name};
+ if (!exists $config->{$name} || !defined($config->{$name}) || $config->{$name} eq '') {
+ push @missing, $option;
+ }
+ }
+ if (@missing) {
+ my $connector = $self->{_name};
+ @missing = map { $_->{label} } @missing;
+ if (scalar @missing == 1) {
+ die "The option '$missing[0]' for the connector '$connector' is mandatory\n";
+ } else {
+ die "The following options for the connector '$connector' are mandatory:\n "
+ . join("\n ", @missing) . "\n";
+ }
+ }
+}
+
+sub _validate_config {
+ my ($self, $config) = @_;
+ $self->_remove_invalid_options($config);
+
+ my @errors;
+ foreach my $option ($self->options) {
+ my $name = $option->{name};
+ next unless exists $config->{$name} && exists $option->{validate};
+ eval {
+ $option->{validate}->($config->{$name}, $config);
+ };
+ push @errors, $@ if $@;
+ }
+ die join("\n", @errors) if @errors;
+
+ if ($self->{_name} ne 'global') {
+ my $class = 'Bugzilla::Extension::Push::Connector::' . $self->{_name};
+ $class->options_validate($config);
+ }
+}
+
+sub _cipher {
+ my ($self) = @_;
+ $self->{_cipher} ||= Crypt::CBC->new(
+ -key => Bugzilla->localconfig->{'site_wide_secret'},
+ -cipher => 'DES_EDE3');
+ return $self->{_cipher};
+}
+
+sub _decrypt {
+ my ($self, $value) = @_;
+ my $result;
+ eval { $result = $self->_cipher->decrypt_hex($value) };
+ return $@ ? '' : $result;
+}
+
+sub _encrypt {
+ my ($self, $value) = @_;
+ return $self->_cipher->encrypt_hex($value);
+}
+
+1;
diff --git a/extensions/Push/lib/Connector.disabled/ServiceNow.pm b/extensions/Push/lib/Connector.disabled/ServiceNow.pm
new file mode 100644
index 000000000..832cc9262
--- /dev/null
+++ b/extensions/Push/lib/Connector.disabled/ServiceNow.pm
@@ -0,0 +1,434 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::Push::Connector::ServiceNow;
+
+use strict;
+use warnings;
+
+use base 'Bugzilla::Extension::Push::Connector::Base';
+
+use Bugzilla::Attachment;
+use Bugzilla::Bug;
+use Bugzilla::Component;
+use Bugzilla::Constants;
+use Bugzilla::Extension::Push::Constants;
+use Bugzilla::Extension::Push::Serialise;
+use Bugzilla::Extension::Push::Util;
+use Bugzilla::Field;
+use Bugzilla::Mailer;
+use Bugzilla::Product;
+use Bugzilla::User;
+use Bugzilla::Util qw(trim trick_taint);
+use Email::MIME;
+use FileHandle;
+use LWP;
+use MIME::Base64;
+use Net::LDAP;
+
+use constant SEND_COMPONENTS => (
+ {
+ product => 'mozilla.org',
+ component => 'Server Operations: Desktop Issues',
+ },
+);
+
+sub options {
+ return (
+ {
+ name => 'bugzilla_user',
+ label => 'Bugzilla Service-Now User',
+ type => 'string',
+ default => 'service.now@bugzilla.tld',
+ required => 1,
+ validate => sub {
+ Bugzilla::User->new({ name => $_[0] })
+ || die "Invalid Bugzilla user ($_[0])\n";
+ },
+ },
+ {
+ name => 'ldap_scheme',
+ label => 'Mozilla LDAP Scheme',
+ type => 'select',
+ values => [ 'LDAP', 'LDAPS' ],
+ default => 'LDAPS',
+ required => 1,
+ },
+ {
+ name => 'ldap_host',
+ label => 'Mozilla LDAP Host',
+ type => 'string',
+ default => '',
+ required => 1,
+ },
+ {
+ name => 'ldap_user',
+ label => 'Mozilla LDAP Bind Username',
+ type => 'string',
+ default => '',
+ required => 1,
+ },
+ {
+ name => 'ldap_pass',
+ label => 'Mozilla LDAP Password',
+ type => 'password',
+ default => '',
+ required => 1,
+ },
+ {
+ name => 'ldap_poll',
+ label => 'Mozilla LDAP Poll Frequency',
+ type => 'string',
+ default => '3',
+ required => 1,
+ help => 'minutes',
+ validate => sub {
+ $_[0] =~ /\D/
+ && die "LDAP Poll Frequency must be an integer\n";
+ $_[0] == 0
+ && die "LDAP Poll Frequency cannot be less than one minute\n";
+ },
+ },
+ {
+ name => 'service_now_url',
+ label => 'Service Now JSON URL',
+ type => 'string',
+ default => 'https://mozilladev.service-now.com',
+ required => 1,
+ help => "Must start with https:// and end with ?JSON",
+ validate => sub {
+ $_[0] =~ m#^https://[^\.\/]+\.service-now\.com\/#
+ || die "Invalid Service Now JSON URL\n";
+ $_[0] =~ m#\?JSON$#
+ || die "Invalid Service Now JSON URL (must end with ?JSON)\n";
+ },
+ },
+ {
+ name => 'service_now_user',
+ label => 'Service Now JSON Username',
+ type => 'string',
+ default => '',
+ required => 1,
+ },
+ {
+ name => 'service_now_pass',
+ label => 'Service Now JSON Password',
+ type => 'password',
+ default => '',
+ required => 1,
+ },
+ );
+}
+
+sub options_validate {
+ my ($self, $config) = @_;
+ my $host = $config->{ldap_host};
+ trick_taint($host);
+ my $scheme = lc($config->{ldap_scheme});
+ eval {
+ my $ldap = Net::LDAP->new($host, scheme => $scheme, onerror => 'die', timeout => 5)
+ or die $!;
+ $ldap->bind($config->{ldap_user}, password => $config->{ldap_pass});
+ };
+ if ($@) {
+ die sprintf("Failed to connect to %s://%s/: %s\n", $scheme, $host, $@);
+ }
+}
+
+my $_instance;
+
+sub init {
+ my ($self) = @_;
+ $_instance = $self;
+}
+
+sub load_config {
+ my ($self) = @_;
+ $self->SUPER::load_config(@_);
+ $self->{bugzilla_user} ||= Bugzilla::User->new({ name => $self->config->{bugzilla_user} });
+}
+
+sub should_send {
+ my ($self, $message) = @_;
+
+ my $data = $message->payload_decoded;
+ my $bug_data = $self->_get_bug_data($data)
+ || return 0;
+
+ # we don't want to send the initial comment in a separate message
+ # because we inject it into the inital message
+ if (exists $data->{comment} && $data->{comment}->{number} == 0) {
+ return 0;
+ }
+
+ my $target = $data->{event}->{target};
+ unless ($target eq 'bug' || $target eq 'comment' || $target eq 'attachment') {
+ return 0;
+ }
+
+ # ensure the service-now user can see the bug
+ if (!$self->{bugzilla_user} || !$self->{bugzilla_user}->is_enabled) {
+ return 0;
+ }
+ $self->{bugzilla_user}->can_see_bug($bug_data->{id})
+ || return 0;
+
+ # don't push changes made by the service-now account
+ $data->{event}->{user}->{id} == $self->{bugzilla_user}->id
+ && return 0;
+
+ # filter based on the component
+ my $bug = Bugzilla::Bug->new($bug_data->{id});
+ my $send = 0;
+ foreach my $rh (SEND_COMPONENTS) {
+ if ($bug->product eq $rh->{product} && $bug->component eq $rh->{component}) {
+ $send = 1;
+ last;
+ }
+ }
+ return $send;
+}
+
+sub send {
+ my ($self, $message) = @_;
+ my $logger = Bugzilla->push_ext->logger;
+ my $config = $self->config;
+
+ # should_send intiailises bugzilla_user; make sure we return a useful error message
+ if (!$self->{bugzilla_user}) {
+ return (PUSH_RESULT_TRANSIENT, "Invalid bugzilla-user (" . $self->config->{bugzilla_user} . ")");
+ }
+
+ # load the bug
+ my $data = $message->payload_decoded;
+ my $bug_data = $self->_get_bug_data($data);
+ my $bug = Bugzilla::Bug->new($bug_data->{id});
+
+ if ($message->routing_key eq 'bug.create') {
+ # inject the comment into the data for new bugs
+ my $comment = shift @{ $bug->comments };
+ if ($comment->body ne '') {
+ $bug_data->{comment} = Bugzilla::Extension::Push::Serialise->instance->object_to_hash($comment, 1);
+ }
+
+ } elsif ($message->routing_key eq 'attachment.create') {
+ # inject the attachment payload
+ my $attachment = Bugzilla::Attachment->new($data->{attachment}->{id});
+ $data->{attachment}->{data} = encode_base64($attachment->data);
+ }
+
+ # map bmo login to ldap login and insert into json payload
+ $self->_add_ldap_logins($data, {});
+
+ # flatten json data
+ $self->_flatten($data);
+
+ # add sysparm_action
+ $data->{sysparm_action} = 'insert';
+
+ if ($logger->debugging) {
+ $logger->debug(to_json(ref($data) ? $data : from_json($data), 1));
+ }
+
+ # send to service-now
+ my $request = HTTP::Request->new(POST => $self->config->{service_now_url});
+ $request->content_type('application/json');
+ $request->content(to_json($data));
+ $request->authorization_basic($self->config->{service_now_user}, $self->config->{service_now_pass});
+
+ $self->{lwp} ||= LWP::UserAgent->new(agent => Bugzilla->params->{urlbase});
+ my $result = $self->{lwp}->request($request);
+
+ # http level errors
+ if (!$result->is_success) {
+ # treat these as transient
+ return (PUSH_RESULT_TRANSIENT, $result->status_line);
+ }
+
+ # empty response
+ if (length($result->content) == 0) {
+ # malformed request, treat as transient to allow code to fix
+ # may also be misconfiguration on servicenow, also transient
+ return (PUSH_RESULT_TRANSIENT, "Empty response");
+ }
+
+ # json errors
+ my $result_data;
+ eval {
+ $result_data = from_json($result->content);
+ };
+ if ($@) {
+ return (PUSH_RESULT_TRANSIENT, clean_error($@));
+ }
+ if ($logger->debugging) {
+ $logger->debug(to_json($result_data, 1));
+ }
+ if (exists $result_data->{error}) {
+ return (PUSH_RESULT_ERROR, $result_data->{error});
+ };
+
+ # malformed/unexpected json response
+ if (!exists $result_data->{records}
+ || ref($result_data->{records}) ne 'ARRAY'
+ || scalar(@{$result_data->{records}}) == 0
+ ) {
+ return (PUSH_RESULT_ERROR, "Malformed JSON response from ServiceNow: missing or empty 'records' array");
+ }
+
+ my $record = $result_data->{records}->[0];
+ if (ref($record) ne 'HASH') {
+ return (PUSH_RESULT_ERROR, "Malformed JSON response from ServiceNow: 'records' array does not contain an object");
+ }
+
+ # sys_id is the unique identifier for this action
+ if (!exists $record->{sys_id} || $record->{sys_id} eq '') {
+ return (PUSH_RESULT_ERROR, "Malformed JSON response from ServiceNow: 'records object' does not contain a valid sys_id");
+ }
+
+ # success
+ return (PUSH_RESULT_OK, "sys_id: " . $record->{sys_id});
+}
+
+sub _get_bug_data {
+ my ($self, $data) = @_;
+ my $target = $data->{event}->{target};
+ if ($target eq 'bug') {
+ return $data->{bug};
+ } elsif (exists $data->{$target}->{bug}) {
+ return $data->{$target}->{bug};
+ } else {
+ return;
+ }
+}
+
+sub _flatten {
+ # service-now expects a flat json object
+ my ($self, $data) = @_;
+
+ my $target = $data->{event}->{target};
+
+ # delete unnecessary deep objects
+ if ($target eq 'comment' || $target eq 'attachment') {
+ $data->{$target}->{bug_id} = $data->{$target}->{bug}->{id};
+ delete $data->{$target}->{bug};
+ }
+ delete $data->{event}->{changes};
+
+ $self->_flatten_hash($data, $data, 'u');
+}
+
+sub _flatten_hash {
+ my ($self, $base_hash, $hash, $prefix) = @_;
+ foreach my $key (keys %$hash) {
+ if (ref($hash->{$key}) eq 'HASH') {
+ $self->_flatten_hash($base_hash, $hash->{$key}, $prefix . "_$key");
+ } elsif (ref($hash->{$key}) ne 'ARRAY') {
+ $base_hash->{$prefix . "_$key"} = $hash->{$key};
+ }
+ delete $hash->{$key};
+ }
+}
+
+sub _add_ldap_logins {
+ my ($self, $rh, $cache) = @_;
+ if (exists $rh->{login}) {
+ my $login = $rh->{login};
+ $cache->{$login} ||= $self->_bmo_to_ldap($login);
+ Bugzilla->push_ext->logger->debug("BMO($login) --> LDAP(" . $cache->{$login} . ")");
+ $rh->{ldap} = $cache->{$login};
+ }
+ foreach my $key (keys %$rh) {
+ next unless ref($rh->{$key}) eq 'HASH';
+ $self->_add_ldap_logins($rh->{$key}, $cache);
+ }
+}
+
+sub _bmo_to_ldap {
+ my ($self, $login) = @_;
+ my $ldap = $self->_ldap_cache();
+
+ return '' unless $login =~ /\@mozilla\.(?:com|org)$/;
+
+ foreach my $check ($login, canon_email($login)) {
+ # check for matching bugmail entry
+ foreach my $mail (keys %$ldap) {
+ next unless $ldap->{$mail}{bugmail_canon} eq $check;
+ return $mail;
+ }
+
+ # check for matching mail
+ if (exists $ldap->{$check}) {
+ return $check;
+ }
+
+ # check for matching email alias
+ foreach my $mail (sort keys %$ldap) {
+ next unless grep { $check eq $_ } @{$ldap->{$mail}{aliases}};
+ return $mail;
+ }
+ }
+
+ return '';
+}
+
+sub _ldap_cache {
+ my ($self) = @_;
+ my $logger = Bugzilla->push_ext->logger;
+ my $config = $self->config;
+
+ # cache of all ldap entries; updated infrequently
+ if (!$self->{ldap_cache_time} || (time) - $self->{ldap_cache_time} > $config->{ldap_poll} * 60) {
+ $logger->debug('refreshing LDAP cache');
+
+ my $cache = {};
+
+ my $host = $config->{ldap_host};
+ trick_taint($host);
+ my $scheme = lc($config->{ldap_scheme});
+ my $ldap = Net::LDAP->new($host, scheme => $scheme, onerror => 'die')
+ or die $!;
+ $ldap->bind($config->{ldap_user}, password => $config->{ldap_pass});
+ foreach my $ldap_base ('o=com,dc=mozilla', 'o=org,dc=mozilla') {
+ my $result = $ldap->search(
+ base => $ldap_base,
+ scope => 'sub',
+ filter => '(mail=*)',
+ attrs => ['mail', 'bugzillaEmail', 'emailAlias', 'cn', 'employeeType'],
+ );
+ foreach my $entry ($result->entries) {
+ my ($name, $bugMail, $mail, $type) =
+ map { $entry->get_value($_) || '' }
+ qw(cn bugzillaEmail mail employeeType);
+ next if $type eq 'DISABLED';
+ $mail = lc $mail;
+ $bugMail = '' if $bugMail !~ /\@/;
+ $bugMail = trim($bugMail);
+ if ($bugMail =~ / /) {
+ $bugMail = (grep { /\@/ } split / /, $bugMail)[0];
+ }
+ $name =~ s/\s+/ /g;
+ $cache->{$mail}{name} = trim($name);
+ $cache->{$mail}{bugmail} = $bugMail;
+ $cache->{$mail}{bugmail_canon} = canon_email($bugMail);
+ $cache->{$mail}{aliases} = [];
+ foreach my $alias (
+ @{$entry->get_value('emailAlias', asref => 1) || []}
+ ) {
+ push @{$cache->{$mail}{aliases}}, canon_email($alias);
+ }
+ }
+ }
+
+ $self->{ldap_cache} = $cache;
+ $self->{ldap_cache_time} = (time);
+ }
+
+ return $self->{ldap_cache};
+}
+
+1;
+
diff --git a/extensions/Push/lib/Connector/AMQP.pm b/extensions/Push/lib/Connector/AMQP.pm
new file mode 100644
index 000000000..7b7d4aa72
--- /dev/null
+++ b/extensions/Push/lib/Connector/AMQP.pm
@@ -0,0 +1,230 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::Push::Connector::AMQP;
+
+use strict;
+use warnings;
+
+use base 'Bugzilla::Extension::Push::Connector::Base';
+
+use Bugzilla::Constants;
+use Bugzilla::Extension::Push::Constants;
+use Bugzilla::Extension::Push::Util;
+use Bugzilla::Util qw(generate_random_password);
+use DateTime;
+
+sub init {
+ my ($self) = @_;
+ $self->{mq} = 0;
+ $self->{channel} = 1;
+
+ if ($self->config->{queue}) {
+ $self->{queue_name} = $self->config->{queue};
+ } else {
+ my $queue_name = Bugzilla->params->{'urlbase'};
+ $queue_name =~ s#^https?://##;
+ $queue_name =~ s#/$#|#;
+ $queue_name .= generate_random_password(16);
+ $self->{queue_name} = $queue_name;
+ }
+}
+
+sub options {
+ return (
+ {
+ name => 'host',
+ label => 'AMQP Hostname',
+ type => 'string',
+ default => 'localhost',
+ required => 1,
+ },
+ {
+ name => 'port',
+ label => 'AMQP Port',
+ type => 'string',
+ default => '5672',
+ required => 1,
+ validate => sub {
+ $_[0] =~ /\D/ && die "Invalid port (must be numeric)\n";
+ },
+ },
+ {
+ name => 'username',
+ label => 'Username',
+ type => 'string',
+ default => 'guest',
+ required => 1,
+ },
+ {
+ name => 'password',
+ label => 'Password',
+ type => 'password',
+ default => 'guest',
+ required => 1,
+ },
+ {
+ name => 'vhost',
+ label => 'Virtual Host',
+ type => 'string',
+ default => '/',
+ required => 1,
+ },
+ {
+ name => 'exchange',
+ label => 'Exchange',
+ type => 'string',
+ default => '',
+ required => 1,
+ },
+ {
+ name => 'queue',
+ label => 'Queue',
+ type => 'string',
+ },
+ );
+}
+
+sub stop {
+ my ($self) = @_;
+ if ($self->{mq}) {
+ Bugzilla->push_ext->logger->debug('AMQP: disconnecting');
+ $self->{mq}->disconnect();
+ $self->{mq} = 0;
+ }
+}
+
+sub _connect {
+ my ($self) = @_;
+ my $logger = Bugzilla->push_ext->logger;
+ my $config = $self->config;
+
+ $self->stop();
+
+ $logger->debug('AMQP: Connecting to RabbitMQ ' . $config->{host} . ':' . $config->{port});
+ require Net::RabbitMQ;
+ my $mq = Net::RabbitMQ->new();
+ $mq->connect(
+ $config->{host},
+ {
+ port => $config->{port},
+ user => $config->{username},
+ password => $config->{password},
+ }
+ );
+ $self->{mq} = $mq;
+
+ $logger->debug('AMQP: Opening channel ' . $self->{channel});
+ $self->{mq}->channel_open($self->{channel});
+
+ $logger->debug('AMQP: Declaring queue ' . $self->{queue_name});
+ $self->{mq}->queue_declare(
+ $self->{channel},
+ $self->{queue_name},
+ {
+ passive => 0,
+ durable => 1,
+ exclusive => 0,
+ auto_delete => 0,
+ },
+ );
+}
+
+sub _bind {
+ my ($self, $message) = @_;
+ my $logger = Bugzilla->push_ext->logger;
+ my $config = $self->config;
+
+ # bind to queue (also acts to verify the connection is still valid)
+ if ($self->{mq}) {
+ eval {
+ $logger->debug('AMQP: binding queue(' . $self->{queue_name} . ') with exchange(' . $config->{exchange} . ')');
+ $self->{mq}->queue_bind(
+ $self->{channel},
+ $self->{queue_name},
+ $config->{exchange},
+ $message->routing_key,
+ );
+ };
+ if ($@) {
+ $logger->debug('AMQP: ' . clean_error($@));
+ $self->{mq} = 0;
+ }
+ }
+
+}
+
+sub should_send {
+ my ($self, $message) = @_;
+ my $logger = Bugzilla->push_ext->logger;
+
+ my $payload = $message->payload_decoded();
+ my $target = $payload->{event}->{target};
+ my $is_private = $payload->{$target}->{is_private} ? 1 : 0;
+ if (!$is_private && exists $payload->{$target}->{bug}) {
+ $is_private = $payload->{$target}->{bug}->{is_private} ? 1 : 0;
+ }
+
+ if ($is_private) {
+ # we only want to push the is_private message from the change_set, as
+ # this is guaranteed to contain public information only
+ if ($message->routing_key !~ /\.modify:is_private$/) {
+ $logger->debug('AMQP: Ignoring private message');
+ return 0;
+ }
+ $logger->debug('AMQP: Sending change of message to is_private');
+ }
+ return 1;
+}
+
+sub send {
+ my ($self, $message) = @_;
+ my $logger = Bugzilla->push_ext->logger;
+ my $config = $self->config;
+
+ # don't push comments to pulse
+ if ($message->routing_key =~ /^comment\./) {
+ $logger->debug('AMQP: Ignoring comment');
+ return PUSH_RESULT_IGNORED;
+ }
+
+ # don't push private data
+ $self->should_push($message)
+ || return PUSH_RESULT_IGNORED;
+
+ $self->_bind($message);
+
+ eval {
+ # reconnect if required
+ if (!$self->{mq}) {
+ $self->_connect();
+ }
+
+ # send message
+ $logger->debug('AMQP: Publishing message');
+ $self->{mq}->publish(
+ $self->{channel},
+ $message->routing_key,
+ $message->payload,
+ {
+ exchange => $config->{exchange},
+ },
+ {
+ content_type => 'text/plain',
+ content_encoding => '8bit',
+ },
+ );
+ };
+ if ($@) {
+ return (PUSH_RESULT_TRANSIENT, clean_error($@));
+ }
+
+ return PUSH_RESULT_OK;
+}
+
+1;
+
diff --git a/extensions/Push/lib/Connector/Base.pm b/extensions/Push/lib/Connector/Base.pm
new file mode 100644
index 000000000..290ea9740
--- /dev/null
+++ b/extensions/Push/lib/Connector/Base.pm
@@ -0,0 +1,106 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::Push::Connector::Base;
+
+use strict;
+use warnings;
+
+use Bugzilla;
+use Bugzilla::Extension::Push::Config;
+use Bugzilla::Extension::Push::BacklogMessage;
+use Bugzilla::Extension::Push::BacklogQueue;
+use Bugzilla::Extension::Push::Backoff;
+
+sub new {
+ my ($class) = @_;
+ my $self = {};
+ bless($self, $class);
+ ($self->{name}) = $class =~ /^.+:(.+)$/;
+ $self->init();
+ return $self;
+}
+
+sub name {
+ my $self = shift;
+ return $self->{name};
+}
+
+sub init {
+ my ($self) = @_;
+ # abstract
+ # perform any initialisation here
+ # will be run when created by the web pages or by the daemon
+ # and also when the configuration needs to be reloaded
+}
+
+sub stop {
+ my ($self) = @_;
+ # abstract
+ # run from the daemon only; disconnect from remote hosts, etc
+}
+
+sub should_send {
+ my ($self, $message) = @_;
+ # abstract
+ # return boolean indicating if the connector will be sending the message.
+ # this will be called each message, and should be a very quick simple test.
+ # the connector can perform a more exhaustive test in the send() method.
+ return 0;
+}
+
+sub send {
+ my ($self, $message) = @_;
+ # abstract
+ # deliver the message, daemon only
+}
+
+sub options {
+ my ($self) = @_;
+ # abstract
+ # return an array of configuration variables
+ return ();
+}
+
+sub options_validate {
+ my ($class, $config) = @_;
+ # abstract, static
+ # die if a combination of options in $config is invalid
+}
+
+#
+#
+#
+
+sub config {
+ my ($self) = @_;
+ if (!$self->{config}) {
+ $self->load_config();
+ }
+ return $self->{config};
+}
+
+sub load_config {
+ my ($self) = @_;
+ my $config = Bugzilla::Extension::Push::Config->new($self->name, $self->options);
+ $config->load();
+ $self->{config} = $config;
+}
+
+sub enabled {
+ my ($self) = @_;
+ return $self->config->{enabled} eq 'Enabled';
+}
+
+sub backlog {
+ my ($self) = @_;
+ $self->{backlog} ||= Bugzilla::Extension::Push::BacklogQueue->new($self->name);
+ return $self->{backlog};
+}
+
+1;
+
diff --git a/extensions/Push/lib/Connector/File.pm b/extensions/Push/lib/Connector/File.pm
new file mode 100644
index 000000000..2a8f4193d
--- /dev/null
+++ b/extensions/Push/lib/Connector/File.pm
@@ -0,0 +1,68 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::Push::Connector::File;
+
+use strict;
+use warnings;
+
+use base 'Bugzilla::Extension::Push::Connector::Base';
+
+use Bugzilla::Constants;
+use Bugzilla::Extension::Push::Constants;
+use Bugzilla::Extension::Push::Util;
+use Encode;
+use FileHandle;
+
+sub init {
+ my ($self) = @_;
+}
+
+sub options {
+ return (
+ {
+ name => 'filename',
+ label => 'Filename',
+ type => 'string',
+ default => 'push.log',
+ required => 1,
+ validate => sub {
+ my $filename = shift;
+ $filename =~ m#^/#
+ && die "Absolute paths are not permitted\n";
+ },
+ },
+ );
+}
+
+sub should_send {
+ my ($self, $message) = @_;
+ return 1;
+}
+
+sub send {
+ my ($self, $message) = @_;
+
+ # pretty-format json payload
+ my $payload = $message->payload_decoded;
+ $payload = to_json($payload, 1);
+
+ my $filename = bz_locations()->{'datadir'} . '/' . $self->config->{filename};
+ Bugzilla->push_ext->logger->debug("File: Appending to $filename");
+ my $fh = FileHandle->new(">>$filename");
+ $fh->binmode(':utf8');
+ $fh->print(
+ "[" . scalar(localtime) . "]\n" .
+ $payload . "\n\n"
+ );
+ $fh->close;
+
+ return PUSH_RESULT_OK;
+}
+
+1;
+
diff --git a/extensions/Push/lib/Connector/TCL.pm b/extensions/Push/lib/Connector/TCL.pm
new file mode 100644
index 000000000..b6e531b8f
--- /dev/null
+++ b/extensions/Push/lib/Connector/TCL.pm
@@ -0,0 +1,241 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::Push::Connector::TCL;
+
+use strict;
+use warnings;
+
+use base 'Bugzilla::Extension::Push::Connector::Base';
+
+use Bugzilla::Constants;
+use Bugzilla::Extension::Push::Constants;
+use Bugzilla::Extension::Push::Serialise;
+use Bugzilla::Extension::Push::Util;
+use Bugzilla::User;
+
+use Digest::MD5 qw(md5_hex);
+use File::Temp;
+
+sub options {
+ return (
+ {
+ name => 'tcl_user',
+ label => 'Bugzilla TCL User',
+ type => 'string',
+ default => 'tcl@bugzilla.tld',
+ required => 1,
+ validate => sub {
+ Bugzilla::User->new({ name => $_[0] })
+ || die "Invalid Bugzilla user ($_[0])\n";
+ },
+ },
+ {
+ name => 'sftp_host',
+ label => 'SFTP Host',
+ type => 'string',
+ default => '',
+ required => 1,
+ },
+ {
+ name => 'sftp_port',
+ label => 'SFTP Port',
+ type => 'string',
+ default => '22',
+ required => 1,
+ validate => sub {
+ $_[0] =~ /\D/ && die "SFTP Port must be an integer\n";
+ },
+ },
+ {
+ name => 'sftp_user',
+ label => 'SFTP Username',
+ type => 'string',
+ default => '',
+ required => 1,
+ },
+ {
+ name => 'sftp_pass',
+ label => 'SFTP Password',
+ type => 'password',
+ default => '',
+ required => 1,
+ },
+ {
+ name => 'sftp_remote_path',
+ label => 'SFTP Remote Path',
+ type => 'string',
+ default => '',
+ required => 0,
+ },
+ );
+}
+
+my $_instance;
+
+sub init {
+ my ($self) = @_;
+ $_instance = $self;
+}
+
+sub load_config {
+ my ($self) = @_;
+ $self->SUPER::load_config(@_);
+}
+
+sub should_send {
+ my ($self, $message) = @_;
+
+ my $data = $message->payload_decoded;
+ my $bug_data = $self->_get_bug_data($data)
+ || return 0;
+
+ # sanity check user
+ $self->{tcl_user} ||= Bugzilla::User->new({ name => $self->config->{tcl_user} });
+ if (!$self->{tcl_user} || !$self->{tcl_user}->is_enabled) {
+ return 0;
+ }
+
+ # only send bugs created by the tcl user
+ unless ($bug_data->{reporter}->{id} == $self->{tcl_user}->id) {
+ return 0;
+ }
+
+ # don't push changes made by the tcl user
+ if ($data->{event}->{user}->{id} == $self->{tcl_user}->id) {
+ return 0;
+ }
+
+ # send comments
+ if ($data->{event}->{routing_key} eq 'comment.create') {
+ return 0 if $data->{comment}->{is_private};
+ return 1;
+ }
+
+ # send status and resolution updates
+ foreach my $change (@{ $data->{event}->{changes} }) {
+ return 1 if $change->{field} eq 'bug_status' || $change->{field} eq 'resolution';
+ }
+
+ # and nothing else
+ return 0;
+}
+
+sub send {
+ my ($self, $message) = @_;
+ my $logger = Bugzilla->push_ext->logger;
+ my $config = $self->config;
+
+ require XML::Simple;
+ require Net::SFTP;
+
+ $self->{tcl_user} ||= Bugzilla::User->new({ name => $self->config->{tcl_user} });
+ if (!$self->{tcl_user}) {
+ return (PUSH_RESULT_TRANSIENT, "Invalid bugzilla-user (" . $self->config->{tcl_user} . ")");
+ }
+
+ # load the bug
+ my $data = $message->payload_decoded;
+ my $bug_data = $self->_get_bug_data($data);
+
+ # build payload
+ my %xml = (
+ Mozilla_ID => $bug_data->{id},
+ When => $data->{event}->{time},
+ Who => $data->{event}->{user}->{login},
+ Status => $bug_data->{status}->{name},
+ Resolution => $bug_data->{resolution},
+ );
+ if ($data->{event}->{routing_key} eq 'comment.create') {
+ $xml{Comment} = $data->{comment}->{body};
+ }
+
+ # convert to xml
+ my $xml = XML::Simple::XMLout(
+ \%xml,
+ NoAttr => 1,
+ RootName => 'sync',
+ XMLDecl => 1,
+ );
+
+ # generate md5
+ my $md5 = md5_hex($xml);
+
+ # build filename
+ my ($sec, $min, $hour, $day, $mon, $year) = localtime(time);
+ my $change_set = $data->{event}->{change_set};
+ $change_set =~ s/\.//g;
+ my $filename = sprintf(
+ '%04s%02d%02d%02d%02d%02d%s',
+ $year + 1900,
+ $mon + 1,
+ $day,
+ $hour,
+ $min,
+ $sec,
+ $change_set,
+ );
+
+ # create temp files;
+ my $temp_dir = File::Temp->newdir();
+ my $local_dir = $temp_dir->dirname;
+ _write_file("$local_dir/$filename.sync", $xml);
+ _write_file("$local_dir/$filename.sync.check", $md5);
+ _write_file("$local_dir/$filename.done", '');
+
+ my $remote_dir = $self->config->{sftp_remote_path} eq ''
+ ? ''
+ : $self->config->{sftp_remote_path} . '/';
+
+ # send files via sftp
+ $logger->debug("Connecting to " . $self->config->{sftp_host} . ":" . $self->config->{sftp_port});
+ my $sftp = Net::SFTP->new(
+ $self->config->{sftp_host},
+ ssh_args => {
+ port => $self->config->{sftp_port},
+ },
+ user => $self->config->{sftp_user},
+ password => $self->config->{sftp_pass},
+ );
+
+ $logger->debug("Uploading $local_dir/$filename.add");
+ $sftp->put("$local_dir/$filename.add", "$remote_dir$filename.add")
+ or return (PUSH_RESULT_ERROR, "Failed to upload $local_dir/$filename.add");
+
+ $logger->debug("Uploading $local_dir/$filename.add.check");
+ $sftp->put("$local_dir/$filename.add.check", "$remote_dir$filename.add.check")
+ or return (PUSH_RESULT_ERROR, "Failed to upload $local_dir/$filename.add.check");
+
+ $logger->debug("Uploading $local_dir/$filename.done");
+ $sftp->put("$local_dir/$filename.done", "$remote_dir$filename.done")
+ or return (PUSH_RESULT_ERROR, "Failed to upload $local_dir/$filename.done");
+
+ # success
+ return (PUSH_RESULT_OK, "uploaded $filename.add");
+}
+
+sub _get_bug_data {
+ my ($self, $data) = @_;
+ my $target = $data->{event}->{target};
+ if ($target eq 'bug') {
+ return $data->{bug};
+ } elsif (exists $data->{$target}->{bug}) {
+ return $data->{$target}->{bug};
+ } else {
+ return;
+ }
+}
+
+sub _write_file {
+ my ($filename, $content) = @_;
+ open(my $fh, ">$filename") or die "Failed to write to $filename: $!\n";
+ print $fh $content;
+ close($fh) or die "Failed to write to $filename: $!\n";
+}
+
+1;
+
diff --git a/extensions/Push/lib/Connectors.pm b/extensions/Push/lib/Connectors.pm
new file mode 100644
index 000000000..e765b4a43
--- /dev/null
+++ b/extensions/Push/lib/Connectors.pm
@@ -0,0 +1,115 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::Push::Connectors;
+
+use strict;
+use warnings;
+
+use Bugzilla::Extension::Push::Util;
+use Bugzilla::Constants;
+use Bugzilla::Util qw(trick_taint);
+use File::Basename;
+
+sub new {
+ my ($class) = @_;
+ my $self = {};
+ bless($self, $class);
+
+ $self->{names} = [];
+ $self->{objects} = {};
+ $self->{path} = bz_locations->{'extensionsdir'} . '/Push/lib/Connector';
+
+ my $logger = Bugzilla->push_ext->logger;
+ foreach my $file (glob($self->{path} . '/*.pm')) {
+ my $name = basename($file);
+ $name =~ s/\.pm$//;
+ next if $name eq 'Base';
+ if (length($name) > 32) {
+ $logger->info("Ignoring connector '$name': Name longer than 32 characters");
+ }
+ push @{$self->{names}}, $name;
+ $logger->debug("Found connector '$name'");
+ }
+
+ return $self;
+}
+
+sub _load {
+ my ($self) = @_;
+ return if scalar keys %{$self->{objects}};
+
+ my $logger = Bugzilla->push_ext->logger;
+ foreach my $name (@{$self->{names}}) {
+ next if exists $self->{objects}->{$name};
+ my $file = $self->{path} . "/$name.pm";
+ trick_taint($file);
+ require $file;
+ my $package = "Bugzilla::Extension::Push::Connector::$name";
+
+ $logger->debug("Loading connector '$name'");
+ my $old_error_mode = Bugzilla->error_mode;
+ Bugzilla->error_mode(ERROR_MODE_DIE);
+ eval {
+ my $connector = $package->new();
+ $connector->load_config();
+ $self->{objects}->{$name} = $connector;
+ };
+ if ($@) {
+ $logger->error("Connector '$name' failed to load: " . clean_error($@));
+ }
+ Bugzilla->error_mode($old_error_mode);
+ }
+}
+
+sub stop {
+ my ($self) = @_;
+ my $logger = Bugzilla->push_ext->logger;
+ foreach my $connector ($self->list) {
+ next unless $connector->enabled;
+ $logger->debug("Stopping '" . $connector->name . "'");
+ eval {
+ $connector->stop();
+ };
+ if ($@) {
+ $logger->error("Connector '" . $connector->name . "' failed to stop: " . clean_error($@));
+ $logger->debug("Connector '" . $connector->name . "' failed to stop: $@");
+ }
+ }
+}
+
+sub reload {
+ my ($self) = @_;
+ $self->stop();
+ $self->{objects} = {};
+ $self->_load();
+}
+
+sub names {
+ my ($self) = @_;
+ return @{$self->{names}};
+}
+
+sub list {
+ my ($self) = @_;
+ $self->_load();
+ return sort { $a->name cmp $b->name } values %{$self->{objects}};
+}
+
+sub exists {
+ my ($self, $name) = @_;
+ $self->by_name($name) ? 1 : 0;
+}
+
+sub by_name {
+ my ($self, $name) = @_;
+ return unless exists $self->{objects}->{$name};
+ return $self->{objects}->{$name};
+}
+
+1;
+
diff --git a/extensions/Push/lib/Constants.pm b/extensions/Push/lib/Constants.pm
new file mode 100644
index 000000000..18b12d511
--- /dev/null
+++ b/extensions/Push/lib/Constants.pm
@@ -0,0 +1,41 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::Push::Constants;
+
+use strict;
+use base 'Exporter';
+
+our @EXPORT = qw(
+ PUSH_RESULT_OK
+ PUSH_RESULT_IGNORED
+ PUSH_RESULT_TRANSIENT
+ PUSH_RESULT_ERROR
+ PUSH_RESULT_UNKNOWN
+ push_result_to_string
+
+ POLL_INTERVAL_SECONDS
+);
+
+use constant PUSH_RESULT_OK => 1;
+use constant PUSH_RESULT_IGNORED => 2;
+use constant PUSH_RESULT_TRANSIENT => 3;
+use constant PUSH_RESULT_ERROR => 4;
+use constant PUSH_RESULT_UNKNOWN => 5;
+
+sub push_result_to_string {
+ my ($result) = @_;
+ return 'OK' if $result == PUSH_RESULT_OK;
+ return 'OK-IGNORED' if $result == PUSH_RESULT_IGNORED;
+ return 'TRANSIENT-ERROR' if $result == PUSH_RESULT_TRANSIENT;
+ return 'FATAL-ERROR' if $result == PUSH_RESULT_ERROR;
+ return 'UNKNOWN' if $result == PUSH_RESULT_UNKNOWN;
+}
+
+use constant POLL_INTERVAL_SECONDS => 30;
+
+1;
diff --git a/extensions/Push/lib/Daemon.pm b/extensions/Push/lib/Daemon.pm
new file mode 100644
index 000000000..66e15783e
--- /dev/null
+++ b/extensions/Push/lib/Daemon.pm
@@ -0,0 +1,96 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::Push::Daemon;
+
+use strict;
+use warnings;
+
+use Bugzilla::Constants;
+use Bugzilla::Extension::Push::Push;
+use Bugzilla::Extension::Push::Logger;
+use Carp qw(confess);
+use Daemon::Generic;
+use File::Basename;
+use Pod::Usage;
+
+sub start {
+ newdaemon();
+}
+
+#
+# daemon::generic config
+#
+
+sub gd_preconfig {
+ my $self = shift;
+ my $pidfile = $self->{gd_args}{pidfile};
+ if (!$pidfile) {
+ $pidfile = bz_locations()->{datadir} . '/' . $self->{gd_progname} . ".pid";
+ }
+ return (pidfile => $pidfile);
+}
+
+sub gd_getopt {
+ my $self = shift;
+ $self->SUPER::gd_getopt();
+ if ($self->{gd_args}{progname}) {
+ $self->{gd_progname} = $self->{gd_args}{progname};
+ } else {
+ $self->{gd_progname} = basename($0);
+ }
+ $self->{_original_zero} = $0;
+ $0 = $self->{gd_progname};
+}
+
+sub gd_postconfig {
+ my $self = shift;
+ $0 = delete $self->{_original_zero};
+}
+
+sub gd_more_opt {
+ my $self = shift;
+ return (
+ 'pidfile=s' => \$self->{gd_args}{pidfile},
+ 'n=s' => \$self->{gd_args}{progname},
+ );
+}
+
+sub gd_usage {
+ pod2usage({ -verbose => 0, -exitval => 'NOEXIT' });
+ return 0;
+};
+
+sub gd_redirect_output {
+ my $self = shift;
+
+ my $filename = bz_locations()->{datadir} . '/' . $self->{gd_progname} . ".log";
+ open(STDERR, ">>$filename") or (print "could not open stderr: $!" && exit(1));
+ close(STDOUT);
+ open(STDOUT, ">&STDERR") or die "redirect STDOUT -> STDERR: $!";
+ $SIG{HUP} = sub {
+ close(STDERR);
+ open(STDERR, ">>$filename") or (print "could not open stderr: $!" && exit(1));
+ };
+}
+
+sub gd_setup_signals {
+ my $self = shift;
+ $self->SUPER::gd_setup_signals();
+ $SIG{TERM} = sub { $self->gd_quit_event(); }
+}
+
+sub gd_run {
+ my $self = shift;
+ $::SIG{__DIE__} = \&Carp::confess if $self->{debug};
+ my $push = Bugzilla->push_ext;
+ $push->logger->{debug} = $self->{debug};
+ $push->is_daemon(1);
+ $push->start();
+}
+
+1;
diff --git a/extensions/Push/lib/Log.pm b/extensions/Push/lib/Log.pm
new file mode 100644
index 000000000..6faabea97
--- /dev/null
+++ b/extensions/Push/lib/Log.pm
@@ -0,0 +1,45 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::Push::Log;
+
+use strict;
+use warnings;
+
+use Bugzilla;
+use Bugzilla::Extension::Push::Message;
+
+sub new {
+ my ($class) = @_;
+ my $self = {};
+ bless($self, $class);
+ return $self;
+}
+
+sub count {
+ my ($self) = @_;
+ my $dbh = Bugzilla->dbh;
+ return $dbh->selectrow_array("SELECT COUNT(*) FROM push_log");
+}
+
+sub list {
+ my ($self, %args) = @_;
+ $args{limit} ||= 10;
+ $args{filter} ||= '';
+ my @result;
+ my $dbh = Bugzilla->dbh;
+
+ my $ids = $dbh->selectcol_arrayref("
+ SELECT id
+ FROM push_log
+ ORDER BY processed_ts DESC " .
+ $dbh->sql_limit(100)
+ );
+ return Bugzilla::Extension::Push::LogEntry->new_from_list($ids);
+}
+
+1;
diff --git a/extensions/Push/lib/LogEntry.pm b/extensions/Push/lib/LogEntry.pm
new file mode 100644
index 000000000..b883ee095
--- /dev/null
+++ b/extensions/Push/lib/LogEntry.pm
@@ -0,0 +1,66 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::Push::LogEntry;
+
+use strict;
+use warnings;
+
+use base 'Bugzilla::Object';
+
+use Bugzilla;
+use Bugzilla::Error;
+use Bugzilla::Extension::Push::Constants;
+
+#
+# initialisation
+#
+
+use constant DB_TABLE => 'push_log';
+use constant DB_COLUMNS => qw(
+ id
+ message_id
+ change_set
+ routing_key
+ connector
+ push_ts
+ processed_ts
+ result
+ data
+);
+use constant VALIDATORS => {
+ data => \&_check_data,
+};
+use constant NAME_FIELD => '';
+use constant LIST_ORDER => 'processed_ts DESC';
+
+#
+# accessors
+#
+
+sub message_id { return $_[0]->{'message_id'}; }
+sub change_set { return $_[0]->{'change_set'}; }
+sub routing_key { return $_[0]->{'routing_key'}; }
+sub connector { return $_[0]->{'connector'}; }
+sub push_ts { return $_[0]->{'push_ts'}; }
+sub processed_ts { return $_[0]->{'processed_ts'}; }
+sub result { return $_[0]->{'result'}; }
+sub data { return $_[0]->{'data'}; }
+
+sub result_string { return push_result_to_string($_[0]->result) }
+
+#
+# validators
+#
+
+sub _check_data {
+ my ($invocant, $value) = @_;
+ return $value eq '' ? undef : $value;
+}
+
+1;
+
diff --git a/extensions/Push/lib/Logger.pm b/extensions/Push/lib/Logger.pm
new file mode 100644
index 000000000..68cec1e69
--- /dev/null
+++ b/extensions/Push/lib/Logger.pm
@@ -0,0 +1,70 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::Push::Logger;
+
+use strict;
+use warnings;
+
+use Apache2::Log;
+use Bugzilla::Extension::Push::Constants;
+use Bugzilla::Extension::Push::LogEntry;
+
+sub new {
+ my ($class) = @_;
+ my $self = {};
+ bless($self, $class);
+ return $self;
+}
+
+sub info { shift->_log_it('INFO', @_) }
+sub error { shift->_log_it('ERROR', @_) }
+sub debug { shift->_log_it('DEBUG', @_) }
+
+sub debugging {
+ my ($self) = @_;
+ return $self->{debug};
+}
+
+sub _log_it {
+ my ($self, $method, $message) = @_;
+ return if $method eq 'DEBUG' && !$self->debugging;
+ chomp $message;
+ if ($ENV{MOD_PERL}) {
+ Apache2::ServerRec::warn("Push $method: $message");
+ } elsif ($ENV{SCRIPT_FILENAME}) {
+ print STDERR "Push $method: $message\n";
+ } else {
+ print STDERR '[' . localtime(time) ."] $method: $message\n";
+ }
+}
+
+sub result {
+ my ($self, $connector, $message, $result, $data) = @_;
+ $data ||= '';
+
+ $self->info(sprintf(
+ "%s: Message #%s: %s %s",
+ $connector->name,
+ $message->message_id,
+ push_result_to_string($result),
+ $data
+ ));
+
+ Bugzilla::Extension::Push::LogEntry->create({
+ message_id => $message->message_id,
+ change_set => $message->change_set,
+ routing_key => $message->routing_key,
+ connector => $connector->name,
+ push_ts => $message->push_ts,
+ processed_ts => Bugzilla->dbh->selectrow_array('SELECT NOW()'),
+ result => $result,
+ data => $data,
+ });
+}
+
+1;
diff --git a/extensions/Push/lib/Message.pm b/extensions/Push/lib/Message.pm
new file mode 100644
index 000000000..3d112a2e1
--- /dev/null
+++ b/extensions/Push/lib/Message.pm
@@ -0,0 +1,99 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::Push::Message;
+
+use strict;
+use warnings;
+
+use base 'Bugzilla::Object';
+
+use Bugzilla;
+use Bugzilla::Error;
+use Bugzilla::Extension::Push::Util;
+use Encode;
+
+#
+# initialisation
+#
+
+use constant DB_TABLE => 'push';
+use constant DB_COLUMNS => qw(
+ id
+ push_ts
+ payload
+ change_set
+ routing_key
+);
+use constant LIST_ORDER => 'push_ts';
+use constant VALIDATORS => {
+ push_ts => \&_check_push_ts,
+ payload => \&_check_payload,
+ change_set => \&_check_change_set,
+ routing_key => \&_check_routing_key,
+};
+
+# this creates an object which doesn't exist on the database
+sub new_transient {
+ my $invocant = shift;
+ my $class = ref($invocant) || $invocant;
+ my $object = shift;
+ bless($object, $class) if $object;
+ return $object;
+}
+
+# take a transient object and commit
+sub create_from_transient {
+ my ($self) = @_;
+ return $self->create($self);
+}
+
+#
+# accessors
+#
+
+sub push_ts { return $_[0]->{'push_ts'}; }
+sub payload { return $_[0]->{'payload'}; }
+sub change_set { return $_[0]->{'change_set'}; }
+sub routing_key { return $_[0]->{'routing_key'}; }
+sub message_id { return $_[0]->id; }
+
+sub payload_decoded {
+ my ($self) = @_;
+ return from_json($self->{'payload'});
+}
+
+#
+# validators
+#
+
+sub _check_push_ts {
+ my ($invocant, $value) = @_;
+ $value ||= Bugzilla->dbh->selectrow_array('SELECT NOW()');
+ return $value;
+}
+
+sub _check_payload {
+ my ($invocant, $value) = @_;
+ length($value) || ThrowCodeError('push_invalid_payload');
+ return $value;
+}
+
+sub _check_change_set {
+ my ($invocant, $value) = @_;
+ (defined($value) && length($value)) || ThrowCodeError('push_invalid_change_set');
+ return $value;
+}
+
+sub _check_routing_key {
+ my ($invocant, $value) = @_;
+ (defined($value) && length($value)) || ThrowCodeError('push_invalid_routing_key');
+ return $value;
+}
+
+1;
+
diff --git a/extensions/Push/lib/Option.pm b/extensions/Push/lib/Option.pm
new file mode 100644
index 000000000..25d529f98
--- /dev/null
+++ b/extensions/Push/lib/Option.pm
@@ -0,0 +1,66 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::Push::Option;
+
+use strict;
+use warnings;
+
+use base 'Bugzilla::Object';
+
+use Bugzilla;
+use Bugzilla::Error;
+use Bugzilla::Util;
+
+#
+# initialisation
+#
+
+use constant DB_TABLE => 'push_options';
+use constant DB_COLUMNS => qw(
+ id
+ connector
+ option_name
+ option_value
+);
+use constant UPDATE_COLUMNS => qw(
+ option_value
+);
+use constant VALIDATORS => {
+ connector => \&_check_connector,
+};
+use constant LIST_ORDER => 'connector';
+
+#
+# accessors
+#
+
+sub connector { return $_[0]->{'connector'}; }
+sub name { return $_[0]->{'option_name'}; }
+sub value { return $_[0]->{'option_value'}; }
+
+#
+# mutators
+#
+
+sub set_value { $_[0]->{'option_value'} = $_[1]; }
+
+#
+# validators
+#
+
+sub _check_connector {
+ my ($invocant, $value) = @_;
+ $value eq '*'
+ || $value eq 'global'
+ || Bugzilla->push_ext->connectors->exists($value)
+ || ThrowCodeError('push_invalid_connector');
+ return $value;
+}
+
+1;
+
diff --git a/extensions/Push/lib/Push.pm b/extensions/Push/lib/Push.pm
new file mode 100644
index 000000000..76b82dda4
--- /dev/null
+++ b/extensions/Push/lib/Push.pm
@@ -0,0 +1,249 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::Push::Push;
+
+use strict;
+use warnings;
+
+use Bugzilla::Extension::Push::BacklogMessage;
+use Bugzilla::Extension::Push::Config;
+use Bugzilla::Extension::Push::Connectors;
+use Bugzilla::Extension::Push::Constants;
+use Bugzilla::Extension::Push::Log;
+use Bugzilla::Extension::Push::Logger;
+use Bugzilla::Extension::Push::Message;
+use Bugzilla::Extension::Push::Option;
+use Bugzilla::Extension::Push::Queue;
+use Bugzilla::Extension::Push::Util;
+use DateTime;
+
+sub new {
+ my ($class) = @_;
+ my $self = {};
+ bless($self, $class);
+ $self->{is_daemon} = 0;
+ return $self;
+}
+
+sub is_daemon {
+ my ($self, $value) = @_;
+ if (defined $value) {
+ $self->{is_daemon} = $value ? 1 : 0;
+ }
+ return $self->{is_daemon};
+}
+
+sub start {
+ my ($self) = @_;
+ my $connectors = $self->connectors;
+ $self->{config_last_modified} = $self->get_config_last_modified();
+ $self->{config_last_checked} = (time);
+
+ foreach my $connector ($connectors->list) {
+ $connector->backlog->reset_backoff();
+ }
+
+ while(1) {
+ $self->_reload();
+ $self->push();
+ sleep(POLL_INTERVAL_SECONDS);
+ }
+}
+
+sub push {
+ my ($self) = @_;
+ my $logger = $self->logger;
+ my $connectors = $self->connectors;
+
+ my $enabled = 0;
+ foreach my $connector ($connectors->list) {
+ if ($connector->enabled) {
+ $enabled = 1;
+ last;
+ }
+ }
+ return unless $enabled;
+
+ $logger->debug("polling");
+
+ # process each message
+ while(my $message = $self->queue->oldest) {
+ foreach my $connector ($connectors->list) {
+ next unless $connector->enabled;
+ next unless $connector->should_send($message);
+ $logger->debug("pushing to " . $connector->name);
+
+ my $is_backlogged = $connector->backlog->count;
+
+ if (!$is_backlogged) {
+ # connector isn't backlogged, immediate send
+ $logger->debug("immediate send");
+ my ($result, $data);
+ eval {
+ ($result, $data) = $connector->send($message);
+ };
+ if ($@) {
+ $result = PUSH_RESULT_TRANSIENT;
+ $data = clean_error($@);
+ }
+ if (!$result) {
+ $logger->error($connector->name . " failed to return a result code");
+ $result = PUSH_RESULT_UNKNOWN;
+ }
+ $logger->result($connector, $message, $result, $data);
+
+ if ($result == PUSH_RESULT_TRANSIENT) {
+ $is_backlogged = 1;
+ }
+ }
+
+ # if the connector is backlogged, push to the backlog queue
+ if ($is_backlogged) {
+ $logger->debug("backlogged");
+ my $backlog = Bugzilla::Extension::Push::BacklogMessage->create_from_message($message, $connector);
+ }
+ }
+
+ # message processed
+ $message->remove_from_db();
+ }
+
+ # process backlog
+ foreach my $connector ($connectors->list) {
+ next unless $connector->enabled;
+ my $message = $connector->backlog->oldest();
+ next unless $message;
+
+ $logger->debug("processing backlog for " . $connector->name);
+ while ($message) {
+ my ($result, $data);
+ eval {
+ ($result, $data) = $connector->send($message);
+ };
+ if ($@) {
+ $result = PUSH_RESULT_TRANSIENT;
+ $data = $@;
+ }
+ $message->inc_attempts($result == PUSH_RESULT_OK ? '' : $data);
+ if (!$result) {
+ $logger->error($connector->name . " failed to return a result code");
+ $result = PUSH_RESULT_UNKNOWN;
+ }
+ $logger->result($connector, $message, $result, $data);
+
+ if ($result == PUSH_RESULT_TRANSIENT) {
+ # connector is still down, stop trying
+ $connector->backlog->inc_backoff();
+ last;
+ }
+
+ # message was processed
+ $message->remove_from_db();
+
+ $message = $connector->backlog->oldest();
+ }
+ }
+}
+
+sub _reload {
+ my ($self) = @_;
+
+ # check for updated config every 60 seconds
+ my $now = (time);
+ if ($now - $self->{config_last_checked} < 60) {
+ return;
+ }
+ $self->{config_last_checked} = $now;
+
+ $self->logger->debug('Checking for updated configuration');
+ if ($self->get_config_last_modified eq $self->{config_last_modified}) {
+ return;
+ }
+ $self->{config_last_modified} = $self->get_config_last_modified();
+
+ $self->logger->debug('Configuration has been updated');
+ $self->connectors->reload();
+}
+
+sub get_config_last_modified {
+ my ($self) = @_;
+ my $options_list = Bugzilla::Extension::Push::Option->match({
+ connector => '*',
+ option_name => 'last-modified',
+ });
+ if (@$options_list) {
+ return $options_list->[0]->value;
+ } else {
+ return $self->set_config_last_modified();
+ }
+}
+
+sub set_config_last_modified {
+ my ($self) = @_;
+ my $options_list = Bugzilla::Extension::Push::Option->match({
+ connector => '*',
+ option_name => 'last-modified',
+ });
+ my $now = DateTime->now->datetime();
+ if (@$options_list) {
+ $options_list->[0]->set_value($now);
+ $options_list->[0]->update();
+ } else {
+ Bugzilla::Extension::Push::Option->create({
+ connector => '*',
+ option_name => 'last-modified',
+ option_value => $now,
+ });
+ }
+ return $now;
+}
+
+sub config {
+ my ($self) = @_;
+ if (!$self->{config}) {
+ $self->{config} = Bugzilla::Extension::Push::Config->new(
+ 'global',
+ {
+ name => 'log_purge',
+ label => 'Purge logs older than (days)',
+ type => 'string',
+ default => '7',
+ required => '1',
+ validate => sub { $_[0] =~ /\D/ && die "Invalid purge duration (must be numeric)\n"; },
+ },
+ );
+ $self->{config}->load();
+ }
+ return $self->{config};
+}
+
+sub logger {
+ my ($self, $value) = @_;
+ $self->{logger} = $value if $value;
+ return $self->{logger};
+}
+
+sub connectors {
+ my ($self, $value) = @_;
+ $self->{connectors} = $value if $value;
+ return $self->{connectors};
+}
+
+sub queue {
+ my ($self) = @_;
+ $self->{queue} ||= Bugzilla::Extension::Push::Queue->new();
+ return $self->{queue};
+}
+
+sub log {
+ my ($self) = @_;
+ $self->{log} ||= Bugzilla::Extension::Push::Log->new();
+ return $self->{log};
+}
+
+1;
diff --git a/extensions/Push/lib/Queue.pm b/extensions/Push/lib/Queue.pm
new file mode 100644
index 000000000..d89cb23c3
--- /dev/null
+++ b/extensions/Push/lib/Queue.pm
@@ -0,0 +1,72 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::Push::Queue;
+
+use strict;
+use warnings;
+
+use Bugzilla;
+use Bugzilla::Extension::Push::Message;
+
+sub new {
+ my ($class) = @_;
+ my $self = {};
+ bless($self, $class);
+ return $self;
+}
+
+sub count {
+ my ($self) = @_;
+ my $dbh = Bugzilla->dbh;
+ return $dbh->selectrow_array("SELECT COUNT(*) FROM push");
+}
+
+sub oldest {
+ my ($self) = @_;
+ my @messages = $self->list(limit => 1);
+ return scalar(@messages) ? $messages[0] : undef;
+}
+
+sub by_id {
+ my ($self, $id) = @_;
+ my @messages = $self->list(
+ limit => 1,
+ filter => "AND (push.id = $id)",
+ );
+ return scalar(@messages) ? $messages[0] : undef;
+}
+
+sub list {
+ my ($self, %args) = @_;
+ $args{limit} ||= 10;
+ $args{filter} ||= '';
+ my @result;
+ my $dbh = Bugzilla->dbh;
+
+ my $sth = $dbh->prepare("
+ SELECT id, push_ts, payload, change_set, routing_key
+ FROM push
+ WHERE (1 = 1) " .
+ $args{filter} . "
+ ORDER BY push_ts " .
+ $dbh->sql_limit($args{limit})
+ );
+ $sth->execute();
+ while (my $row = $sth->fetchrow_hashref()) {
+ push @result, Bugzilla::Extension::Push::Message->new({
+ id => $row->{id},
+ push_ts => $row->{push_ts},
+ payload => $row->{payload},
+ change_set => $row->{change_set},
+ routing_key => $row->{routing_key},
+ });
+ }
+ return @result;
+}
+
+1;
diff --git a/extensions/Push/lib/Serialise.pm b/extensions/Push/lib/Serialise.pm
new file mode 100644
index 000000000..ad1cc0452
--- /dev/null
+++ b/extensions/Push/lib/Serialise.pm
@@ -0,0 +1,318 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::Push::Serialise;
+
+use strict;
+use warnings;
+
+use Bugzilla::Constants;
+use Bugzilla::Extension::Push::Util;
+use Bugzilla::Version;
+
+use Scalar::Util 'blessed';
+use JSON ();
+
+my $_instance;
+sub instance {
+ $_instance ||= Bugzilla::Extension::Push::Serialise->_new();
+ return $_instance;
+}
+
+sub _new {
+ my ($class) = @_;
+ my $self = {};
+ bless($self, $class);
+ return $self;
+}
+
+# given an object, serliase to a hash
+sub object_to_hash {
+ my ($self, $object, $is_shallow) = @_;
+
+ my $method = lc(blessed($object));
+ $method =~ s/::/_/g;
+ $method =~ s/^bugzilla//;
+ return unless $self->can($method);
+ (my $name = $method) =~ s/^_//;
+
+ # check for a cached hash
+ my $cache = Bugzilla->request_cache;
+ my $cache_id = "push." . ($is_shallow ? 'shallow.' : 'deep.') . $object;
+ if (exists($cache->{$cache_id})) {
+ return wantarray ? ($cache->{$cache_id}, $name) : $cache->{$cache_id};
+ }
+
+ # call the right method to serialise to a hash
+ my $rh = $self->$method($object, $is_shallow);
+
+ # store in cache
+ if ($cache_id) {
+ $cache->{$cache_id} = $rh;
+ }
+
+ return wantarray ? ($rh, $name) : $rh;
+}
+
+# given a changes hash, return an event hash
+sub changes_to_event {
+ my ($self, $changes) = @_;
+
+ my $event = {};
+
+ # create common (created and modified) fields
+ $event->{'user'} = $self->object_to_hash(Bugzilla->user);
+ my $timestamp =
+ $changes->{'timestamp'}
+ || Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
+ $event->{'time'} = datetime_to_timestamp($timestamp);
+
+ foreach my $change (@{$changes->{'changes'}}) {
+ if (exists $change->{'field'}) {
+ # map undef to emtpy
+ hash_undef_to_empty($change);
+
+ # custom_fields change from undef to empty, ignore these changes
+ return if ($change->{'added'} || "") eq "" &&
+ ($change->{'removed'} || "") eq "";
+
+ # use saner field serialisation
+ my $field = $change->{'field'};
+ $change->{'field'} = $field;
+
+ if ($field eq 'priority' || $field eq 'target_milestone') {
+ $change->{'added'} = _select($change->{'added'});
+ $change->{'removed'} = _select($change->{'removed'});
+
+ } elsif ($field =~ /^cf_/) {
+ $change->{'added'} = _custom_field($field, $change->{'added'});
+ $change->{'removed'} = _custom_field($field, $change->{'removed'});
+ }
+
+ $event->{'changes'} = [] unless exists $event->{'changes'};
+ push @{$event->{'changes'}}, $change;
+ }
+ }
+
+ return $event;
+}
+
+# bugzilla returns '---' or '--' for single-select fields that have no value
+# selected. it makes more sense to return an empty string.
+sub _select {
+ my ($value) = @_;
+ return '' if $value eq '---' or $value eq '--';
+ return $value;
+}
+
+# return an object which serialises to a json boolean, but still acts as a perl
+# boolean
+sub _boolean {
+ my ($value) = @_;
+ return $value ? JSON::true : JSON::false;
+}
+
+sub _string {
+ my ($value) = @_;
+ return defined($value) ? $value : '';
+}
+
+sub _time {
+ my ($value) = @_;
+ return defined($value) ? datetime_to_timestamp($value) : undef;
+}
+
+sub _integer {
+ my ($value) = @_;
+ return $value + 0;
+}
+
+sub _custom_field {
+ my ($field, $value) = @_;
+ $field = Bugzilla::Field->new({ name => $field }) unless blessed $field;
+
+ if ($field->type == FIELD_TYPE_DATETIME) {
+ return _time($value);
+
+ } elsif ($field->type == FIELD_TYPE_SINGLE_SELECT) {
+ return _select($value);
+
+ } elsif ($field->type == FIELD_TYPE_MULTI_SELECT) {
+ # XXX
+ die "not implemented";
+
+ } else {
+ return _string($value);
+ }
+}
+
+#
+# class mappings
+# automatically derrived from the class name
+# Bugzilla::Bug --> _bug, Bugzilla::User --> _user, etc
+#
+
+sub _bug {
+ my ($self, $bug) = @_;
+
+ my $version = $bug->can('version_obj')
+ ? $bug->version_obj
+ : Bugzilla::Version->new({ name => $bug->version, product => $bug->product_obj });
+
+ my $milestone;
+ if (_select($bug->target_milestone) ne '') {
+ $milestone = $bug->can('target_milestone_obj')
+ ? $bug->target_milestone_obj
+ : Bugzilla::Milestone->new({ name => $bug->target_milestone, product => $bug->product_obj });
+ }
+
+ my $status = $bug->can('status_obj')
+ ? $bug->status_obj
+ : Bugzilla::Status->new({ name => $bug->bug_status });
+
+ my $rh = {
+ id => _integer($bug->bug_id),
+ alias => _string($bug->alias),
+ assigned_to => $self->_user($bug->assigned_to),
+ classification => _string($bug->classification),
+ component => $self->_component($bug->component_obj),
+ creation_time => _time($bug->creation_ts || $bug->delta_ts),
+ flags => (mapr { $self->_flag($_) } $bug->flags),
+ is_private => _boolean(!is_public($bug)),
+ keywords => (mapr { _string($_->name) } $bug->keyword_objects),
+ last_change_time => _time($bug->delta_ts),
+ operating_system => _string($bug->op_sys),
+ platform => _string($bug->rep_platform),
+ priority => _select($bug->priority),
+ product => $self->_product($bug->product_obj),
+ qa_contact => $self->_user($bug->qa_contact),
+ reporter => $self->_user($bug->reporter),
+ resolution => _string($bug->resolution),
+ severity => _string($bug->bug_severity),
+ status => $self->_status($status),
+ summary => _string($bug->short_desc),
+ target_milestone => $self->_milestone($milestone),
+ url => _string($bug->bug_file_loc),
+ version => $self->_version($version),
+ whiteboard => _string($bug->status_whiteboard),
+ };
+
+ # add custom fields
+ my @custom_fields = Bugzilla->active_custom_fields;
+ foreach my $field (@custom_fields) {
+ my $name = $field->name;
+
+ # skip custom fields that are hidded from this product/component
+ next if Bugzilla::Extension::BMO::cf_hidden_in_product(
+ $name, $bug->product, $bug->component);
+
+ $rh->{$name} = _custom_field($field, $bug->$name);
+ }
+
+ return $rh;
+}
+
+sub _user {
+ my ($self, $user) = @_;
+ return undef unless $user;
+ return {
+ id => _integer($user->id),
+ login => _string($user->login),
+ real_name => _string($user->name),
+ };
+}
+
+sub _component {
+ my ($self, $component) = @_;
+ return {
+ id => _integer($component->id),
+ name => _string($component->name),
+ };
+}
+
+sub _attachment {
+ my ($self, $attachment, $is_shallow) = @_;
+ my $rh = {
+ id => _integer($attachment->id),
+ content_type => _string($attachment->contenttype),
+ creation_time => _time($attachment->attached),
+ description => _string($attachment->description),
+ file_name => _string($attachment->filename),
+ flags => (mapr { $self->_flag($_) } $attachment->flags),
+ is_obsolete => _boolean($attachment->isobsolete),
+ is_patch => _boolean($attachment->ispatch),
+ is_private => _boolean(!is_public($attachment)),
+ last_change_time => _time($attachment->modification_time),
+ };
+ if (!$is_shallow) {
+ $rh->{bug} = $self->_bug($attachment->bug);
+ }
+ return $rh;
+}
+
+sub _comment {
+ my ($self, $comment, $is_shallow) = @_;
+ my $rh = {
+ id => _integer($comment->bug_id),
+ body => _string($comment->body),
+ creation_time => _time($comment->creation_ts),
+ is_private => _boolean($comment->is_private),
+ number => _integer($comment->count),
+ };
+ if (!$is_shallow) {
+ $rh->{bug} = $self->_bug($comment->bug);
+ }
+ return $rh;
+}
+
+sub _product {
+ my ($self, $product) = @_;
+ return {
+ id => _integer($product->id),
+ name => _string($product->name),
+ };
+}
+
+sub _flag {
+ my ($self, $flag) = @_;
+ my $rh = {
+ id => _integer($flag->id),
+ name => _string($flag->type->name),
+ value => _string($flag->status),
+ };
+ if ($flag->requestee) {
+ $rh->{'requestee'} = $self->_user($flag->requestee);
+ }
+ return $rh;
+}
+
+sub _version {
+ my ($self, $version) = @_;
+ return {
+ id => _integer($version->id),
+ name => _string($version->name),
+ };
+}
+
+sub _milestone {
+ my ($self, $milestone) = @_;
+ return undef unless $milestone;
+ return {
+ id => _integer($milestone->id),
+ name => _string($milestone->name),
+ };
+}
+
+sub _status {
+ my ($self, $status) = @_;
+ return {
+ id => _integer($status->id),
+ name => _string($status->name),
+ };
+}
+
+1;
diff --git a/extensions/Push/lib/Util.pm b/extensions/Push/lib/Util.pm
new file mode 100644
index 000000000..f52db6936
--- /dev/null
+++ b/extensions/Push/lib/Util.pm
@@ -0,0 +1,162 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::Push::Util;
+
+use strict;
+use warnings;
+
+use Bugzilla;
+use Bugzilla::Constants;
+use Bugzilla::Util qw(datetime_from trim);
+use Data::Dumper;
+use Encode;
+use JSON ();
+use Scalar::Util qw(blessed);
+use Time::HiRes;
+
+use base qw(Exporter);
+our @EXPORT = qw(
+ datetime_to_timestamp
+ debug_dump
+ get_first_value
+ hash_undef_to_empty
+ is_public
+ mapr
+ clean_error
+ change_set_id
+ canon_email
+ to_json from_json
+);
+
+# returns true if the specified object is public
+sub is_public {
+ my ($object) = @_;
+
+ my $default_user = Bugzilla::User->new();
+
+ if ($object->isa('Bugzilla::Bug')) {
+ return unless $default_user->can_see_bug($object->bug_id);
+ return 1;
+
+ } elsif ($object->isa('Bugzilla::Comment')) {
+ return if $object->is_private;
+ return unless $default_user->can_see_bug($object->bug_id);
+ return 1;
+
+ } elsif ($object->isa('Bugzilla::Attachment')) {
+ return if $object->isprivate;
+ return unless $default_user->can_see_bug($object->bug_id);
+ return 1;
+
+ } else {
+ warn "Unsupported class " . blessed($object) . " passed to is_public()\n";
+ }
+
+ return 1;
+}
+
+# return the first existing value from the hashref for the given list of keys
+sub get_first_value {
+ my ($rh, @keys) = @_;
+ foreach my $field (@keys) {
+ return $rh->{$field} if exists $rh->{$field};
+ }
+ return;
+}
+
+# wrapper for map that works on array references
+sub mapr(&$) {
+ my ($filter, $ra) = @_;
+ my @result = map(&$filter, @$ra);
+ return \@result;
+}
+
+
+# convert datetime string (from db) to a UTC json friendly datetime
+sub datetime_to_timestamp {
+ my ($datetime_string) = @_;
+ return '' unless $datetime_string;
+ return datetime_from($datetime_string, 'UTC')->datetime();
+}
+
+# replaces all undef values in a hashref with an empty string (deep)
+sub hash_undef_to_empty {
+ my ($rh) = @_;
+ foreach my $key (keys %$rh) {
+ my $value = $rh->{$key};
+ if (!defined($value)) {
+ $rh->{$key} = '';
+ } elsif (ref($value) eq 'HASH') {
+ hash_undef_to_empty($value);
+ }
+ }
+}
+
+# debugging methods
+sub debug_dump {
+ my ($object) = @_;
+ local $Data::Dumper::Sortkeys = 1;
+ my $output = Dumper($object);
+ $output =~ s/</&lt;/g;
+ print "<pre>$output</pre>";
+}
+
+# removes stacktrace and "at /some/path ..." from errors
+sub clean_error {
+ my ($error) = @_;
+ my $path = bz_locations->{'extensionsdir'};
+ $error = $1 if $error =~ /^(.+?) at \Q$path/s;
+ $path = '/loader/0x';
+ $error = $1 if $error =~ /^(.+?) at \Q$path/s;
+ $error =~ s/(^\s+|\s+$)//g;
+ return $error;
+}
+
+# generate a new change_set id
+sub change_set_id {
+ return "$$." . Time::HiRes::time();
+}
+
+# remove guff from email addresses
+sub clean_email {
+ my $email = shift;
+ $email = trim($email);
+ $email = $1 if $email =~ /^(\S+)/;
+ $email =~ s/&#64;/@/;
+ $email = lc $email;
+ return $email;
+}
+
+# resolve to canonised email form
+# eg. glob+bmo@mozilla.com --> glob@mozilla.com
+sub canon_email {
+ my $email = shift;
+ $email = clean_email($email);
+ $email =~ s/^([^\+]+)\+[^\@]+(\@.+)$/$1$2/;
+ return $email;
+}
+
+# json helpers
+sub to_json {
+ my ($object, $pretty) = @_;
+ if ($pretty) {
+ return decode('utf8', JSON->new->utf8(1)->pretty(1)->encode($object));
+ } else {
+ return JSON->new->ascii(1)->shrink(1)->encode($object);
+ }
+}
+
+sub from_json {
+ my ($json) = @_;
+ if (utf8::is_utf8($json)) {
+ $json = encode('utf8', $json);
+ }
+ return JSON->new->utf8(1)->decode($json);
+}
+
+1;
diff --git a/extensions/Push/template/en/default/hook/admin/admin-end_links_right.html.tmpl b/extensions/Push/template/en/default/hook/admin/admin-end_links_right.html.tmpl
new file mode 100644
index 000000000..78e314ab2
--- /dev/null
+++ b/extensions/Push/template/en/default/hook/admin/admin-end_links_right.html.tmpl
@@ -0,0 +1,18 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF user.in_group('admin') %]
+ <dt id="push">
+ Push
+ </dt>
+ <dd>
+ <a href="page.cgi?id=push_config.html">Configuration</a><br>
+ <a href="page.cgi?id=push_queues.html">Queues</a><br>
+ <a href="page.cgi?id=push_log.html">Log</a><br>
+ </dd>
+[% END %]
diff --git a/extensions/Push/template/en/default/hook/global/code-error-errors.html.tmpl b/extensions/Push/template/en/default/hook/global/code-error-errors.html.tmpl
new file mode 100644
index 000000000..515f00fa8
--- /dev/null
+++ b/extensions/Push/template/en/default/hook/global/code-error-errors.html.tmpl
@@ -0,0 +1,25 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF error == "push_invalid_payload" %]
+ [% title = "Invalid payload" %]
+ An invalid or empty payload was passed to Push.
+
+[% ELSIF error == "push_invalid_change_set" %]
+ [% title = "Invalid change_set" %]
+ An invalid or empty change_set was passed to Push.
+
+[% ELSIF error == "push_invalid_routing_key" %]
+ [% title = "Invalid routing_key" %]
+ An invalid or empty routing_key was passed to Push.
+
+[% ELSIF error == "push_invalid_connector" %]
+ [% title = "Invalid connector" %]
+ An invalid connector was passed to Push.
+
+[% END %]
diff --git a/extensions/Push/template/en/default/hook/global/messages-messages.html.tmpl b/extensions/Push/template/en/default/hook/global/messages-messages.html.tmpl
new file mode 100644
index 000000000..e4a016aee
--- /dev/null
+++ b/extensions/Push/template/en/default/hook/global/messages-messages.html.tmpl
@@ -0,0 +1,16 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF message_tag == "push_config_updated" %]
+ Changes to the configuration have been saved.
+ Please allow up to 60 seconds for the change to be active.
+
+[% ELSIF message_tag == "push_message_deleted" %]
+ The message has been deleted.
+
+[% END %]
diff --git a/extensions/Push/template/en/default/hook/global/user-error-errors.html.tmpl b/extensions/Push/template/en/default/hook/global/user-error-errors.html.tmpl
new file mode 100644
index 000000000..2b8a1c4e0
--- /dev/null
+++ b/extensions/Push/template/en/default/hook/global/user-error-errors.html.tmpl
@@ -0,0 +1,11 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF error == "push_error" %]
+ [% error_message FILTER html %]
+[% END %]
diff --git a/extensions/Push/template/en/default/pages/push_config.html.tmpl b/extensions/Push/template/en/default/pages/push_config.html.tmpl
new file mode 100644
index 000000000..6e6507a39
--- /dev/null
+++ b/extensions/Push/template/en/default/pages/push_config.html.tmpl
@@ -0,0 +1,134 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% PROCESS global/header.html.tmpl
+ title = "Push Administration: Configuration"
+ javascript_urls = [ 'extensions/Push/web/admin.js' ]
+ style_urls = [ 'extensions/Push/web/admin.css' ]
+%]
+
+<script>
+var push_defaults = new Array();
+[% FOREACH option = push.config.options %]
+ [% IF option.name != 'enabled' && option.default != '' %]
+ push_defaults['global_[% option.name FILTER js %]'] = '[% option.default FILTER js %]';
+ [% END %]
+[% END %]
+[% FOREACH connector = connectors.list %]
+ [% FOREACH option = connector.config.options %]
+ [% IF option.name != 'enabled' && option.default != '' %]
+ push_defaults['[% connector.name FILTER js %]_[% option.name FILTER js %]'] = '[% option.default FILTER js %]';
+ [% END %]
+ [% END %]
+[% END %]
+</script>
+
+<form method="POST" action="page.cgi">
+<input type="hidden" name="id" value="push_config.html">
+<input type="hidden" name="save" value="1">
+
+<table border="0" cellspacing="0" cellpadding="5" width="100%">
+
+[% PROCESS options
+ name = 'global',
+ config = push.config
+%]
+
+[% FOREACH connector = connectors.list %]
+ [% PROCESS options
+ name = connector.name
+ config = connector.config
+ %]
+[% END %]
+
+<tr>
+ <td>&nbsp;</td>
+ <td colspan="2"><hr></td>
+</tr>
+
+<tr>
+ <td>&nbsp;</td>
+ <td colspan="2">
+ <input type="submit" value="Submit Changes">
+ <input type="submit" value="Reset to Defaults" onclick="reset_to_defaults(); return false">
+ </td>
+</tr>
+
+
+<tr>
+ <td style="min-width: 10em">&nbsp;</td>
+ <td>&nbsp;</td>
+ <td width="100%">&nbsp;</td>
+</tr>
+
+</table>
+
+</form>
+
+[% INCLUDE global/footer.html.tmpl %]
+
+[% BLOCK options %]
+ <tr class="connector">
+ <th>[% name FILTER ucfirst FILTER html %]</th>
+ <td colspan="2"><hr></td>
+ </tr>
+ [% FOREACH option = config.options %]
+ [% class = name _ '_tr' IF option.name != 'enabled' %]
+ <tr class="[% class FILTER html %] option">
+ <th>
+ [% IF option.required %]
+ <span class="required_option" title="Mandatory option">*</span>&nbsp;
+ [% END %]
+ [% option.label FILTER html %]
+ </th>
+ <td>
+ [% IF option.type == 'string' %]
+ <input type="text" name="[% name FILTER html %].[% option.name FILTER html %]"
+ value="[% config.${option.name} FILTER html %]" size="60"
+ id="[% name FILTER html %]_[% option.name FILTER html %]">
+
+ [% ELSIF option.type == 'password' %]
+ <input type="password" name="[% name FILTER html %].[% option.name FILTER html %]"
+ value="[% config.${option.name} FILTER html %]" size="60"
+ id="[% name FILTER html %]_[% option.name FILTER html %]">
+
+ [% ELSIF option.type == 'select' %]
+ <select name="[% name FILTER html %].[% option.name FILTER html %]"
+ id="[% name FILTER html %]_[% option.name FILTER html %]"
+ [% IF option.name == 'enabled' && name != 'global' %]
+ onchange="toggle_options(this.value == 'Enabled', '[% name FILTER js %]')"
+ [% END %]
+ >
+ [% IF option.name != 'enabled' && !option.required %]
+ <option value="""
+ [% ' selected' IF config.${option.name} == "" %]></option>
+ [% END %]
+ [% FOREACH value = option.values %]
+ <option value="[% value FILTER html %]"
+ [% ' selected' IF config.${option.name} == value %]>[% value FILTER html %]</option>
+ [% END %]
+ </select>
+
+ [% ELSE %]
+ unsupported option type '[% option.type FILTER html %]'
+ [% END %]
+ </td>
+ [% IF option.help %]
+ <td class="help">[% option.help FILTER html %]</td>
+ [% ELSE %]
+ <td>&nbsp;</td>
+ [% END %]
+ </tr>
+ [% END %]
+ [% IF name != 'global' %]
+ <script>
+ var is_enabled = document.getElementById('[% name FILTER js %]_enabled').value == 'Enabled';
+ toggle_options(is_enabled, '[% name FILTER js %]');
+ </script>
+ [% END %]
+[% END %]
diff --git a/extensions/Push/template/en/default/pages/push_log.html.tmpl b/extensions/Push/template/en/default/pages/push_log.html.tmpl
new file mode 100644
index 000000000..a51cb22cf
--- /dev/null
+++ b/extensions/Push/template/en/default/pages/push_log.html.tmpl
@@ -0,0 +1,45 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% PROCESS global/header.html.tmpl
+ title = "Push Administration: Logs"
+ javascript_urls = [ 'extensions/Push/web/admin.js' ]
+ style_urls = [ 'extensions/Push/web/admin.css' ]
+%]
+[% logs = push.log %]
+
+<table id="report" cellspacing="0">
+
+[% IF logs.count %]
+ <tr class="report-subheader">
+ <th nowrap>Connector</th>
+ <th nowrap>Event Timestamp</th>
+ <th nowrap>Processed Timestamp</th>
+ <th nowrap>Status</th>
+ <th nowrap>Message</th>
+ </tr>
+[% END %]
+
+[% FOREACH log = logs.list %]
+ <tr class="row [% loop.count % 2 == 1 ? "report_row_odd" : "report_row_even" %]">
+ <td nowrap>[% log.connector FILTER html %]</td>
+ <td nowrap>[% log.push_ts FILTER time FILTER html %]</td>
+ <td nowrap>[% log.processed_ts FILTER time FILTER html %]</td>
+ <td nowrap>[% log.result_string FILTER html %]</td>
+ <td>[% log.data FILTER html %]</td>
+ </tr>
+[% END %]
+
+<tr>
+ <td colspan="5">&nbsp;</td>
+</tr>
+
+</table>
+
+[% INCLUDE global/footer.html.tmpl %]
+
diff --git a/extensions/Push/template/en/default/pages/push_queues.html.tmpl b/extensions/Push/template/en/default/pages/push_queues.html.tmpl
new file mode 100644
index 000000000..67f079f92
--- /dev/null
+++ b/extensions/Push/template/en/default/pages/push_queues.html.tmpl
@@ -0,0 +1,102 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% PROCESS global/header.html.tmpl
+ title = "Push Administration: Queues"
+ javascript_urls = [ 'extensions/Push/web/admin.js' ]
+ style_urls = [ 'extensions/Push/web/admin.css' ]
+%]
+
+<table id="report" cellspacing="0">
+
+[% PROCESS show_queue
+ queue = push.queue
+ title = 'Pending'
+ pending = 1
+%]
+
+[% FOREACH connector = push.connectors.list %]
+ [% NEXT UNLESS connector.enabled %]
+ [% PROCESS show_queue
+ queue = connector.backlog
+ title = connector.name _ ' Backlog'
+ pending = 0
+ %]
+[% END %]
+
+</table>
+
+[% INCLUDE global/footer.html.tmpl %]
+
+[% BLOCK show_queue %]
+ [% count = queue.count %]
+ <tr class="report-header">
+ <th colspan="2">
+ [% title FILTER html %] Queue ([% count FILTER html %])
+ </th>
+ [% IF queue.backoff && count %]
+ <th class="rhs" colspan="5">
+ Next Attempt: [% queue.backoff.next_attempt_ts FILTER time %]
+ </th>
+ [% ELSE %]
+ <th colspan="5">&nbsp;</td>
+ [% END %]
+ </tr>
+
+ [% IF count %]
+ <tr class="report-subheader">
+ <th nowrap>Timestamp</th>
+ <th nowrap>Change Set</th>
+ [% IF pending %]
+ <th nowrap colspan="4">Routing Key</th>
+ [% ELSE %]
+ <th nowrap>Routing Key</th>
+ <th nowrap>Last Attempt</th>
+ <th nowrap>Attempts</th>
+ <th nowrap>Last Error</th>
+ [% END %]
+ <th>&nbsp;</th>
+ </tr>
+ [% END %]
+
+ [% FOREACH message = queue.list('limit', 10) %]
+ <tr class="row [% loop.count % 2 == 1 ? "report_row_odd" : "report_row_even" %]">
+ <td nowrap>[% message.push_ts FILTER html %]</td>
+ <td nowrap>[% message.change_set FILTER html %]</td>
+ [% IF pending %]
+ <td nowrap colspan="4">[% message.routing_key FILTER html %]</td>
+ [% ELSE %]
+ <td nowrap>[% message.routing_key FILTER html %]</td>
+ [% IF message.attempt_ts %]
+ <td nowrap>[% message.attempt_ts FILTER time %]</td>
+ <td nowrap>[% message.attempts FILTER html %]</td>
+ <td width="100%">
+ [% IF message.last_error.length > 40 %]
+ [% last_error = message.last_error.substr(0, 40) _ '...' %]
+ [% ELSE %]
+ [% last_error = message.last_error %]
+ [% END %]
+ [% last_error FILTER html %]</td>
+ [% ELSE %]
+ <td>-</td>
+ <td>-</td>
+ <td width="100%">-</td>
+ [% END %]
+ [% END %]
+ <td class="rhs">
+ <a href="?id=push_queues_view.html&amp;[% ~%]
+ message=[% message.id FILTER url_quote %]&amp;[% ~%]
+ connector=[% queue.connector FILTER url_quote %]">View</a>
+ </td>
+ </tr>
+ [% END %]
+
+ <tr>
+ <td colspan="7">&nbsp;</td>
+ </tr>
+[% END %]
diff --git a/extensions/Push/template/en/default/pages/push_queues_view.html.tmpl b/extensions/Push/template/en/default/pages/push_queues_view.html.tmpl
new file mode 100644
index 000000000..0e8449b0c
--- /dev/null
+++ b/extensions/Push/template/en/default/pages/push_queues_view.html.tmpl
@@ -0,0 +1,80 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% PROCESS global/header.html.tmpl
+ title = "Push Administration: Queues: Payload"
+ javascript_urls = [ 'extensions/Push/web/admin.js' ]
+ style_urls = [ 'extensions/Push/web/admin.css' ]
+%]
+
+[% IF !message_obj %]
+ <a href="?id=push_queues.html">Return</a>
+ [% RETURN %]
+[% END %]
+
+<table id="report" cellspacing="0">
+
+<tr>
+ <th class="report-header" nowrap>Connector</th>
+ <td width="100%">[% message_obj.connector || '-' FILTER html %]</td>
+</tr>
+<tr>
+ <th class="report-header" nowrap>Message ID</th>
+ <td width="100%">[% message_obj.message_id FILTER html %]</td>
+</tr>
+<tr>
+ <th class="report-header" nowrap>Push Time</th>
+ <td width="100%">[% message_obj.push_ts FILTER time FILTER html %]</td>
+</tr>
+<tr>
+ <th class="report-header" nowrap>Change Set</th>
+ <td width="100%">[% message_obj.change_set FILTER html %]</td>
+</tr>
+<tr>
+ <th class="report-header" nowrap>Routing Key</th>
+ <td width="100%">[% message_obj.routing_key FILTER html %]</td>
+</tr>
+
+[% IF message_obj.attempts %]
+ <tr>
+ <th class="report-header" nowrap>Attempts</th>
+ <td width="100%">[% message_obj.attempts FILTER html %]</td>
+ </tr>
+ <tr>
+ <th class="report-header" nowrap>Last Attempt Time</th>
+ <td width="100%">[% message_obj.attempt_ts FILTER time FILTER html %]</td>
+ </tr>
+ <tr>
+ <th class="report-header" nowrap>Last Error</th>
+ <td width="100%"><b>[% message_obj.last_error FILTER html %]</b></td>
+ </tr>
+[% END %]
+
+<tr>
+ <td colspan="2">
+ [% IF json %]
+ <pre>[% json FILTER html %]</pre>
+ [% ELSE %]
+ <pre>[% message_obj.payload FILTER html %]</pre>
+ [% END %]
+ </td>
+</tr>
+
+<tr class="report-header">
+ <th colspan="2">
+ <a href="?id=push_queues.html">Return</a> |
+ <a onclick="return confirm('Are you sure you want to delete this message forever (a long time)?')"
+ href="?id=push_queues_view.html&amp;delete=1
+ [%- %]&amp;message=[% message_obj.id FILTER url_quote %]
+ [%- %]&amp;connector=[% message_obj.connector FILTER url_quote %]">Delete</a>
+ </th>
+</tr>
+
+</table>
+
+[% INCLUDE global/footer.html.tmpl %]
diff --git a/extensions/Push/template/en/default/setup/strings.txt.pl b/extensions/Push/template/en/default/setup/strings.txt.pl
new file mode 100644
index 000000000..bb135f5bb
--- /dev/null
+++ b/extensions/Push/template/en/default/setup/strings.txt.pl
@@ -0,0 +1,11 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+%strings = (
+ feature_push_amqp => 'Push: AMQP Support',
+ feature_push_stomp => 'Push: STOMP Support',
+);
diff --git a/extensions/Push/web/admin.css b/extensions/Push/web/admin.css
new file mode 100644
index 000000000..c204fa62a
--- /dev/null
+++ b/extensions/Push/web/admin.css
@@ -0,0 +1,71 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0. */
+
+.connector th {
+ text-align: left;
+ vertical-align: middle !important;
+}
+
+.option th {
+ text-align: right;
+ font-weight: normal !important;
+ vertical-align: middle !important;
+}
+
+.option .help {
+ font-style: italic;
+}
+
+.hidden {
+ display: none;
+}
+
+.required_option {
+ color: red;
+ cursor: help;
+}
+
+#report {
+ border: 1px solid #888888;
+ width: 100%;
+}
+
+#report td, #report th {
+ padding: 3px 10px 3px 3px;
+ border: 0px;
+}
+
+#report th {
+ text-align: left;
+}
+
+.report-header {
+ background: #cccccc;
+}
+
+.report-subheader {
+ background: #ffffff;
+}
+
+.report_row_odd {
+ background-color: #eeeeee;
+ color: #000000;
+}
+
+.report_row_even {
+ background-color: #ffffff;
+ color: #000000;
+}
+
+#report tr.row:hover {
+ background-color: #ccccff;
+}
+
+.rhs {
+ text-align: right !important;
+}
+
diff --git a/extensions/Push/web/admin.js b/extensions/Push/web/admin.js
new file mode 100644
index 000000000..599bfd742
--- /dev/null
+++ b/extensions/Push/web/admin.js
@@ -0,0 +1,37 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0. */
+
+var Dom = YAHOO.util.Dom;
+
+function toggle_options(visible, name) {
+ var rows = Dom.getElementsByClassName(name + '_tr');
+ for (var i = 0, l = rows.length; i < l; i++) {
+ if (visible) {
+ Dom.removeClass(rows[i], 'hidden');
+ } else {
+ Dom.addClass(rows[i], 'hidden');
+ }
+ }
+}
+
+function reset_to_defaults() {
+ if (!push_defaults) return;
+ for (var id in push_defaults) {
+ var el = Dom.get(id);
+ if (!el) continue;
+ if (el.nodeName == 'INPUT') {
+ el.value = push_defaults[id];
+ } else if (el.nodeName == 'SELECT') {
+ for (var i = 0, l = el.options.length; i < l; i++) {
+ if (el.options[i].value == push_defaults[id]) {
+ el.options[i].selected = true;
+ break;
+ }
+ }
+ }
+ }
+}
diff --git a/extensions/REMO/Config.pm b/extensions/REMO/Config.pm
new file mode 100644
index 000000000..625e2afd9
--- /dev/null
+++ b/extensions/REMO/Config.pm
@@ -0,0 +1,34 @@
+# -*- 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 REMO Bugzilla Extension.
+#
+# The Initial Developer of the Original Code is Mozilla Foundation
+# Portions created by the Initial Developer are Copyright (C) 2011 the
+# Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+# Byron Jones <glob@mozilla.com>
+# David Lawrence <dkl@mozilla.com>
+
+package Bugzilla::Extension::REMO;
+use strict;
+
+use constant NAME => 'REMO';
+
+use constant REQUIRED_MODULES => [
+];
+
+use constant OPTIONAL_MODULES => [
+];
+
+__PACKAGE__->NAME;
diff --git a/extensions/REMO/Extension.pm b/extensions/REMO/Extension.pm
new file mode 100644
index 000000000..3df35357a
--- /dev/null
+++ b/extensions/REMO/Extension.pm
@@ -0,0 +1,233 @@
+# -*- 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 REMO Bugzilla Extension.
+#
+# The Initial Developer of the Original Code is Mozilla Foundation
+# Portions created by the Initial Developer are Copyright (C) 2011 the
+# Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+# Byron Jones <glob@mozilla.com>
+# David Lawrence <dkl@mozilla.com>
+
+package Bugzilla::Extension::REMO;
+use strict;
+use base qw(Bugzilla::Extension);
+
+use Bugzilla::Constants;
+use Bugzilla::Util qw(trick_taint trim detaint_natural);
+use Bugzilla::Token;
+use Bugzilla::Error;
+
+our $VERSION = '0.01';
+
+sub page_before_template {
+ my ($self, $args) = @_;
+ my $page = $args->{'page_id'};
+ my $vars = $args->{'vars'};
+
+ if ($page eq 'remo-form-payment.html') {
+ _remo_form_payment($vars);
+ }
+}
+
+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 post_bug_after_creation {
+ my ($self, $args) = @_;
+ my $vars = $args->{vars};
+ my $bug = $vars->{bug};
+ my $template = Bugzilla->template;
+
+ if (Bugzilla->input_params->{format}
+ && Bugzilla->input_params->{format} eq 'remo-swag')
+ {
+ # If the attachment cannot be successfully added to the bug,
+ # we notify the user, but we don't interrupt the bug creation process.
+ my $error_mode_cache = Bugzilla->error_mode;
+ Bugzilla->error_mode(ERROR_MODE_DIE);
+
+ my $attachment;
+ eval {
+ my $xml;
+ $template->process("bug/create/create-remo-swag.xml.tmpl", {}, \$xml)
+ || ThrowTemplateError($template->error());
+
+ $attachment = Bugzilla::Attachment->create(
+ { bug => $bug,
+ creation_ts => $bug->creation_ts,
+ data => $xml,
+ description => 'Remo Swag Request (XML)',
+ filename => 'remo-swag.xml',
+ ispatch => 0,
+ isprivate => 0,
+ isurl => 0,
+ mimetype => 'text/xml',
+ store_in_file => 0,
+ });
+ };
+
+ if ($attachment) {
+ # Insert comment for attachment
+ $bug->add_comment('', { isprivate => 0,
+ type => CMT_ATTACHMENT_CREATED,
+ extra_data => $attachment->id });
+ $bug->update($bug->creation_ts);
+ }
+ else {
+ $vars->{'message'} = 'attachment_creation_failed';
+ }
+
+ Bugzilla->error_mode($error_mode_cache);
+ }
+}
+
+__PACKAGE__->NAME;
diff --git a/extensions/REMO/template/en/default/bug/create/comment-mozreps.txt.tmpl b/extensions/REMO/template/en/default/bug/create/comment-mozreps.txt.tmpl
new file mode 100644
index 000000000..5e1275e0b
--- /dev/null
+++ b/extensions/REMO/template/en/default/bug/create/comment-mozreps.txt.tmpl
@@ -0,0 +1,95 @@
+[%# 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 REMO 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 <glob@mozilla.com>
+ #%]
+[% 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 %]
+
+Mozillians.org Account:
+[% IF cgi.param('mozillian') %]
+[%+ cgi.param('mozillian') %]
+[% 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 %]
+
+When First Contributed:
+[% IF cgi.param('firstcontribute') %]
+[%+ cgi.param('firstcontribute') %]
+[% 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/REMO/template/en/default/bug/create/comment-remo-budget.txt.tmpl b/extensions/REMO/template/en/default/bug/create/comment-remo-budget.txt.tmpl
new file mode 100644
index 000000000..2ac4d9caa
--- /dev/null
+++ b/extensions/REMO/template/en/default/bug/create/comment-remo-budget.txt.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 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 <gerv@gerv.net>
+ #%]
+[%# 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') %]
+Profile page: [% cgi.param('profilepage') %]
+Event page: [% cgi.param('eventpage') %]
+Mentor Email: [% cgi.param('mentoremail') %]
+Paypal Account: [% cgi.param('paypal') %]
+Country You Reside: [% cgi.param('country') %]
+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') %]
+
+[%+ cgi.param("comment") IF cgi.param("comment") %]
+
diff --git a/extensions/REMO/template/en/default/bug/create/comment-remo-swag.txt.tmpl b/extensions/REMO/template/en/default/bug/create/comment-remo-swag.txt.tmpl
new file mode 100644
index 000000000..dba982310
--- /dev/null
+++ b/extensions/REMO/template/en/default/bug/create/comment-remo-swag.txt.tmpl
@@ -0,0 +1,71 @@
+[%# 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 <gerv@gerv.net>
+ #%]
+[%# 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:
+
+First name: [% cgi.param('firstname') %]
+Last name: [% cgi.param('lastname') %]
+Profile page: [% cgi.param('profilepage') %]
+Event name: [% cgi.param('eventname') %]
+Event page: [% cgi.param('eventpage') %]
+Estimated attendance: [% cgi.param('attendance') %]
+
+Shipping details:
+
+Ship swag before: [% cgi.param('cf_due_date') %]
+
+First name: [% cgi.param("shiptofirstname") %]
+Last name: [% cgi.param("shiptolastname") %]
+Address line 1: [% cgi.param("shiptoaddress1") %]
+Address line 2: [% cgi.param("shiptoaddress2") %]
+City: [% cgi.param("shiptocity") %]
+State/Region: [% cgi.param("shiptostate") %]
+Postal code: [% cgi.param("shiptopcode") %]
+Country: [% cgi.param("shiptocountry") %]
+Phone: [% cgi.param("shiptophone") %]
+[%+ IF cgi.param("shiptoidrut") %]Custom reference: [% 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 %]
+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 banner: [% IF cgi.param('horizontalbanner') %]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/REMO/template/en/default/bug/create/create-mozreps.html.tmpl b/extensions/REMO/template/en/default/bug/create/create-mozreps.html.tmpl
new file mode 100644
index 000000000..bd918d803
--- /dev/null
+++ b/extensions/REMO/template/en/default/bug/create/create-mozreps.html.tmpl
@@ -0,0 +1,241 @@
+[%# 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 REMO 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 <glob@mozilla.com>
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+
+[% PROCESS global/header.html.tmpl
+ title = "Mozilla Reps - Application Form"
+ style_urls = [ "extensions/REMO/web/styles/moz_reps.css" ]
+%]
+
+[% USE Bugzilla %]
+[% mandatory = '<span class="mandatory" title="Required">*</span>' %]
+
+<script type="text/javascript">
+var Dom = YAHOO.util.Dom;
+
+function mandatory(ids) {
+ result = true;
+ for (i in ids) {
+ id = ids[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;
+}
+
+function underageWarning (el) {
+ if (el.checked) {
+ Dom.removeClass('underage_warning', 'bz_default_hidden');
+ Dom.get('submit').disabled = true;
+ }
+ else {
+ Dom.addClass('underage_warning', 'bz_default_hidden');
+ Dom.get('submit').disabled = false;
+ }
+}
+
+function submitForm() {
+ if (!mandatory([ 'first_name', 'last_name', 'sex', 'city', 'country',
+ 'languages', 'learn', 'motivation', 'privacy' ])
+ ) {
+ alert('Please enter all the required fields.');
+ return false;
+ }
+
+ Dom.get('short_desc').value =
+ "Application Form: " + Dom.get('first_name').value + ' ' + Dom.get('last_name').value;
+
+ return true;
+}
+
+</script>
+
+<noscript>
+<h1>Javascript is required to use this form.</h1>
+</noscript>
+
+<h1>Mozilla Reps - Application Form</h1>
+
+<form method="post" action="post_bug.cgi" id="tmRequestForm">
+<input type="hidden" name="product" value="Mozilla Reps">
+<input type="hidden" name="component" value="Mentorship">
+<input type="hidden" name="bug_severity" value="normal">
+<input type="hidden" name="rep_platform" value="All">
+<input type="hidden" name="priority" value="--">
+<input type="hidden" name="op_sys" value="Other">
+<input type="hidden" name="version" value="unspecified">
+<input type="hidden" name="groups" value="mozilla-reps">
+<input type="hidden" name="format" value="[% format FILTER html %]">
+<input type="hidden" name="created-format" value="[% format FILTER html %]">
+<input type="hidden" name="comment" id="comment" value="">
+<input type="hidden" name="short_desc" id="short_desc" value="">
+<input type="hidden" name="token" value="[% token FILTER html %]">
+
+<table id="reps-form">
+
+<tr class="odd">
+ <th>First Name:[% mandatory FILTER none %]</th>
+ <td><input id="first_name" name="first_name" size="40" placeholder="John"></td>
+</tr>
+
+<tr class="even">
+ <th>Last Name:[% mandatory FILTER none %]</th>
+ <td><input id="last_name" name="last_name" size="40" placeholder="Doe"></td>
+</tr>
+
+<tr class="odd">
+ <th>Are you under 18 years old?:</th>
+ <td>
+ <input type="checkbox" id="underage" name="underage"
+ value="1" onclick="underageWarning(this);"><br>
+ </td>
+</tr>
+
+<tr id="underage_warning" class="odd bz_default_hidden">
+ <td colspan="2">
+ Mozilla Reps program is not currently accepting people under 18 years old.
+ Sorry for the inconvenience. In the meantime please check with your local Mozilla
+ group for other contribution opportunities
+ </td>
+</tr>
+
+<tr class="even">
+ <th>Sex:[% mandatory FILTER none %]</th>
+ <td>
+ <select id="sex" name="sex">
+ <option value="Male">Male</option>
+ <option value="Female">Female</option>
+ <option value="Other">Other</option>
+ </select>
+ </td>
+</tr>
+
+<tr class="odd">
+ <th>City:[% mandatory FILTER none %]</th>
+ <td><input id="city" name="city" size="40" placeholder="Your city"></td>
+</tr>
+
+<tr class="even">
+ <th>Country:[% mandatory FILTER none %]</th>
+ <td><input id="country" name="country" size="40" placeholder="Your country"></td>
+</tr>
+
+<tr class="odd">
+ <th>Local Community you participate in:</th>
+ <td><input id="community" name="community" size="40" placeholder="Name of your community"></td>
+</tr>
+
+<tr class="even">
+ <th>IM (specify service):</th>
+ <td><input id="im" name="im" size="40"></td>
+</tr>
+
+<tr class="odd">
+ <th>Mozillians.org Account:</th>
+ <td><input id="mozillian" name="mozillian" size="40"></td>
+</tr>
+
+<tr class="even">
+ <th colspan="2">
+ References:
+ </th>
+</tr>
+<tr class="even">
+ <td colspan="2">
+ <textarea id="references" name="references" rows="4"
+ placeholder="Add contact info of people referencing you."></textarea>
+ </td>
+</tr>
+
+<tr class="odd">
+ <th colspan="2">
+ How are you involved with Mozilla?
+ </th>
+</tr>
+<tr class="odd">
+ <td colspan="2">
+ <textarea id="involved" name="involved" rows="4" placeholder="Add-ons, l10n, SUMO, QA, ..."></textarea>
+ </td>
+</tr>
+
+<tr class="even">
+ <th>
+ When did you first start contributing to Mozilla?
+ </th>
+ <td><input id="firstcontribute" name="firstcontribute" size="40"></td>
+</tr>
+
+<tr class="odd">
+ <th>Languages Spoken:[% mandatory FILTER none %]</th>
+ <td><input id="languages" name="languages" size="40"></td>
+</tr>
+
+<tr class="even">
+ <th>How did you learn about Mozilla Reps?[% mandatory FILTER none %]</th>
+ <td><input id="learn" name="learn" size="40"></td>
+</tr>
+
+<tr class="odd">
+ <th colspan="2">What motivates you most about joining Mozilla Reps?[% mandatory FILTER none %]</th>
+</tr>
+<tr class="odd">
+ <td colspan="2"><textarea id="motivation" name="motivation" rows="4"></textarea></td>
+</tr>
+
+<tr class="even">
+ <th colspan="2">Comments:</th>
+</tr>
+<tr class="even">
+ <td colspan="2"><textarea id="comments" name="comments" rows="4"></textarea></td>
+</tr>
+
+<tr class="odd">
+ <th>
+ I have read the
+ <a href="http://www.mozilla.com/en-US/privacy-policy" target="_blank">Mozilla Privacy Policy</a>:[% mandatory FILTER none %]
+ </th>
+ <td><input id="privacy" type="checkbox"></td>
+</tr>
+
+<tr class="even">
+ <td>&nbsp;</td>
+ <td align="right">
+ <input id="submit" type="submit" value="Submit" onclick="return submitForm()">
+ </td>
+</tr>
+
+</table>
+
+</form>
+
+[% PROCESS global/footer.html.tmpl %]
diff --git a/extensions/REMO/template/en/default/bug/create/create-remo-budget.html.tmpl b/extensions/REMO/template/en/default/bug/create/create-remo-budget.html.tmpl
new file mode 100644
index 000000000..663d81ef1
--- /dev/null
+++ b/extensions/REMO/template/en/default/bug/create/create-remo-budget.html.tmpl
@@ -0,0 +1,248 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+
+[% PROCESS global/header.html.tmpl
+ title = "Mozilla Reps Budget Request Form"
+ style_urls = [ 'extensions/REMO/web/styles/moz_reps.css' ]
+ javascript_urls = [ 'extensions/REMO/web/js/form_validate.js',
+ 'js/util.js',
+ 'js/field.js' ]
+%]
+
+[% IF user.in_group("mozilla-reps") %]
+
+<p>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
+Rep team.</p>
+
+<script language="javascript" type="text/javascript">
+function trySubmit() {
+ var firstname = document.getElementById('firstname').value;
+ var lastname = document.getElementById('lastname').value;
+ var eventpage = document.getElementById('eventpage').value;
+ var shortdesc = 'Budget Request - ' + firstname + ' ' + lastname + ' - ' + eventpage;
+ document.getElementById('short_desc').value = shortdesc;
+ document.getElementById('cc').value = document.getElementById('mentoremail').value;
+ return true;
+}
+
+function validateAndSubmit() {
+ var alert_text = '';
+ if(!isFilledOut('firstname')) alert_text += "Please enter your first name\n";
+ if(!isFilledOut('lastname')) alert_text += "Please enter your last name\n";
+ if(!isFilledOut('profilepage')) alert_text += "Please enter a Mozilla Reps profile page.\n";
+ if(!isFilledOut('eventpage')) alert_text += "Please enter a event page address.\n";
+ if(!isFilledOut('mentoremail')) alert_text += "Please enter a valid [% terms.Bugzilla %] email for mentor.\n";
+ if(!isFilledOut('country')) alert_text += "Please enter a valid value for country.\n";
+ if(!isFilledOut('budgettotal')) alert_text += "Please enter the total budget for the event.\n";
+ if(!isFilledOut('service1') || !isFilledOut('cost1')) alert_text += "Please enter at least one service and cost value.\n";
+
+ //Everything required is filled out..try to submit the form!
+ if(alert_text == '') {
+ return trySubmit();
+ }
+
+ //alert text, stay here on the pagee
+ alert(alert_text);
+ return false;
+}
+</script>
+
+<h1>Mozilla Reps - Budget Request Form</h1>
+
+<p>
+ If your request is Community IT related please file it
+ <a href="https://bugzilla.mozilla.org/enter_bug.cgi?product=Mozilla%20Reps;component=Community%20IT%20Requests">here</a>.
+</p>
+
+<p>
+ <span class="required_star">*</span> - <span class="required_explanation">Required Fields</span>
+</p>
+
+<form method="post" action="post_bug.cgi" id="swagRequestForm" enctype="multipart/form-data"
+ onSubmit="return validateAndSubmit();">
+
+ <input type="hidden" name="format" value="remo-budget">
+ <input type="hidden" name="created-format" value="remo-budget">
+ <input type="hidden" name="product" value="Mozilla Reps">
+ <input type="hidden" name="component" value="Budget Requests">
+ <input type="hidden" name="rep_platform" value="All">
+ <input type="hidden" name="op_sys" value="Other">
+ <input type="hidden" name="priority" value="--">
+ <input type="hidden" name="version" value="unspecified">
+ <input type="hidden" name="bug_severity" id="bug_severity" value="normal">
+ <input type="hidden" name="short_desc" id="short_desc" value="">
+ <input type="hidden" name="cc" id="cc" value="">
+ <input type="hidden" name="groups" value="mozilla-reps">
+ <input type="hidden" name="token" value="[% token FILTER html %]">
+
+<table id="reps-form">
+
+<tr class="odd">
+ <th class="field_label required">First Name:</th>
+ <td>
+ <input type="text" name="firstname" id="firstname" value="" size="40" placeholder="John">
+ </td>
+</tr>
+
+<tr class="even">
+ <th class="field_label required">Last Name:</th>
+ <td>
+ <input type="text" name="lastname" id="lastname" value="" size="40" placeholder="Doe">
+ </td>
+</tr>
+
+<tr class="odd">
+ <th class="field_label required">Mozilla Reps Profile Page:</th>
+ <td>
+ <input type="text" name="profilepage" id="profilepage"
+ value="" size="40" placeholder="https://reps.mozilla.org/u/JohnDoe">
+ </td>
+</tr>
+
+<tr class="even">
+ <th class="field_label required">Event Page:</th>
+ <td>
+ <input type="text" name="eventpage" id="eventpage"
+ value="" size="40" placeholder="https://reps.mozilla.org/e/TestEvent">
+ </td>
+</tr>
+
+<tr class="odd">
+ <th class="field_label required">[% terms.Bugzilla %] Email of Your Mentor:</th>
+ <td>
+ [% INCLUDE global/userselect.html.tmpl
+ id => "mentoremail"
+ name => "mentoremail"
+ value => ""
+ size => 40
+ %]
+ </td>
+</tr>
+
+<tr class="even">
+ <th class="field_label">Paypal Account Email:</th>
+ <td>
+ <input type="text" name="paypal" id="paypal"
+ value="" size="40" placeholder=""><br>
+ <span style="font-size: smaller;">
+ * Currently, you CANNOT make payments using other online payment services.</span>
+ </td>
+</tr>
+
+<tr class="odd">
+ <th class="field_label required">Country You Reside:</th>
+ <td>
+ <input type="text" name="country" id="country"
+ value="" size="40" placeholder="USA">
+ </td>
+</tr>
+
+<tr class="even">
+ <th class="field_label">Is advance payment needed?</th>
+ <td>
+ <input type="checkbox" name="advancepayment" id="advancepayment" value="1">
+ </td>
+</tr>
+
+<tr class="odd">
+ <td><!--spacer-->&nbsp;</td>
+ <td><!--spacer-->&nbsp;</td>
+</tr>
+
+<tr class="even">
+ <th colspan="2" class="field_label">Budget Request:</th>
+</tr>
+
+<tr class="even">
+ <th class="field_label required">Total amount requested in $USD:</th>
+ <td>
+ <input type="text" name="budgettotal" id="budgettotal" value="" size="40">
+ </td>
+ </tr>
+
+<tr class="even">
+ <th colspan="2" class="field_label">Costs per service:</th>
+</tr>
+
+<tr class="even">
+ <td colspan="2">
+ <table>
+ <tr>
+ <th class="field_label required">Service 1:</th>
+ <td><input type="text" id="service1" name="service1" size="30"></td>
+ <th class="field_label required">Cost 1:</th>
+ <td><input type="text" id="cost1" name="cost1" size="30"></td>
+ </tr>
+ <tr>
+ <th class="field_lable">Service 2:</th>
+ <td><input type="text" id="service2" name="service2" size="30"></td>
+ <th class="field_lable">Cost 2:</th>
+ <td><input type="text" id="cost2" name="cost2" size="30"></td>
+ </tr>
+ <tr>
+ <th class="field_lable">Service 3:</th>
+ <td><input type="text" id="service3" name="service3" size="30"></td>
+ <th class="field_lable">Cost 3:</th>
+ <td><input type="text" id="cost3" name="cost3" size="30"></td>
+ </tr>
+ <tr>
+ <th class="field_lable">Service 4:</th>
+ <td><input type="text" id="service4" name="service4" size="30"></td>
+ <th class="field_lable">Cost 4:</th>
+ <td><input type="text" id="cost4" name="cost4" size="30"></td>
+ </tr>
+ <tr>
+ <th class="field_lable">Service 5:</th>
+ <td><input type="text" id="service5" name="service5" size="30"></td>
+ <th class="field_lable">Cost 5:</th>
+ <td><input type="text" id="cost5" name="cost5" size="30"></td>
+ </tr>
+ </table>
+ </td>
+</tr>
+
+<tr class="even">
+ <th colspan="2" class="field_label">Additional costs:</th>
+</tr>
+
+<tr class="even">
+ <td colspan="2">
+ <textarea id="costadditional" name="costadditional" rows="5" cols="50"></textarea>
+ </td>
+</tr>
+
+<tr class="odd">
+ <td>&nbsp;</td>
+ <td align="right">
+ <input type="submit" id="commit" value="Submit Request">
+ </td>
+</tr>
+
+</table>
+
+</form>
+
+<p style="font-weight:bold;">
+ Budget requests received less than 3 weeks before the targeted launch date of the
+ event/activity in question will automatically be rejected (exceptions can be made
+ but only with council approval). This 3-week “buffer” guarantees that each budget
+ request undergoes the same thorough selection process.
+</p>
+
+<p>
+ Thanks for contacting us.
+</p>
+
+[% ELSE %]
+ <p>Sorry, you do not have access to this page.</p>
+[% END %]
+
+[% PROCESS global/footer.html.tmpl %]
diff --git a/extensions/REMO/template/en/default/bug/create/create-remo-swag.html.tmpl b/extensions/REMO/template/en/default/bug/create/create-remo-swag.html.tmpl
new file mode 100644
index 000000000..cd4fb1a16
--- /dev/null
+++ b/extensions/REMO/template/en/default/bug/create/create-remo-swag.html.tmpl
@@ -0,0 +1,306 @@
+[%# 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 <reed@mozilla.com>
+ # David Tran <dtran@mozilla.com>
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+
+[% PROCESS global/header.html.tmpl
+ title = "Mozilla Reps Swag Request Form"
+ javascript_urls = [ 'extensions/REMO/web/js/swag.js',
+ 'extensions/REMO/web/js/form_validate.js',
+ 'js/field.js',
+ 'js/util.js' ]
+ style_urls = [ "extensions/REMO/web/styles/moz_reps.css" ]
+ yui = [ 'calendar' ]
+%]
+
+[% IF user.in_group("mozilla-reps") %]
+
+<p>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 Rep team.</p>
+
+<script language="javascript" type="text/javascript">
+function trySubmit() {
+ var eventname = document.getElementById('eventname').value;
+ var shortdesc = 'Swag Request - ' + eventname;
+ document.getElementById('short_desc').value = shortdesc;
+ return true;
+}
+
+function validateAndSubmit() {
+ var alert_text = '';
+ if(!isFilledOut('firstname')) alert_text += "Please enter your first name\n";
+ if(!isFilledOut('lastname')) alert_text += "Please enter your last name\n";
+ if(!isFilledOut('profilepage')) alert_text += "Please enter your Mozilla Reps profile page\n";
+ if(!isFilledOut('eventname')) alert_text += "Please enter your event name\n";
+ if(!isFilledOut('eventpage')) alert_text += "Please enter the event page.\n";
+ if(!isFilledOut('attendance')) alert_text += "Please enter the estimated attendance.\n";
+ if(!isFilledOut('shiptofirstname')) alert_text += "Please enter the shipping first name\n";
+ if(!isFilledOut('shiptolastname')) alert_text += "Please enter the shipping last name\n";
+ if(!isFilledOut('shiptoaddress1')) alert_text += "Please enter the ship to address\n";
+ if(!isFilledOut('shiptocity')) alert_text += "Please enter the ship to city\n";
+ if(!isFilledOut('shiptocountry')) alert_text += "Please enter the ship to country\n";
+ if(!isFilledOut('shiptopcode')) alert_text += "Please enter the ship to postal code\n";
+ if(!isFilledOut('shiptophone')) alert_text += "Please enter the ship to contact number\n";
+
+ //Everything required is filled out..try to submit the form!
+ if(alert_text == '') {
+ return trySubmit();
+ }
+
+ //alert text, stay here on the pagee
+ alert(alert_text);
+ return false;
+}
+
+</script>
+
+<h1>Mozilla Reps - Swag Request Form</h1>
+
+<form method="post" action="post_bug.cgi" id="swagRequestForm" enctype="multipart/form-data"
+ onSubmit="return validateAndSubmit();">
+
+ <input type="hidden" name="format" value="remo-swag">
+ <input type="hidden" name="product" value="Mozilla Reps">
+ <input type="hidden" name="component" value="Swag Requests">
+ <input type="hidden" name="rep_platform" value="All">
+ <input type="hidden" name="op_sys" value="Other">
+ <input type="hidden" name="priority" value="--">
+ <input type="hidden" name="version" value="unspecified">
+ <input type="hidden" name="bug_severity" id="bug_severity" value="normal">
+ <input type="hidden" name="short_desc" id="short_desc" value="">
+ <input type="hidden" name="groups" value="mozilla-reps">
+ <input type="hidden" name="token" value="[% token FILTER html %]">
+
+<table id="reps-form">
+
+<tr class="odd">
+ <td><strong>First Name: <span style="color: red;" title="Required">*</span></strong></td>
+ <td>
+ <input type="text" name="firstname" id="firstname" placeholder="John" size="40">
+ </td>
+</tr>
+
+<tr class="even">
+ <td><strong>Last Name: <span style="color: red;" title="Required">*</span></strong></td>
+ <td>
+ <input type="text" name="lastname" id="lastname" placeholder="Doe" size="40">
+ </td>
+</tr>
+
+<tr class="odd">
+ <td>
+ <strong>Mozilla Reps Profile Page:
+ <span style="color: red;" title="Required">*</span></strong>
+ </td>
+ <td>
+ <input type="text" name="profilepage" id="profilepage" size="40">
+ </td>
+</tr>
+
+<tr class="even">
+ <td><strong>Event Name: <span style="color: red;" title="Required">*</span></strong></td>
+ <td>
+ <input type="text" name="eventname" id="eventname" size="40">
+ </td>
+</tr>
+
+<tr class="odd">
+ <td><strong>Event Page: <span style="color: red;" title="Required">*</span></strong></td>
+ <td>
+ <input type="text" name="eventpage" id="eventpage" size="40">
+ </td>
+</tr>
+
+<tr class="even">
+ <td><strong>Estimated Attendance: <span style="color: red;" title="Required">*</span></strong></td>
+ <td>
+ <select id="attendance" name="attendance">
+ <option value="1-50">1-50</option>
+ <option value="51-200">51-200</option>
+ <option value="201-500">201-500</option>
+ <option value="501-1000+">501-1000+</option>
+ </select>
+ </td>
+</tr
+
+<tr class="odd">
+ <td><!--spacer-->&nbsp;</td>
+ <td><!--spacer-->&nbsp;</td>
+</tr>
+
+<tr class="even">
+ <td colspan="2"><strong>Shipping Details:</strong></td>
+</tr>
+
+<tr class="odd">
+ <td><strong>Ship Before:</strong>
+ <td>
+ [% INCLUDE bug/field.html.tmpl
+ bug = default,
+ field = bug_fields.cf_due_date
+ value = default.cf_due_date,
+ editable = 1,
+ no_tds = 1
+ %]
+ </td>
+</tr>
+
+<tr class="even">
+ <td><strong>First Name: <span style="color: red;" title="Required">*</span></strong></td>
+ <td><input name="shiptofirstname" id="shiptofirstname" placeholder="John" size="40"></td>
+</tr>
+
+<tr class="odd">
+ <td><strong>Last Name: <span style="color: red;" title="Required">*</span></strong></td>
+ <td><input name="shiptolastname" id="shiptolastname" placeholder="Doe" size="40"></td>
+</tr>
+
+<tr class="even">
+ <td><strong>Address Line 1: <span style="color: red;" title="Required">*</span></strong></td>
+ <td><input name="shiptoaddress1" id="shiptoaddress1" placeholder="123 Main St." size="40"></td>
+</tr>
+
+<tr class="odd">
+ <td><strong>Address Line 2:</strong></td>
+ <td><input name="shiptoaddress2" id="shiptoaddress2" size="40"></td>
+</tr>
+
+<tr class="even">
+ <td><strong>City: <span style="color: red;" title="Required">*</span></strong></td>
+ <td><input name="shiptocity" id="shiptocity" size="40" placeholder="Anytown"></td>
+</tr>
+
+<tr class="odd">
+ <td><strong>State/Region (if applicable):</strong></td>
+ <td><input name="shiptostate" id="shiptostate" placeholder="CA" size="40"></td>
+</tr>
+
+<tr class="even">
+ <td><strong>Country: <span style="color: red;" title="Required">*</span></strong></td>
+ <td><input name="shiptocountry" id="shiptocountry" placeholder="USA" size="40"></td>
+</tr>
+
+<tr class="odd">
+ <td><strong>Postal Code: <span style="color: red;" title="Required">*</span></strong></td>
+ <td><input name="shiptopcode" id="shiptopcode" placeholder="90210" size="40"></td>
+</tr>
+
+<tr class="even">
+ <td><strong>Phone (including country code): <span style="color: red;" title="Required">*</span></strong></td>
+ <td><input name="shiptophone" id="shiptophone" placeholder="919-555-1212" size="40"></td>
+</tr>
+
+<tr class="odd">
+ <td><strong>Custom Reference<br>
+ (Fiscal or VAT-number, if known):</strong><br><small>(if your country requires this)</small>
+ </td>
+ <td><input name="shiptoidrut" id="shiptoidrut" size="40"></td>
+</tr>
+
+<tr class="even">
+ <td colspan="2">
+ <strong>Addition information for delivery person:</strong><br>
+ <textarea id="shipadditional" name="shipadditional" rows="4"></textarea>
+ </td>
+</tr>
+
+<tr class="odd">
+ <td><!--spacer-->&nbsp;</td>
+ <td><!--spacer-->&nbsp;</td>
+</tr>
+
+<tr class="even">
+ <td colspan="2"><strong>Swag Requested:</strong></td>
+</tr>
+
+<tr class="odd">
+ <td><strong>Stickers:</strong></td>
+ <td><input type="checkbox" id="stickers" name="stickers" value="1"></td>
+</tr>
+
+<tr class="even">
+ <td><strong>Buttons:</strong></td>
+ <td><input type="checkbox" id="buttons" name="buttons" value="1"></td>
+</tr>
+
+<tr class="odd">
+ <td><strong>Lanyards:</strong></td>
+ <td><input type="checkbox" id="lanyards" name="lanyards" value="1"></td>
+</tr>
+
+<tr class="even">
+ <td><strong>T-Shirts:</strong></td>
+ <td><input type="checkbox" id="tshirts" name="tshirts" value="1"></td>
+</tr>
+
+<tr class="odd">
+ <td><strong>Roll-Up Banners:</strong></td>
+ <td><input type="checkbox" id="rollupbanners" name="rollupbanners" value="1"></td>
+</tr>
+
+<tr class="even">
+ <td><strong>Horizontal Banner:</strong></td>
+ <td><input type="checkbox" id="horizontalbanner" name="horizontalbanner" value="1"></td>
+</tr>
+
+<tr class="odd">
+ <td><strong>Booth Cloth:</strong></td>
+ <td><input type="checkbox" id="boothcloth" name="boothcloth" value="1"></td>
+</tr>
+
+<tr class="even">
+ <td><strong>Pens:</strong></td>
+ <td><input type="checkbox" id="pens" name="pens" value="1"></td>
+</tr>
+
+<tr class="odd">
+ <td><strong>Other:</strong> (please specify)</td>
+ <td><input type="text" id="otherswag" name="otherswag" size="40"></td>
+</tr>
+
+<tr class="even">
+ <td>&nbsp;</td>
+ <td align="right">
+ <input type="submit" id="commit" value="Submit Request">
+ </td>
+</tr>
+
+</table>
+
+<p>
+ Quantities of different swag items requested that will actually be shipped
+ depend on stock availability and number of attendees. Mozilla cannot guarantee
+ that all items requested will be in stock at the time of shipment and you will
+ be notified in case an item cannot be shipped. Please request swag at least 1
+ month before desired delivery date.
+</p>
+
+<p>
+ <strong><span style="color: red;">*</span></strong> - Required field<br />
+ Thanks for contacting us.
+ You will be notified by email of any progress made in resolving your request.
+</p>
+
+[% ELSE %]
+ <p>Sorry, you do not have access to this page.</p>
+[% END %]
+
+[% PROCESS global/footer.html.tmpl %]
diff --git a/extensions/REMO/template/en/default/bug/create/create-remo-swag.xml.tmpl b/extensions/REMO/template/en/default/bug/create/create-remo-swag.xml.tmpl
new file mode 100644
index 000000000..7e43de664
--- /dev/null
+++ b/extensions/REMO/template/en/default/bug/create/create-remo-swag.xml.tmpl
@@ -0,0 +1,116 @@
+[%# 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 REMO 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):
+ # David Lawrence <dkl@mozilla.com>
+ #%]
+
+[% USE Bugzilla %]
+[% cgi = Bugzilla.cgi %]
+<?xml version="1.0" [% IF Param('utf8') %]encoding="UTF-8" [% END %]standalone="yes" ?>
+<!DOCTYPE remoswag [
+<!ELEMENT remoswag (firstname,
+ lastname,
+ wikiprofile,
+ eventname,
+ wikipage,
+ attendance,
+ shipping,
+ swagrequested)>
+<!ELEMENT firstname (#PCDATA)>
+<!ELEMENT lastname (#PCDATA)>
+<!ELEMENT wikiprofile (#PCDATA)>
+<!ELEMENT eventname (#PCDATA)>
+<!ELEMENT wikipage (#PCDATA)>
+<!ELEMENT attendance (#PCDATA)>
+<!ELEMENT shipping (shipbeforedate,
+ shiptofirstname,
+ shiptolastname,
+ shiptoaddress1,
+ shiptoaddress2,
+ shiptocity,
+ shiptostate,
+ shiptopcode,
+ shiptocountry,
+ shiptophone,
+ shiptoidrut,
+ shipadditional)>
+<!ELEMENT shipbeforedate (#PCDATA)>
+<!ELEMENT shiptofirstname (#PCDATA)>
+<!ELEMENT shiptolastname (#PCDATA)>
+<!ELEMENT shiptoaddress1 (#PCDATA)>
+<!ELEMENT shiptoaddress2 (#PCDATA)>
+<!ELEMENT shiptocity (#PCDATA)>
+<!ELEMENT shiptostate (#PCDATA)>
+<!ELEMENT shiptopcode (#PCDATA)>
+<!ELEMENT shiptocountry (#PCDATA)>
+<!ELEMENT shiptophone (#PCDATA)>
+<!ELEMENT shiptoidrut (#PCDATA)>
+<!ELEMENT shipadditional (#PCDATA)>
+<!ELEMENT swagrequested (stickers,
+ buttons,
+ posters,
+ lanyards,
+ tshirts,
+ rollupbanners,
+ horizontalbanner,
+ boothcloth,
+ pens,
+ otherswag)>
+<!ELEMENT stickers (#PCDATA)>
+<!ELEMENT buttons (#PCDATA)>
+<!ELEMENT posters (#PCDATA)>
+<!ELEMENT lanyards (#PCDATA)>
+<!ELEMENT tshirts (#PCDATA)>
+<!ELEMENT rollupbanners (#PCDATA)>
+<!ELEMENT horizontalbanners (#PCDATA)>
+<!ELEMENT boothcloth (#PCDATA)>
+<!ELEMENT pens (#PCDATA)>
+<!ELEMENT otherswag (#PCDATA)>]>
+<remoswag>
+ <firstname>[% cgi.param('firstname') FILTER xml %]</firstname>
+ <lastname>[% cgi.param('lastname') FILTER xml %]</lastname>
+ <wikiprofile>[% cgi.param('wikiprofile') FILTER xml %]</wikiprofile>
+ <eventname>[% cgi.param('eventname') FILTER xml %]</eventname>
+ <wikipage>[% cgi.param('wikipage') FILTER xml %]</wikipage>
+ <attendance> [% cgi.param('attendance') FILTER xml %]</attendance>
+ <shipping>
+ <shipbeforedate>[% cgi.param('cf_due_date') FILTER xml %]</shipbeforedate>
+ <shiptofirstname>[% cgi.param("shiptofirstname") FILTER xml %]</shiptofirstname>
+ <shiptolastname>[% cgi.param("shiptolastname") FILTER xml %]</shiptolastname>
+ <shiptoaddress1>[% cgi.param("shiptoaddress1") FILTER xml %]</shiptoaddress1>
+ <shiptoaddress2>[% cgi.param("shiptoaddress2") FILTER xml %]</shiptoaddress2>
+ <shiptocity>[% cgi.param("shiptocity") FILTER xml %]</shiptocity>
+ <shiptostate>[% cgi.param("shiptostate") FILTER xml %]</shiptostate>
+ <shiptopcode>[% cgi.param("shiptopcode") FILTER xml %]</shiptopcode>
+ <shiptocountry>[% cgi.param("shiptocountry") FILTER xml %]</shiptocountry>
+ <shiptophone>[% cgi.param("shiptophone") FILTER xml %]</shiptophone>
+ <shiptoidrut>[% cgi.param("shiptoidrut") FILTER xml %]</shiptoidrut>
+ <shipadditional>[% cgi.param('shipadditional') || '' FILTER xml %]</shipadditional>
+ </shipping>
+ <swagrequested>
+ <stickers>[% (cgi.param('stickers') ? 1 : 0) FILTER xml %]</stickers>
+ <buttons>[% (cgi.param('buttons') ? 1 : 0) FILTER xml %]</buttons>
+ <posters>[% (cgi.param('posters') ? 1 : 0) FILTER xml %]</posters>
+ <lanyards>[% (cgi.param('lanyards') ? 1 : 0) FILTER xml %]</lanyards>
+ <tshirts>[% (cgi.param('tshirts') ? 1 : 0) FILTER xml %]</tshirts>
+ <rollupbanners>[% (cgi.param('rollupbanners') ? 1 : 0) FILTER xml %]</rollupbanners>
+ <horizontalbanner>[% (cgi.param('horizontalbanner') ? 1 : 0) FILTER xml %]</horizontalbanner>
+ <boothcloth>[% (cgi.param('boothcloth') ? 1 : 0) FILTER xml %]</boothcloth>
+ <pens>[% (cgi.param('pens') ? 1 : 0) FILTER xml %]</pens>
+ <otherswag>[% cgi.param('otherswag') || '' FILTER xml %]</otherswag>
+ </swagrequested>
+</remoswag>
diff --git a/extensions/REMO/template/en/default/bug/create/created-mozreps.html.tmpl b/extensions/REMO/template/en/default/bug/create/created-mozreps.html.tmpl
new file mode 100644
index 000000000..a8a3ca112
--- /dev/null
+++ b/extensions/REMO/template/en/default/bug/create/created-mozreps.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 REMO 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 <glob@mozilla.com>
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+
+[% PROCESS global/header.html.tmpl
+ title = "Mozilla Reps - Application Form"
+
+%]
+
+<h1>Thank you!</h1>
+
+<p>
+Thank you for submitting your Mozilla Reps Application Form. A Mozilla Rep
+mentor will contact you shortly at your bugzilla email address.
+</p>
+
+<p style="font-size: x-small">
+Reference: <a href="show_bug.cgi?id=[% id FILTER uri %]">#[% id FILTER html %]</a>
+</p>
+
+[% PROCESS global/footer.html.tmpl %]
diff --git a/extensions/REMO/template/en/default/bug/create/created-remo-budget.html.tmpl b/extensions/REMO/template/en/default/bug/create/created-remo-budget.html.tmpl
new file mode 100644
index 000000000..62430bf9c
--- /dev/null
+++ b/extensions/REMO/template/en/default/bug/create/created-remo-budget.html.tmpl
@@ -0,0 +1,27 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+
+[% PROCESS global/header.html.tmpl
+ title = "Mozilla Reps Budget Request Form"
+%]
+
+<h1>Thank you!</h1>
+
+<p>
+ Your budget request has been successfully submitted. Please make sure to
+ follow-up with your mentor so (s)he can verify your request. CC him/her
+ on the [% terms.bug %] if needed.
+</p>
+
+<p style="font-size: x-small">
+ Reference: <a href="show_bug.cgi?id=[% id FILTER uri %]">#[% id FILTER html %]</a>
+</p>
+
+[% PROCESS global/footer.html.tmpl %]
diff --git a/extensions/REMO/template/en/default/hook/global/user-error-errors.html.tmpl b/extensions/REMO/template/en/default/hook/global/user-error-errors.html.tmpl
new file mode 100644
index 000000000..200e678be
--- /dev/null
+++ b/extensions/REMO/template/en/default/hook/global/user-error-errors.html.tmpl
@@ -0,0 +1,40 @@
+[%# 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 REMO 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 <bjones@mozilla.com>
+ # David Lawrence <dkl@mozilla.com>
+ #%]
+
+[% IF error == "remo_payment_invalid_product" %]
+ [% title = "Mozilla Reps Payment Invalid 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 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
+ <a href="[% urlbase FILTER html %]attachment.cgi?id=[% attachid FILTER uri %]&action=edit">
+ attachment [% attachid FILTER uri %]</a>.<br>
+ <br>
+ You can either <a href="[% urlbase FILTER html %]page.cgi?id=remo-form-payment.html">
+ create a new payment request</a> or [% "go back to $terms.bug $bugid" FILTER bug_link(bugid) FILTER none %].
+
+[% END %]
diff --git a/extensions/REMO/template/en/default/pages/comment-remo-form-payment.txt.tmpl b/extensions/REMO/template/en/default/pages/comment-remo-form-payment.txt.tmpl
new file mode 100644
index 000000000..95c0af6e8
--- /dev/null
+++ b/extensions/REMO/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 REMO 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 <dkl@mozilla.com>
+ #%]
+
+[% 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/REMO/template/en/default/pages/remo-form-payment.html.tmpl b/extensions/REMO/template/en/default/pages/remo-form-payment.html.tmpl
new file mode 100644
index 000000000..0f5f206d3
--- /dev/null
+++ b/extensions/REMO/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 REMO 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 <dkl@mozilla.com>
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+
+[% PROCESS global/header.html.tmpl
+ title = "Mozilla Reps Payment Form"
+ style_urls = [ 'extensions/REMO/web/styles/moz_reps.css' ]
+ javascript_urls = [ 'extensions/REMO/web/js/form_validate.js',
+ 'js/util.js',
+ 'js/field.js' ]
+ yui = ['connection', 'json']
+%]
+
+<script language="javascript" type="text/javascript">
+
+var bug_cache = {};
+
+function validateAndSubmit() {
+ var alert_text = '';
+ if(!isFilledOut('firstname')) alert_text += "Please enter your first name\n";
+ if(!isFilledOut('lastname')) alert_text += "Please enter your last name\n";
+ if(!isFilledOut('wikiprofile')) alert_text += "Please enter a wiki user profile.\n";
+ if(!isFilledOut('wikipage')) alert_text += "Please enter a wiki page address.\n";
+ if(!isFilledOut('bug_id')) alert_text += "Please enter a valid [% terms.bug %] id to attach this additional information to.\n";
+ if(!isFilledOut('expenseform')) alert_text += "Please enter an expense form to upload.\n";
+ if(!isFilledOut('receipts')) alert_text += "Please enter a receipts file to upload.\n";
+
+ if (alert_text) {
+ alert(alert_text);
+ return false;
+ }
+
+ return true;
+}
+
+function togglePaymentInfo (e) {
+ var div = document.getElementById('paymentinfo');
+ if (e.checked == false) {
+ div.style.display = 'block';
+ }
+ else {
+ div.style.display = 'none';
+ }
+}
+
+function getBugInfo (e, div) {
+ var bug_id = e.value;
+ div = document.getElementById(div);
+
+ if (!bug_id) {
+ div.innerHTML = "";
+ return true;
+ }
+
+ div.style.display = 'block';
+
+ if (bug_cache[bug_id]) {
+ div.innerHTML = bug_cache[bug_id];
+ e.disabled = false;
+ return true;
+ }
+
+ e.disabled = true;
+ div.innerHTML = 'Getting [% terms.bug %] info...';
+
+ YAHOO.util.Connect.setDefaultPostHeader('application/json', true);
+ YAHOO.util.Connect.asyncRequest(
+ 'POST',
+ 'jsonrpc.cgi',
+ {
+ success: function(res) {
+ var bug_message = "";
+ data = YAHOO.lang.JSON.parse(res.responseText);
+ if (data.error) {
+ bug_message = "Get [% terms.bug %] failed: " + data.error.message;
+ }
+ else if (data.result) {
+ if (data.result.bugs[0].product !== 'Mozilla Reps'
+ || data.result.bugs[0].component !== 'Budget Requests')
+ {
+ bug_message = "You can only attach budget payment " +
+ "information to [% terms.bugs %] under the product " +
+ "'Mozilla Reps' and component 'Budget Requests'.";
+ }
+ else {
+ bug_message = "[% terms.Bug %] " + bug_id + " - " + data.result.bugs[0].status +
+ " - " + data.result.bugs[0].summary;
+ }
+ }
+ else {
+ bug_message = "Get [% terms.bug %] failed: " + res.responseText;
+ }
+ div.innerHTML = bug_message;
+ bug_cache[bug_id] = bug_message;
+ e.disabled = false;
+ },
+ failure: function(res) {
+ if (res.responseText) {
+ div.innerHTML = "Get [% terms.bug %] failed: " + res.responseText;
+ }
+ }
+ },
+ YAHOO.lang.JSON.stringify({
+ version: "1.1",
+ method: "Bug.get",
+ id: bug_id,
+ params: {
+ ids: [ bug_id ],
+ include_fields: [ 'product', 'component', 'status', 'summary' ]
+ }
+ })
+ );
+}
+
+</script>
+
+<h1>Mozilla Reps - Payment Form</h1>
+
+<form method="post" action="page.cgi" id="paymentForm" enctype="multipart/form-data"
+ onSubmit="return validateAndSubmit();">
+<input type="hidden" id="id" name="id" value="remo-form-payment.html">
+<input type="hidden" id="token" name="token" value="[% token FILTER html %]">
+<input type="hidden" id="action" name="action" value="commit">
+
+<table id="reps-form">
+
+<tr class="odd">
+ <td width="25%"><strong>First Name: <span style="color: red;">*</span></strong></td>
+ <td>
+ <input type="text" name="firstname" id="firstname" value="" size="40" placeholder="John">
+ </td>
+</tr>
+
+<tr class="even">
+ <td><strong>Last Name: <span style="color: red;">*</span></strong></td>
+ <td>
+ <input type="text" name="lastname" id="lastname" value="" size="40" placeholder="Doe">
+ </td>
+</tr>
+
+<tr class="odd">
+ <td><strong>Wiki user profile:<span style="color: red;">*</span></strong></td>
+ <td>
+ <input type="text" name="wikiprofile" id="wikiprofile" value="" size="40" placeholder="JohnDoe">
+ </td>
+</tr>
+
+<tr class="even">
+ <td><strong>Event wiki page: <span style="color: red;">*</span></strong></td>
+ <td>
+ <input type="text" name="wikipage" id="wikipage" value="" size="40">
+ </td>
+</tr>
+
+<tr class="odd">
+ <td><strong>Budget request [% terms.bug %]: <span style="color: red;">*</span></strong></td>
+ <td>
+ <input type="text" name="bug_id" id="bug_id" value="" size="40"
+ onblur="getBugInfo(this,'bug_info');")>
+ </td>
+</tr>
+
+<tr class="odd">
+ <td colspan="2">
+ <div id="bug_info" style="display:none;"></div>
+ </td>
+</tr>
+
+<tr class="even">
+ <td colspan="2">
+ <strong>Have you already received payment for this event?</strong>
+ <input type="checkbox" name="receivedpayment" id="receivedpayment" value="1"
+ onchange="togglePaymentInfo(this);" checked="true">
+ <div id="paymentinfo" style="display:none;">
+ Please send an email to William at mozilla.com with all the information below:<br>
+ <br>
+ Payment information:<br>
+ Bank name:<br>
+ Bank address: <br>
+ IBAN:<br>
+ Swift code/BIC:<br>
+ Additional bank details (if necessary):
+ </div>
+ </td>
+</tr>
+
+<tr class="odd">
+ <td colspan="2">
+ <strong>Expense form and scanned receipts/invoices:</strong>
+ </td>
+</tr>
+
+<tr class="odd">
+ <td>Expense Form: <span style="color: red;">*</span></td>
+ <td><input type="file" id="expenseform" name="expenseform" size="40"></td>
+</tr>
+
+<tr class="odd">
+ <td valign="top">Receipts File: <span style="color: red;">*</span></td>
+ <td>
+ <input type="file" id="receipts" name="receipts" size="40"><br>
+ <font style="color:red;">
+ Please black out any bank account information included<br>
+ on receipts before attaching them.
+ </font>
+ </td>
+</tr>
+
+<tr class="even">
+ <td>&nbsp;</td>
+ <td align="right">
+ <input type="submit" id="commit" value="Submit Request">
+ </td>
+</tr>
+
+</table>
+
+</form>
+
+<p>
+ <strong><span style="color: red;">*</span></strong> - Required field<br>
+ Thanks for contacting us.
+</p>
+
+[% PROCESS global/footer.html.tmpl %]
diff --git a/extensions/REMO/web/js/form_validate.js b/extensions/REMO/web/js/form_validate.js
new file mode 100644
index 000000000..6c8fa6f07
--- /dev/null
+++ b/extensions/REMO/web/js/form_validate.js
@@ -0,0 +1,21 @@
+/**
+ * Some Form Validation and Interaction
+ **/
+//Makes sure that there is an '@' in the address with a '.'
+//somewhere after it (and at least one character in between them
+
+function isValidEmail(email) {
+ var at_index = email.indexOf("@");
+ var last_dot = email.lastIndexOf(".");
+ return at_index > 0 && last_dot > (at_index + 1);
+}
+
+//Takes a DOM element id and makes sure that it is filled out
+function isFilledOut(elem_id) {
+ var str = document.getElementById(elem_id).value;
+ return str.length>0 && str!="noneselected";
+}
+
+function isChecked(elem_id) {
+ return document.getElementById(elem_id).checked;
+}
diff --git a/extensions/REMO/web/js/swag.js b/extensions/REMO/web/js/swag.js
new file mode 100644
index 000000000..3b69bbab8
--- /dev/null
+++ b/extensions/REMO/web/js/swag.js
@@ -0,0 +1,60 @@
+/**
+ * Swag Request Form Functions
+ * Form Interal Swag Request Form
+ * dtran
+ * 7/6/09
+ **/
+
+
+function evalToNumber(numberString) {
+ if(numberString=='') return 0;
+ return parseInt(numberString);
+}
+
+function evalToNumberString(numberString) {
+ if(numberString=='') return '0';
+ return numberString;
+}
+//item_array should be an array of DOM element ids
+function getTotal(item_array) {
+ var total = 0;
+ for(var i in item_array) {
+ total += evalToNumber(document.getElementById(item_array[i]).value);
+ }
+ return total;
+}
+
+function calculateTotalSwag() {
+ document.getElementById('Totalswag').value =
+ getTotal( new Array('Lanyards',
+ 'Stickers',
+ 'Bracelets',
+ 'Tattoos',
+ 'Buttons',
+ 'Posters'));
+
+}
+
+
+function calculateTotalMensShirts() {
+ document.getElementById('mens_total').value =
+ getTotal( new Array('mens_s',
+ 'mens_m',
+ 'mens_l',
+ 'mens_xl',
+ 'mens_xxl',
+ 'mens_xxxl'));
+
+}
+
+
+function calculateTotalWomensShirts() {
+ document.getElementById('womens_total').value =
+ getTotal( new Array('womens_s',
+ 'womens_m',
+ 'womens_l',
+ 'womens_xl',
+ 'womens_xxl',
+ 'womens_xxxl'));
+
+}
diff --git a/extensions/REMO/web/styles/moz_reps.css b/extensions/REMO/web/styles/moz_reps.css
new file mode 100644
index 000000000..989733c41
--- /dev/null
+++ b/extensions/REMO/web/styles/moz_reps.css
@@ -0,0 +1,44 @@
+#reps-form {
+ width: 700px;
+ border-spacing: 0px;
+ border: 4px solid #e0e0e0;
+}
+
+#reps-form th, #reps-form td {
+ padding: 5px;
+}
+
+#reps-form .even th, #reps-form .even td {
+ background: #e0e0e0;
+}
+
+#reps-form th {
+ text-align: left;
+}
+
+#reps-form textarea {
+ font-family: Verdana, sans-serif;
+ font-size: small;
+ width: 590px;
+}
+
+#reps-form .mandatory {
+ color: red;
+ font-size: 80%;
+}
+
+#reps-form .missing {
+ box-shadow: #FF0000 0 0 1.5px 1px;
+}
+
+#reps-form .hidden {
+ display: none;
+}
+
+#reps-form .subTH {
+ padding-left: 2em;
+}
+
+#reps-form .missing {
+ background: #FFC1C1;
+}
diff --git a/extensions/RequestWhiner/Config.pm b/extensions/RequestWhiner/Config.pm
new file mode 100644
index 000000000..fb08bd7af
--- /dev/null
+++ b/extensions/RequestWhiner/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 RequestWhiner Bugzilla Extension.
+#
+# The Initial Developer of the Original Code is Gervase Markham
+# Portions created by the Initial Developer are Copyright (C) 2011 the
+# Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+# Gervase Markham <written.to.the.glory.of.god@gerv.net>
+
+package Bugzilla::Extension::RequestWhiner;
+use strict;
+
+use constant NAME => 'RequestWhiner';
+
+use constant REQUIRED_MODULES => [
+];
+
+use constant OPTIONAL_MODULES => [
+];
+
+__PACKAGE__->NAME; \ No newline at end of file
diff --git a/extensions/RequestWhiner/Extension.pm b/extensions/RequestWhiner/Extension.pm
new file mode 100644
index 000000000..3f1ee1f27
--- /dev/null
+++ b/extensions/RequestWhiner/Extension.pm
@@ -0,0 +1,43 @@
+# -*- 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 RequestWhiner 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):
+# Gervase Markham <written.to.the.glory.of.god@gerv.net>
+
+package Bugzilla::Extension::RequestWhiner;
+use strict;
+use base qw(Bugzilla::Extension);
+
+use Bugzilla::Constants qw(bz_locations);
+use Bugzilla::Install::Filesystem;
+
+our $VERSION = '0.01';
+
+sub install_filesystem {
+ my ($self, $args) = @_;
+ my $files = $args->{'files'};
+
+ my $extensionsdir = bz_locations()->{'extensionsdir'};
+ my $scriptname = $extensionsdir . "/" . __PACKAGE__->NAME . "/bin/whineatrequests.pl";
+
+ $files->{$scriptname} = {
+ perms => Bugzilla::Install::Filesystem::WS_EXECUTE
+ };
+}
+
+__PACKAGE__->NAME;
diff --git a/extensions/RequestWhiner/bin/whineatrequests.pl b/extensions/RequestWhiner/bin/whineatrequests.pl
new file mode 100755
index 000000000..f7cb61dbb
--- /dev/null
+++ b/extensions/RequestWhiner/bin/whineatrequests.pl
@@ -0,0 +1,155 @@
+#!/usr/bin/perl -wT
+# -*- 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 Netscape Communications
+# Corporation. Portions created by Netscape are
+# Copyright (C) 1998 Netscape Communications Corporation. All
+# Rights Reserved.
+#
+# Contributor(s): Erik Stambaugh <erik@dasbistro.com>
+# Gervase Markham <gerv@gerv.net>
+
+use strict;
+
+BEGIN {
+ use lib qw(lib .);
+ use Bugzilla;
+ Bugzilla->extensions;
+}
+
+use lib qw(. lib);
+
+use Bugzilla;
+use Bugzilla::User;
+use Bugzilla::Mailer;
+use Email::MIME;
+
+use Bugzilla::Extension::RequestWhiner::Constants;
+
+my $dbh = Bugzilla->dbh;
+
+my $sth_get_requests =
+ $dbh->prepare("SELECT profiles.login_name,
+ flagtypes.name,
+ flags.attach_id,
+ bugs.bug_id,
+ bugs.short_desc, " .
+ $dbh->sql_to_days('NOW()') .
+ " - " .
+ $dbh->sql_to_days('flags.modification_date') . "
+ AS age_in_days
+ FROM flags
+ JOIN bugs ON bugs.bug_id = flags.bug_id,
+ flagtypes,
+ profiles
+ WHERE flags.status = '?'
+ AND flags.requestee_id = profiles.userid
+ AND flags.type_id = flagtypes.id
+ AND " . $dbh->sql_to_days('NOW()') .
+ " - " .
+ $dbh->sql_to_days('flags.modification_date') .
+ " > " .
+ WHINE_AFTER_DAYS . "
+ ORDER BY flags.modification_date");
+
+$sth_get_requests->execute();
+
+# Build data structure
+my $requests = {};
+
+while (my ($login_name,
+ $flag_name,
+ $attach_id,
+ $bug_id,
+ $short_desc,
+ $age_in_days) = $sth_get_requests->fetchrow_array())
+{
+ if (!defined($requests->{$login_name})) {
+ $requests->{$login_name} = {};
+ }
+
+ if (!defined($requests->{$login_name}->{$flag_name})) {
+ $requests->{$login_name}->{$flag_name} = [];
+ }
+
+ push(@{ $requests->{$login_name}->{$flag_name} }, {
+ bug_id => $bug_id,
+ attach_id => $attach_id,
+ summary => $short_desc,
+ age => $age_in_days
+ });
+}
+
+$sth_get_requests->finish();
+
+foreach my $recipient (keys %$requests) {
+ my $user = new Bugzilla::User({ name => $recipient });
+
+ next if $user->email_disabled;
+
+ mail({
+ from => Bugzilla->params->{'mailfrom'},
+ recipient => $user,
+ subject => "Your Outstanding Requests",
+ requests => $requests->{$recipient},
+ threshold => WHINE_AFTER_DAYS
+ });
+}
+
+exit;
+
+###############################################################################
+# Functions
+#
+# Note: this function is exactly the same as the one in whine.pl, just using
+# different templates for the messages themselves.
+###############################################################################
+sub mail {
+ my $args = shift;
+ my $addressee = $args->{recipient};
+ my $template = Bugzilla->template;
+ my ($content, @parts);
+
+ $template->process("requestwhiner/mail.txt.tmpl", $args, \$content)
+ || die($template->error());
+ push(@parts, Email::MIME->create(
+ attributes => {
+ content_type => "text/plain",
+ },
+ body => $content,
+ ));
+
+ $content = '';
+ $template->process("requestwhiner/mail.html.tmpl", $args, \$content)
+ || die($template->error());
+
+ push(@parts, Email::MIME->create(
+ attributes => {
+ content_type => "text/html",
+ },
+ body => $content,
+ ));
+
+ $content = '';
+ $template->process("requestwhiner/header.txt.tmpl", $args, \$content)
+ || die($template->error());
+
+ # TT trims the trailing newline
+ my $email = new Email::MIME("$content\n");
+ $email->content_type_set('multipart/alternative');
+ $email->parts_set(\@parts);
+
+ MessageToMTA($email);
+}
diff --git a/extensions/RequestWhiner/lib/Constants.pm b/extensions/RequestWhiner/lib/Constants.pm
new file mode 100644
index 000000000..0e24ae1f0
--- /dev/null
+++ b/extensions/RequestWhiner/lib/Constants.pm
@@ -0,0 +1,31 @@
+# -*- 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 RequestWhiner 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):
+# Gervase Markham <written.to.the.glory.of.god@gerv.net>
+
+package Bugzilla::Extension::RequestWhiner::Constants;
+use strict;
+use base qw(Exporter);
+our @EXPORT = qw(
+ WHINE_AFTER_DAYS
+);
+
+use constant WHINE_AFTER_DAYS => 7;
+
+1;
diff --git a/extensions/RequestWhiner/template/en/default/requestwhiner/header.txt.tmpl b/extensions/RequestWhiner/template/en/default/requestwhiner/header.txt.tmpl
new file mode 100644
index 000000000..390fd3e2f
--- /dev/null
+++ b/extensions/RequestWhiner/template/en/default/requestwhiner/header.txt.tmpl
@@ -0,0 +1,6 @@
+[% PROCESS global/variables.none.tmpl %]
+From: [% from %]
+To: [% recipient.email %]
+Subject: [[% terms.Bugzilla %]] [% subject %]
+X-Bugzilla-Type: whine
+
diff --git a/extensions/RequestWhiner/template/en/default/requestwhiner/mail.html.tmpl b/extensions/RequestWhiner/template/en/default/requestwhiner/mail.html.tmpl
new file mode 100644
index 000000000..07b2b31ee
--- /dev/null
+++ b/extensions/RequestWhiner/template/en/default/requestwhiner/mail.html.tmpl
@@ -0,0 +1,62 @@
+[%# 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 RequestWhiner Bugzilla Extension.
+ #
+ # The Initial Developer of the Original Code is Gervase Markham
+ # Portions created by the Initial Developer are Copyright (C) 2011 the
+ # Initial Developer. All Rights Reserved.
+ #
+ # Contributor(s): Gervase Markham <written.to.the.glory.of.god@gerv.net>
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+[% PROCESS 'global/field-descs.none.tmpl' %]
+
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<html>
+ <head>
+ <title>
+ [[% terms.Bugzilla %]] [% subject FILTER html %]
+ </title>
+ </head>
+ <body bgcolor="#FFFFFF">
+
+<p>The following is a list of requests people have made of you, which have been
+outstanding more than [% threshold FILTER html %] days. To avoid disappointing
+others, please deal with them as quickly as possible.
+(<a href="https://wiki.mozilla.org/BMO/Handling_Requests">Here is some
+guidance on handling requests</a>.)
+</p>
+
+[% FOREACH request_type = requests.keys %]
+<h3>[% request_type FILTER html %]</h3>
+
+<ul>
+ [% FOREACH request = requests.$request_type %]
+ <li>
+ <a href="[% urlbase FILTER none %]show_bug.cgi?id=[% request.bug_id FILTER none %]">[% terms.Bug %]
+ [%+ request.bug_id FILTER none %]: [% request.summary FILTER html %]</a> ([% request.age FILTER none %] days old)
+ [% IF request.attach_id %]
+ <br>
+ <small>(<a href="[% urlbase FILTER none %]attachment.cgi?id=[% request.attach_id FILTER none %]&action=edit">Details</a> |
+ <a href="[% urlbase FILTER none %]attachment.cgi?id=[% request.attach_id FILTER none %]&action=diff">Diff</a> |
+ <a href="[% urlbase FILTER none %]page.cgi?id=splinter.html&bug=[% request.bug_id FILTER none %]&attachment=[% request.attach_id FILTER none %]">Splinter Review</a>)</small>
+ [% END %]
+ </li>
+ [% END %]
+</ul>
+
+[% END %]
+
+<p><a href="[% urlbase FILTER none %]request.cgi?action=queue&requestee=[% recipient.email FILTER uri %]&group=type">See all your outstanding requests</a>.</p>
+
+ </body>
+</html>
diff --git a/extensions/RequestWhiner/template/en/default/requestwhiner/mail.txt.tmpl b/extensions/RequestWhiner/template/en/default/requestwhiner/mail.txt.tmpl
new file mode 100644
index 000000000..ef21563c9
--- /dev/null
+++ b/extensions/RequestWhiner/template/en/default/requestwhiner/mail.txt.tmpl
@@ -0,0 +1,41 @@
+[%# 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 RequestWhiner Bugzilla Extension.
+ #
+ # The Initial Developer of the Original Code is Gervase Markham
+ # Portions created by the Initial Developer are Copyright (C) 2011 the
+ # Initial Developer. All Rights Reserved.
+ #
+ # Contributor(s): Gervase Markham <written.to.the.glory.of.god@gerv.net>
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+[% PROCESS 'global/field-descs.none.tmpl' %]
+
+The following is a list of requests people have made of you, which have been
+outstanding more than [% threshold %] days. To avoid disappointing others, please deal with
+them as quickly as possible. Here is some guidance on handling requests:
+ https://wiki.mozilla.org/BMO/Handling_Requests
+
+[% FOREACH request_type = requests.keys %]
+[%+ request_type +%]
+[%+ "-" FILTER repeat(request_type.length) +%]
+
+ [% FOREACH request = requests.$request_type %]
+ [%+ terms.Bug +%] [%+ request.bug_id %]: [% request.summary +%] ([% request.age %] days old)
+ [%+ urlbase %]show_bug.cgi?id=[% request.bug_id +%]
+ [% IF request.attach_id %]
+ [%+ urlbase %]attachment.cgi?id=[% request.attach_id %]&action=edit
+ [% END %]
+ [%+ END +%]
+[% END %]
+To see all your outstanding requests, visit:
+[%+ urlbase %]request.cgi?action=queue&requestee=[% recipient.email FILTER url %]&group=type
diff --git a/extensions/SecureMail/Config.pm b/extensions/SecureMail/Config.pm
new file mode 100644
index 000000000..5b53ddf67
--- /dev/null
+++ b/extensions/SecureMail/Config.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 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 <mkanat@bugzilla.org>
+# Gervase Markham <gerv@gerv.net>
+
+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 => 'HTML-Tree',
+ module => 'HTML::Tree',
+ version => 0,
+ }
+];
+
+__PACKAGE__->NAME;
diff --git a/extensions/SecureMail/Extension.pm b/extensions/SecureMail/Extension.pm
new file mode 100644
index 000000000..3730d23e6
--- /dev/null
+++ b/extensions/SecureMail/Extension.pm
@@ -0,0 +1,604 @@
+# -*- 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 <mkanat@bugzilla.org>
+# Gervase Markham <gerv@gerv.net>
+
+package Bugzilla::Extension::SecureMail;
+use strict;
+use base qw(Bugzilla::Extension);
+
+use Bugzilla::Attachment;
+use Bugzilla::Comment;
+use Bugzilla::Group;
+use Bugzilla::Object;
+use Bugzilla::User;
+use Bugzilla::Util qw(correct_urlbase trim trick_taint is_7bit_clean);
+use Bugzilla::Error;
+use Bugzilla::Mailer;
+
+use Crypt::OpenPGP::Armour;
+use Crypt::OpenPGP::KeyRing;
+use Crypt::OpenPGP;
+use Crypt::SMIME;
+use Encode;
+use HTML::Tree;
+
+our $VERSION = '0.5';
+
+use constant SECURE_NONE => 0;
+use constant SECURE_BODY => 1;
+use constant SECURE_ALL => 2;
+
+##############################################################################
+# 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
+##############################################################################
+
+BEGIN {
+ *Bugzilla::Group::secure_mail = \&_secure_mail;
+}
+
+sub _secure_mail { return $_[0]->{'secure_mail'}; }
+
+# 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.
+ if (!Crypt::OpenPGP::Armour->unarmour($value)) {
+ ThrowUserError('securemail_invalid_key',
+ { errstr => Crypt::OpenPGP::Armour->errstr });
+ }
+ }
+ 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',
+ { errstr => $@ });
+ }
+ }
+ 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'};
+ my $params = Bugzilla->input_params;
+
+ 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) {
+ $user->set('public_key', $params->{'public_key'});
+ $user->update();
+
+ # Send user a test email
+ if ($user->{'public_key'}) {
+ _send_test_email($user);
+ $vars->{'test_email_sent'} = 1;
+ }
+ }
+
+ $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;
+}
+
+sub _send_test_email {
+ my ($user) = @_;
+ my $template = Bugzilla->template_inner($user->settings->{'lang'}->{'value'});
+
+ my $vars = {
+ to_user => $user->email,
+ };
+
+ my $msg = "";
+ $template->process("account/email/securemail-test.txt.tmpl", $vars, \$msg)
+ || ThrowTemplateError($template->error());
+
+ MessageToMTA($msg);
+}
+
+##############################################################################
+# Encrypting the email
+##############################################################################
+sub mailer_before_send {
+ my ($self, $args) = @_;
+
+ my $email = $args->{'email'};
+ my $body = $email->body;
+
+ # 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') ||
+ $email->header('X-Bugzilla-Type') eq 'request';
+ my $is_passwordmail = !$is_bugmail && ($body =~ /cfmpw.*cxlpw/s);
+ my $is_test_email = $email->header('X-Bugzilla-Type') =~ /securemail-test/ ? 1 : 0;
+ my $is_whine_email = $email->header('X-Bugzilla-Type') eq 'whine' ? 1 : 0;
+
+ if ($is_bugmail || $is_passwordmail || $is_test_email || $is_whine_email) {
+ # 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 = SECURE_ALL;
+
+ 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 (!_should_secure_bug($bug)) {
+ $make_secure = SECURE_NONE;
+ }
+ # If the insider group has securemail enabled..
+ my $insider_group = Bugzilla::Group->new({ name => Bugzilla->params->{'insidergroup'} });
+ if ($insider_group->secure_mail && $make_secure == SECURE_NONE) {
+ my $comment_is_private = Bugzilla->dbh->selectcol_arrayref(
+ "SELECT isprivate FROM longdescs WHERE bug_id=? ORDER BY bug_when",
+ undef, $bug_id);
+ # Encrypt if there are private comments on an otherwise public bug
+ while ($body =~ /[\r\n]--- Comment #(\d+)/g) {
+ my $comment_number = $1;
+ if ($comment_number && $comment_is_private->[$comment_number]) {
+ $make_secure = SECURE_BODY;
+ last;
+ }
+ }
+ # Encrypt if updating a private attachment without a comment
+ if ($email->header('X-Bugzilla-Changed-Fields')
+ && $email->header('X-Bugzilla-Changed-Fields') =~ /Attachment #(\d+)/)
+ {
+ my $attachment = Bugzilla::Attachment->new($1);
+ if ($attachment && $attachment->isprivate) {
+ $make_secure = SECURE_BODY;
+ }
+ }
+ }
+ }
+ 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 = SECURE_NONE;
+ }
+ }
+ elsif ($is_whine_email) {
+ # When a whine email has one or more secure bugs in the body, then
+ # encrypt the entire email body. Subject can be left alone as it
+ # comes from the whine settings.
+ $make_secure = _should_secure_whine($email) ? SECURE_BODY : SECURE_NONE;
+ }
+
+ # 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'} : '';
+
+ # Check if the new bugmail prefix should be added to the subject.
+ my $add_new = ($email->header('X-Bugzilla-Type') eq 'new' &&
+ $user &&
+ $user->settings->{'bugmail_new_prefix'}->{'value'} eq 'on') ? 1 : 0;
+
+ if ($make_secure == SECURE_NONE) {
+ # Filter the bug_links in HTML email in case the bugs the links
+ # point are "secured" bugs and the user may not be able to see
+ # the summaries.
+ _filter_bug_links($email);
+ }
+ else {
+ _make_secure($email, $public_key, $is_bugmail && $make_secure == SECURE_ALL, $add_new);
+ }
+ }
+}
+
+# Custom hook for bugzilla.mozilla.org (see bug 752400)
+sub bugmail_referenced_bugs {
+ my ($self, $args) = @_;
+ # Sanitise subjects of referenced bugs.
+ my $referenced_bugs = $args->{'referenced_bugs'};
+ # No need to sanitise subjects if the entire email will be secured.
+ return if _should_secure_bug($args->{'updated_bug'});
+ # Replace the subject if required
+ foreach my $ref (@$referenced_bugs) {
+ if (grep($_->secure_mail, @{ $ref->{'bug'}->groups_in })) {
+ $ref->{'short_desc'} = "(Secure bug)";
+ }
+ }
+}
+
+sub _should_secure_bug {
+ my ($bug) = @_;
+ # If there's a problem with the bug, err on the side of caution and mark it
+ # as secure.
+ return
+ !$bug
+ || $bug->{'error'}
+ || grep($_->secure_mail, @{ $bug->groups_in });
+}
+
+sub _should_secure_whine {
+ my ($email) = @_;
+ my $should_secure = 0;
+ $email->walk_parts(sub {
+ my $part = shift;
+ my $content_type = $part->content_type;
+ return if !$content_type || $content_type !~ /^text\/plain/;
+ my $body = $part->body;
+ my @bugids = $body =~ /Bug (\d+):/g;
+ foreach my $id (@bugids) {
+ $id = trim($id);
+ next if !$id;
+ my $bug = new Bugzilla::Bug($id);
+ if ($bug && _should_secure_bug($bug)) {
+ $should_secure = 1;
+ last;
+ }
+ }
+ });
+ return $should_secure ? 1 : 0;
+}
+
+sub _make_secure {
+ my ($email, $key, $sanitise_subject, $add_new) = @_;
+
+ my $subject = $email->header('Subject');
+ my ($bug_id) = $subject =~ /\[\D+(\d+)\]/;
+
+ my $key_type = 0;
+ if ($key && $key =~ /PUBLIC KEY/) {
+ $key_type = 'PGP';
+ }
+ elsif ($key && $key =~ /BEGIN CERTIFICATE/) {
+ $key_type = 'S/MIME';
+ }
+
+ if ($key_type eq 'PGP') {
+ ##################
+ # PGP Encryption #
+ ##################
+
+ my $pubring = new Crypt::OpenPGP::KeyRing(Data => $key);
+ my $pgp = new Crypt::OpenPGP(PubRing => $pubring);
+
+ if (scalar $email->parts > 1) {
+ my $old_boundary = $email->{ct}{attributes}{boundary};
+ my $to_encrypt = "Content-Type: " . $email->content_type . "\n\n";
+
+ # We need to do some fix up of each part for proper encoding and then
+ # stringify all parts for encrypting. We have to retain the old
+ # boundaries as well so that the email client can reconstruct the
+ # original message properly.
+ $email->walk_parts(\&_fix_part);
+
+ $email->walk_parts(sub {
+ my ($part) = @_;
+ if ($sanitise_subject) {
+ _insert_subject($part, $subject);
+ }
+ return if $part->parts > 1; # Top-level
+ $to_encrypt .= "--$old_boundary\n" . $part->as_string . "\n";
+ });
+ $to_encrypt .= "--$old_boundary--";
+
+ # Now create the new properly formatted PGP parts containing the
+ # encrypted original message
+ my @new_parts = (
+ Email::MIME->create(
+ attributes => {
+ content_type => 'application/pgp-encrypted',
+ encoding => '7bit',
+ },
+ body => "Version: 1\n",
+ ),
+ Email::MIME->create(
+ attributes => {
+ content_type => 'application/octet-stream',
+ filename => 'encrypted.asc',
+ disposition => 'inline',
+ encoding => '7bit',
+ },
+ body => _pgp_encrypt($pgp, $to_encrypt)
+ ),
+ );
+ $email->parts_set(\@new_parts);
+ my $new_boundary = $email->{ct}{attributes}{boundary};
+ # Redo the old content type header with the new boundaries
+ # and other information needed for PGP
+ $email->header_set("Content-Type",
+ "multipart/encrypted; " .
+ "protocol=\"application/pgp-encrypted\"; " .
+ "boundary=\"$new_boundary\"");
+ }
+ else {
+ _fix_part($email);
+ if ($sanitise_subject) {
+ _insert_subject($email, $subject);
+ }
+ $email->body_set(_pgp_encrypt($pgp, $email->body));
+ }
+ }
+
+ elsif ($key_type eq 'S/MIME') {
+ #####################
+ # S/MIME Encryption #
+ #####################
+
+ $email->walk_parts(\&_fix_part);
+
+ if ($sanitise_subject) {
+ $email->walk_parts(sub { _insert_subject($_[0], $subject) });
+ }
+
+ 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->parts_set([]);
+ $email->body_set($enc_obj->body());
+ $email->content_type_set('application/pkcs7-mime');
+ $email->charset_set('UTF-8') if Bugzilla->params->{'utf8'};
+ }
+ 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->parts_set([]);
+ $email->content_type_set('text/plain');
+ $email->body_set($message);
+ }
+
+ if ($sanitise_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.
+ my $new = $add_new ? ' New:' : '';
+ $subject =~ s/($bug_id\])\s+(.*)$/$1$new (Secure bug $bug_id updated)/;
+ $email->header_set('Subject', $subject);
+ }
+}
+
+sub _pgp_encrypt {
+ my ($pgp, $text) = @_;
+ # "@" 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 => $text,
+ Recipients => "@",
+ Cipher => 'CAST5',
+ Armour => 1);
+ if (!defined $encrypted) {
+ return 'Error during Encryption: ' . $pgp->errstr;
+ }
+ return $encrypted;
+}
+
+# Insert the subject into the part's body, as the subject of the message will
+# be sanitised.
+# XXX this incorrectly assumes all parts of the message are the body
+# we should only alter parts who's parent is multipart/alternative
+sub _insert_subject {
+ my ($part, $subject) = @_;
+ my $content_type = $part->content_type or return;
+ if ($content_type =~ /^text\/plain/) {
+ if (!is_7bit_clean($subject)) {
+ $part->encoding_set('quoted-printable');
+ }
+ $part->body_str_set("Subject: $subject\015\012\015\012" . $part->body_str);
+ }
+ elsif ($content_type =~ /^text\/html/) {
+ my $tree = HTML::Tree->new->parse_content($part->body_str);
+ my $body = $tree->look_down(qw(_tag body));
+ $body->unshift_content(['div', "Subject: $subject"], ['br']);
+ _set_body_from_tree($part, $tree);
+ }
+}
+
+# Copied from Bugzilla/Mailer as this extension runs before
+# this code there and Mailer.pm will no longer see the original
+# message.
+sub _fix_part {
+ my ($part) = @_;
+ return if $part->parts > 1; # Top-level
+ my $content_type = $part->content_type || '';
+ $content_type =~ /charset=['"](.+)['"]/;
+ # If no charset is defined or is the default us-ascii,
+ # then we encode the email to UTF-8 if Bugzilla has utf8 enabled.
+ # XXX - This is a hack to workaround bug 723944.
+ if (!$1 || $1 eq 'us-ascii') {
+ my $body = $part->body;
+ if (Bugzilla->params->{'utf8'}) {
+ $part->charset_set('UTF-8');
+ # encoding_set works only with bytes, not with utf8 strings.
+ my $raw = $part->body_raw;
+ if (utf8::is_utf8($raw)) {
+ utf8::encode($raw);
+ $part->body_set($raw);
+ }
+ }
+ $part->encoding_set('quoted-printable') if !is_7bit_clean($body);
+ }
+}
+
+sub _filter_bug_links {
+ my ($email) = @_;
+ $email->walk_parts(sub {
+ my $part = shift;
+ my $content_type = $part->content_type;
+ return if !$content_type || $content_type !~ /text\/html/;
+ my $tree = HTML::Tree->new->parse_content($part->body);
+ my @links = $tree->look_down( _tag => q{a}, class => qr/bz_bug_link/ );
+ my $updated = 0;
+ foreach my $link (@links) {
+ my $href = $link->attr('href');
+ my ($bug_id) = $href =~ /\Qshow_bug.cgi?id=\E(\d+)/;
+ my $bug = new Bugzilla::Bug($bug_id);
+ if ($bug && _should_secure_bug($bug)) {
+ $link->attr('title', '(secure bug)');
+ $link->attr('class', 'bz_bug_link');
+ $updated = 1;
+ }
+ }
+ if ($updated) {
+ _set_body_from_tree($part, $tree);
+ }
+ });
+}
+
+sub _set_body_from_tree {
+ my ($part, $tree) = @_;
+ $part->body_set($tree->as_HTML);
+ $part->charset_set('UTF-8') if Bugzilla->params->{'utf8'};
+ $part->encoding_set('quoted-printable');
+}
+
+__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/email/securemail-test.txt.tmpl b/extensions/SecureMail/template/en/default/account/email/securemail-test.txt.tmpl
new file mode 100644
index 000000000..e4f4c9242
--- /dev/null
+++ b/extensions/SecureMail/template/en/default/account/email/securemail-test.txt.tmpl
@@ -0,0 +1,23 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+
+From: [% Param('mailfrom') %]
+To: [% to_user %]
+Subject: [% terms.Bugzilla %] SecureMail Test Email
+X-Bugzilla-Type: securemail-test
+
+Congratulations! If you can read this, then your SecureMail encryption
+key uploaded to [% terms.Bugzilla %] is working properly.
+
+To update your SecureMail preferences at any time, please go to:
+[%+ urlbase %]userprefs.cgi?tab=securemail
+
+Sincerely,
+Your Friendly [% terms.Bugzilla %] Administrator
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..db595a23f
--- /dev/null
+++ b/extensions/SecureMail/template/en/default/account/prefs/securemail.html.tmpl
@@ -0,0 +1,40 @@
+[%# 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 <mkanat@bugzilla.org>
+ #%]
+
+[% IF test_email_sent %]
+ <div id="message">
+ An encrypted test email has been sent to your address.
+ </div>
+[% END %]
+
+<p>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 %].</p>
+
+<p>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.</p>
+
+<p><a href="page.cgi?id=securemail/help.html">More help is available</a>.</p>
+
+[% Hook.process('moreinfo') %]
+
+<textarea id="public_key" name="public_key" cols="72" rows="12">
+ [%- public_key FILTER html %]</textarea>
+
+<p>Submitting valid changes will automatically send an encrypted test email to your address.</p>
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 <mkanat@bugzilla.org>
+ # Gervase Markham <gerv@gerv.net>
+ #%]
+
+[% 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 <mkanat@bugzilla.org>
+ #%]
+<tr>
+ <th>Secure Bugmail:</th>
+ <td colspan="3">
+ <input type="checkbox" id="secure_mail" name="secure_mail"
+ [% ' checked="checked"' IF group.secure_mail %]>
+ </td>
+</tr>
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..253fed29e
--- /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 <mkanat@bugzilla.org>
+ #%]
+[% IF group.is_bug_group || group.name == Param('insidergroup') %]
+ <tr>
+ <th>Secure Bugmail:</th>
+ <td>
+ <input type="checkbox" id="secure_mail" name="secure_mail"
+ [% ' checked="checked"' IF group.secure_mail %]>
+ </td>
+ </tr>
+[% 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..46b093674
--- /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 <mkanat@bugzilla.org>
+ #%]
+
+[% 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.<br><br>[% errstr FILTER html %]
+[% END %]
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..f87ac82cb
--- /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 <mkanat@bugzilla.org>
+ # Gervase Markham <gerv@gerv.net>
+ # Dave Lawrence <dkl@mozilla.com>
+ #%]
+
+[% 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 <a href="[% urlbase FILTER none %]userprefs.cgi?tab=securemail">SecureMail preferences tab</a>.<br>
+<br>
+In addition, if you have uploaded a S/MIME or GPG/PGP key using the <a href="[% urlbase FILTER none %]userprefs.cgi?tab=securemail">
+SecureMail preferences tab</a>, 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.
+
+<h2>S/MIME</h2>
+
+<b>S/MIME Keys must be in PEM format - i.e. Base64-encoded text, with the first line containing BEGIN CERTIFICATE.</b></p>
+
+<p>
+S/MIME certificates can be obtained from a number of providers. You can get a free one from <a href="https://www.startssl.com/?app=12">StartCom</a>.
+Once you have it, <a href="https://www.startssl.com/?app=25#52">export it from your browser as a .p12 file and import it into your mail client</a>.
+You'll need to provide a password when you export - pick a strong one, and then back up the .p12 file somewhere safe.</p>
+
+<p>
+Then, you need to convert it to a .pem file. If you have OpenSSL installed, one way is as follows:</p>
+
+<p>
+<code>openssl pkcs12 -in certificate.p12 -out certificate.pem -nodes</code></p>
+
+<p>
+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 <b>not</b> the section beginning
+"BEGIN RSA PRIVATE KEY", and it is not any of the intermediate certificates or root certificates.</p>
+
+<p>
+<b>Note: the .pem file has your private key in plaintext. Delete it once you have copied the public key out of it!</b></p>
+
+<h2>PGP</h2>
+
+<b>PGP keys must be ASCII-armoured - i.e. text, with the first line containing BEGIN PGP PUBLIC KEY.</b></p>
+
+<p>
+If you already have your own PGP key in a keyring, skip straight to step 3. Otherwise:</p>
+
+<ol>
+
+<li>Install the GPG suite of utilities for your operating system, either using your package manager or downloaded from <a href="http://www.gnupg.org/download/index.en.html">gnupg.org</a>.</p>
+
+<li><p>Generate a private key.</p>
+
+<p><code>gpg --gen-key</code></p>
+
+<p>
+You’ll have to answer several questions:</p>
+
+<p>
+<ul>
+ <li>What kind and size of key you want; the defaults are probably good enough.</li>
+ <li>How long the key should be valid; you can safely choose a non-expiring key.</li>
+ <li>Your real name and e-mail address; these are necessary for identifying your key in a larger set of keys.</li>
+ <li>A comment for your key; the comment can be empty.</li>
+ <li>A passphrase. Whatever you do, don’t forget it! Your key, and all your encrypted files, will be useless if you do.</li>
+</ul>
+
+<li><p>Generate an ASCII version of your public key.</p>
+
+<p><code>gpg --armor --output pubkey.txt --export 'Your Name'</code></p>
+
+<p>Paste the contents of pubkey.txt into the SecureMail text field in [% terms.Bugzilla %].
+
+<li>Configure your email client to use your associated private key to decrypt the encrypted emails. For Thunderbird, you need the <a href="https://addons.mozilla.org/en-us/thunderbird/addon/enigmail/">Enigmail</a> extension.</p>
+</ol>
+
+<p>
+Further reading: <a href="http://www.madboa.com/geek/gpg-quickstart">GPG Quickstart</a>.
+
+[% PROCESS global/footer.html.tmpl %]
+
+
diff --git a/extensions/ShadowBugs/Config.pm b/extensions/ShadowBugs/Config.pm
new file mode 100644
index 000000000..6999edaf3
--- /dev/null
+++ b/extensions/ShadowBugs/Config.pm
@@ -0,0 +1,15 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::ShadowBugs;
+use strict;
+
+use constant NAME => 'ShadowBugs';
+use constant REQUIRED_MODULES => [];
+use constant OPTIONAL_MODULES => [];
+
+__PACKAGE__->NAME;
diff --git a/extensions/ShadowBugs/Extension.pm b/extensions/ShadowBugs/Extension.pm
new file mode 100644
index 000000000..a9a1e0861
--- /dev/null
+++ b/extensions/ShadowBugs/Extension.pm
@@ -0,0 +1,99 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::ShadowBugs;
+
+use strict;
+
+use base qw(Bugzilla::Extension);
+
+use Bugzilla::Bug;
+use Bugzilla::Error;
+use Bugzilla::Field;
+use Bugzilla::User;
+
+our $VERSION = '1';
+
+BEGIN {
+ *Bugzilla::is_cf_shadow_bug_hidden = \&_is_cf_shadow_bug_hidden;
+ *Bugzilla::Bug::cf_shadow_bug_obj = \&_cf_shadow_bug_obj;
+}
+
+# Determine if the shadow-bug / shadowed-by fields are visibile on the
+# specified bug.
+sub _is_cf_shadow_bug_hidden {
+ my ($self, $bug) = @_;
+
+ # completely hide unless you're a member of the right group
+ return 1 unless Bugzilla->user->in_group('can_shadow_bugs');
+
+ my $is_public = Bugzilla::User->new()->can_see_bug($bug->id);
+ if ($is_public) {
+ # hide on public bugs, unless it's shadowed
+ my $related = $bug->related_bugs(Bugzilla->process_cache->{shadow_bug_field});
+ return 1 if !@$related;
+ }
+}
+
+sub _cf_shadow_bug_obj {
+ my ($self) = @_;
+ return unless $self->cf_shadow_bug;
+ return $self->{cf_shadow_bug_obj} ||= Bugzilla::Bug->new($self->cf_shadow_bug);
+}
+
+sub template_before_process {
+ my ($self, $args) = @_;
+ my $file = $args->{'file'};
+ my $vars = $args->{'vars'};
+
+ Bugzilla->process_cache->{shadow_bug_field} ||= Bugzilla::Field->new({ name => 'cf_shadow_bug' });
+
+ return unless Bugzilla->user->in_group('can_shadow_bugs');
+ return unless
+ $file eq 'bug/edit.html.tmpl'
+ || $file eq 'bug/show.html.tmpl'
+ || $file eq 'bug/show-header.html.tmpl';
+ my $bug = exists $vars->{'bugs'} ? $vars->{'bugs'}[0] : $vars->{'bug'};
+ return unless $bug && $bug->cf_shadow_bug;
+ $vars->{is_shadow_bug} = 1;
+
+ if ($file eq 'bug/edit.html.tmpl') {
+ # load comments from other bug
+ $vars->{shadow_comments} = $bug->cf_shadow_bug_obj->comments;
+ }
+}
+
+sub bug_end_of_update {
+ my ($self, $args) = @_;
+
+ # don't allow shadowing non-public bugs
+ if (exists $args->{changes}->{cf_shadow_bug}) {
+ my ($old_id, $new_id) = @{ $args->{changes}->{cf_shadow_bug} };
+ if ($new_id) {
+ if (!Bugzilla::User->new()->can_see_bug($new_id)) {
+ ThrowUserError('illegal_shadow_bug_public', { id => $new_id });
+ }
+ }
+ }
+
+ # if a shadow bug is made public, clear the shadow_bug field
+ if (exists $args->{changes}->{bug_group}) {
+ my $bug = $args->{bug};
+ return unless my $shadow_id = $bug->cf_shadow_bug;
+ my $is_public = Bugzilla::User->new()->can_see_bug($bug->id);
+ if ($is_public) {
+ Bugzilla->dbh->do(
+ "UPDATE bugs SET cf_shadow_bug=NULL WHERE bug_id=?",
+ undef, $bug->id);
+ LogActivityEntry($bug->id, 'cf_shadow_bug', $shadow_id, '',
+ Bugzilla->user->id, $args->{timestamp});
+
+ }
+ }
+}
+
+__PACKAGE__->NAME;
diff --git a/extensions/ShadowBugs/template/en/default/hook/bug/comments-aftercomments.html.tmpl b/extensions/ShadowBugs/template/en/default/hook/bug/comments-aftercomments.html.tmpl
new file mode 100644
index 000000000..d8dae521a
--- /dev/null
+++ b/extensions/ShadowBugs/template/en/default/hook/bug/comments-aftercomments.html.tmpl
@@ -0,0 +1,70 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% RETURN UNLESS is_shadow_bug %]
+
+[% public_bug = bug.cf_shadow_bug_obj %]
+[% count = 0 %]
+[% FOREACH comment = shadow_comments %]
+ [% IF count >= start_at %]
+ [% PROCESS a_comment %]
+ [% END %]
+ [% count = count + increment %]
+[% END %]
+
+[% BLOCK a_comment %]
+ [% RETURN IF comment.is_private AND NOT (user.is_insider || user.id == comment.author.id) %]
+ [% comment_text = comment.body_full %]
+ [% RETURN IF comment_text == '' %]
+
+ <div id="pc[% count %]" class="bz_comment[% " bz_private" IF comment.is_private %]
+ shadow_bug_comment bz_default_hidden
+ [% " bz_first_comment" IF count == description %]">
+ [% IF count == description %]
+ [% class_name = "bz_first_comment_head" %]
+ [% comment_label = "Public Description" %]
+ [% ELSE %]
+ [% class_name = "bz_comment_head" %]
+ [% comment_label = "Public Comment " _ count %]
+ [% END %]
+
+ <div class="[% class_name FILTER html %]">
+ <span class="bz_comment_number">
+ <a href="show_bug.cgi?id=[% public_bug.bug_id FILTER none %]#c[% count %]">
+ [%- comment_label FILTER html %]</a>
+ </span>
+
+ <span class="bz_comment_user">
+ [% commenter_id = comment.author.id %]
+ [% UNLESS user_cache.$commenter_id %]
+ [% user_cache.$commenter_id = BLOCK %]
+ [% INCLUDE global/user.html.tmpl who = comment.author %]
+ [% END %]
+ [% END %]
+ [% user_cache.$commenter_id FILTER none %]
+ [% Hook.process('user', 'bug/comments.html.tmpl') %]
+ </span>
+
+ <span class="bz_comment_user_images">
+ [% FOREACH group = comment.author.groups_with_icon %]
+ <img src="[% group.icon_url FILTER html %]"
+ alt="[% group.name FILTER html %]"
+ title="[% group.name FILTER html %] - [% group.description FILTER html %]">
+ [% END %]
+ </span>
+
+ <span class="bz_comment_time">
+ [%+ comment.creation_ts FILTER time %]
+ </span>
+ </div>
+
+<pre class="bz_comment_text">
+ [%- comment_text FILTER quoteUrls(public_bug, comment) -%]
+</pre>
+ </div>
+[% END %]
diff --git a/extensions/ShadowBugs/template/en/default/hook/bug/edit-after_comment_textarea.html.tmpl b/extensions/ShadowBugs/template/en/default/hook/bug/edit-after_comment_textarea.html.tmpl
new file mode 100644
index 000000000..9873ea3d7
--- /dev/null
+++ b/extensions/ShadowBugs/template/en/default/hook/bug/edit-after_comment_textarea.html.tmpl
@@ -0,0 +1,13 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% RETURN UNLESS is_shadow_bug %]
+
+<br>
+<a href="show_bug.cgi?id=[% bug.cf_shadow_bug FILTER none %]#comment">Add public comment</a>
+
diff --git a/extensions/ShadowBugs/template/en/default/hook/bug/edit-after_custom_fields.html.tmpl b/extensions/ShadowBugs/template/en/default/hook/bug/edit-after_custom_fields.html.tmpl
new file mode 100644
index 000000000..8e8327ef2
--- /dev/null
+++ b/extensions/ShadowBugs/template/en/default/hook/bug/edit-after_custom_fields.html.tmpl
@@ -0,0 +1,27 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% RETURN IF Bugzilla.is_cf_shadow_bug_hidden(bug) %]
+[% field = Bugzilla.process_cache.shadow_bug_field %]
+[% shadowed_by = bug.related_bugs(field).pop %]
+<tr>
+ [% IF shadowed_by && user.can_see_bug(shadowed_by) %]
+ <th class="field_label">
+ [% field.reverse_desc FILTER html %]:
+ </th>
+ <td>
+ [% shadowed_by.id FILTER bug_link(shadowed_by, use_alias => 1) FILTER none %][% " " %]
+ </td>
+ [% ELSE %]
+ [% PROCESS bug/field.html.tmpl
+ value = bug.cf_shadow_bug
+ editable = bug.check_can_change_field(field.name, 0, 1)
+ no_tds = false
+ value_span = 2 %]
+ [% END %]
+</tr>
diff --git a/extensions/ShadowBugs/template/en/default/hook/bug/edit-custom_field.html.tmpl b/extensions/ShadowBugs/template/en/default/hook/bug/edit-custom_field.html.tmpl
new file mode 100644
index 000000000..4389b27ad
--- /dev/null
+++ b/extensions/ShadowBugs/template/en/default/hook/bug/edit-custom_field.html.tmpl
@@ -0,0 +1,9 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% field.hidden = field.name == 'cf_shadow_bug' %]
diff --git a/extensions/ShadowBugs/template/en/default/hook/bug/show-header-end.html.tmpl b/extensions/ShadowBugs/template/en/default/hook/bug/show-header-end.html.tmpl
new file mode 100644
index 000000000..5786b3df6
--- /dev/null
+++ b/extensions/ShadowBugs/template/en/default/hook/bug/show-header-end.html.tmpl
@@ -0,0 +1,12 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF is_shadow_bug %]
+ [% style_urls.push('extensions/ShadowBugs/web/style.css') %]
+ [% javascript_urls.push('extensions/ShadowBugs/web/shadow-bugs.js') %]
+[% END %]
diff --git a/extensions/ShadowBugs/template/en/default/hook/global/user-error-errors.html.tmpl b/extensions/ShadowBugs/template/en/default/hook/global/user-error-errors.html.tmpl
new file mode 100644
index 000000000..775a7721e
--- /dev/null
+++ b/extensions/ShadowBugs/template/en/default/hook/global/user-error-errors.html.tmpl
@@ -0,0 +1,14 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF error == 'illegal_shadow_bug_public' %]
+ [% title = "Invalid Shadow " _ terms.Bug %]
+ You cannot shadow [% terms.bug %] [%+ id FILTER html %] because it is not a
+ public [% terms.bug %].
+[% END %]
+
diff --git a/extensions/ShadowBugs/web/shadow-bugs.js b/extensions/ShadowBugs/web/shadow-bugs.js
new file mode 100644
index 000000000..ff320e117
--- /dev/null
+++ b/extensions/ShadowBugs/web/shadow-bugs.js
@@ -0,0 +1,51 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0. */
+
+var shadow_bug = {
+ init: function() {
+ var Dom = YAHOO.util.Dom;
+ var comment_divs = Dom.getElementsByClassName('bz_comment', 'div', 'comments');
+ var comments = new Array();
+ for (var i = 0, l = comment_divs.length; i < l; i++) {
+ var time_spans = Dom.getElementsByClassName('bz_comment_time', 'span', comment_divs[i]);
+ if (!time_spans.length) continue;
+ var date = this.parse_date(time_spans[0].innerHTML);
+ if (!date) continue;
+
+ var comment = {};
+ comment.div = comment_divs[i];
+ comment.date = date;
+ comment.shadow = Dom.hasClass(comment.div, 'shadow_bug_comment');
+ comments.push(comment);
+ }
+
+ for (var i = 0, l = comments.length; i < l; i++) {
+ if (!comments[i].shadow) continue;
+ for (var j = 0, jl = comments.length; j < jl; j++) {
+ if (comments[j].shadow) continue;
+ if (comments[j].date > comments[i].date) {
+ comments[j].div.parentNode.insertBefore(comments[i].div, comments[j].div);
+ break;
+ }
+ }
+ Dom.removeClass(comments[i].div, 'bz_default_hidden');
+ }
+
+ Dom.get('comment').placeholder = 'Add non-public comment';
+ },
+
+ parse_date: function(date) {
+ var matches = date.match(/^\s*(\d+)-(\d+)-(\d+) (\d+):(\d+):(\d+)/);
+ if (!matches) return;
+ return (matches[1] + matches[2] + matches[3] + matches[4] + matches[5] + matches[6]) + 0;
+ }
+};
+
+
+YAHOO.util.Event.onDOMReady(function() {
+ shadow_bug.init();
+});
diff --git a/extensions/ShadowBugs/web/style.css b/extensions/ShadowBugs/web/style.css
new file mode 100644
index 000000000..0c104130f
--- /dev/null
+++ b/extensions/ShadowBugs/web/style.css
@@ -0,0 +1,10 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0. */
+
+.shadow_bug_comment {
+ background: transparent !important;
+}
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 <mkanat@bugzilla.org>
+# Dave Lawrence <dkl@mozilla.com>
+
+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..f36fa8c81
--- /dev/null
+++ b/extensions/SiteMapIndex/Extension.pm
@@ -0,0 +1,157 @@
+# -*- 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 <mkanat@bugzilla.org>
+# Dave Lawrence <dkl@mozilla.com>
+
+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::CGI_WRITE
+ | Bugzilla::Install::Filesystem::DIR_ALSO_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 => <<EOT
+# Allow access to sitemap files created by the SiteMapIndex extension
+<FilesMatch ^sitemap.*\\.xml(.gz)?\$>
+ Allow from all
+</FilesMatch>
+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 <mkanat@bugzilla.org>
+
+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..b0e4c6eab
--- /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 <mkanat@bugzilla.org>
+# Dave Lawrence <dkl@mozilla.com>
+
+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 && $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;
+<?xml version="1.0" encoding="UTF-8"?>
+<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
+END
+
+ foreach my $filename (@$filelist) {
+ $index_xml .= "
+ <sitemap>
+ <loc>" . correct_urlbase() . "data/$extension_name/$filename</loc>
+ <lastmod>$timestamp</lastmod>
+ </sitemap>
+";
+ }
+
+ $index_xml .= <<END;
+</sitemapindex>
+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;
+<?xml version="1.0" encoding="UTF-8"?>
+<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
+END
+
+ foreach my $product (@$products) {
+ $sitemap_xml .= "
+ <url>
+ <loc>" . $product_url . url_quote($product->name) . "</loc>
+ <changefreq>daily</changefreq>
+ <priority>0.4</priority>
+ </url>
+";
+ }
+
+ foreach my $bug (@$bugs) {
+ $sitemap_xml .= "
+ <url>
+ <loc>" . $bug_url . $bug->{bug_id} . "</loc>
+ <lastmod>" . datetime_from($bug->{delta_ts}, 'UTC')->iso8601 . 'Z' . "</lastmod>
+ </url>
+";
+ }
+
+ $sitemap_xml .= <<END;
+</urlset>
+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 <mkanat@bugzilla.org>
+ #%]
+
+[% SET meta_robots = ['noarchive'] %]
+[% meta_robots.push('noindex') IF sitemap_noindex %]
+<meta name="robots" content="[% meta_robots.join(',') FILTER html %]">
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 <mkanat@bugzilla.org>
+ #%]
+
+[% 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..9c8be4beb
--- /dev/null
+++ b/extensions/Splinter/Extension.pm
@@ -0,0 +1,136 @@
+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)};
+
+ 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.
+
+ my $input = Bugzilla->input_params;
+ 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..95b9f5dfa
--- /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 <mkanat@bugzilla.org>
+# Bradley Baetz <bbaetz@acm.org>
+# Owen Taylor <otaylor@redhat.com>
+
+package Bugzilla::Extension::Splinter::Config;
+
+use strict;
+use warnings;
+
+use Bugzilla::Config::Common;
+
+our $sortkey = 1350;
+
+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..6305395f9
--- /dev/null
+++ b/extensions/Splinter/lib/Util.pm
@@ -0,0 +1,161 @@
+# -*- 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 <otaylor@fishsoup.net>
+
+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
+);
+
+# Validates an attachment ID.
+# Takes a parameter containing the ID to be validated.
+# If the second parameter is true, the attachment ID will be validated,
+# however the current user's access to the attachment will not be checked.
+# Will return false if 1) attachment ID is not a valid number,
+# 2) attachment does not exist, or 3) user isn't allowed to access the
+# attachment.
+#
+# Returns an attachment object.
+# Based on code from attachment.cgi
+sub attachment_id_is_valid {
+ my ($attach_id, $dont_validate_access) = @_;
+
+ # Validate the specified attachment id.
+ detaint_natural($attach_id) || return 0;
+
+ # Make sure the attachment exists in the database.
+ my $attachment = new Bugzilla::Attachment($attach_id) || return 0;
+
+ return $attachment
+ if ($dont_validate_access || attachment_is_visible($attachment));
+}
+
+# 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 = attachment_id_is_valid($attach_id);
+
+ return ($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 = attachment_id_is_valid($attach_id);
+
+ if ($attachment && $attachment->ispatch) {
+ return "<a href='" . html_quote(get_review_url($attachment->bug, $attach_id)) .
+ "'>$link_text</a>";
+ }
+}
+
+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..c92c62e5d
--- /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 <bbaetz@acm.org>
+ # Owen Taylor <otaylor@redhat.com>
+ #%]
+[%
+ 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, with the Apache HTTP server, you can add " _
+ "the following lines to the .htaccess for Bugzilla: " _
+ "<pre>" _
+ "RewriteEngine On\n" _
+ "RewriteRule ^review(\?(.*))? page.cgi?id=splinter.html&amp;&#x0024;2 [L]" _
+ "</pre>"
+ }
+%]
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 <otaylor@redhat.com>
+ # David Lawrence <dkl@mozilla.com>
+ #%]
+
+[% IF attachment.ispatch %]
+&#x0020; |
+ [% IF Param("splinter_base").search('\?') %]
+ <a href="[% urlbase FILTER none %][% Param("splinter_base") FILTER html %]&amp;bug=[% attachment.bug_id FILTER uri %]&amp;attachment=[% attachment.id FILTER uri%]">
+ Splinter Review</a>
+ [% ELSE %]
+ <a href="[% urlbase FILTER none %][% Param("splinter_base") FILTER html %]?bug=[% attachment.bug_id FILTER uri %]&amp;attachment=[% attachment.id FILTER uri %]">
+ Splinter Review</a>
+ [% 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 <otaylor@redhat.com>
+ # David Lawrence <dkl@mozilla.com>
+ #%]
+
+[% IF attachment.ispatch %]
+&#x0020; |
+ [% IF Param("splinter_base").search('\?') %]
+ <a href="[% urlbase FILTER none %][% Param("splinter_base") FILTER html %]&amp;bug=[% bugid FILTER uri %]&amp;attachment=[% attachment.id FILTER uri %]">
+ Splinter Review</a>
+ [% ELSE %]
+ <a href="[% urlbase FILTER none %][% Param("splinter_base") FILTER html %]?bug=[% bugid FILTER uri %]&amp;attachment=[% attachment.id FILTER uri %]">
+ Splinter Review</a>
+ [% 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 %]
+ &nbsp;
+ [% IF Param("splinter_base").search('\?') %]
+ <a href="[% urlbase FILTER none %][% Param("splinter_base") FILTER html %]&amp;bug=[% request.bug_id FILTER uri %]&amp;attachment=[% request.attach_id FILTER uri %]">[review]</a>
+ [% ELSE %]
+ <a href="[% urlbase FILTER none %][% Param("splinter_base") FILTER html %]?bug=[% request.bug_id FILTER uri %]&amp;attachment=[% request.attach_id FILTER uri %]">[review]</a>
+ [% 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..96550fd5a
--- /dev/null
+++ b/extensions/Splinter/template/en/default/pages/splinter.html.tmpl
@@ -0,0 +1,270 @@
+[%#
+ # 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 <otaylor@redhat.com>
+ #%]
+
+[% bodyclasses = [] %]
+[% FOREACH group = bug.groups_in %]
+ [% bodyclasses.push("bz_group_$group.name") %]
+[% END %]
+
+[% 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",
+ "skins/custom/bug_groups.css" ]
+ javascript_urls = [ "js/yui/element/element-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" ]
+ bodyclasses = bodyclasses
+%]
+
+[% can_edit = 0 %]
+
+<script type="text/javascript">
+ Splinter.configBase = '[% urlbase FILTER none %][% Param('splinter_base') FILTER js %]';
+ Splinter.configBugUrl = '[% urlbase FILTER none %]';
+ Splinter.configHaveExtension = true;
+ Splinter.configHelp = '[% urlbase FILTER none %]page.cgi?id=splinter/help.html';
+ Splinter.configNote = '';
+
+ Splinter.configAttachmentStatuses = [
+ [% FOREACH status = attachment_statuses %]
+ '[% status FILTER js %]',
+ [% END %]
+ ];
+
+ Splinter.bugId = Splinter.Utils.isDigits('[% bug_id FILTER js %]') ? parseInt('[% bug_id FILTER js %]') : NaN;
+ Splinter.attachmentId = Splinter.Utils.isDigits('[% attach_id FILTER html %]') ? parseInt('[% attach_id FILTER js %]') : NaN;
+
+ if (!isNaN(Splinter.bugId)) {
+ var theBug = new Splinter.Bug.Bug();
+ theBug.id = parseInt('[% bug.id FILTER js %]');
+ theBug.token = '[% update_token FILTER js %]';
+ theBug.shortDesc = Splinter.Utils.strip('[% bug.short_desc FILTER js %]');
+ theBug.creationDate = Splinter.Bug.parseDate('[% bug.creation_ts FILTER time("%Y-%m-%d %T %z") FILTER js %]');
+ theBug.reporterEmail = Splinter.Utils.strip('[% bug.reporter.email FILTER js %]');
+ theBug.reporterName = Splinter.Utils.strip('[% bug.reporter.name FILTER js %]');
+
+ [% FOREACH comment = bug.comments %]
+ [% NEXT IF comment.is_private && !user.is_insider %]
+ [% NEXT UNLESS comment.thetext.match('(?i)^\s*review\s+of\s+attachment\s+\d+\s*:') %]
+ var comment = new Splinter.Bug.Comment();
+ comment.whoName = Splinter.Utils.strip('[% comment.author.name FILTER js %]');
+ comment.whoEmail = Splinter.Utils.strip('[% comment.author.email FILTER js %]');
+ comment.date = Splinter.Bug.parseDate('[% comment.creation_ts FILTER time("%Y-%m-%d %T %z") FILTER js %]');
+ comment.text = '[% comment.thetext FILTER js %]';
+ theBug.comments.push(comment);
+ [% END %]
+
+ [% FOREACH attachment = bug.attachments %]
+ [% NEXT IF attachment.isprivate && !user.is_insider && attachment.attacher.id != user.id %]
+ [% NEXT IF !attachment.ispatch %]
+ var attachid = parseInt('[% attachment.id FILTER js %]');
+ var attachment = new Splinter.Bug.Attachment('', attachid);
+ [% IF attachment.id == attach_id && attachment.ispatch %]
+ [% flag_types = attachment.flag_types %]
+ [% can_edit = attachment.validate_can_edit %]
+ attachment.data = '[% attach_data FILTER js %]';
+ attachment.token = '[% issue_hash_token([attachment.id, attachment.modification_time]) FILTER js %]';
+ [% END %]
+ attachment.description = Splinter.Utils.strip('[% attachment.description FILTER js %]');
+ attachment.filename = Splinter.Utils.strip('[% attachment.filename FILTER js %]');
+ attachment.contenttypeentry = Splinter.Utils.strip('[% attachment.contenttypeentry FILTER js %]');
+ attachment.date = Splinter.Bug.parseDate('[% attachment.attached FILTER time("%Y-%m-%d %T %z") FILTER js %]');
+ attachment.whoName = Splinter.Utils.strip('[% attachment.attacher.name FILTER js %]');
+ attachment.whoEmail = Splinter.Utils.strip('[% attachment.attacher.email FILTER js %]');
+ attachment.isPatch = [% attachment.ispatch ? 1 : 0 %];
+ attachment.isObsolete = [% attachment.isobsolete ? 1 : 0 %];
+ attachment.isPrivate = [% attachment.isprivate ? 1 : 0 %];
+ theBug.attachments.push(attachment);
+ [% END %]
+
+ Splinter.theBug = theBug;
+ }
+</script>
+
+<!--[if lt IE 7]>
+<p style="border: 1px solid #880000; padding: 1em; background: #ffee88; font-size: 120%;">
+ Splinter Patch Review requires a modern browser, such as
+ <a href="http://www.firefox.com">Firefox</a>, for correct operation.
+</p>
+<![endif]-->
+
+<div id="helpful-links">
+ <a id="allReviewsLink" href="[% urlbase FILTER none %][% Param('splinter_base') FILTER js %]">
+ [reviews]</a>
+ <a id='helpLink' target='splinterHelp'
+ href="[% urlbase FILTER none %]page.cgi?id=splinter/help.html">
+ [help]</a>
+</div>
+
+<div id="bugInfo" style="display: none;">
+ <b>[% terms.Bug %]<a id="bugLink"><span id="bugId"></span></a>:</b>
+ <span id="bugShortDesc"></span> -
+ <span id="bugReporter"></span> -
+ <span id="bugCreationDate"></span>
+</div>
+
+<div id="attachInfo" style="display:none;">
+ <span id="attachObsolete"></span>
+ <b>Attachment <a id="attachLink"><span id="attachId"></span></a>:</b>
+ <span id="attachDesc"></span> -
+ <span id="attachCreator"></span> -
+ <span id="attachDate"></span>
+ [% IF feature_enabled('patch_viewer') %]
+ <a href="[% urlbase FILTER none %]attachment.cgi?id=[% attach_id FILTER uri %]&amp;action=diff"
+ target="_blank">[diff]</a>
+ [% END %]
+ <a href="[% urlbase FILTER none %]attachment.cgi?id=[% attach_id FILTER uri %]&amp;action=edit"
+ target="_blank">[details]</a>
+</div>
+
+<div id="error" style="display: none;"> </div>
+
+<div id="enterBug" style="display: none;">
+ [% terms.Bug %] to review:
+ <input id="enterBugInput" />
+ <input id="enterBugGo" type="button" value="Go" />
+ <div id="chooseReview" style="display: none;">
+ Drafts and published reviews:
+ <div id="chooseReviewTable"></div>
+ </div>
+</div>
+
+<div id="chooseAttachment" style="display: none;">
+ <div id="chooseAttachmentTable"></div>
+</div>
+
+<div id="quickHelpShow" style="display:none;">
+ <p>
+ <a href="javascript:Splinter.quickHelpToggle();" title="Show the quick help section" id="quickHelpToggle">
+ Show Quick Help</a>
+ </p>
+</div>
+
+<div id="quickHelpContent" style="display:none;">
+ <p>
+ <a href="javascript:Splinter.quickHelpToggle();" title="Hide the quick help section" id="quickHelpToggle">Close Quick Help</a>
+ </p>
+ <ul id="quickHelpList">
+ <li>From the Overview page, you can add a more generic overview comment that will appear at the beginning of your review.</li>
+ <li>To comment on a specific lines in the patch, first select the filename from the file navigation links.</li>
+ <li>Then double click the line you want to review and a comment box will appear below the line.</li>
+ <li>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.</li>
+ <li>For more detailed instructions, read the Splinter
+ <a id='helpLink' target='splinterHelp' href="[% urlbase FILTER none %]page.cgi?id=splinter/help.html">help page</a>.
+ </li>
+ </ul>
+</div>
+
+<div id="navigationContainer" style="display: none;">
+ <b>Navigation:</b> <span id="navigation"></span>
+</div>
+
+<div id="overview" style="display: none;">
+ <div id="patchIntro"></div>
+ <div>
+ <span id="restored" style="display: none;">
+ (Restored from draft; last edited <span id="restoredLastModified"></span>)
+ </span>
+ </div>
+ <div>
+ <div id="myCommentFrame">
+ <textarea id="myComment"></textarea>
+ <div id="emptyCommentNotice">&lt;Overall Comment&gt;</div>
+ </div>
+ <div id="myPatchComments"></div>
+ <form id="publish" method="post" action="attachment.cgi" onsubmit="normalizeComments();">
+ <input type="hidden" id="publish_token" name="token" value="">
+ <input type="hidden" id="publish_action" name="action" value="update">
+ <input type="hidden" id="publish_review" name="comment" value="">
+ <input type="hidden" id="publish_attach_id" name="id" value="">
+ <input type="hidden" id="publish_attach_desc" name="description" value="">
+ <input type="hidden" id="publish_attach_filename" name="filename" value="">
+ <input type="hidden" id="publish_attach_contenttype" name="contenttypeentry" value="">
+ <input type="hidden" id="publish_attach_ispatch" name="ispatch" value="">
+ <input type="hidden" id="publish_attach_isobsolete" name="isobsolete" value="">
+ <input type="hidden" id="publish_attach_isprivate" name="isprivate" value="">
+ <div id="attachment_flags">
+ [% any_flags_requesteeble = 0 %]
+ [% FOREACH flag_type = flag_types %]
+ [% NEXT UNLESS flag_type.is_active %]
+ [% SET any_flags_requesteeble = 1 IF flag_type.is_requestable && flag_type.is_requesteeble %]
+ [% END %]
+ [% IF flag_types.size > 0 %]
+ [% PROCESS "flag/list.html.tmpl" bug_id = bug_id
+ attach_id = attach_d
+ flag_types = flag_types
+ read_only_flags = !can_edit
+ any_flags_requesteeble = any_flags_requesteeble
+ %]
+ [% END %]
+ <script>
+ [% FOREACH flag_type = flag_types %]
+ [% NEXT UNLESS flag_type.is_active %]
+ Event.addListener('flag_type-[% flag_type.id FILTER js %]', 'change',
+ function() { Splinter.flagChanged = 1;
+ Splinter.queueUpdateHaveDraft(); });
+ [% FOREACH flag = flag_type.flags %]
+ Event.addListener('flag-[% flag.id FILTER js %]', 'change',
+ function() { Splinter.flagChanged = 1;
+ Splinter.queueUpdateHaveDraft(); });
+ [% END %]
+ [% END %]
+ </script>
+ </div>
+ </form>
+ <div id="buttonBox">
+ <span id="attachmentStatusSpan">Patch Status:
+ <select id="attachmentStatus"> </select>
+ </span>
+ <input id="publishButton" type="button" value="Publish" />
+ <input id="cancelButton" type="button" value="Cancel" />
+ </div>
+ <div class="clear"></div>
+ </div>
+ <div id="oldReviews" style="display: none;">
+ <div class="review-title">
+ Previous Reviews
+ </div>
+ </div>
+</div>
+
+<div id="files" style="display: none;">
+ <div id="file-collapse-all" style="display:none;">
+ <a href="javascript:void(0);" onclick="Splinter.toggleCollapsed('', 'none')">Collapse All</a> |
+ <a href="javascript:void(0);" onclick="Splinter.toggleCollapsed('', 'block')">Expand All</a>
+ </div>
+</div>
+
+<div id="credits">
+ Powered by <a href="http://fishsoup.net/software/splinter">Splinter</a>
+</div>
+
+<div id="saveDraftNotice" style="display: none;"></div>
+
+[% 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..87f082427
--- /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 <otaylor@redhat.com>
+ #%]
+
+[% PROCESS global/header.html.tmpl
+ title = "Patch Review Help"
+ header = "Patch Review Help"
+%]
+
+<h2>Splinter Patch Review</h2>
+<p>
+ Splinter is an add-on for [% terms.Bugzilla %] to allow conveniently
+ reviewing patches that people have attached to
+ [%+ terms.Bugzilla %]. <a href="http://fishsoup.net/software/splinter">More
+ information about Splinter</a>.
+</p>
+<h3>The patch review view</h3>
+<p>
+ 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.
+</p>
+<p>
+ On the Overview page, from top to bottom are shown:
+</p>
+<ul>
+ <li>Introductory text to the patch. For a patch that was created
+ using 'git format-patch' this will be the Git commit
+ message.</li>
+ <li>Controls for creating a new review</li>
+ <li>Previous reviews that other people have written.</li>
+</ul>
+<p>
+ 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.
+</p>
+<p>
+ 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.
+</p>
+<h3>Reviewing an existing patch</h3>
+<p>
+ There are three components to a review:
+</p>
+<ul>
+ <li>
+ An overall comment. The text area on the first page allows
+ you to enter your overall thoughts on the [% terms.bug %].
+ </li>
+ <li>
+ 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.
+ </li>
+ <li>
+ 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.
+ </li>
+</ul>
+<p>
+ 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.
+</p>
+<h3>Saved drafts</h3>
+<p>
+ 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.
+</p>
+<p>
+ 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.
+</p>
+<h3>Responding to someone's review</h3>
+<p>
+ 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.
+</p>
+<h3>Uploading patches for review</h3>
+<p>
+ Splinter doesn't really care how patches are provided to
+ [%+ terms.Bugzilla %], as long as tmey are well-formatted patches. If you are
+ using Git for version control, you can either format changes as
+ patches
+ using <a href="http://www.kernel.org/pub/software/scm/git/docs/git-format-patch.html">'git
+ format-patch</a> and attach them manually to the [% terms.bug %], or you
+ can
+ use <a href="http://fishsoup.net/software/git-bz">git-bz</a>.
+ 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.
+</p>
+<h3>The [% terms.bug %] review view</h3>
+<p>
+ 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.
+</p>
+<h3>Your reviews</h3>
+<p>
+ 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.
+</p>
+
+[% 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..014751b08
--- /dev/null
+++ b/extensions/Splinter/web/splinter.css
@@ -0,0 +1,419 @@
+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;
+}
+
+#attachObsolete {
+ font-weight: bold;
+ color: #c00000;
+}
+
+.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;
+}
+
+.file-review-label {
+ font-size: 80%;
+}
+
+.file-reviewed-nav {
+ text-decoration: line-through;
+}
+
+.trailing-whitespace {
+ background: #ffaaaa;
+}
diff --git a/extensions/Splinter/web/splinter.js b/extensions/Splinter/web/splinter.js
new file mode 100644
index 000000000..18f445325
--- /dev/null
+++ b/extensions/Splinter/web/splinter.js
@@ -0,0 +1,2572 @@
+// Splinter - patch review add-on for Bugzilla
+// By Owen Taylor <otaylor@fishsoup.net>
+// 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 <dkl@mozilla.com>
+
+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,
+
+ GIT_FILE_RE : /^diff --git a\/(\S+).*\n(?:(new|deleted) file mode \d+\n)?(?:index.*\n)?GIT binary patch\n(delta )?/mg,
+
+ _cleanIntro : function(intro) {
+ var m;
+
+ intro = Splinter.Utils.strip(intro) + "\n\n";
+
+ // Git: remove binary diffs
+ var binary_re = /^(?:diff --git .*\n|literal \d+\n)(?:.+\n)+\n/mg;
+ m = binary_re.exec(intro);
+ while (m) {
+ intro = intro.substr(m.index + m[0].length);
+ binary_re.lastIndex = 0;
+ m = binary_re.exec(intro);
+ }
+
+ // Git: remove leading 'From <commit_id> <date>'
+ m = /^From\s+[a-f0-9]{40}.*\n/.exec(intro);
+ if (m) {
+ intro = intro.substr(m.index + m[0].length);
+ }
+
+ // Git: remove 'diff --stat' output from the end
+ m = /^---\n(?:^\s.*\n)+\s+\d+\s+files changed.*\n?(?!.)/m.exec(intro);
+ if (m) {
+ intro = intro.substr(0, m.index);
+ }
+
+ return Splinter.Utils.strip(intro);
+ }
+};
+
+Splinter.Patch.Hunk = function(oldStart, oldCount, newStart, newCount, functionLine, text) {
+ this._init(oldStart, oldCount, newStart, newCount, functionLine, text);
+};
+
+Splinter.Patch.Hunk.prototype = {
+ _init : function(oldStart, oldCount, newStart, newCount, functionLine, text) {
+ var rawlines = text.split("\n");
+ if (rawlines.length > 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++;
+ }
+ }
+
+ // git mail-formatted patches end with --\n<git version> 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;
+ this.fileReviewed = false;
+
+ 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);
+ var bm = Splinter.Patch.GIT_FILE_RE.exec(text);
+ if (m == null && bm == null)
+ throw "Not a patch";
+ this.intro = m == null ? '' : Splinter.Patch._cleanIntro(text.substring(0, m.index));
+
+ // show binary files in the intro
+
+ if (bm && this.intro.length)
+ this.intro += "\n\n";
+ while (bm != null) {
+ if (bm[2]) {
+ // added or deleted file
+ this.intro += bm[2].charAt(0).toUpperCase() + bm[2].slice(1) + ' Binary File: ' + bm[1] + "\n";
+ } else {
+ // delta
+ this.intro += 'Modified Binary File: ' + bm[1] + "\n";
+ }
+ bm = Splinter.Patch.GIT_FILE_RE.exec(text);
+ }
+
+ 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 "... <N> more ... " line
+ // 5 new lines or 4 new lines and a "... <N> 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 {
+ if (console)
+ console.log("WARNING: Bad content in hunk: " + line);
+ if (line != 'NaN more ...') {
+ // Tack onto current comment even thou it's invalid
+ if (commentText == null) {
+ commentText = line;
+ } else {
+ commentText += "\n" + line;
+ }
+ }
+ }
+
+ if ((oldStart == null || oldLine == oldStart + oldCount) &&
+ (newStart == null || newLine == newStart + newCount))
+ {
+ commentText = rawlines.slice(i + 1).join("\n");
+ break;
+ }
+ }
+
+ if (commentText == null) {
+ if (console)
+ console.log("WARNING: No comment found in hunk");
+ commentText = "";
+ }
+
+
+ var location;
+ try {
+ 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);
+ }
+ } catch(e) {
+ if (console)
+ console.error(e);
+ location = 0;
+ }
+ 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, extraProps) {
+ var propertyName = this._reviewPropertyName(bug, attachment);
+ if (!extraProps) {
+ extraProps = {};
+ }
+ extraProps.isDraft = true;
+ this._updateOrCreateReviewInfo(bug, attachment, extraProps);
+ 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 = /<title>\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.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;
+ }
+ }
+
+ for (i = 0; i < Splinter.thePatch.files.length; i++) {
+ if (Splinter.thePatch.files[i].fileReviewed) {
+ 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()) {
+ var filesReviewed = {};
+ for (var i = 0; i < Splinter.thePatch.files.length; i++) {
+ var file = Splinter.thePatch.files[i];
+ if (file.fileReviewed) {
+ filesReviewed[file.filename] = true;
+ }
+ }
+ Splinter.reviewStorage.saveDraft(Splinter.theBug, Splinter.theAttachment, Splinter.theReview,
+ { 'filesReviewed' : filesReviewed });
+ 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');
+ Dom.setAttribute(commentTextArea, 'tabindex', 1);
+ commentTextArea.appendChild(document.createTextNode(previousText));
+ commentTextArea.appendTo(commentTextFrame);
+ Event.addListener('commentTextArea', 'keydown', function (e) {
+ if (e.which == 13 && e.ctrlKey) {
+ Splinter.saveComment();
+ } else if (e.which == 27) {
+ var comment = Dom.get('commentTextArea').value;
+ if (previousText == comment || comment == '') {
+ Splinter.cancelComment(previousText);
+ }
+ } 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');
+ Dom.setAttribute(commentCancel, 'tabindex', 4);
+ 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');
+ Dom.setAttribute(commentDelete, 'tabindex', 3);
+ 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');
+ Dom.setAttribute(commentSave, 'tabindex', 2);
+ 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.textTD = function (cls, text, title) {
+ if (text == "") {
+ return Splinter.EL("td", cls, "\u00a0", title);
+ }
+ var m = text.match(/^(.*?)(\s+)$/);
+ if (m) {
+ var td = Splinter.EL("td", cls, m[1], title);
+ td.insertBefore(Splinter.EL("span", cls + " trailing-whitespace", m[2], title), null);
+ return td;
+ } else {
+ return Splinter.EL("td", cls, text, title);
+ }
+}
+
+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) {
+ // CHANGED comments disabled due to breakage
+ // type = Splinter.Patch.CHANGED;
+ type = Splinter.Patch.ADDED;
+ } 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.textTD("old-line " + oldStyle, oldText, 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.textTD("new-line " + newStyle, newText, 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 fileReviewed = new Element(document.createElement('span'));
+ Dom.addClass(fileReviewed, 'file-review');
+ Dom.setAttribute(fileReviewed, 'title', 'Indicates that a review has been completed for this file. ' +
+ 'This is for personal tracking purposes only and has no effect ' +
+ 'on the published review.');
+ fileReviewed.appendTo(fileLabel);
+
+ var fileReviewedInput = new Element(document.createElement('input'));
+ Dom.setAttribute(fileReviewedInput, 'type', 'checkbox');
+ Dom.setAttribute(fileReviewedInput, 'id', 'file-review-checkbox-' + encodeURIComponent(file.filename));
+ Dom.setAttribute(fileReviewedInput, 'onchange', "Splinter.toggleFileReviewed('" +
+ encodeURIComponent(file.filename) + "');");
+ if (file.fileReviewed) {
+ Dom.setAttribute(fileReviewedInput, 'checked', 'true');
+ }
+ fileReviewedInput.appendTo(fileReviewed);
+
+ var fileReviewedLabel = new Element(document.createElement('label'));
+ Dom.addClass(fileReviewedLabel, 'file-review-label')
+ Dom.setAttribute(fileReviewedLabel, 'for', 'file-review-checkbox-' + encodeURIComponent(file.filename));
+ fileReviewedLabel.appendChild(document.createTextNode(' Reviewed'));
+ fileReviewedLabel.appendTo(fileReviewed);
+
+ 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 || filename == file.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.toggleFileReviewed = function (filename) {
+ var checkbox = Dom.get('file-review-checkbox-' + filename);
+ if (checkbox) {
+ filename = decodeURIComponent(filename);
+ for (var i = 0; i < Splinter.thePatch.files.length; i++) {
+ var file = Splinter.thePatch.files[i];
+ if (file.filename == filename) {
+ file.fileReviewed = checkbox.checked;
+
+ Splinter.saveDraft();
+ Splinter.queueUpdateHaveDraft();
+
+ // Strike through file names to show review was completed
+ var fileNavLink = Dom.get('switch-' + encodeURIComponent(filename));
+ if (file.fileReviewed) {
+ Dom.addClass(fileNavLink, 'file-reviewed-nav');
+ }
+ else {
+ Dom.removeClass(fileNavLink, 'file-reviewed-nav');
+ }
+ }
+ }
+ }
+}
+
+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.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');
+ Dom.setStyle('attachmentStatusSpan', 'display', 'none');
+
+ if (Splinter.thePatch.intro) {
+ Splinter.Utils.preWrapLines(Dom.get('patchIntro'), Splinter.thePatch.intro);
+ } else {
+ Dom.setStyle('patchIntro', 'display', 'none');
+ }
+
+ Splinter.addNavigationLink('__OVERVIEW__', "Overview", Splinter.showOverview, true);
+ Splinter.addNavigationLink('__ALL__', "All Files", Splinter.showAllFiles, false);
+
+ var i;
+ for (i = 0; i < Splinter.thePatch.files.length; i++) {
+ Splinter.addFileNavigationLink(Splinter.thePatch.files[i]);
+ }
+
+ var navigation = Dom.get('navigation');
+
+ var haveDraftNotice = new Element(document.createElement('div'));
+ Dom.setAttribute(haveDraftNotice, 'id', 'haveDraftNotice');
+ haveDraftNotice.appendChild(document.createTextNode('Draft'));
+ haveDraftNotice.appendTo(navigation);
+
+ var clear = new Element(document.createElement('div'));
+ Dom.addClass(clear, 'clear');
+ clear.appendTo(navigation);
+
+ var numReviewers = 0;
+ for (i = 0; i < Splinter.theBug.comments.length; i++) {
+ var comment = Splinter.theBug.comments[i];
+ var m = Splinter.Review.REVIEW_RE.exec(comment.text);
+
+ if (m && parseInt(m[1], 10) == Splinter.attachmentId) {
+ var review = new Splinter.Review.Review(Splinter.thePatch, comment.getWho(), comment.date);
+ review.parse(comment.text.substr(m[0].length));
+
+ var reviewerIndex;
+ if (review.who in Splinter.reviewers) {
+ reviewerIndex = Splinter.reviewers[review.who];
+ } else {
+ reviewerIndex = ++numReviewers;
+ Splinter.reviewers[review.who] = reviewerIndex;
+ }
+
+ var reviewDiv = new Element(document.createElement('div'));
+ Dom.addClass(reviewDiv, 'review');
+ Dom.addClass(reviewDiv, Splinter.getReviewerClass(review));
+ reviewDiv.appendTo(Dom.get('oldReviews'));
+
+ var reviewerBox = new Element(document.createElement('div'));
+ Dom.addClass(reviewerBox, 'reviewer-box');
+ reviewerBox.appendTo(reviewDiv);
+
+ var reviewer = new Element(document.createElement('div'));
+ Dom.addClass(reviewer, 'reviewer');
+ reviewer.appendChild(document.createTextNode(review.who));
+ reviewer.appendTo(reviewerBox);
+
+ var reviewDate = new Element(document.createElement('div'));
+ Dom.addClass(reviewDate, 'review-date');
+ reviewDate.appendChild(document.createTextNode(Splinter.Utils.formatDate(review.date)));
+ reviewDate.appendTo(reviewerBox);
+
+ var reviewInfoBottom = new Element(document.createElement('div'));
+ Dom.addClass(reviewInfoBottom, 'review-info-bottom');
+ reviewInfoBottom.appendTo(reviewerBox);
+
+ var reviewIntro = new Element(document.createElement('div'));
+ Dom.addClass(reviewIntro, 'review-intro');
+ Splinter.Utils.preWrapLines(reviewIntro, review.intro? review.intro : "");
+ reviewIntro.appendTo(reviewerBox);
+
+ Dom.setStyle('oldReviews', 'display', 'block');
+
+ Splinter.appendReviewComments(review, reviewerBox);
+ }
+ }
+
+ // We load the saved draft or create a new review *after* inserting the existing reviews
+ // so that the ordering comes out right.
+
+ if (Splinter.reviewStorage) {
+ Splinter.theReview = Splinter.reviewStorage.loadDraft(Splinter.theBug, Splinter.theAttachment, Splinter.thePatch);
+ if (Splinter.theReview) {
+ var storedReviews = Splinter.reviewStorage.listReviews();
+ Dom.setStyle('restored', 'display', 'block');
+ for (i = 0; i < storedReviews.length; i++) {
+ if (storedReviews[i].bugId == Splinter.theBug.id &&
+ storedReviews[i].attachmentId == Splinter.theAttachment.id)
+ {
+ Dom.get("restoredLastModified").innerHTML = Splinter.Utils.formatDate(new Date(storedReviews[i].modificationTime));
+ // Restore file reviewed checkboxes
+ if (storedReviews[i].filesReviewed) {
+ for (var j = 0; j < Splinter.thePatch.files.length; j++) {
+ var file = Splinter.thePatch.files[j];
+ if (storedReviews[i].filesReviewed[file.filename]) {
+ file.fileReviewed = true;
+ // Strike through file names to show that review was completed
+ var fileNavLink = Dom.get('switch-' + encodeURIComponent(file.filename));
+ Dom.addClass(fileNavLink, 'file-reviewed-nav');
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ if (!Splinter.theReview) {
+ Splinter.theReview = new Splinter.Review.Review(Splinter.thePatch);
+ }
+
+ if (Splinter.theReview.intro) {
+ Dom.setStyle('emptyCommentNotice', 'display', 'none');
+ }
+
+ var myComment = Dom.get('myComment');
+ myComment.value = Splinter.theReview.intro ? Splinter.theReview.intro : "";
+ Event.addListener(myComment, 'focus', function () {
+ Dom.setStyle('emptyCommentNotice', 'display', 'none');
+ });
+ Event.addListener(myComment, 'blur', function () {
+ if (myComment.value == '') {
+ Dom.setStyle('emptyCommentNotice', 'display', 'block');
+ }
+ });
+ Event.addListener(myComment, 'keydown', function () {
+ Splinter.queueSaveDraft();
+ Splinter.queueUpdateHaveDraft();
+ });
+
+ Splinter.updateMyPatchComments();
+
+ Splinter.queueUpdateHaveDraft();
+
+ Event.addListener("publishButton", "click", Splinter.publishReview);
+ Event.addListener("cancelButton", "click", Splinter.discardReview);
+};
+
+Splinter.newPageUrl = function (newBugId, newAttachmentId) {
+ var newUrl = Splinter.configBase;
+ if (newBugId != null) {
+ newUrl += (newUrl.indexOf("?") < 0) ? "?" : "&";
+ newUrl += "bug=" + escape("" + newBugId);
+ if (newAttachmentId != null) {
+ newUrl += "&attachment=" + escape("" + newAttachmentId);
+ }
+ }
+
+ return newUrl;
+};
+
+Splinter.showNote = function () {
+ var noteDiv = Dom.get("note");
+ if (noteDiv && Splinter.configNote) {
+ noteDiv.innerHTML = Splinter.configNote;
+ Dom.setStyle(noteDiv, 'display', 'block');
+ }
+};
+
+Splinter.showEnterBug = function () {
+ Splinter.showNote();
+
+ Event.addListener("enterBugGo", "click", function () {
+ var newBugId = Splinter.Utils.strip(Dom.get("enterBugInput").value);
+ document.location = Splinter.newPageUrl(newBugId);
+ });
+
+ Dom.setStyle('enterBug', 'display', 'block');
+
+ if (!Splinter.reviewStorage) {
+ return;
+ }
+
+ var storedReviews = Splinter.reviewStorage.listReviews();
+ if (storedReviews.length == 0) {
+ return;
+ }
+
+ var i;
+ var reviewData = [];
+ for (i = storedReviews.length - 1; 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 = "<a href=\"" + Splinter.newPageUrl(splitResult[0], splitResult[1]) +
+ "\">" + splitResult[1] + " - " + splitResult[2] + "</a>";
+ };
+
+ var bugLink = function (elLiner, oRecord, oColumn, oData) {
+ elLiner.innerHTML = "<a href=\"" + Splinter.newPageUrl(oData) +
+ "\">" + oData + "</a>";
+ };
+
+ 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 = "<a href=\"" + Splinter.newPageUrl(Splinter.theBug.id, oData) +
+ "\">" + oData + "</a>";
+ };
+
+ 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.configBugUrl + "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.configBugUrl + "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);
+ if (Splinter.theAttachment.isObsolete) {
+ Dom.get("attachObsolete").innerHTML = 'OBSOLETE';
+ }
+ 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 <bjones@mozilla.com>
+
+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..ab71eeda8
--- /dev/null
+++ b/extensions/TagNewUsers/Extension.pm
@@ -0,0 +1,264 @@
+# -*- 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 <bjones@mozilla.com>
+
+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;
+use Scalar::Util qw(blessed);
+
+# 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);
+}
+
+sub mailer_before_send {
+ my ($self, $args) = @_;
+ my $email = $args->{email};
+
+ my ($bug_id) = ($email->header('Subject') =~ /^[^\d]+(\d+)/);
+ my $changer_login = $email->header('X-Bugzilla-Who');
+ my $changed_fields = $email->header('X-Bugzilla-Changed-Fields');
+
+ if ($bug_id
+ && $changer_login
+ && $changed_fields =~ /attachments.created/)
+ {
+ my $changer = Bugzilla::User->new({ name => $changer_login });
+ if ($changer
+ && $changer->first_patch_bug_id
+ && $changer->first_patch_bug_id == $bug_id)
+ {
+ $email->header_set('X-Bugzilla-FirstPatch' => $bug_id);
+ }
+ }
+}
+
+sub webservice_user_get {
+ my ($self, $args) = @_;
+ my ($webservice, $params, $users) = @$args{qw(webservice params users)};
+
+ foreach my $user (@$users) {
+ # Most of the time the hash values are XMLRPC::Data objects
+ my $email = blessed $user->{'email'} ? $user->{'email'}->value : $user->{'email'};
+ if ($email) {
+ my $user_obj = Bugzilla::User->new({ name => $email });
+ $user->{'is_new'}
+ = $webservice->type('boolean', $self->_user_is_new($user_obj) ? 1 : 0);
+ }
+ }
+}
+
+__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 <bjones@mozilla.com>
+ #%]
+
+<link
+ href="[% 'extensions/TagNewUsers/web/style.css' FILTER mtime FILTER html %]"
+ rel="stylesheet" type="text/css" >
+
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..8f4e9431d
--- /dev/null
+++ b/extensions/TagNewUsers/template/en/default/hook/bug/comments-user.html.tmpl
@@ -0,0 +1,40 @@
+[%#
+ # 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 <bjones@mozilla.com>
+ #%]
+
+[% IF comment.author.is_new %]
+<span
+ class="new_user"
+ title="
+[%- comment.author.comment_count FILTER html %] comment[% "s" IF comment.author.comment_count != 1 -%]
+, created [%
+IF comment.author.creation_age == 0 %]today[%
+ELSIF comment.author.creation_age > 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 %])
+</span>
+[% END %]
+[% IF comment.is_about_attachment
+ && comment.author.first_patch_bug_id == bug.id
+ && comment.attachment.ispatch
+%]
+<span class="new_user">(First Patch)</span>
+[% 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/TellUsMore/Config.pm b/extensions/TellUsMore/Config.pm
new file mode 100644
index 000000000..9a20858b7
--- /dev/null
+++ b/extensions/TellUsMore/Config.pm
@@ -0,0 +1,14 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::TellUsMore;
+
+use strict;
+
+use constant NAME => 'TellUsMore';
+
+__PACKAGE__->NAME;
diff --git a/extensions/TellUsMore/Extension.pm b/extensions/TellUsMore/Extension.pm
new file mode 100644
index 000000000..deffec9fe
--- /dev/null
+++ b/extensions/TellUsMore/Extension.pm
@@ -0,0 +1,140 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::TellUsMore;
+use strict;
+use base qw(Bugzilla::Extension);
+
+use Bugzilla::Extension::TellUsMore::Constants;
+use Bugzilla::Extension::TellUsMore::VersionMirror qw(update_versions);
+use Bugzilla::Extension::TellUsMore::Process;
+
+use Scalar::Util;
+use Bugzilla::Util qw(url_quote);
+
+our $VERSION = '1';
+
+#
+# initialisation
+#
+
+sub db_schema_abstract_schema {
+ my ($self, $args) = @_;
+ $args->{'schema'}->{'tell_us_more'} = {
+ FIELDS => [
+ id => {
+ TYPE => 'MEDIUMSERIAL',
+ NOTNULL => 1,
+ PRIMARYKEY => 1,
+ },
+ token => {
+ TYPE => 'varchar(16)',
+ NOTNULL => 1,
+ },
+ mail => {
+ TYPE => 'varchar(255)',
+ NOTNULL => 1,
+ },
+ creation_ts => {
+ TYPE => 'DATETIME',
+ NOTNULL => 1,
+ },
+ content => {
+ TYPE => 'LONGBLOB',
+ NOTNULL => 1,
+ },
+ ],
+ };
+}
+
+sub install_before_final_checks {
+ my ($self, $args) = @_;
+ # trigger a version sync during checksetup
+ my $mirror = Bugzilla::Extension::TellUsMore::VersionMirror->new();
+ if (!$mirror->check_setup(1)) {
+ print $mirror->setup_error, "\n";
+ return;
+ }
+ $mirror->refresh();
+}
+
+#
+# version mirror hooks
+#
+
+sub object_end_of_create {
+ my ($self, $args) = @_;
+ my $object = $args->{'object'};
+
+ if ($self->is_version($object)) {
+ $self->_mirror->created($object);
+ }
+}
+
+sub object_end_of_update {
+ my ($self, $args) = @_;
+ my $object = $args->{'object'};
+
+ if ($self->is_version($object)) {
+ $self->_mirror->updated($args->{'old_object'}, $object);
+ }
+}
+
+sub object_before_delete {
+ my ($self, $args) = @_;
+ my $object = $args->{'object'};
+
+ if ($self->is_version($object)) {
+ $self->_mirror->deleted($object);
+ }
+}
+
+sub is_version {
+ my ($self, $object) = @_;
+ my $class = Scalar::Util::blessed($object);
+ return $class eq 'Bugzilla::Version';
+}
+
+sub _mirror {
+ my ($self) = @_;
+ $self->{'mirror'} ||= Bugzilla::Extension::TellUsMore::VersionMirror->new();
+ return $self->{'mirror'};
+}
+
+#
+# token validation page
+#
+
+sub page_before_template {
+ my ($self, $args) = @_;
+ my $page = $args->{'page_id'};
+
+ if ($page eq 'tellusmore.html') {
+ my $process = Bugzilla::Extension::TellUsMore::Process->new();
+ my ($bug, $is_new_user) = $process->execute(Bugzilla->input_params->{'token'});
+ my $url;
+ if ($bug) {
+ $url = sprintf(RESULT_URL_SUCCESS, url_quote($bug->id), ($is_new_user ? '1' : '0'));
+ } else {
+ $url = sprintf(RESULT_URL_FAILURE, url_quote($process->error));
+ }
+ print Bugzilla->cgi->redirect($url);
+ exit;
+ }
+}
+
+#
+# web service
+#
+
+sub webservice {
+ my ($self, $args) = @_;
+ my $dispatch = $args->{dispatch};
+ $dispatch->{TellUsMore} = "Bugzilla::Extension::TellUsMore::WebService";
+}
+
+__PACKAGE__->NAME;
diff --git a/extensions/TellUsMore/disabled b/extensions/TellUsMore/disabled
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/extensions/TellUsMore/disabled
diff --git a/extensions/TellUsMore/lib/Constants.pm b/extensions/TellUsMore/lib/Constants.pm
new file mode 100644
index 000000000..110146ef6
--- /dev/null
+++ b/extensions/TellUsMore/lib/Constants.pm
@@ -0,0 +1,89 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::TellUsMore::Constants;
+
+use strict;
+use base qw(Exporter);
+
+our @EXPORT = qw(
+ TELL_US_MORE_LOGIN
+
+ MAX_ATTACHMENT_COUNT
+ MAX_ATTACHMENT_SIZE
+
+ MAX_REPORTS_PER_MINUTE
+
+ TARGET_PRODUCT
+ SECURITY_GROUP
+
+ DEFAULT_VERSION
+ DEFAULT_COMPONENT
+
+ MANDATORY_BUG_FIELDS
+ OPTIONAL_BUG_FIELDS
+
+ MANDATORY_ATTACH_FIELDS
+ OPTIONAL_ATTACH_FIELDS
+
+ TOKEN_EXPIRY_DAYS
+
+ VERSION_SOURCE_PRODUCTS
+ VERSION_TARGET_PRODUCT
+
+ RESULT_URL_SUCCESS
+ RESULT_URL_FAILURE
+);
+
+use constant TELL_US_MORE_LOGIN => 'tellusmore@input.bugs';
+
+use constant MAX_ATTACHMENT_COUNT => 2;
+use constant MAX_ATTACHMENT_SIZE => 512; # kilobytes
+
+use constant MAX_REPORTS_PER_MINUTE => 2;
+
+use constant TARGET_PRODUCT => 'Untriaged Bugs';
+use constant SECURITY_GROUP => 'core-security';
+
+use constant DEFAULT_VERSION => 'unspecified';
+use constant DEFAULT_COMPONENT => 'General';
+
+use constant MANDATORY_BUG_FIELDS => qw(
+ creator
+ description
+ product
+ summary
+ user_agent
+);
+
+use constant OPTIONAL_BUG_FIELDS => qw(
+ attachments
+ creator_name
+ restricted
+ url
+ version
+);
+
+use constant MANDATORY_ATTACH_FIELDS => qw(
+ filename
+ content_type
+ content
+);
+
+use constant OPTIONAL_ATTACH_FIELDS => qw(
+ description
+);
+
+use constant TOKEN_EXPIRY_DAYS => 7;
+
+use constant VERSION_SOURCE_PRODUCTS => ('Firefox', 'Fennec');
+use constant VERSION_TARGET_PRODUCT => 'Untriaged Bugs';
+
+use constant RESULT_URL_SUCCESS => 'http://input.mozilla.org/bug/thanks/?bug_id=%s&is_new_user=%s';
+use constant RESULT_URL_FAILURE => 'http://input.mozilla.org/bug/thanks/?error=%s';
+
+1;
diff --git a/extensions/TellUsMore/lib/Process.pm b/extensions/TellUsMore/lib/Process.pm
new file mode 100644
index 000000000..a73866468
--- /dev/null
+++ b/extensions/TellUsMore/lib/Process.pm
@@ -0,0 +1,263 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::TellUsMore::Process;
+
+use strict;
+use warnings;
+
+use Bugzilla::Bug;
+use Bugzilla::Component;
+use Bugzilla::Constants;
+use Bugzilla::Error;
+use Bugzilla::Hook;
+use Bugzilla::Product;
+use Bugzilla::User;
+use Bugzilla::Util;
+use Bugzilla::Version;
+
+use Bugzilla::Extension::TellUsMore::Constants;
+
+use Data::Dumper;
+use File::Basename;
+use MIME::Base64;
+use Safe;
+
+sub new {
+ my $invocant = shift;
+ my $class = ref($invocant) || $invocant;
+ my $object = {};
+ bless($object, $class);
+ return $object;
+}
+
+sub execute {
+ my ($self, $token) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ my ($bug, $user, $is_new_user);
+ Bugzilla->error_mode(ERROR_MODE_DIE);
+ eval {
+ $self->_delete_stale_issues();
+ my ($mail, $params) = $self->_deserialise_token($token);
+
+ $dbh->bz_start_transaction();
+
+ $self->_fix_invalid_params($params);
+
+ ($user, $is_new_user) = $self->_get_user($mail, $params);
+
+ $bug = $self->_create_bug($user, $params);
+ $self->_post_bug_hook($bug);
+
+ $self->_delete_token($token);
+ $dbh->bz_commit_transaction();
+
+ $self->_send_mail($bug, $user);
+ };
+ $self->{error} = $@;
+ Bugzilla->error_mode(ERROR_MODE_WEBPAGE);
+ return $self->{error} ? undef : ($bug, $is_new_user);
+}
+
+sub error {
+ my ($self) = @_;
+ return $self->{error};
+}
+
+sub _delete_stale_issues {
+ my ($self) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ # delete issues older than TOKEN_EXPIRY_DAYS
+
+ $dbh->do("
+ DELETE FROM tell_us_more
+ WHERE creation_ts < NOW() - " .
+ $dbh->sql_interval(TOKEN_EXPIRY_DAYS, 'DAY')
+ );
+}
+
+sub _deserialise_token {
+ my ($self, $token) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ # validate token
+
+ trick_taint($token);
+ my ($mail, $params) = $dbh->selectrow_array(
+ "SELECT mail,content FROM tell_us_more WHERE token=?",
+ undef, $token
+ );
+ ThrowUserError('token_does_not_exist') unless $mail;
+
+ # deserialise, return ($mail, $params)
+
+ my $compartment = Safe->new();
+ $compartment->reval($params)
+ || ThrowUserError('token_does_not_exist');
+ $params = ${$compartment->varglob('VAR1')};
+
+ return ($mail, $params);
+}
+
+sub _fix_invalid_params {
+ my ($self, $params) = @_;
+
+ # silently adjust any params which are no longer valid
+ # so we don't lose the submission
+
+ my $product = Bugzilla::Product->new({ name => TARGET_PRODUCT })
+ || ThrowUserError('invalid_product_name', { product => TARGET_PRODUCT });
+
+ # component --> general
+
+ my $component = Bugzilla::Component->new({ product => $product, name => $params->{component} })
+ || Bugzilla::Component->new({ product => $product, name => DEFAULT_COMPONENT })
+ || ThrowUserError('tum_invalid_component', { product => TARGET_PRODUCT, name => DEFAULT_COMPONENT });
+ $params->{component} = $component->name;
+
+ # version --> unspecified
+
+ my $version = Bugzilla::Version->new({ product => $product, name => $params->{version} })
+ || Bugzilla::Version->new({ product => $product, name => DEFAULT_VERSION });
+ $params->{version} = $version->name;
+}
+
+sub _get_user {
+ my ($self, $mail, $params) = @_;
+
+ # return existing bmo user
+
+ my $user = Bugzilla::User->new({ name => $mail });
+ return ($user, 0) if $user;
+
+ # or create new user
+
+ $user = Bugzilla::User->create({
+ login_name => $mail,
+ cryptpassword => '*',
+ realname => $params->{creator_name},
+ });
+ return ($user, 1);
+}
+
+sub _create_bug {
+ my ($self, $user, $params) = @_;
+ my $template = Bugzilla->template;
+ my $vars = {};
+
+ # login as the user
+
+ Bugzilla->set_user($user);
+
+ # create the bug
+
+ my $create = {
+ product => $params->{product},
+ component => $params->{component},
+ short_desc => $params->{summary},
+ comment => $params->{description},
+ version => $params->{version},
+ rep_platform => $params->{rep_platform},
+ op_sys => $params->{op_sys},
+ bug_severity => $params->{bug_severity},
+ priority => $params->{priority},
+ bug_file_loc => $params->{bug_file_loc},
+ };
+ if ($params->{group}) {
+ $create->{groups} = [ $params->{group} ];
+ };
+
+ my $bug = Bugzilla::Bug->create($create);
+
+ # add attachments
+
+ foreach my $attachment (@{$params->{attachments}}) {
+ $self->_add_attachment($bug, $attachment);
+ }
+ if (scalar @{$params->{attachments}}) {
+ $bug->update();
+ }
+
+ return $bug;
+}
+
+sub _add_attachment {
+ my ($self, $bug, $params) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ # init
+
+ my $timestamp = $dbh->selectrow_array('SELECT creation_ts FROM bugs WHERE bug_id=?', undef, $bug->bug_id);
+ my $data = decode_base64($params->{content});
+
+ my $description;
+ if ($params->{description}) {
+ $description = $params->{description};
+ } else {
+ $description = $params->{filename};
+ $description =~ s/\\/\//g;
+ $description = basename($description);
+ }
+
+ # trigger content-type auto detection
+
+ Bugzilla->input_params->{'contenttypemethod'} = 'autodetect';
+
+ # add attachment
+
+ my $attachment = Bugzilla::Attachment->create({
+ bug => $bug,
+ creation_ts => $timestamp,
+ data => $data,
+ description => $description,
+ filename => $params->{filename},
+ mimetype => $params->{content_type},
+ });
+
+ # add comment
+
+ $bug->add_comment('', {
+ isprivate => 0,
+ type => CMT_ATTACHMENT_CREATED,
+ extra_data => $attachment->id,
+ });
+}
+
+sub _post_bug_hook {
+ my ($self, $bug) = @_;
+
+ # trigger post_bug_after_creation hook
+
+ my $vars = {
+ id => $bug->bug_id,
+ bug => $bug,
+ };
+ Bugzilla::Hook::process('post_bug_after_creation', { vars => $vars });
+}
+
+sub _send_mail {
+ my ($self, $bug, $user) = @_;
+
+ # send new-bug email
+
+ Bugzilla::BugMail::Send($bug->bug_id, { changer => $user });
+}
+
+sub _delete_token {
+ my ($self, $token) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ # delete token
+
+ trick_taint($token);
+ $dbh->do('DELETE FROM tell_us_more WHERE token=?', undef, $token);
+}
+
+1;
+
diff --git a/extensions/TellUsMore/lib/VersionMirror.pm b/extensions/TellUsMore/lib/VersionMirror.pm
new file mode 100644
index 000000000..24c645d91
--- /dev/null
+++ b/extensions/TellUsMore/lib/VersionMirror.pm
@@ -0,0 +1,207 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::TellUsMore::VersionMirror;
+
+use strict;
+use base qw(Exporter);
+our @EXPORT_OK = qw(update_versions);
+
+use Bugzilla::Constants;
+use Bugzilla::Product;
+
+use Bugzilla::Extension::TellUsMore::Constants;
+
+sub new {
+ my $invocant = shift;
+ my $class = ref($invocant) || $invocant;
+ my $object = {};
+ bless($object, $class);
+ return $object;
+}
+
+sub created {
+ my ($self, $created) = @_;
+ return unless $self->_should_process($created);
+
+ my $version = $self->_get($created);
+ if ($version) {
+ # version already exists, reactivate if required
+ if (!$version->is_active) {
+ $version->set_is_active(1);
+ $version->update();
+ }
+ } else {
+ # create version
+ $self->_create_version($created->name);
+ }
+}
+
+sub updated {
+ my ($self, $old, $new) = @_;
+ return unless $self->_should_process($old);
+
+ my $version = $self->_get($old)
+ or return;
+
+ my $updated = 0;
+ if ($version->name ne $new->name) {
+ if ($version->bug_count) {
+ # version renamed, but old name has bugs
+ # create a new version to avoid touching bugs
+ $self->_create_version($new->name);
+ return;
+ } else {
+ # renaming the version is safe as it is unused
+ $version->set_name($new->name);
+ $updated = 1;
+ }
+ }
+
+ if ($version->is_active != $new->is_active) {
+ if ($new->is_active) {
+ # activating, always safe
+ $version->set_is_active(1);
+ $updated = 1;
+ } else {
+ # can only deactivate when all source products agree
+ my $active = 0;
+ foreach my $product ($self->_sources) {
+ foreach my $product_version (@{$product->versions}) {
+ next unless _version_eq($product_version, $new);
+ if ($product_version->is_active) {
+ $active = 1;
+ last;
+ }
+ }
+ last if $active;
+ }
+ if (!$active) {
+ $version->set_is_active(0);
+ $updated = 1;
+ }
+ }
+ }
+
+ if ($updated) {
+ $version->update();
+ }
+}
+
+sub deleted {
+ my ($self, $deleted) = @_;
+ return unless $self->_should_process($deleted);
+
+ my $version = $self->_get($deleted)
+ or return;
+
+ # can only delete when all source products agreee
+ foreach my $product ($self->_sources) {
+ next if $product->name eq $deleted->product->name;
+ if (grep { _version_eq($_, $version) } @{$product->versions}) {
+ return;
+ }
+ }
+
+ if ($version->bug_count) {
+ # if there's active bugs, deactivate instead of deleting
+ $version->set_is_active(0);
+ $version->update();
+ } else {
+ # no bugs, safe to delete
+ $version->remove_from_db();
+ }
+}
+
+sub check_setup {
+ my ($self, $full) = @_;
+ $self->{setup_error} = '';
+
+ if (!$self->_target) {
+ $self->{setup_error} = "TellUsMore: Error: Target product '" . VERSION_TARGET_PRODUCT . "' does not exist.\n";
+ return 0;
+ }
+ return 1 unless $full;
+
+ foreach my $name (VERSION_SOURCE_PRODUCTS) {
+ my $product = Bugzilla::Product->new({ name => $name });
+ if (!$product) {
+ $self->{setup_error} .= "TellUsMore: Warning: Source product '$name' does not exist.\n";
+ next;
+ }
+ my $component = Bugzilla::Component->new({ product => $self->_target, name => $name });
+ if (!$component) {
+ $self->{setup_error} .= "TellUsMore: Warning: Target component '$name' does not exist.\n";
+ }
+ }
+ return $self->{setup_error} ? 0 : 1;
+}
+
+sub setup_error {
+ my ($self) = @_;
+ return $self->{setup_error};
+}
+
+sub refresh {
+ my ($self) = @_;
+ foreach my $product ($self->_sources) {
+ foreach my $version (@{$product->versions}) {
+ if (!$self->_get($version)) {
+ $self->created($version);
+ }
+ }
+ }
+}
+
+sub _should_process {
+ my ($self, $version) = @_;
+ return 0 unless $self->check_setup();
+ foreach my $product ($self->_sources) {
+ return 1 if $version->product->name eq $product->name;
+ }
+ return 0;
+}
+
+sub _get {
+ my ($self, $query) = @_;
+ my $name = ref($query) ? $query->name : $query;
+ my @versions = grep { $_->name eq $name } @{$self->_target->versions};
+ return scalar @versions ? $versions[0] : undef;
+}
+
+sub _sources {
+ my ($self) = @_;
+ if (!$self->{sources} || scalar(@{$self->{sources}}) != scalar VERSION_SOURCE_PRODUCTS) {
+ my @sources;
+ foreach my $name (VERSION_SOURCE_PRODUCTS) {
+ my $product = Bugzilla::Product->new({ name => $name });
+ push @sources, $product if $product;
+ }
+ $self->{sources} = \@sources;
+ }
+ return @{$self->{sources}};
+}
+
+sub _target {
+ my ($self) = @_;
+ $self->{target} ||= Bugzilla::Product->new({ name => VERSION_TARGET_PRODUCT });
+ return $self->{target};
+}
+
+sub _version_eq {
+ my ($version_a, $version_b) = @_;
+ return lc($version_a->name) eq lc($version_b->name);
+}
+
+sub _create_version {
+ my ($self, $name) = @_;
+ Bugzilla::Version->create({ product => $self->_target, value => $name });
+ # remove bugzilla's cached list of versions
+ delete $self->_target->{versions};
+}
+
+1;
diff --git a/extensions/TellUsMore/lib/WebService.pm b/extensions/TellUsMore/lib/WebService.pm
new file mode 100644
index 000000000..3ace06ef3
--- /dev/null
+++ b/extensions/TellUsMore/lib/WebService.pm
@@ -0,0 +1,259 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::TellUsMore::WebService;
+
+use strict;
+use warnings;
+
+use base qw(Bugzilla::WebService Bugzilla::Extension);
+
+use Bugzilla::Component;
+use Bugzilla::Constants;
+use Bugzilla::Error;
+use Bugzilla::Mailer;
+use Bugzilla::Product;
+use Bugzilla::User;
+use Bugzilla::UserAgent;
+use Bugzilla::Util;
+use Bugzilla::Version;
+
+use Bugzilla::Extension::TellUsMore::Constants;
+
+use Data::Dumper;
+use Email::MIME;
+use MIME::Base64;
+
+sub submit {
+ my ($self, $params) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ # validation
+
+ my $user = Bugzilla->login(LOGIN_REQUIRED);
+ if ($user->email ne TELL_US_MORE_LOGIN) {
+ ThrowUserError('tum_auth_failure');
+ }
+
+ if (Bugzilla->params->{disable_bug_updates}) {
+ ThrowUserError('tum_updates_disabled');
+ }
+
+ $self->_validate_params($params);
+ $self->_set_missing_params($params);
+
+ my $creator = $self->_get_user($params->{creator});
+ if ($creator && $creator->disabledtext ne '') {
+ ThrowUserError('tum_account_disabled', { user => $creator });
+ }
+
+ $self->_validate_rate($params);
+
+ # create transient entry and email
+
+ $dbh->bz_start_transaction();
+ my $token = Bugzilla::Token::GenerateUniqueToken('tell_us_more', 'token');
+ my $id = $self->_insert($params, $token);
+ my $email = $self->_generate_email($params, $token, $creator);
+ $dbh->bz_commit_transaction();
+
+ # send email
+
+ MessageToMTA($email);
+
+ # done, return the id from the tell_us_more table
+
+ return $id;
+}
+
+sub _validate_params {
+ my ($self, $params) = @_;
+
+ $self->_validate_mandatory($params, 'Submission', MANDATORY_BUG_FIELDS);
+ $self->_remove_invalid_fields($params, MANDATORY_BUG_FIELDS, OPTIONAL_BUG_FIELDS);
+
+ if (!validate_email_syntax($params->{creator})) {
+ ThrowUserError('illegal_email_address', { addr => $params->{creator} });
+ }
+
+ if ($params->{attachments}) {
+ if (scalar @{$params->{attachments}} > MAX_ATTACHMENT_COUNT) {
+ ThrowUserError('tum_too_many_attachments', { max => MAX_ATTACHMENT_COUNT });
+ }
+ my $i = 0;
+ foreach my $attachment (@{$params->{attachments}}) {
+ $i++;
+ $self->_validate_mandatory($attachment, "Attachment $i", MANDATORY_ATTACH_FIELDS);
+ $self->_remove_invalid_fields($attachment, MANDATORY_ATTACH_FIELDS, OPTIONAL_ATTACH_FIELDS);
+ if (length(decode_base64($attachment->{content})) > MAX_ATTACHMENT_SIZE * 1024) {
+ ThrowUserError('tum_attachment_too_large', { filename => $attachment->{filename}, max => MAX_ATTACHMENT_SIZE });
+ }
+ }
+ }
+
+ # products are mapped to components of the target-product
+
+ Bugzilla::Component->new({ name => $params->{product}, product => $self->_target_product })
+ || ThrowUserError('invalid_product_name', { product => $params->{product} });
+}
+
+sub _set_missing_params {
+ my ($self, $params) = @_;
+
+ # set the product and component correctly
+
+ $params->{component} = $params->{product};
+ $params->{product} = TARGET_PRODUCT;
+
+ # priority, bug_severity
+
+ $params->{priority} = Bugzilla->params->{defaultpriority};
+ $params->{bug_severity} = Bugzilla->params->{defaultseverity};
+
+ # map invalid versions to 'unspecified'
+
+ if (!$params->{version}) {
+ $params->{version} = DEFAULT_VERSION;
+ } else {
+ Bugzilla::Version->new({ product => $self->_target_product, name => $params->{version} })
+ || ($params->{version} = DEFAULT_VERSION);
+ }
+
+ # set url
+
+ $params->{bug_file_loc} = $params->{url};
+
+ # detect the opsys and platform from user_agent
+
+ $ENV{HTTP_USER_AGENT} = $params->{user_agent};
+ $params->{rep_platform} = detect_platform();
+ $params->{op_sys} = detect_op_sys();
+
+ # set group based on restricted
+
+ $params->{group} = $params->{restricted} ? SECURITY_GROUP : '';
+ delete $params->{restricted};
+}
+
+sub _get_user {
+ my ($self, $email) = @_;
+
+ return Bugzilla::User->new({ name => $email });
+}
+
+sub _insert {
+ my ($self, $params, $token) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ local $Data::Dumper::Purity = 1;
+ local $Data::Dumper::Sortkeys = 1;
+ my $content = Dumper($params);
+ trick_taint($content);
+
+ my $sth = $dbh->prepare('
+ INSERT INTO tell_us_more(token, mail, creation_ts, content)
+ VALUES(?, ?, ?, ?)
+ ');
+ $sth->bind_param(1, $token);
+ $sth->bind_param(2, $params->{creator});
+ $sth->bind_param(3, $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'));
+ $sth->bind_param(4, $content, $dbh->BLOB_TYPE);
+ $sth->execute();
+
+ return $dbh->bz_last_key('tell_us_more', 'id');
+}
+
+sub _generate_email {
+ my ($self, $params, $token, $user) = @_;
+
+ # create email parts
+
+ my $template = Bugzilla->template_inner;
+ my ($message_header, $message_text, $message_html);
+ my $vars = {
+ token_url => correct_urlbase() . 'page.cgi?id=tellusmore.html&token=' . url_quote($token),
+ recipient_email => $params->{creator},
+ recipient_name => ($user ? $user->name : $params->{creator_name}),
+ };
+
+ my $prefix = $user ? 'existing' : 'new';
+ $template->process("email/$prefix-account.header.tmpl", $vars, \$message_header)
+ || ThrowCodeError('template_error', { template_error_msg => $template->error() });
+ $template->process("email/$prefix-account.txt.tmpl", $vars, \$message_text)
+ || ThrowCodeError('template_error', { template_error_msg => $template->error() });
+ $template->process("email/$prefix-account.html.tmpl", $vars, \$message_html)
+ || ThrowCodeError('template_error', { template_error_msg => $template->error() });
+
+ # create email object
+
+ my @parts = (
+ Email::MIME->create(
+ attributes => { content_type => "text/plain" },
+ body => $message_text,
+ ),
+ Email::MIME->create(
+ attributes => { content_type => "text/html" },
+ body => $message_html,
+ ),
+ );
+ my $email = new Email::MIME("$message_header\n");
+ $email->content_type_set('multipart/alternative');
+ $email->parts_set(\@parts);
+
+ return $email;
+}
+
+sub _validate_mandatory {
+ my ($self, $params, $name, @fields) = @_;
+
+ my @missing_fields;
+ foreach my $field (@fields) {
+ if (!exists $params->{$field} || $params->{$field} eq '') {
+ push @missing_fields, $field;
+ }
+ }
+
+ if (scalar @missing_fields) {
+ ThrowUserError('tum_missing_fields', { name => $name, missing => \@missing_fields });
+ }
+}
+
+sub _remove_invalid_fields {
+ my ($self, $params, @valid_fields) = @_;
+
+ foreach my $field (keys %$params) {
+ if (!grep { $_ eq $field } @valid_fields) {
+ delete $params->{$field};
+ }
+ }
+}
+
+sub _validate_rate {
+ my ($self, $params) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ my ($report_count) = $dbh->selectrow_array('
+ SELECT COUNT(*)
+ FROM tell_us_more
+ WHERE mail = ?
+ AND creation_ts >= NOW() - ' . $dbh->sql_interval(1, 'MINUTE')
+ , undef, $params->{creator}
+ );
+ if ($report_count + 1 > MAX_REPORTS_PER_MINUTE) {
+ ThrowUserError('tum_rate_exceeded', { max => MAX_REPORTS_PER_MINUTE });
+ }
+}
+
+sub _target_product {
+ my ($self) = @_;
+
+ my $product = Bugzilla::Product->new({ name => TARGET_PRODUCT })
+ || ThrowUserError('invalid_product_name', { product => TARGET_PRODUCT });
+ return $product;
+}
+
+1;
diff --git a/extensions/TellUsMore/template/en/default/email/existing-account.header.tmpl b/extensions/TellUsMore/template/en/default/email/existing-account.header.tmpl
new file mode 100644
index 000000000..b4d5b10dc
--- /dev/null
+++ b/extensions/TellUsMore/template/en/default/email/existing-account.header.tmpl
@@ -0,0 +1,10 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+Subject: Thank you filing a [% terms.bug %] on Firefox!
+From: Mozilla Development Team <nobody@mozilla.org>
+To: [% recipient_name FILTER none %] <[% recipient_email FILTER none %]>
diff --git a/extensions/TellUsMore/template/en/default/email/existing-account.html.tmpl b/extensions/TellUsMore/template/en/default/email/existing-account.html.tmpl
new file mode 100644
index 000000000..923caded6
--- /dev/null
+++ b/extensions/TellUsMore/template/en/default/email/existing-account.html.tmpl
@@ -0,0 +1,36 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+<p>
+Thank you filing a [% terms.bug %] on Firefox! We appreciate you taking the time out of
+your day to enhance the quality of our favorite open web browser.
+</p>
+
+<p>
+Wait one second though! Before this [% terms.bug %] is filed, we need you to confirm this
+is a real e-mail account. So, please click on the link below to confirm that it
+is!
+</p>
+
+<p>
+<a href="[% token_url FILTER none %]">[% token_url FILTER html %]</a>
+</p>
+
+<p>
+If you'd like to get involved with the Mozilla Community in other ways such as
+localization, testing, development and design, please look at our Get Involved
+page:
+</p>
+
+<p>
+<a href="http://www.mozilla.org/contribute/">http://www.mozilla.org/contribute/</a>
+</p>
+
+<p>
+Thank You For Your Help,<br>
+The Firefox Development Team
+</p>
diff --git a/extensions/TellUsMore/template/en/default/email/existing-account.txt.tmpl b/extensions/TellUsMore/template/en/default/email/existing-account.txt.tmpl
new file mode 100644
index 000000000..e3d82e37f
--- /dev/null
+++ b/extensions/TellUsMore/template/en/default/email/existing-account.txt.tmpl
@@ -0,0 +1,24 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+Thank you filing a [% terms.bug %] on Firefox! We appreciate you taking the time out of
+your day to enhance the quality of our favorite open web browser.
+
+Wait one second though! Before this [% terms.bug %] is filed, we need you to confirm this
+is a real e-mail account. So, please click on the link below to confirm that it
+is!
+
+[% token_url FILTER none %]
+
+If you'd like to get involved with the Mozilla Community in other ways such as
+localization, testing, development and design, please look at our Get Involved
+page:
+
+http://www.mozilla.org/contribute/
+
+Thank You For Your Help,
+The Firefox Development Team
diff --git a/extensions/TellUsMore/template/en/default/email/new-account.header.tmpl b/extensions/TellUsMore/template/en/default/email/new-account.header.tmpl
new file mode 100644
index 000000000..b4d5b10dc
--- /dev/null
+++ b/extensions/TellUsMore/template/en/default/email/new-account.header.tmpl
@@ -0,0 +1,10 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+Subject: Thank you filing a [% terms.bug %] on Firefox!
+From: Mozilla Development Team <nobody@mozilla.org>
+To: [% recipient_name FILTER none %] <[% recipient_email FILTER none %]>
diff --git a/extensions/TellUsMore/template/en/default/email/new-account.html.tmpl b/extensions/TellUsMore/template/en/default/email/new-account.html.tmpl
new file mode 100644
index 000000000..41006592b
--- /dev/null
+++ b/extensions/TellUsMore/template/en/default/email/new-account.html.tmpl
@@ -0,0 +1,36 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+<p>
+Thank you filing a [% terms.bugs %] on Firefox! We appreciate you taking the time out of
+your day to enhance the quality of our favorite open web browser.
+</p>
+
+<p>
+Wait one second though! Before this [% terms.bugs %] is filed, we need you to confirm this
+is a real e-mail account. So, please click on the link below to confirm that it
+is!
+</p>
+
+<p>
+<a href="[% token_url FILTER none %]">[% token_url FILTER html %]</a>
+</p>
+
+<p>
+If you'd like to get involved with the Mozilla Community in other ways such as
+localization, testing, development and design, please look at our Get Involved
+page:
+</p>
+
+<p>
+<a href="http://www.mozilla.org/contribute/">http://www.mozilla.org/contribute/</a>
+</p>
+
+<p>
+Thank You For Your Help,<br>
+The Firefox Development Team
+</p>
diff --git a/extensions/TellUsMore/template/en/default/email/new-account.txt.tmpl b/extensions/TellUsMore/template/en/default/email/new-account.txt.tmpl
new file mode 100644
index 000000000..93d2a3eea
--- /dev/null
+++ b/extensions/TellUsMore/template/en/default/email/new-account.txt.tmpl
@@ -0,0 +1,24 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+Thank you filing a [% terms.bugs %] on Firefox! We appreciate you taking the time out of
+your day to enhance the quality of our favorite open web browser.
+
+Wait one second though! Before this [% terms.bugs %] is filed, we need you to confirm this
+is a real e-mail account. So, please click on the link below to confirm that it
+is!
+
+[% token_url FILTER none %]
+
+If you'd like to get involved with the Mozilla Community in other ways such as
+localization, testing, development and design, please look at our Get Involved
+page:
+
+http://www.mozilla.org/contribute/
+
+Thank You For Your Help,
+The Firefox Development Team
diff --git a/extensions/TellUsMore/template/en/default/hook/global/user-error-errors.html.tmpl b/extensions/TellUsMore/template/en/default/hook/global/user-error-errors.html.tmpl
new file mode 100644
index 000000000..16fe2f1a3
--- /dev/null
+++ b/extensions/TellUsMore/template/en/default/hook/global/user-error-errors.html.tmpl
@@ -0,0 +1,39 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF error == "tum_auth_failure" %]
+ You are not authorized to use this feature.
+
+[% ELSIF error == "tum_updates_disabled" %]
+ Issue reporting is temporarily disabled; please wait a few minutes and try again.
+
+[% ELSIF error == "tum_account_disabled" %]
+ The account associated with '[% user.login FILTER none %]' is disabled:
+ [% user.disabledtext FILTER none %]
+
+[% ELSIF error == "tum_too_many_attachments" %]
+ More than [% max FILTER none %] attachments were provided.
+
+[% ELSIF error == "tum_attachment_too_large" %]
+ [% filename FILTER none %] is larger than [% max FILTER none %]k.
+
+[% ELSIF error == "tum_missing_fields" %]
+ [% name FILTER none %] is missing the following mandatory field
+ [%~ "s" IF missing.size > 1 %]: [%%]
+ [% FOREACH field = missing %]
+ [% field FILTER none %]
+ [%~ ", " UNLESS loop.last() %]
+ [% END %]
+
+[% ELSIF error == "tum_rate_exceeded" %]
+ You cannot report more than [% max FILTER none %] issues per minute.
+
+[% ELSIF error == "tum_invalid_component" %]
+ The product [% product FILTER none %] is missing the component [% name FILTER none %].
+
+[% END %]
diff --git a/extensions/TryAutoLand/Config.pm b/extensions/TryAutoLand/Config.pm
new file mode 100644
index 000000000..8b299183b
--- /dev/null
+++ b/extensions/TryAutoLand/Config.pm
@@ -0,0 +1,19 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::TryAutoLand;
+use strict;
+
+use constant NAME => 'TryAutoLand';
+
+use constant REQUIRED_MODULES => [
+];
+
+use constant OPTIONAL_MODULES => [
+];
+
+__PACKAGE__->NAME;
diff --git a/extensions/TryAutoLand/Extension.pm b/extensions/TryAutoLand/Extension.pm
new file mode 100644
index 000000000..40dbb70d9
--- /dev/null
+++ b/extensions/TryAutoLand/Extension.pm
@@ -0,0 +1,323 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::TryAutoLand;
+
+use strict;
+
+use base qw(Bugzilla::Extension);
+
+use Bugzilla::Bug;
+use Bugzilla::Attachment;
+use Bugzilla::User;
+use Bugzilla::Util qw(trick_taint diff_arrays);
+use Bugzilla::Error;
+
+use Bugzilla::Extension::TryAutoLand::Constants;
+
+our $VERSION = '0.01';
+
+BEGIN {
+ *Bugzilla::Bug::autoland_branches = \&_autoland_branches;
+ *Bugzilla::Bug::autoland_try_syntax = \&_autoland_try_syntax;
+ *Bugzilla::Attachment::autoland_checked = \&_autoland_attachment_checked;
+ *Bugzilla::Attachment::autoland_who = \&_autoland_attachment_who;
+ *Bugzilla::Attachment::autoland_status = \&_autoland_attachment_status;
+ *Bugzilla::Attachment::autoland_status_when = \&_autoland_attachment_status_when;
+ *Bugzilla::Attachment::autoland_update_status = \&_autoland_attachment_update_status;
+ *Bugzilla::Attachment::autoland_remove = \&_autoland_attachment_remove;
+}
+
+sub db_schema_abstract_schema {
+ my ($self, $args) = @_;
+ $args->{'schema'}->{'autoland_branches'} = {
+ FIELDS => [
+ bug_id => {
+ TYPE => 'INT3',
+ NOTNULL => 1,
+ PRIMARYKEY => 1,
+ REFERENCES => {
+ TABLE => 'bugs',
+ COLUMN => 'bug_id',
+ DELETE => 'CASCADE'
+ }
+ },
+ branches => {
+ TYPE => 'VARCHAR(255)',
+ NOTNULL => 1
+ },
+ try_syntax => {
+ TYPE => 'VARCHAR(255)',
+ NOTNULL => 1,
+ DEFAULT => "''",
+ }
+ ],
+ };
+
+ $args->{'schema'}->{'autoland_attachments'} = {
+ FIELDS => [
+ attach_id => {
+ TYPE => 'INT3',
+ NOTNULL => 1,
+ PRIMARYKEY => 1,
+ REFERENCES => {
+ TABLE => 'attachments',
+ COLUMN => 'attach_id',
+ DELETE => 'CASCADE'
+ },
+ },
+ who => {
+ TYPE => 'INT3',
+ NOTNULL => 1,
+ REFERENCES => {
+ TABLE => 'profiles',
+ COLUMN => 'userid',
+ },
+ },
+ status => {
+ TYPE => 'varchar(64)',
+ NOTNULL => 1
+ },
+ status_when => {
+ TYPE => 'DATETIME',
+ NOTNULL => 1,
+ },
+ ],
+ };
+}
+
+sub install_update_db {
+ my ($self) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ if (!$dbh->bz_column_info('autoland_branches', 'try_syntax')) {
+ $dbh->bz_add_column('autoland_branches', 'try_syntax', {
+ TYPE => 'VARCHAR(255)',
+ NOTNULL => 1,
+ DEFAULT => "''",
+ });
+ }
+}
+
+sub _autoland_branches {
+ my $self = shift;
+ return $self->{'autoland_branches'} if exists $self->{'autoland_branches'};
+ _preload_bug_data($self);
+ return $self->{'autoland_branches'};
+}
+
+sub _autoland_try_syntax {
+ my $self = shift;
+ return $self->{'autoland_try_syntax'} if exists $self->{'autoland_try_syntax'};
+ _preload_bug_data($self);
+ return $self->{'autoland_try_syntax'};
+}
+
+sub _preload_bug_data {
+ my ($self) = @_;
+ my $dbh = Bugzilla->dbh;
+ my $result = $dbh->selectrow_hashref("SELECT branches, try_syntax FROM autoland_branches
+ WHERE bug_id = ?", { Slice => {} }, $self->id);
+ if ($result) {
+ $self->{'autoland_branches'} = $result->{'branches'};
+ $self->{'autoland_try_syntax'} = $result->{'try_syntax'};
+ }
+ else {
+ $self->{'autoland_branches'} = undef;
+ $self->{'autoland_try_syntax'} = undef;
+ }
+}
+
+sub _autoland_attachment_checked {
+ my $self = shift;
+ my $dbh = Bugzilla->dbh;
+ return $self->{'autoland_checked'} if exists $self->{'autoland_checked'};
+ my $result = $dbh->selectrow_hashref("SELECT who, status, status_when
+ FROM autoland_attachments
+ WHERE attach_id = ?", { Slice => {} }, $self->id);
+ if ($result) {
+ $self->{'autoland_checked'} = 1;
+ $self->{'autoland_who'} = Bugzilla::User->new($result->{'who'});
+ $self->{'autoland_status'} = $result->{'status'};
+ $self->{'autoland_status_when'} = $result->{'status_when'};
+ }
+ else {
+ $self->{'autoland_checked'} = 0;
+ $self->{'autoland_who'} = undef;
+ $self->{'autoland_status'} = undef;
+ $self->{'autoland_status_when'} = undef;
+ }
+ return $self->{'autoland_checked'};
+}
+
+sub _autoland_attachment_who {
+ my $self = shift;
+ return undef if !$self->autoland_checked;
+ return $self->{'autoland_who'};
+}
+
+sub _autoland_attachment_status {
+ my $self = shift;
+ return undef if !$self->autoland_checked;
+ return $self->{'autoland_status'};
+}
+
+sub _autoland_attachment_status_when {
+ my $self = shift;
+ return undef if !$self->autoland_checked;
+ return $self->{'autoland_status_when'};
+}
+
+sub _autoland_attachment_update_status {
+ my ($self, $status) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ return undef if !$self->autoland_checked;
+
+ grep($_ eq $status, VALID_STATUSES)
+ || ThrowUserError('autoland_invalid_status',
+ { status => $status,
+ valid => [ VALID_STATUSES ] });
+
+ if ($self->autoland_status ne $status) {
+ my $timestamp = $dbh->selectrow_array("SELECT LOCALTIMESTAMP(0)");
+ trick_taint($status);
+ $dbh->do("UPDATE autoland_attachments SET status = ?, status_when = ?
+ WHERE attach_id = ?", undef, $status, $timestamp, $self->id);
+ $self->{'autoland_status'} = $status;
+ $self->{'autoland_status_when'} = $timestamp;
+ }
+
+ return 1;
+}
+
+sub _autoland_attachment_remove {
+ my ($self) = @_;
+ my $dbh = Bugzilla->dbh;
+ return undef if !$self->autoland_checked;
+ $dbh->do("DELETE FROM autoland_attachments WHERE attach_id = ?", undef, $self->id);
+ delete $self->{'autoland_checked'};
+ delete $self->{'autoland_who'};
+ delete $self->{'autoland_status'};
+ delete $self->{'autoland_status_when'};
+}
+
+sub object_end_of_update {
+ my ($self, $args) = @_;
+ my $object = $args->{'object'};
+ my $user = Bugzilla->user;
+ my $dbh = Bugzilla->dbh;
+ my $cgi = Bugzilla->cgi;
+ my $params = Bugzilla->input_params;
+
+ return if !$user->in_group('autoland');
+
+ if ($object->isa('Bugzilla::Bug')) {
+ # First make any needed changes to the branches and try_syntax fields
+ my $bug_id = $object->bug_id;
+ my $bug_result = $dbh->selectrow_hashref("SELECT branches, try_syntax
+ FROM autoland_branches
+ WHERE bug_id = ?",
+ { Slice => {} }, $bug_id);
+
+ my $old_branches = '';
+ my $old_try_syntax = '';
+ if ($bug_result) {
+ $old_branches = $bug_result->{'branches'};
+ $old_try_syntax = $bug_result->{'try_syntax'};
+ }
+
+ my $new_branches = $params->{'autoland_branches'} || '';
+ my $new_try_syntax = $params->{'autoland_try_syntax'} || '';
+
+ my $set_attachments = [];
+ if (ref $params->{'autoland_attachments'}) {
+ $set_attachments = $params->{'autoland_attachments'};
+ } elsif ($params->{'autoland_attachments'}) {
+ $set_attachments = [ $params->{'autoland_attachments'} ];
+ }
+
+ # Check for required values
+ (!$new_branches && @{$set_attachments})
+ && ThrowUserError('autoland_empty_branches');
+ ($new_branches && !$new_try_syntax)
+ && ThrowUserError('autoland_empty_try_syntax');
+
+ trick_taint($new_branches);
+ if (!$new_branches && $old_branches) {
+ $dbh->do("DELETE FROM autoland_branches WHERE bug_id = ?",
+ undef, $bug_id);
+ }
+ elsif ($new_branches && !$old_branches) {
+ $dbh->do("INSERT INTO autoland_branches (bug_id, branches)
+ VALUES (?, ?)", undef, $bug_id, $new_branches);
+ }
+ elsif ($old_branches ne $new_branches) {
+ $dbh->do("UPDATE autoland_branches SET branches = ? WHERE bug_id = ?",
+ undef, $new_branches, $bug_id);
+ }
+
+ trick_taint($new_try_syntax);
+ if (($old_try_syntax ne $new_try_syntax) && $new_branches) {
+ $dbh->do("UPDATE autoland_branches SET try_syntax = ? WHERE bug_id = ?",
+ undef, $new_try_syntax, $bug_id);
+ }
+
+ # Next make any changes needed to each of the attachments.
+ # 1. If an attachment is checked it has a row in the table, if
+ # there is no row in the table it is not checked.
+ # 2. Do not allow changes to checked state if status == 'running' or status == 'waiting'
+ my $check_attachments = ref $params->{'defined_autoland_attachments'}
+ ? $params->{'defined_autoland_attachments'}
+ : [ $params->{'defined_autoland_attachments'} ];
+ my ($removed_attachments) = diff_arrays($check_attachments, $set_attachments);
+ foreach my $attachment (@{$object->attachments}) {
+ next if !$attachment->ispatch;
+ my $attach_id = $attachment->id;
+
+ my $checked = (grep $_ == $attach_id, @$set_attachments) ? 1 : 0;
+ my $unchecked = (grep $_ == $attach_id, @$removed_attachments) ? 1 : 0;
+ my $old_checked = $dbh->selectrow_array("SELECT 1 FROM autoland_attachments
+ WHERE attach_id = ?", undef, $attach_id) || 0;
+
+ next if $checked && $old_checked;
+
+ if ($unchecked && $old_checked && $attachment->autoland_status =~ /^(failed|success)$/) {
+ $dbh->do("DELETE FROM autoland_attachments WHERE attach_id = ?", undef, $attach_id);
+ }
+ elsif ($checked && !$old_checked) {
+ $dbh->do("INSERT INTO autoland_attachments (attach_id, who, status, status_when)
+ VALUES (?, ?, 'waiting', now())", undef, $attach_id, $user->id);
+ }
+ }
+
+ }
+}
+
+sub template_before_process {
+ my ($self, $args) = @_;
+ my $file = $args->{'file'};
+ my $vars = $args->{'vars'};
+
+ # in the header we just need to set the var to ensure the css gets included
+ if ($file eq 'bug/show-header.html.tmpl' && Bugzilla->user->in_group('autoland') ) {
+ $vars->{'autoland'} = 1;
+ }
+
+ if ($file eq 'bug/edit.html.tmpl') {
+ $vars->{'autoland_default_try_syntax'} = DEFAULT_TRY_SYNTAX;
+ }
+}
+
+sub webservice {
+ my ($self, $args) = @_;
+
+ my $dispatch = $args->{dispatch};
+ $dispatch->{TryAutoLand} = "Bugzilla::Extension::TryAutoLand::WebService";
+}
+
+__PACKAGE__->NAME;
diff --git a/extensions/TryAutoLand/bin/TryAutoLand.getBugs.pl b/extensions/TryAutoLand/bin/TryAutoLand.getBugs.pl
new file mode 100644
index 000000000..5d05831a8
--- /dev/null
+++ b/extensions/TryAutoLand/bin/TryAutoLand.getBugs.pl
@@ -0,0 +1,60 @@
+#!/usr/bin/perl -w
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+use XMLRPC::Lite;
+use Data::Dumper;
+use HTTP::Cookies;
+
+###################################
+# Need to login first #
+###################################
+
+my $username = shift;
+my $password = shift;
+
+my $cookie_jar = new HTTP::Cookies( file => "/tmp/lwp_cookies.dat" );
+
+my $rpc = new XMLRPC::Lite;
+
+$rpc->proxy('http://fedora/726193/xmlrpc.cgi');
+
+$rpc->encoding('UTF-8');
+
+$rpc->transport->cookie_jar($cookie_jar);
+
+my $call = $rpc->call( 'User.login',
+ { login => $username, password => $password } );
+
+if ( $call->faultstring ) {
+ print $call->faultstring . "\n";
+ exit;
+}
+
+# Save the cookies in the cookie file
+$rpc->transport->cookie_jar->extract_cookies(
+ $rpc->transport->http_response );
+$rpc->transport->cookie_jar->save;
+
+print "Successfully logged in.\n";
+
+###################################
+# Main call here #
+###################################
+
+$call = $rpc->call('TryAutoLand.getBugs', { status => [] });
+
+my $result = "";
+if ( $call->faultstring ) {
+ print $call->faultstring . "\n";
+ exit;
+}
+else {
+ $result = $call->result;
+}
+
+print Dumper($result);
diff --git a/extensions/TryAutoLand/bin/TryAutoLand.updateStatus.pl b/extensions/TryAutoLand/bin/TryAutoLand.updateStatus.pl
new file mode 100644
index 000000000..4a8f92089
--- /dev/null
+++ b/extensions/TryAutoLand/bin/TryAutoLand.updateStatus.pl
@@ -0,0 +1,65 @@
+#!/usr/bin/perl -w
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+use XMLRPC::Lite;
+use Data::Dumper;
+use HTTP::Cookies;
+
+###################################
+# Need to login first #
+###################################
+
+my $username = shift;
+my $password = shift;
+
+my $cookie_jar = new HTTP::Cookies( file => "/tmp/lwp_cookies.dat" );
+
+my $rpc = new XMLRPC::Lite;
+
+$rpc->proxy('http://fedora/726193/xmlrpc.cgi');
+
+$rpc->encoding('UTF-8');
+
+$rpc->transport->cookie_jar($cookie_jar);
+
+my $call = $rpc->call( 'User.login',
+ { login => $username, password => $password } );
+
+if ( $call->faultstring ) {
+ print $call->faultstring . "\n";
+ exit;
+}
+
+# Save the cookies in the cookie file
+$rpc->transport->cookie_jar->extract_cookies(
+ $rpc->transport->http_response );
+$rpc->transport->cookie_jar->save;
+
+print "Successfully logged in.\n";
+
+###################################
+# Main call here #
+###################################
+
+my $attach_id = shift;
+my $action = shift;
+my $status = shift;
+
+$call = $rpc->call('TryAutoLand.update',
+ { attach_id => $attach_id, action => $action, status => $status });
+
+my $result = "";
+if ( $call->faultstring ) {
+ print $call->faultstring . "\n";
+ exit;
+}
+else {
+ $result = $call->result;
+}
+
+print Dumper($result);
diff --git a/extensions/TryAutoLand/bin/TryAutoLand.updateStatus_json.pl b/extensions/TryAutoLand/bin/TryAutoLand.updateStatus_json.pl
new file mode 100644
index 000000000..f39b55229
--- /dev/null
+++ b/extensions/TryAutoLand/bin/TryAutoLand.updateStatus_json.pl
@@ -0,0 +1,65 @@
+#!/usr/bin/perl -w
+
+use JSON::RPC::Client;
+use Data::Dumper;
+use HTTP::Cookies;
+
+###################################
+# Need to login first #
+###################################
+
+my $username = shift;
+my $password = shift;
+
+my $cookie_jar = HTTP::Cookies->new( file => "/tmp/lwp_cookies.dat" );
+
+my $rpc = new JSON::RPC::Client;
+
+$rpc->ua->ssl_opts(verify_hostname => 0);
+
+my $uri = "http://fedora/726193/jsonrpc.cgi";
+
+#$rpc->ua->cookie_jar($cookie_jar);
+
+#my $result = $rpc->call($uri, { method => 'User.login', params =>
+# { login => $username, password => $password } });
+
+#if ($result) {
+# if ($result->is_error) {
+# print "Error : ", $result->error_message;
+# exit;
+# }
+# else {
+# print "Successfully logged in.\n";
+# }
+#}
+#else {
+# print $rpc->status_line;
+#}
+
+###################################
+# Main call here #
+###################################
+
+my $attach_id = shift;
+my $action = shift;
+my $status = shift;
+
+$result = $rpc->call($uri, { method => 'TryAutoLand.update',
+ params => { attach_id => $attach_id,
+ action => $action,
+ status => $status,
+ Bugzilla_login => $username,
+ Bugzilla_password => $password } });
+
+if ($result) {
+ if ($result->is_error) {
+ print "Error : ", $result->error_message;
+ exit;
+ }
+}
+else {
+ print $rpc->status_line;
+}
+
+print Dumper($result->result);
diff --git a/extensions/TryAutoLand/lib/Constants.pm b/extensions/TryAutoLand/lib/Constants.pm
new file mode 100644
index 000000000..53bad630a
--- /dev/null
+++ b/extensions/TryAutoLand/lib/Constants.pm
@@ -0,0 +1,31 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::TryAutoLand::Constants;
+
+use strict;
+
+use base qw(Exporter);
+
+our @EXPORT = qw(
+ VALID_STATUSES
+ WEBSERVICE_USER
+ DEFAULT_TRY_SYNTAX
+);
+
+use constant VALID_STATUSES => qw(
+ waiting
+ running
+ failed
+ success
+);
+
+use constant WEBSERVICE_USER => 'autoland-try@mozilla.bugs';
+
+use constant DEFAULT_TRY_SYNTAX => '-b do -p all -u none -t none';
+
+1;
diff --git a/extensions/TryAutoLand/lib/WebService.pm b/extensions/TryAutoLand/lib/WebService.pm
new file mode 100644
index 000000000..1088386dd
--- /dev/null
+++ b/extensions/TryAutoLand/lib/WebService.pm
@@ -0,0 +1,189 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Extension::TryAutoLand::WebService;
+
+use strict;
+use warnings;
+
+use base qw(Bugzilla::WebService);
+
+use Bugzilla::Error;
+use Bugzilla::Util qw(trick_taint);
+
+use Bugzilla::Extension::TryAutoLand::Constants;
+
+use constant READ_ONLY => qw(
+ getBugs
+);
+
+# TryAutoLand.getBugs
+# Params: status - List of statuses to filter attachments (only 'waiting' is default)
+# Returns: List of bugs, each being a hash of data needed by the AutoLand polling server
+# Params
+# [ { bug_id => $bug_id1, attachments => [ $attach_id1, $attach_id2 ] }, branches => $branchListFromTextField ... ]
+
+sub getBugs {
+ my ($self, $params) = @_;
+ my $user = Bugzilla->user;
+ my $dbh = Bugzilla->dbh;
+ my %bugs;
+
+ if ($user->login ne WEBSERVICE_USER) {
+ ThrowUserError("auth_failure", { action => "access",
+ object => "autoland_attachments" });
+ }
+
+ my $status_where = "AND status = 'waiting'";
+ my $status_values = [];
+ if (exists $params->{'status'}) {
+ my $statuses = ref $params->{'status'}
+ ? $params->{'status'}
+ : [ $params->{'status'} ];
+ foreach my $status (@$statuses) {
+ if (grep($_ eq $status, VALID_STATUSES)) {
+ trick_taint($status);
+ push(@$status_values, $status);
+ }
+ }
+ if (@$status_values) {
+ my @qmarks = ("?") x @$status_values;
+ $status_where = "AND " . $dbh->sql_in('status', \@qmarks);
+ }
+
+ }
+
+ my $attachments = $dbh->selectall_arrayref("
+ SELECT attachments.bug_id,
+ attachments.attach_id,
+ autoland_attachments.who,
+ autoland_attachments.status,
+ autoland_attachments.status_when
+ FROM attachments, autoland_attachments
+ WHERE attachments.attach_id = autoland_attachments.attach_id
+ $status_where
+ ORDER BY attachments.bug_id",
+ undef, @$status_values);
+
+ foreach my $row (@$attachments) {
+ my ($bug_id, $attach_id, $al_who, $al_status, $al_status_when) = @$row;
+
+ my $al_user = Bugzilla::User->new($al_who);
+
+ # Silent Permission checks
+ next if !$user->can_see_bug($bug_id);
+ my $attachment = Bugzilla::Attachment->new($attach_id);
+ next if !$attachment
+ || $attachment->isobsolete
+ || ($attachment->isprivate && !$user->is_insider);
+
+ $bugs{$bug_id} = {} if !exists $bugs{$bug_id};
+
+ if (!$bugs{$bug_id}{'branches'}) {
+ my $bug_result = $dbh->selectrow_hashref("SELECT branches, try_syntax
+ FROM autoland_branches
+ WHERE bug_id = ?",
+ undef, $bug_id);
+ $bugs{$bug_id}{'branches'} = $bug_result->{'branches'};
+ $bugs{$bug_id}{'try_syntax'} = $bug_result->{'try_syntax'};
+ }
+
+ $bugs{$bug_id}{'attachments'} = [] if !exists $bugs{$bug_id}{'attachments'};
+
+ push(@{$bugs{$bug_id}{'attachments'}}, {
+ id => $self->type('int', $attach_id),
+ who => $self->type('string', $al_user->login),
+ status => $self->type('string', $al_status),
+ status_when => $self->type('dateTime', $al_status_when),
+ });
+ }
+
+ return [
+ map
+ { { bug_id => $_, attachments => $bugs{$_}{'attachments'},
+ branches => $bugs{$_}{'branches'}, try_syntax => $bugs{$_}{'try_syntax'} } }
+ keys %bugs
+ ];
+}
+
+# TryAutoLand.update({ attach_id => $attach_id, action => $action, status => $status })
+# Let's BMO know if a patch has landed or not and BMO will update the auto_land table accordingly
+# If $action eq 'status', $status will be a predetermined set of status values -- when waiting,
+# the UI for submitting autoland will be locked and once complete status update occurs or the
+# mapping is removed, the UI can be unlocked for the $attach_id
+# Allowed statuses: waiting, running, failed, or success
+#
+# If $action eq 'remove', the attach_id will be removed from the mapping table and the UI
+# will be unlocked for the $attach_id.
+
+sub update {
+ my ($self, $params) = @_;
+ my $user = Bugzilla->user;
+ my $dbh = Bugzilla->dbh;
+
+ if ($user->login ne WEBSERVICE_USER) {
+ ThrowUserError("auth_failure", { action => "modify",
+ object => "autoland_attachments" });
+ }
+
+ foreach my $param ('attach_id', 'action') {
+ defined $params->{$param}
+ || ThrowCodeError('param_required',
+ { param => $param });
+ }
+
+ my $action = delete $params->{'action'};
+ my $attach_id = delete $params->{'attach_id'};
+ my $status = delete $params->{'status'};
+
+ if ($action eq 'status' && !$status) {
+ ThrowCodeError('param_required', { param => 'status' });
+ }
+
+ grep($_ eq $action, ('remove', 'status'))
+ || ThrowUserError('autoland_update_invalid_action',
+ { action => $action,
+ valid => ["remove", "status"] });
+
+ my $attachment = Bugzilla::Attachment->new($attach_id);
+ $attachment
+ || ThrowUserError('autoland_invalid_attach_id',
+ { attach_id => $attach_id });
+
+ # Loud Permission checks
+ if (!$user->can_see_bug($attachment->bug_id)) {
+ ThrowUserError("bug_access_denied", { bug_id => $attachment->bug_id });
+ }
+ if ($attachment->isprivate && !$user->is_insider) {
+ ThrowUserError('auth_failure', { action => 'access',
+ object => 'attachment',
+ attach_id => $attachment->id });
+ }
+
+ $attachment->autoland_checked
+ || ThrowUserError('autoland_invalid_attach_id',
+ { attach_id => $attach_id });
+
+ if ($action eq 'status') {
+ # Update the status
+ $attachment->autoland_update_status($status);
+
+ return {
+ id => $self->type('int', $attachment->id),
+ who => $self->type('string', $attachment->autoland_who->login),
+ status => $self->type('string', $attachment->autoland_status),
+ status_when => $self->type('dateTime', $attachment->autoland_status_when),
+ };
+ }
+ elsif ($action eq 'remove') {
+ $attachment->autoland_remove();
+ }
+
+ return {};
+}
+
+1;
diff --git a/extensions/TryAutoLand/template/en/default/hook/bug/edit-after_custom_fields.html.tmpl b/extensions/TryAutoLand/template/en/default/hook/bug/edit-after_custom_fields.html.tmpl
new file mode 100644
index 000000000..ed6224afe
--- /dev/null
+++ b/extensions/TryAutoLand/template/en/default/hook/bug/edit-after_custom_fields.html.tmpl
@@ -0,0 +1,101 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF user.in_group('autoland') %]
+ [% autoland_attachments = [] %]
+ [% autoland_waiting = 0 %]
+ [% autoland_running = 0 %]
+ [% autoland_finished = 0 %]
+ [% FOREACH attachment = bug.attachments %]
+ [% NEXT IF attachment.isprivate && !user.is_insider && attachment.attacher.id != user.id %]
+ [% NEXT IF attachment.isobsolete %]
+ [% NEXT IF !attachment.ispatch %]
+ [% autoland_attachments.push(attachment) %]
+ [% IF attachment.autoland_checked %]
+ [% IF attachment.autoland_status == 'waiting' %]
+ [% autoland_waiting = autoland_waiting + 1 %]
+ [% END %]
+ [% IF attachment.autoland_status == 'running' %]
+ [% autoland_running = autoland_running + 1 %]
+ [% END %]
+ [% IF attachment.autoland_status == 'success' || attachment.autoland_status == 'failed' %]
+ [% autoland_finished = autoland_finished + 1 %]
+ [% END %]
+ [% END %]
+ [% END %]
+ [% IF autoland_attachments.size %]
+ <tr>
+ <th class="field_label field_land_autoland">
+ <a title="[% help_html.autoland FILTER txt FILTER collapse FILTER html %]"
+ class="field_help_link" href="https://wiki.mozilla.org/Build:Autoland">
+ AutoLand:</a>
+ </th>
+ <td>
+ <span id="autoland_edit_container">
+ (<a href="#" id="autoland_edit_action">edit</a>)
+ Total: [% autoland_attachments.size FILTER html %] -
+ <span class="autoland_waiting">Waiting:</span> [% autoland_waiting FILTER html %] -
+ <span class="autoland_running">Running:</span> [% autoland_running FILTER html %] -
+ <span class="autoland_success">Finished:</span> [% autoland_finished FILTER html %]
+ </span>
+ <div id="autoland_edit_input">
+ Branches (required):<br>
+ <input type="text" id="autoland_branches" name="autoland_branches"
+ value="[% bug.autoland_branches FILTER html %]" size="40"
+ class="text_input"><br>
+ Try Syntax (required): (Default: [% autoland_default_try_syntax FILTER html %])<br>
+ <input type="text" id="autoland_try_syntax" name="autoland_try_syntax"
+ value="[% bug.autoland_try_syntax || autoland_default_try_syntax FILTER html %]" size="40"
+ class="text_input"><br>
+ Patches:
+ <br>
+ <table id="autoland_edit_table">
+ [% FOREACH attachment = autoland_attachments %]
+ <tr>
+ <td>
+ [% IF attachment.autoland_checked %]
+ <input type="hidden" name="defined_autoland_attachments"
+ value="[% attachment.id FILTER html %]">
+ [% END %]
+ <input type="checkbox" name="autoland_attachments" value="[% attachment.id FILTER html %]"
+ [% ' checked="checked"' IF attachment.autoland_checked %]
+ [% IF attachment.autoland_status == 'running' || attachment.autoland_status == 'waiting' %]
+ disabled="disabled"
+ [% END %]>
+ </td>
+ <td>
+ <span title="[% attachment.description FILTER html %]">
+ [% attachment.filename FILTER html %]
+ </span>
+ <td>
+ [% IF attachment.autoland_checked %]
+ <span class="autoland_[% attachment.autoland_status FILTER html %]">
+ [% attachment.autoland_status FILTER html %]
+ </span>
+ [% END %]
+ </td>
+ <td>
+ [% IF attachment.autoland_checked %]
+ [% attachment.autoland_status_when FILTER time('%Y-%m-%d %H:%M') %]
+ [% END %]
+ </td>
+ </tr>
+ [% END %]
+ </table>
+ </div>
+ <script type="text/javascript">
+ hideEditableField('autoland_edit_container',
+ 'autoland_edit_input',
+ 'autoland_edit_action',
+ '',
+ '');
+ </script>
+ </td>
+ </tr>
+ [% END %]
+[% END %]
diff --git a/extensions/TryAutoLand/template/en/default/hook/bug/field-help-end.none.tmpl b/extensions/TryAutoLand/template/en/default/hook/bug/field-help-end.none.tmpl
new file mode 100644
index 000000000..899db60c4
--- /dev/null
+++ b/extensions/TryAutoLand/template/en/default/hook/bug/field-help-end.none.tmpl
@@ -0,0 +1,15 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[%
+ vars.help_html.autoland =
+ "TryAutoLand is a BMO extension that allows integration with the $terms.Bugzilla
+ AutoLanding system. Select patches on a $terms.bug will be picked up
+ automatically and landed on the try build server for specified branches.
+ Results of the try build will be sent back to the bug report as comments."
+%]
diff --git a/extensions/TryAutoLand/template/en/default/hook/bug/show-header-end.html.tmpl b/extensions/TryAutoLand/template/en/default/hook/bug/show-header-end.html.tmpl
new file mode 100644
index 000000000..c61f478ea
--- /dev/null
+++ b/extensions/TryAutoLand/template/en/default/hook/bug/show-header-end.html.tmpl
@@ -0,0 +1,11 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF autoland %]
+ [% style_urls.push('extensions/TryAutoLand/web/style.css') %]
+[% END %]
diff --git a/extensions/TryAutoLand/template/en/default/hook/global/user-error-auth_failure_object.html.tmpl b/extensions/TryAutoLand/template/en/default/hook/global/user-error-auth_failure_object.html.tmpl
new file mode 100644
index 000000000..50a1e48d5
--- /dev/null
+++ b/extensions/TryAutoLand/template/en/default/hook/global/user-error-auth_failure_object.html.tmpl
@@ -0,0 +1,11 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF object == 'autoland_attachments' %]
+ AutoLand attachments
+[% END %]
diff --git a/extensions/TryAutoLand/template/en/default/hook/global/user-error-errors.html.tmpl b/extensions/TryAutoLand/template/en/default/hook/global/user-error-errors.html.tmpl
new file mode 100644
index 000000000..c12950dcf
--- /dev/null
+++ b/extensions/TryAutoLand/template/en/default/hook/global/user-error-errors.html.tmpl
@@ -0,0 +1,33 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% IF error == "autoland_invalid_status" %]
+ [% title = "AutoLand Invalid Status" %]
+ The status '[% status FILTER html %]' is not a valid
+ status for the AutoLand extension. Valid statuses
+ are [% valid.join(', ') FILTER html %].
+
+[% ELSIF error == "autoland_invalid_attach_id" %]
+ [% title = "AutoLand Invalid Attachment ID" %]
+ The attachment id '[% attach_id FILTER html %]' is not
+ a valid id for the AutoLand extension.
+
+[% ELSIF error == "autoland_empty_try_syntax" %]
+ [% title = "AutoLand Empty Try Syntax" %]
+ You cannot have a value for Branches and have an empty Try Syntax value.
+
+[% ELSIF error == "autoland_empty_branches" %]
+ [% title = "AutoLand Empty Branches" %]
+ You cannot check one or more patches for AutoLanding and have an empty
+ Branches value.
+
+[% ELSIF error == "autoland_update_invalid_action" %]
+ [% title = "AutoLand Update Invalid Action" %]
+ The action '[% action FILTER html %]' is not a valid action.
+ Valid actions are [% valid.join(', ') FILTER html %].
+[% END %]
diff --git a/extensions/TryAutoLand/web/style.css b/extensions/TryAutoLand/web/style.css
new file mode 100644
index 000000000..99409c0c0
--- /dev/null
+++ b/extensions/TryAutoLand/web/style.css
@@ -0,0 +1,23 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0.
+ */
+
+.autoland_waiting {
+ color: blue;
+}
+
+.autoland_running {
+ color: orange;
+}
+
+.autoland_failed {
+ color: red;
+}
+
+.autoland_success {
+ color: green;
+}
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 <gerv@mozilla.org>
+
+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..bf9d9e856
--- /dev/null
+++ b/extensions/TypeSniffer/Extension.pm
@@ -0,0 +1,75 @@
+# -*- 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 <gerv@mozilla.org>
+
+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'} &&
+ $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/extensions/Voting/Extension.pm b/extensions/Voting/Extension.pm
index a5a3bc11b..b8763b4df 100644
--- a/extensions/Voting/Extension.pm
+++ b/extensions/Voting/Extension.pm
@@ -47,6 +47,10 @@ use constant DEFAULT_VOTES_PER_BUG => 1;
use constant CMT_POPULAR_VOTES => 3;
use constant REL_VOTER => 4;
+BEGIN {
+ *Bugzilla::Bug::user_votes = \&_bug_user_votes;
+}
+
################
# Installation #
################
@@ -122,6 +126,15 @@ sub install_update_db {
# Objects #
###########
+sub _bug_user_votes {
+ my ($self) = @_;
+ return $self->{'user_votes'} if exists $self->{'user_votes'};
+ $self->{'user_votes'} = Bugzilla->dbh->selectrow_array(
+ "SELECT vote_count FROM votes WHERE bug_id = ? AND who = ?",
+ undef, $self->id, Bugzilla->user->id);
+ return $self->{'user_votes'};
+}
+
sub object_columns {
my ($self, $args) = @_;
my ($class, $columns) = @$args{qw(class columns)};
diff --git a/extensions/Voting/template/en/default/hook/bug/edit-after_importance.html.tmpl b/extensions/Voting/template/en/default/hook/bug/edit-after_importance.html.tmpl
index f73ffaebd..b57a5cb27 100644
--- a/extensions/Voting/template/en/default/hook/bug/edit-after_importance.html.tmpl
+++ b/extensions/Voting/template/en/default/hook/bug/edit-after_importance.html.tmpl
@@ -29,6 +29,9 @@
[% ELSE %]
votes
[% END %]</a>
+ [% IF bug.user_votes %]
+ including you
+ [% END %]
[% END %]
(<a href="page.cgi?id=voting/user.html&amp;bug_id=
[%- bug.id FILTER uri %]#vote_
diff --git a/js/TUI.js b/js/TUI.js
index 34a79dc16..2dee8ab2e 100644
--- a/js/TUI.js
+++ b/js/TUI.js
@@ -69,8 +69,14 @@ function TUI_hide_default(className) {
function _TUI_toggle_control_link(className) {
var link = document.getElementById(className + "_controller");
if (!link) return;
- var original_text = link.innerHTML;
- link.innerHTML = TUI_alternates[className];
+ var original_text;
+ if (link.nodeName == 'INPUT') {
+ original_text = link.value;
+ link.value = TUI_alternates[className];
+ } else {
+ original_text = link.innerHTML;
+ link.innerHTML = TUI_alternates[className];
+ }
TUI_alternates[className] = original_text;
}
diff --git a/js/comments.js b/js/comments.js
index e7163a0fd..e9a3e209f 100644
--- a/js/comments.js
+++ b/js/comments.js
@@ -143,3 +143,30 @@ function goto_add_comments( anchor ){
},10);
return false;
}
+
+if (typeof Node == 'undefined') {
+ /* MSIE doesn't define Node, so provide a compatibility object */
+ window.Node = {
+ TEXT_NODE: 3,
+ ENTITY_REFERENCE_NODE: 5
+ };
+}
+
+/* Concatenates all text from element's childNodes. This is used
+ * instead of innerHTML because we want the actual text (and
+ * innerText is non-standard).
+ */
+function getText(element) {
+ var child, text = "";
+ for (var i=0; i < element.childNodes.length; i++) {
+ child = element.childNodes[i];
+ var type = child.nodeType;
+ if (type == Node.TEXT_NODE || type == Node.ENTITY_REFERENCE_NODE) {
+ text += child.nodeValue;
+ } else {
+ /* recurse into nodes of other types */
+ text += getText(child);
+ }
+ }
+ return text;
+}
diff --git a/js/create_bug.js b/js/create_bug.js
new file mode 100644
index 000000000..62d24a642
--- /dev/null
+++ b/js/create_bug.js
@@ -0,0 +1,116 @@
+function toggleAdvancedFields() {
+ TUI_toggle_class('expert_fields');
+ var elements = YAHOO.util.Dom.getElementsByClassName('expert_fields');
+ if (YAHOO.util.Dom.hasClass(elements[0], TUI_HIDDEN_CLASS)) {
+ handleWantsBugFlags(false);
+ }
+}
+
+function handleWantsBugFlags(wants) {
+ if (wants) {
+ hideElementById('bug_flags_false');
+ showElementById('bug_flags_true');
+ }
+ else {
+ showElementById('bug_flags_false');
+ hideElementById('bug_flags_true');
+ clearBugFlagFields();
+ }
+}
+
+function clearBugFlagFields() {
+ var flags_table;
+ flags_table = document.getElementById('bug_flags');
+ if (flags_table) {
+ var selects = flags_table.getElementsByTagName('select');
+ for (var i = 0, il = selects.length; i < il; i++) {
+ if (selects[i].value != 'X') {
+ selects[i].value = 'X';
+ toggleRequesteeField(selects[i]);
+ }
+ }
+ }
+ flags_table = document.getElementById('bug_tracking_flags');
+ if (flags_table) {
+ var selects = flags_table.getElementsByTagName('select');
+ for (var i = 0, il = selects.length; i < il; i++) {
+ selects[i].value = '---';
+ }
+ }
+}
+
+YAHOO.util.Event.onDOMReady(function() {
+ function set_width(id, width) {
+ var el = document.getElementById(id);
+ if (!el) return;
+ el.style.width = width + 'px';
+ }
+
+ // force field widths
+
+ var width = document.getElementById('short_desc').clientWidth + 'px';
+ var el;
+
+ el = document.getElementById('comment');
+ el.style.width = width;
+
+ el = document.getElementById('cf_crash_signature');
+ if (el) el.style.width = width;
+
+ // show the bug flags if a flag is set
+
+ var flag_set = false;
+ var flags_table;
+ flags_table = document.getElementById('bug_flags');
+ if (flags_table) {
+ var selects = flags_table.getElementsByTagName('select');
+ for (var i = 0, il = selects.length; i < il; i++) {
+ if (selects[i].value != 'X') {
+ flag_set = true;
+ break;
+ }
+ }
+ }
+ if (!flag_set) {
+ flags_table = document.getElementById('bug_tracking_flags');
+ if (flags_table) {
+ var selects = flags_table.getElementsByTagName('select');
+ for (var i = 0, il = selects.length; i < il; i++) {
+ if (selects[i].value != '---') {
+ flag_set = true;
+ break;
+ }
+ }
+ }
+ }
+
+ if (flag_set) {
+ hideElementById('bug_flags_false');
+ showElementById('bug_flags_true');
+ } else {
+ hideElementById('bug_flags_true');
+ showElementById('bug_flags_false');
+ }
+ showElementById('btn_no_bug_flags')
+});
+
+function take_bug(user) {
+ var el = Dom.get('assigned_to');
+ el.value = user;
+ el.focus();
+ el.select();
+ assignee_change(user);
+ return false;
+}
+
+function assignee_change(user) {
+ var el = Dom.get('take_bug');
+ if (!el) return;
+ el.style.display = Dom.get('assigned_to').value == user ? 'none' : '';
+}
+
+function init_take_handler(user) {
+ YAHOO.util.Event.addListener(
+ 'assigned_to', 'change', function() { assignee_change(user); });
+ assignee_change(user);
+}
diff --git a/js/field.js b/js/field.js
index 07433b2a5..d42fb0c63 100644
--- a/js/field.js
+++ b/js/field.js
@@ -255,6 +255,8 @@ function showEditableField (e, ContainerInputArray) {
inputs.push(document.getElementById(ContainerInputArray[2]));
} else {
inputs = inputArea.getElementsByTagName('input');
+ if ( inputs.length == 0 )
+ inputs = inputArea.getElementsByTagName('textarea');
}
if ( inputs.length > 0 ) {
// Change the first field's value to ContainerInputArray[2]
@@ -288,7 +290,7 @@ function showEditableField (e, ContainerInputArray) {
*
* var e: the event
* var ContainerInputArray: An array containing the (edit) and text area and the input being displayed
- * var ContainerInputArray[0]: the conainer that will be hidden usually shows the (edit) text
+ * var ContainerInputArray[0]: the container that will be hidden usually shows the (edit) text
* var ContainerInputArray[1]: the input area and label that will be displayed
* var ContainerInputArray[2]: the field that is on the page, might get changed by browser autocomplete
* var ContainerInputArray[3]: the original value from the page loading.
@@ -299,7 +301,7 @@ function checkForChangedFieldValues(e, ContainerInputArray ) {
var unhide = false;
if ( el ) {
if ( el.value != ContainerInputArray[3] ||
- ( el.value == "" && el.id != "alias") ) {
+ ( el.value == "" && el.id != "alias" && el.id != "qa_contact" ) ) {
unhide = true;
}
else {
@@ -341,13 +343,19 @@ function showPeopleOnChange( field_id_list ) {
}
}
-function assignToDefaultOnChange(field_id_list) {
- showPeopleOnChange( field_id_list );
- for(var i = 0; i < field_id_list.length; i++) {
- YAHOO.util.Event.addListener( field_id_list[i],'change', setDefaultCheckbox,
- 'set_default_assignee');
- YAHOO.util.Event.addListener( field_id_list[i],'change',setDefaultCheckbox,
- 'set_default_qa_contact');
+function assignToDefaultOnChange(field_id_list, default_assignee, default_qa_contact) {
+ showPeopleOnChange(field_id_list);
+ for(var i = 0, l = field_id_list.length; i < l; i++) {
+ YAHOO.util.Event.addListener(field_id_list[i], 'change', function(evt, defaults) {
+ if (document.getElementById('assigned_to').value == defaults[0]) {
+ setDefaultCheckbox(evt, 'set_default_assignee');
+ }
+ if (document.getElementById('qa_contact')
+ && document.getElementById('qa_contact').value == defaults[1])
+ {
+ setDefaultCheckbox(evt, 'set_default_qa_contact');
+ }
+ }, [default_assignee, default_qa_contact]);
}
}
@@ -444,7 +452,7 @@ function setResolutionToDuplicate(e, duplicate_or_move_bug_status) {
YAHOO.util.Event.preventDefault(e);
}
-function setDefaultCheckbox(e, field_id ) {
+function setDefaultCheckbox(e, field_id) {
var el = document.getElementById(field_id);
var elLabel = document.getElementById(field_id + "_label");
if( el && elLabel ) {
@@ -699,7 +707,8 @@ YAHOO.bugzilla.userAutocomplete = {
id : YAHOO.bugzilla.userAutocomplete.counter,
params : [ {
match : [ decodeURIComponent(enteredText) ],
- include_fields : [ "name", "real_name" ]
+ include_fields : [ "name", "real_name" ],
+ include_disabled : 1
} ]
};
var stringified = YAHOO.lang.JSON.stringify(json_object);
@@ -792,3 +801,51 @@ YAHOO.bugzilla.keywordAutocomplete = {
});
}
};
+
+/**
+ * Force the browser to honour the selected option when a page is refreshed,
+ * but only if the user hasn't explicitly selected a different option.
+ */
+function initDirtyFieldTracking() {
+ // old IE versions don't provide the information we need to make this fix work
+ // however they aren't affected by this issue, so it's ok to ignore them
+ if (YAHOO.env.ua.ie > 0 && YAHOO.env.ua.ie <= 8) return;
+ var selects = document.getElementById('changeform').getElementsByTagName('select');
+ for (var i = 0, l = selects.length; i < l; i++) {
+ var el = selects[i];
+ var el_dirty = document.getElementById(el.name + '_dirty');
+ if (!el_dirty) continue;
+ if (!el_dirty.value) {
+ var preSelected = bz_preselectedOptions(el);
+ if (!el.multiple) {
+ preSelected.selected = true;
+ } else {
+ el.selectedIndex = -1;
+ for (var j = 0, m = preSelected.length; j < m; j++) {
+ preSelected[j].selected = true;
+ }
+ }
+ }
+ YAHOO.util.Event.on(el, "change", function(e) {
+ var el = e.target || e.srcElement;
+ var preSelected = bz_preselectedOptions(el);
+ var currentSelected = bz_selectedOptions(el);
+ var isDirty = false;
+ if (!el.multiple) {
+ isDirty = preSelected.index != currentSelected.index;
+ } else {
+ if (preSelected.length != currentSelected.length) {
+ isDirty = true;
+ } else {
+ for (var i = 0, l = preSelected.length; i < l; i++) {
+ if (currentSelected[i].index != preSelected[i].index) {
+ isDirty = true;
+ break;
+ }
+ }
+ }
+ }
+ document.getElementById(el.name + '_dirty').value = isDirty ? '1' : '';
+ });
+ }
+}
diff --git a/js/instant-search.js b/js/instant-search.js
new file mode 100644
index 000000000..a3f051f2f
--- /dev/null
+++ b/js/instant-search.js
@@ -0,0 +1,201 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This Source Code Form is "Incompatible With Secondary Licenses", as
+ * defined by the Mozilla Public License, v. 2.0. */
+
+var Dom = YAHOO.util.Dom;
+var Event = YAHOO.util.Event;
+
+Event.onDOMReady(function() {
+ YAHOO.bugzilla.instantSearch.onInit();
+ if (YAHOO.bugzilla.instantSearch.getContent().length >= 4) {
+ YAHOO.bugzilla.instantSearch.doSearch(YAHOO.bugzilla.instantSearch.getContent());
+ } else {
+ YAHOO.bugzilla.instantSearch.reset();
+ }
+ Dom.get('content').focus();
+});
+
+YAHOO.bugzilla.instantSearch = {
+ counter: 0,
+ dataTable: null,
+ dataTableColumns: null,
+ elContent: null,
+ elList: null,
+ currentSearchQuery: '',
+ currentSearchProduct: '',
+
+ onInit: function() {
+ YAHOO.util.Connect.setDefaultPostHeader('application/json; charset=UTF-8');
+
+ this.elContent = Dom.get('content');
+ this.elList = Dom.get('results');
+
+ Event.addListener(this.elContent, 'keyup', this.onContentKeyUp);
+ Event.addListener(Dom.get('product'), 'change', this.onProductChange);
+ },
+
+ setLabels: function(labels) {
+ this.dataTableColumns = [
+ { key: "id", label: labels.id, formatter: this.formatId },
+ { key: "summary", label: labels.summary, formatter: "text" },
+ { key: "component", label: labels.component, formatter: "text" },
+ { key: "status", label: labels.status, formatter: this.formatStatus },
+ ];
+ },
+
+ 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() {
+ YAHOO.bugzilla.instantSearch.currentSearchQuery = '';
+ }
+ );
+
+ this.dataTable = new YAHOO.widget.DataTable(
+ 'results',
+ this.dataTableColumns,
+ dataSource,
+ {
+ initialLoad: false,
+ MSG_EMPTY: 'No matching bugs found.',
+ MSG_ERROR: 'An error occurred while searching for bugs, please try again.'
+ }
+ );
+ },
+
+ formatId: function(el, oRecord, oColumn, oData) {
+ el.innerHTML = '<a href="show_bug.cgi?id=' + oData + '" target="_blank">' + oData + '</a>';
+ },
+
+ formatStatus: function(el, oRecord, oColumn, oData) {
+ var resolution = oRecord.getData('resolution');
+ var bugStatus = display_value('bug_status', oData);
+ if (resolution) {
+ el.innerHTML = bugStatus + ' ' + display_value('resolution', resolution);
+ } else {
+ el.innerHTML = bugStatus;
+ }
+ },
+
+ reset: function() {
+ Dom.addClass(this.elList, 'hidden');
+ this.elList.innerHTML = '';
+ this.currentSearchQuery = '';
+ this.currentSearchProduct = '';
+ },
+
+ onContentKeyUp: function(e) {
+ clearTimeout(YAHOO.bugzilla.instantSearch.lastTimeout);
+ YAHOO.bugzilla.instantSearch.lastTimeout = setTimeout(function() {
+ YAHOO.bugzilla.instantSearch.doSearch(YAHOO.bugzilla.instantSearch.getContent()) },
+ 600);
+ },
+
+ onProductChange: function(e) {
+ YAHOO.bugzilla.instantSearch.doSearch(YAHOO.bugzilla.instantSearch.getContent());
+ },
+
+ doSearch: function(query) {
+ if (query.length < 4)
+ return;
+
+ // don't query if we already have the results (or they are pending)
+ var product = Dom.get('product').value;
+ if (YAHOO.bugzilla.instantSearch.currentSearchQuery == query &&
+ YAHOO.bugzilla.instantSearch.currentSearchProduct == product)
+ return;
+ YAHOO.bugzilla.instantSearch.currentSearchQuery = query;
+ YAHOO.bugzilla.instantSearch.currentSearchProduct = product;
+
+ // initialise the datatable as late as possible
+ YAHOO.bugzilla.instantSearch.initDataTable();
+
+ try {
+ // run the search
+ Dom.removeClass(YAHOO.bugzilla.instantSearch.elList, 'hidden');
+
+ YAHOO.bugzilla.instantSearch.dataTable.showTableMessage(
+ 'Searching...&nbsp;&nbsp;&nbsp;' +
+ '<img src="extensions/GuidedBugEntry/web/images/throbber.gif"' +
+ ' width="16" height="11">',
+ YAHOO.widget.DataTable.CLASS_LOADING
+ );
+ var jsonObject = {
+ version: "1.1",
+ method: "Bug.possible_duplicates",
+ id: ++YAHOO.bugzilla.instantSearch.counter,
+ params: {
+ product: YAHOO.bugzilla.instantSearch.getProduct(),
+ summary: query,
+ limit: 20,
+ include_fields: [ "id", "summary", "status", "resolution", "component" ]
+ }
+ };
+
+ YAHOO.bugzilla.instantSearch.dataTable.getDataSource().sendRequest(
+ YAHOO.lang.JSON.stringify(jsonObject),
+ {
+ success: YAHOO.bugzilla.instantSearch.onSearchResults,
+ failure: YAHOO.bugzilla.instantSearch.onSearchResults,
+ scope: YAHOO.bugzilla.instantSearch.dataTable,
+ argument: YAHOO.bugzilla.instantSearch.dataTable.getState()
+ }
+ );
+
+ } catch(err) {
+ if (console)
+ console.error(err.message);
+ }
+ },
+
+ onSearchResults: function(sRequest, oResponse, oPayload) {
+ YAHOO.bugzilla.instantSearch.dataTable.onDataReturnInitializeTable(sRequest, oResponse, oPayload);
+ },
+
+ getContent: function() {
+ var content = YAHOO.lang.trim(this.elContent.value);
+ // work around chrome bug
+ if (content == YAHOO.bugzilla.instantSearch.elContent.getAttribute('placeholder')) {
+ return '';
+ } else {
+ return content;
+ }
+ },
+
+ getProduct: function() {
+ var result = [];
+ var name = Dom.get('product').value;
+ 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;
+ }
+
+};
+
diff --git a/js/util.js b/js/util.js
index 6dcabbbc9..e0e87259f 100644
--- a/js/util.js
+++ b/js/util.js
@@ -202,6 +202,55 @@ function bz_populateSelectFromArray(aSelect, aArray) {
}
/**
+ * Returns all Option elements that are selected in a <select>,
+ * as an array. Returns an empty array if nothing is selected.
+ *
+ * @param aSelect The select you want the selected values of.
+ */
+function bz_selectedOptions(aSelect) {
+ // HTML 5
+ if (aSelect.selectedOptions) {
+ return aSelect.selectedOptions;
+ }
+
+ var start_at = aSelect.selectedIndex;
+ if (start_at == -1) return [];
+ var first_selected = aSelect.options[start_at];
+ if (!aSelect.multiple) return first_selected;
+ // selectedIndex is specified as being the "first selected item",
+ // so we can start from there.
+ var selected = [first_selected];
+ var options_length = aSelect.options.length;
+ // We start after first_selected
+ for (var i = start_at + 1; i < options_length; i++) {
+ var this_option = aSelect.options[i];
+ if (this_option.selected) selected.push(this_option);
+ }
+ return selected;
+}
+
+/**
+ * Returns all Option elements that have the "selected" attribute, as an array.
+ * Returns an empty array if nothing is selected.
+ *
+ * @param aSelect The select you want the pre-selected values of.
+ */
+function bz_preselectedOptions(aSelect) {
+ var options = aSelect.options;
+ var selected = new Array();
+ for (var i = 0, l = options.length; i < l; i++) {
+ var attributes = options[i].attributes;
+ for (var j = 0, m = attributes.length; j < m; j++) {
+ if (attributes[j].name == 'selected') {
+ if (!aSelect.multiple) return options[i];
+ selected.push(options[i]);
+ }
+ }
+ }
+ return selected;
+}
+
+/**
* Tells you whether or not a particular value is selected in a select,
* whether it's a multi-select or a single-select. The check is
* case-sensitive.
diff --git a/js/yui/swfstore/swfstore.swf b/js/yui/swfstore/swfstore.swf
deleted file mode 100644
index 9c26ed137..000000000
--- a/js/yui/swfstore/swfstore.swf
+++ /dev/null
Binary files differ
diff --git a/mod_perl.pl b/mod_perl.pl
index f3dae34c1..e3a50e4ea 100644
--- a/mod_perl.pl
+++ b/mod_perl.pl
@@ -59,9 +59,9 @@ Bugzilla::CGI->compile(qw(:cgi :push));
use Apache2::SizeLimit;
# This means that every httpd child will die after processing
-# a CGI if it is taking up more than 45MB of RAM all by itself,
+# a CGI if it is taking up more than 1600MB of RAM all by itself,
# not counting RAM it is sharing with the other httpd processes.
-Apache2::SizeLimit->set_max_unshared_size(45_000);
+Apache2::SizeLimit->set_max_unshared_size(1_600_000);
my $cgi_path = Bugzilla::Constants::bz_locations()->{'cgi_path'};
@@ -80,7 +80,7 @@ PerlChildInitHandler "sub { Bugzilla::RNG::srand(); srand(); }"
PerlResponseHandler Bugzilla::ModPerl::ResponseHandler
PerlCleanupHandler Apache2::SizeLimit Bugzilla::ModPerl::CleanupHandler
PerlOptions +ParseHeaders
- Options +ExecCGI
+ Options +ExecCGI +FollowSymLinks
AllowOverride Limit FileInfo Indexes
DirectoryIndex index.cgi index.html
</Directory>
diff --git a/post_bug.cgi b/post_bug.cgi
index c0878b0da..af8c2cd2e 100755
--- a/post_bug.cgi
+++ b/post_bug.cgi
@@ -60,6 +60,12 @@ unless ($cgi->param()) {
exit;
}
+# BMO: Don't allow updating of bugs if disabled
+if (Bugzilla->params->{disable_bug_updates}) {
+ ThrowErrorPage('bug/process/updates-disabled.html.tmpl',
+ 'Bug updates are currently disabled.');
+}
+
# Detect if the user already used the same form to submit a bug
my $token = trim($cgi->param('token'));
check_token_data($token, 'create_bug', 'index.cgi');
diff --git a/process_bug.cgi b/process_bug.cgi
index 9272dec60..e5461e962 100755
--- a/process_bug.cgi
+++ b/process_bug.cgi
@@ -93,6 +93,12 @@ sub should_set {
# Begin Data/Security Validation
######################################################################
+# BMO: Don't allow updating of bugs if disabled
+if (Bugzilla->params->{disable_bug_updates}) {
+ ThrowErrorPage('bug/process/updates-disabled.html.tmpl',
+ 'Bug updates are currently disabled.');
+}
+
# Create a list of objects for all bugs being modified in this request.
my @bug_objects;
if (defined $cgi->param('id')) {
@@ -147,23 +153,43 @@ if (defined $cgi->param('delta_ts'))
Bugzilla::Bug::GetBugActivity($first_bug->id, undef,
scalar $cgi->param('delta_ts'));
- $vars->{'title_tag'} = "mid_air";
-
ThrowCodeError('undefined_field', { field => 'longdesclength' })
if !defined $cgi->param('longdesclength');
- $vars->{'start_at'} = $cgi->param('longdesclength');
+ my $start_at = $cgi->param('longdesclength');
+
# Always sort midair collision comments oldest to newest,
# regardless of the user's personal preference.
- $vars->{'comments'} = $first_bug->comments({ order => "oldest_to_newest" });
- $vars->{'bug'} = $first_bug;
+ my $comments = $first_bug->comments({ order => "oldest_to_newest" });
# The token contains the old delta_ts. We need a new one.
$cgi->param('token', issue_hash_token([$first_bug->id, $first_bug->delta_ts]));
- # Warn the user about the mid-air collision and ask them what to do.
- $template->process("bug/process/midair.html.tmpl", $vars)
- || ThrowTemplateError($template->error());
- exit;
+
+ # Show midair if previous changes made other than CC
+ # and/or one or more comments were made
+ my $do_midair = scalar @$comments > $start_at ? 1 : 0;
+
+ if (!$do_midair) {
+ foreach my $operation (@{ $vars->{'operations'} }) {
+ foreach my $change (@{ $operation->{'changes'} }) {
+ $do_midair = 1 if $change->{'fieldname'} ne 'cc';
+ last;
+ }
+ last if $do_midair;
+ }
+ }
+
+ if ($do_midair) {
+ $vars->{'title_tag'} = "mid_air";
+ $vars->{'start_at'} = $start_at;
+ $vars->{'comments'} = $comments;
+ $vars->{'bug'} = $first_bug;
+
+ # Warn the user about the mid-air collision and ask them what to do.
+ $template->process("bug/process/midair.html.tmpl", $vars)
+ || ThrowTemplateError($template->error());
+ exit;
+ }
}
}
diff --git a/quips.cgi b/quips.cgi
index 74c0047a1..14126e43c 100755
--- a/quips.cgi
+++ b/quips.cgi
@@ -78,6 +78,7 @@ if ($action eq "add") {
check_hash_token($token, ['create-quips']);
# Add the quip
+ # Upstreaming: https://bugzilla.mozilla.org/show_bug.cgi?id=621879
my $approved = (Bugzilla->params->{'quip_list_entry_control'} eq "open")
|| $user->in_group('bz_quip_moderators') || 0;
my $comment = $cgi->param("quip");
diff --git a/request.cgi b/request.cgi
index 16d7662e8..c7e1fe3f7 100755
--- a/request.cgi
+++ b/request.cgi
@@ -114,7 +114,11 @@ sub queue {
flags.attach_id, attachments.description,
requesters.realname, requesters.login_name,
requestees.realname, requestees.login_name, COUNT(privs.group_id),
- " . $dbh->sql_date_format('flags.modification_date', '%Y.%m.%d %H:%i') .
+ " . $dbh->sql_date_format('flags.modification_date', '%Y.%m.%d %H:%i') . ",
+ attachments.ispatch,
+ bugs.bug_status,
+ bugs.priority,
+ bugs.bug_severity " .
# Use the flags and flagtypes tables for information about the flags,
# the bugs and attachments tables for target info, the profiles tables
# for setter and requestee info, the products/components tables
@@ -250,9 +254,9 @@ sub queue {
products.name, components.name, flags.attach_id,
attachments.description, requesters.realname,
requesters.login_name, requestees.realname,
- requestees.login_name, flags.modification_date,
+ requestees.login_name, flags.modification_date, attachments.ispatch
cclist_accessible, bugs.reporter, bugs.reporter_accessible,
- bugs.assigned_to');
+ bugs.assigned_to, attachments.ispatch');
# Group the records, in other words order them by the group column
# so the loop in the display template can break them up into separate
@@ -295,7 +299,11 @@ sub queue {
'requester' => ($data[9] ? "$data[9] <$data[10]>" : $data[10]) ,
'requestee' => ($data[11] ? "$data[11] <$data[12]>" : $data[12]) ,
'restricted' => $data[13] ? 1 : 0,
- 'created' => $data[14]
+ 'created' => $data[14],
+ 'ispatch' => $data[15],
+ 'bug_status' => $data[16],
+ 'priority' => $data[17],
+ 'bug_severity' => $data[18],
};
push(@requests, $request);
}
diff --git a/robots.txt b/robots.txt
index 0f823cb24..129fe60a7 100644
--- a/robots.txt
+++ b/robots.txt
@@ -1,3 +1,13 @@
+User-agent: Browsershots
+Disallow:
+
User-agent: *
-Allow: /index.cgi
Disallow: /
+Allow: /*index.cgi
+Allow: /*page.cgi
+Allow: /*show_bug.cgi
+Allow: /*describecomponents.cgi
+Disallow: /*show_bug.cgi*ctype=*
+Disallow: /*show_bug.cgi*format=multiple*
+Disallow: /*page.cgi*id=voting*
+Sitemap: http://bugzilla.mozilla.org/page.cgi?id=sitemap/sitemap.xml
diff --git a/showdependencygraph.cgi b/showdependencygraph.cgi
index 0726760b9..842b4c092 100755
--- a/showdependencygraph.cgi
+++ b/showdependencygraph.cgi
@@ -311,7 +311,8 @@ foreach my $f (@files)
# symlinks), this can't escape to delete anything it shouldn't
# (unless someone moves the location of $webdotdir, of course)
trick_taint($f);
- if (file_mod_time($f) < $since) {
+ my $mtime = file_mod_time($f);
+ if ($mtime && $mtime < $since) {
unlink $f;
}
}
diff --git a/skins/contrib/Dusk-Helvetica/buglist.css b/skins/contrib/Dusk-Helvetica/buglist.css
new file mode 100644
index 000000000..2e14368b1
--- /dev/null
+++ b/skins/contrib/Dusk-Helvetica/buglist.css
@@ -0,0 +1,24 @@
+/* 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 Mike Schrag.
+ * Portions created by Marc Schumann are Copyright (c) 2007 Mike Schrag.
+ * All rights reserved.
+ *
+ * Contributor(s): Mike Schrag <mschrag@pobox.com>
+ * Byron Jones <bugzilla@glob.com.au>
+ * Marc Schumann <wurblzap@gmail.com>
+ */
+
+tr.bz_bugitem:hover {
+ background-color: #ccccff;
+}
diff --git a/skins/contrib/Dusk-Helvetica/global.css b/skins/contrib/Dusk-Helvetica/global.css
new file mode 100644
index 000000000..8478c1a88
--- /dev/null
+++ b/skins/contrib/Dusk-Helvetica/global.css
@@ -0,0 +1,263 @@
+/* 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 Mike Schrag.
+ * Portions created by Marc Schumann are Copyright (c) 2007 Mike Schrag.
+ * All rights reserved.
+ *
+ * Contributor(s): Mike Schrag <mschrag@pobox.com>
+ * Byron Jones <bugzilla@glob.com.au>
+ * Marc Schumann <wurblzap@gmail.com>
+ * Frédéric Buclin <LpSolit@gmail.com>
+ */
+
+body {
+ background: #c8c8c8;
+ font-family: "Helvetica Neue", "Nimbus Sans L", Arial, sans-serif;
+ padding-left: 1em;
+ padding-right: 1em;
+}
+
+body, td, th, input {
+ font-family: "Helvetica Neue", "Nimbus Sans L", Arial, sans-serif;
+}
+
+/* page title */
+
+#titles {
+ -moz-border-radius-topleft: 5px;
+ -moz-border-radius-topright: 5px;
+ border-top-left-radius: 5px;
+ border-top-right-radius: 5px;
+}
+
+#header .links, #footer {
+ background-color: #929bb1;
+ color: #ddd;
+}
+
+#header {
+ -moz-border-radius-bottomleft: 5px;
+ -moz-border-radius-bottomright: 5px;
+ border-bottom-left-radius: 5px;
+ border-bottom-right-radius: 5px;
+ border: none;
+}
+
+#header a, #footer a {
+ color: white;
+ text-decoration: none;
+}
+#header a:hover, #footer a:hover {
+ text-decoration: underline;
+}
+
+/* body */
+
+#bugzilla-body {
+ background: #f0f0f0;
+ color: black;
+ border: 1px solid #747e93;
+ padding: 10px;
+ font-size: 10pt;
+ -moz-border-radius: 5px;
+ border-radius: 5px;
+}
+
+a {
+ color: #6070cf;
+}
+a:hover {
+ color: #8090ef;
+}
+
+hr {
+ border-color: #969696;
+ border-style: dashed;
+ border-width: 1px;
+ margin-top: 10px;
+}
+
+/* edit */
+
+#bugzilla-body th {
+ font-weight: bold;
+ vertical-align: top;
+ white-space: nowrap;
+}
+
+#bug-form td {
+ padding-top: 2px;
+}
+
+/* attachments */
+
+#attachment-list {
+ border: 2px solid #c8c8ba;
+ font-size: 9pt;
+}
+
+#attachment-list th {
+ background-color: #e6e6d8;
+ border: none;
+ border-bottom: 1px solid #c8c8ba;
+ text-align: left;
+}
+
+#attachment-list th a {
+ color: #646456;
+}
+
+#attachment-list td {
+ border: none;
+}
+
+#attachment-list-actions td {
+ border-top: 1px solid #c8c8ba;
+}
+
+/************/
+/* Comments */
+/************/
+
+#comments th {
+ font-size: 9pt;
+ font-weight: bold;
+ padding-top: 5px;
+ padding-right: 5px;
+ padding-bottom: 10px;
+ text-align: right;
+ vertical-align: top;
+ white-space: nowrap;
+}
+
+#comments td {
+ padding-top: 2px;
+}
+
+.reply-button a {
+ padding-left: 2px;
+ padding-right: 2px;
+}
+
+.bz_comment {
+ background-color: #e8e8e8;
+ margin: 1px 1px 10px 1px;
+ border-width: 1px;
+ border-style: solid;
+ border-color: #c8c8ba;
+ padding: 5px;
+ font-size: 9pt;
+}
+
+.bz_comment_head, .bz_first_comment_head {
+ margin: 0; padding: 0;
+ background-color: transparent;
+ font-weight: bold;
+}
+
+.bz_comment_user {
+ margin-left: 0;
+}
+
+.bz_comment.bz_private {
+ background-color: #f0e8e8;
+ border-color: #f8c8ba;
+}
+
+.comment_rule {
+ display: none;
+}
+
+/* footer */
+
+#footer {
+ border: 1px solid #747e93;
+ width: 100%;
+ -moz-border-radius: 5px;
+ border-radius: 5px;
+}
+
+#footer #links-actions,
+#footer #links-edit,
+#footer #links-saved,
+#footer #links-special {
+ margin-top: 2ex;
+}
+
+#footer .links {
+ border-spacing: 30px;
+ margin-bottom: 2ex;
+}
+
+.separator {
+ color: #cccccc;
+}
+
+/* tabs */
+
+.tabbed .tabbody {
+ background: #f8f8f8;
+ padding: 1em;
+ border-style: solid;
+ border-color: #000000;
+ border-width: 0 3px 3px 1px;
+}
+
+.tabs {
+ margin: 0;
+ padding: 0;
+ border-collapse: collapse;
+}
+
+.tabs td {
+ background: #c8c8c8;
+ border-width: 1px;
+}
+
+.tabs td.selected {
+ background: #f8f8f8;
+ border-width: 1px 3px 0 1px;
+}
+
+.tabs td.spacer {
+ background: transparent;
+ border-top: none;
+ border-left: none;
+ border-right: none;
+}
+
+/* other */
+
+.bz_row_odd {
+ background-color: #f0f0f0;
+}
+
+/* Rules specific for printing */
+@media print {
+ #header,
+ #footer,
+ .navigation {
+ display: none;
+ }
+
+ body {
+ background-image: none;
+ background-color: #ffffff;
+ }
+
+ #bugzilla-body {
+ border: none;
+ margin: 0;
+ padding: 0;
+ }
+}
diff --git a/skins/contrib/Dusk-Helvetica/index.css b/skins/contrib/Dusk-Helvetica/index.css
new file mode 100644
index 000000000..c9c2d1705
--- /dev/null
+++ b/skins/contrib/Dusk-Helvetica/index.css
@@ -0,0 +1,9 @@
+/*
+ * Custom rules for index.css.
+ * The rules you put here override rules in that stylesheet.
+ */
+
+ div#page-index .outro
+ {
+ clear:both;
+ }
diff --git a/skins/contrib/Dusk-Segoe/buglist.css b/skins/contrib/Dusk-Segoe/buglist.css
new file mode 100644
index 000000000..2e14368b1
--- /dev/null
+++ b/skins/contrib/Dusk-Segoe/buglist.css
@@ -0,0 +1,24 @@
+/* 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 Mike Schrag.
+ * Portions created by Marc Schumann are Copyright (c) 2007 Mike Schrag.
+ * All rights reserved.
+ *
+ * Contributor(s): Mike Schrag <mschrag@pobox.com>
+ * Byron Jones <bugzilla@glob.com.au>
+ * Marc Schumann <wurblzap@gmail.com>
+ */
+
+tr.bz_bugitem:hover {
+ background-color: #ccccff;
+}
diff --git a/skins/contrib/Dusk-Segoe/global.css b/skins/contrib/Dusk-Segoe/global.css
new file mode 100644
index 000000000..f431aceba
--- /dev/null
+++ b/skins/contrib/Dusk-Segoe/global.css
@@ -0,0 +1,263 @@
+/* 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 Mike Schrag.
+ * Portions created by Marc Schumann are Copyright (c) 2007 Mike Schrag.
+ * All rights reserved.
+ *
+ * Contributor(s): Mike Schrag <mschrag@pobox.com>
+ * Byron Jones <bugzilla@glob.com.au>
+ * Marc Schumann <wurblzap@gmail.com>
+ * Frédéric Buclin <LpSolit@gmail.com>
+ */
+
+body {
+ background: #c8c8c8;
+ font-family: Segoe, "Segoe UI", "Helvetica Neue", Verdana, sans-serif;
+ padding-left: 1em;
+ padding-right: 1em;
+}
+
+body, td, th, input {
+ font-family: Segoe, "Segoe UI", "Helvetica Neue", Verdana, sans-serif;
+}
+
+/* page title */
+
+#titles {
+ -moz-border-radius-topleft: 5px;
+ -moz-border-radius-topright: 5px;
+ border-top-left-radius: 5px;
+ border-top-right-radius: 5px;
+}
+
+#header .links, #footer {
+ background-color: #929bb1;
+ color: #ddd;
+}
+
+#header {
+ -moz-border-radius-bottomleft: 5px;
+ -moz-border-radius-bottomright: 5px;
+ border-bottom-left-radius: 5px;
+ border-bottom-right-radius: 5px;
+ border: none;
+}
+
+#header a, #footer a {
+ color: white;
+ text-decoration: none;
+}
+#header a:hover, #footer a:hover {
+ text-decoration: underline;
+}
+
+/* body */
+
+#bugzilla-body {
+ background: #f0f0f0;
+ color: black;
+ border: 1px solid #747e93;
+ padding: 10px;
+ font-size: 10pt;
+ -moz-border-radius: 5px;
+ border-radius: 5px;
+}
+
+a {
+ color: #6070cf;
+}
+a:hover {
+ color: #8090ef;
+}
+
+hr {
+ border-color: #969696;
+ border-style: dashed;
+ border-width: 1px;
+ margin-top: 10px;
+}
+
+/* edit */
+
+#bugzilla-body th {
+ font-weight: bold;
+ vertical-align: top;
+ white-space: nowrap;
+}
+
+#bug-form td {
+ padding-top: 2px;
+}
+
+/* attachments */
+
+#attachment-list {
+ border: 2px solid #c8c8ba;
+ font-size: 9pt;
+}
+
+#attachment-list th {
+ background-color: #e6e6d8;
+ border: none;
+ border-bottom: 1px solid #c8c8ba;
+ text-align: left;
+}
+
+#attachment-list th a {
+ color: #646456;
+}
+
+#attachment-list td {
+ border: none;
+}
+
+#attachment-list-actions td {
+ border-top: 1px solid #c8c8ba;
+}
+
+/************/
+/* Comments */
+/************/
+
+#comments th {
+ font-size: 9pt;
+ font-weight: bold;
+ padding-top: 5px;
+ padding-right: 5px;
+ padding-bottom: 10px;
+ text-align: right;
+ vertical-align: top;
+ white-space: nowrap;
+}
+
+#comments td {
+ padding-top: 2px;
+}
+
+.reply-button a {
+ padding-left: 2px;
+ padding-right: 2px;
+}
+
+.bz_comment {
+ background-color: #e8e8e8;
+ margin: 1px 1px 10px 1px;
+ border-width: 1px;
+ border-style: solid;
+ border-color: #c8c8ba;
+ padding: 5px;
+ font-size: 9pt;
+}
+
+.bz_comment_head, .bz_first_comment_head {
+ margin: 0; padding: 0;
+ background-color: transparent;
+ font-weight: bold;
+}
+
+.bz_comment_user {
+ margin-left: 0;
+}
+
+.bz_comment.bz_private {
+ background-color: #f0e8e8;
+ border-color: #f8c8ba;
+}
+
+.comment_rule {
+ display: none;
+}
+
+/* footer */
+
+#footer {
+ border: 1px solid #747e93;
+ width: 100%;
+ -moz-border-radius: 5px;
+ border-radius: 5px;
+}
+
+#footer #links-actions,
+#footer #links-edit,
+#footer #links-saved,
+#footer #links-special {
+ margin-top: 2ex;
+}
+
+#footer .links {
+ border-spacing: 30px;
+ margin-bottom: 2ex;
+}
+
+.separator {
+ color: #cccccc;
+}
+
+/* tabs */
+
+.tabbed .tabbody {
+ background: #f8f8f8;
+ padding: 1em;
+ border-style: solid;
+ border-color: #000000;
+ border-width: 0 3px 3px 1px;
+}
+
+.tabs {
+ margin: 0;
+ padding: 0;
+ border-collapse: collapse;
+}
+
+.tabs td {
+ background: #c8c8c8;
+ border-width: 1px;
+}
+
+.tabs td.selected {
+ background: #f8f8f8;
+ border-width: 1px 3px 0 1px;
+}
+
+.tabs td.spacer {
+ background: transparent;
+ border-top: none;
+ border-left: none;
+ border-right: none;
+}
+
+/* other */
+
+.bz_row_odd {
+ background-color: #f0f0f0;
+}
+
+/* Rules specific for printing */
+@media print {
+ #header,
+ #footer,
+ .navigation {
+ display: none;
+ }
+
+ body {
+ background-image: none;
+ background-color: #ffffff;
+ }
+
+ #bugzilla-body {
+ border: none;
+ margin: 0;
+ padding: 0;
+ }
+}
diff --git a/skins/contrib/Dusk-Segoe/index.css b/skins/contrib/Dusk-Segoe/index.css
new file mode 100644
index 000000000..c9c2d1705
--- /dev/null
+++ b/skins/contrib/Dusk-Segoe/index.css
@@ -0,0 +1,9 @@
+/*
+ * Custom rules for index.css.
+ * The rules you put here override rules in that stylesheet.
+ */
+
+ div#page-index .outro
+ {
+ clear:both;
+ }
diff --git a/skins/contrib/Dusk-Segoe/show_bug.css b/skins/contrib/Dusk-Segoe/show_bug.css
new file mode 100644
index 000000000..92e52d02e
--- /dev/null
+++ b/skins/contrib/Dusk-Segoe/show_bug.css
@@ -0,0 +1,3 @@
+.bz_comment {
+ font-size: small;
+}
diff --git a/skins/contrib/Dusk/global.css b/skins/contrib/Dusk/global.css
index 63375672b..33f28965c 100644
--- a/skins/contrib/Dusk/global.css
+++ b/skins/contrib/Dusk/global.css
@@ -22,11 +22,15 @@
body {
background: #c8c8c8;
- font-family: Helvetica, Arial, Geneva;
+ font-family: Verdana, sans-serif;
padding-left: 1em;
padding-right: 1em;
}
+body, td, th, input {
+ font-family: Verdana, sans-serif;
+}
+
/* page title */
#titles {
diff --git a/skins/custom/IE-fixes.css b/skins/custom/IE-fixes.css
new file mode 100644
index 000000000..0d5c47630
--- /dev/null
+++ b/skins/custom/IE-fixes.css
@@ -0,0 +1,4 @@
+.bz_short_desc_column a, .bz_short_short_desc_column a {
+ /* color:inherit */
+ color: expression(this.parentNode.currentStyle['color']);
+}
diff --git a/skins/custom/bug_groups.css b/skins/custom/bug_groups.css
new file mode 100644
index 000000000..96f3b4f3d
--- /dev/null
+++ b/skins/custom/bug_groups.css
@@ -0,0 +1,25 @@
+/* colorize bugs in various groups */
+body[class*=bz_group_] {
+ background-color: #e0e0ff;
+ border-left: solid red 2px;
+ padding-left: 13px;
+}
+
+body[class*=bz_group_] #bugzilla-body {
+ background-color: inherit;
+}
+
+body.bz_group_webtools-security,
+body.bz_group_websites-security,
+body.bz_group_bugzilla-security {
+ background-color: #ffeeee;
+}
+
+body.bz_group_client-services-security,
+body.bz_group_mozilla-services-security {
+ background-color: #ffff80;
+}
+
+body.bz_group_core-security {
+ background-color: #ffe0b0;
+}
diff --git a/skins/custom/buglist.css b/skins/custom/buglist.css
new file mode 100644
index 000000000..397bd95a4
--- /dev/null
+++ b/skins/custom/buglist.css
@@ -0,0 +1,36 @@
+/* For the JS-sorting buglist. */
+
+th.sorttable_sorted,
+th.sorttable_sorted_reverse,
+th.sorted_0 {
+ background-color: #aaa;
+}
+
+th.sorted_1 {
+ background-color: #bbb;
+}
+
+th.sorted_2 {
+ background-color: #ccc;
+}
+
+th.sorted_3 {
+ background-color: #ddd;
+}
+
+th.sorted_4 {
+ background-color: #eee;
+}
+
+th.sorted_5 {
+ background-color: #fff;
+}
+
+.bz_short_desc_column a, .bz_short_short_desc_column a {
+ text-decoration: none;
+ color: inherit;
+}
+
+.bz_short_desc_column a:hover, .bz_short_short_desc_column a:hover {
+ text-decoration: underline;
+}
diff --git a/skins/custom/create_bug.css b/skins/custom/create_bug.css
new file mode 100644
index 000000000..30f8c5d03
--- /dev/null
+++ b/skins/custom/create_bug.css
@@ -0,0 +1,19 @@
+
+#bug_project_flags .field_label,
+#bug_tracking_flags .field_label {
+ font-weight: normal !important;
+}
+
+#guided {
+ margin-top: 30px;
+}
+
+#component {
+ width: 25em;
+}
+
+.hidden_text {
+ opacity: 0;
+ filter: alpha(opacity=0);
+}
+
diff --git a/skins/custom/global.css b/skins/custom/global.css
new file mode 100644
index 000000000..41494cc2e
--- /dev/null
+++ b/skins/custom/global.css
@@ -0,0 +1,76 @@
+/*
+ * Custom rules for skins/standard/global.css.
+ * The rules you put here override rules in that stylesheet.
+ */
+
+body {
+ margin: 0;
+ padding: 15px 15px 2px 15px;
+ background: none;
+}
+
+#header {
+ margin-bottom: 0.5em;
+}
+
+#header .links {
+ font-size: 90%;
+}
+
+#header .btn, #header .txt {
+ font-size: 100%;
+}
+
+#header #information {
+ color: #dddddd;
+ font-size: small;
+}
+
+pre {
+ font-size: medium;
+}
+
+#attachment_table {
+ width: 50em;
+}
+
+#page-index #quicksearchForm {
+ padding-top: 20px;
+}
+
+/* createaccount styling */
+.support_div {
+ width: 40%;
+ font-size: 80%;
+}
+
+.support_div > img {
+ padding: 5px 20px;
+}
+
+a {
+ text-decoration: none;
+}
+
+a:hover {
+ text-decoration: underline;
+}
+
+a.controller {
+ font-size: 100%;
+ border: 1px solid #c0c0c0;
+ padding: 3px;
+}
+
+#footer .outro {
+ text-align:left;
+ padding-left:1ex;
+ padding-bottom:1ex;
+}
+
+.group_secure > th > a {
+ background-image: url("../../images/padlock.png");
+ background-position: center left;
+ background-repeat: no-repeat;
+ padding-left: 18px;
+}
diff --git a/skins/custom/index.css b/skins/custom/index.css
new file mode 100644
index 000000000..0c6884124
--- /dev/null
+++ b/skins/custom/index.css
@@ -0,0 +1,31 @@
+/*
+ * Custom rules for index.css.
+ * The rules you put here override rules in that stylesheet.
+ */
+
+/* index.html.tmpl puts intro hook contents inside a div which causes
+ * the icons to display over two rows when adding the Help icon.
+ * So we change to inline to make it display a single row. */
+#page-index .intro { display: inline; }
+
+#get_help { background: url(../standard/index/help.png) no-repeat; }
+
+.bz_common_actions {
+ display: block;
+ height: 170px;
+ width: 145px;
+ float: left;
+ margin: 0 2ex 2em 0;
+ text-align: center;
+}
+.bz_common_actions span {
+ position: relative;
+ top: 95%;
+ font-weight: bold;
+}
+.bz_common_actions,
+.bz_common_actions:visited,
+.bz_common_actions:hover
+{
+ text-decoration: none;
+}
diff --git a/skins/custom/search_form.css b/skins/custom/search_form.css
new file mode 100644
index 000000000..1855eb445
--- /dev/null
+++ b/skins/custom/search_form.css
@@ -0,0 +1,6 @@
+
+/* let the browser choose the select height from the "size" param */
+.search_field_grid select {
+ height: auto;
+}
+
diff --git a/skins/custom/show_bug.css b/skins/custom/show_bug.css
new file mode 100644
index 000000000..8a88909f7
--- /dev/null
+++ b/skins/custom/show_bug.css
@@ -0,0 +1,82 @@
+/*
+ * Custom rules for show_bug.css.
+ * The rules you put here override rules in that stylesheet.
+ */
+
+.last_comment_link {
+ float: right;
+ font-size: 80%;
+ font-weight: normal;
+ margin-left: 1em;
+}
+
+#legal_disclaimer {
+ width: 40em;
+ padding: 1em;
+ margin: 0 1em 1em 1em;
+ font-weight: bold;
+ border: 1px red solid;
+ background-color: lightyellow;
+}
+
+.bz_patch {
+ background: #ffffcc;
+}
+
+.cc_list_display {
+ list-style: none;
+ margin:0px;
+ padding:5px;
+ padding-right:20px;
+ overflow:auto;
+ float:left;
+ max-width:465px;
+ max-height:100px;
+ border:1px solid #CCC;
+}
+
+.cc_list_display li {
+ margin:0px;
+ padding:0px;
+ white-space:nowrap;
+}
+
+#wave_wand {
+ margin-top: 0px;
+}
+
+/* put the width on the TD rather than the PRE to stop the col resizing
+ when comments are hidden */
+.bz_comment {
+ width: 55em;
+}
+.bz_comment_text {
+ width: auto;
+}
+
+.bz_comment_number {
+ float: right;
+}
+
+/* style all field labels the same */
+
+.field_label, .field_label a {
+ color: #000;
+ font-weight: bold;
+}
+
+.field_label a {
+ cursor: help;
+}
+
+.edit_form table td:first-child {
+ width: 0px;
+}
+
+/* fix flag table's vertical alignment */
+
+table#flags {
+ border-collapse: collapse;
+ border-spacing: 0px;
+}
+
diff --git a/skins/standard/buglist.css b/skins/standard/buglist.css
index ebebfb3ef..a86009def 100644
--- a/skins/standard/buglist.css
+++ b/skins/standard/buglist.css
@@ -119,7 +119,7 @@ td.bz_total {
margin-top: .25em;
}
-.bz_query_explain {
+.bz_query_debug {
text-align: left;
}
diff --git a/skins/standard/enter_bug.css b/skins/standard/enter_bug.css
index 88d9e9e85..34be42f7a 100644
--- a/skins/standard/enter_bug.css
+++ b/skins/standard/enter_bug.css
@@ -69,4 +69,4 @@
/* Make the Add Me to CC button never wrap. */
#possible_duplicates .yui-dt-col-update_token { white-space: nowrap; }
-form#Create #possible_duplicates td { vertical-align: middle; } \ No newline at end of file
+form#Create #possible_duplicates td { vertical-align: middle; }
diff --git a/skins/standard/global.css b/skins/standard/global.css
index 4d4b02153..f50ccd02d 100644
--- a/skins/standard/global.css
+++ b/skins/standard/global.css
@@ -365,6 +365,10 @@ div#docslinks {
white-space: pre;
}
+.bz_comment_text span.quote_wrapped {
+ color: #65379c;
+}
+
table#flags th,
table#flags td {
vertical-align: middle;
@@ -380,7 +384,7 @@ input.requestee {
}
#error_msg {
- font-size: x-large;
+ font-size: large;
}
.warning {
@@ -388,9 +392,9 @@ input.requestee {
}
.throw_error {
- background-color: #ff0000;
+ background-color: #ff6666;
color: black;
- font-size: 120%;
+ font-size: large;
margin: 1em;
padding: 0.5em 1em;
}
diff --git a/skins/standard/guided.css b/skins/standard/guided.css
new file mode 100644
index 000000000..efecfe3ce
--- /dev/null
+++ b/skins/standard/guided.css
@@ -0,0 +1,4 @@
+#somebugs {
+ width: 100%;
+ height: 500px;
+}
diff --git a/skins/standard/reports.css b/skins/standard/reports.css
index 00272fdba..205946550 100644
--- a/skins/standard/reports.css
+++ b/skins/standard/reports.css
@@ -90,3 +90,8 @@
color: #333;
}
+.component_hilite {
+ background-color: lightgreen;
+ margin: 0;
+ padding: 1em 0;
+}
diff --git a/t/001compile.t b/t/001compile.t
index 97a339b2d..a2176babd 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/004template.t b/t/004template.t
index 3b858c0b3..ce18619e7 100644
--- a/t/004template.t
+++ b/t/004template.t
@@ -60,11 +60,16 @@ my $fh;
# fall back to English if necessary.
foreach my $file (@referenced_files) {
- my $path = File::Spec->catfile($english_default_include_path, $file);
- if (-e $path) {
- ok(1, "$path exists");
+ my @path = map(File::Spec->catfile($_, $file), @include_paths);
+ push(@path, File::Spec->catfile($english_default_include_path, $file));
+ my $found;
+ foreach my $path (@path) {
+ $found = $path if -e $path;
+ }
+ if ($found) {
+ ok(1, "$file exists");
} else {
- ok(0, "$path cannot be located --ERROR");
+ ok(0, "$file cannot be located --ERROR");
}
}
diff --git a/t/008filter.t b/t/008filter.t
index e73d23835..d0c0311f6 100644
--- a/t/008filter.t
+++ b/t/008filter.t
@@ -175,7 +175,8 @@ sub directive_ok {
return 1 if $directive =~ /^(IF|END|UNLESS|FOREACH|PROCESS|INCLUDE|
BLOCK|USE|ELSE|NEXT|LAST|DEFAULT|FLUSH|
ELSIF|SET|SWITCH|CASE|WHILE|RETURN|STOP|
- TRY|CATCH|FINAL|THROW|CLEAR|MACRO|FILTER)/x;
+ TRY|CATCH|FINAL|THROW|CLEAR|MACRO|FILTER|
+ CALL)/x;
# ? :
if ($directive =~ /.+\?(.+):(.+)/) {
@@ -224,7 +225,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 {
diff --git a/template/en/default/account/auth/login-small.html.tmpl b/template/en/default/account/auth/login-small.html.tmpl
index cb4335466..6b41c17e3 100644
--- a/template/en/default/account/auth/login-small.html.tmpl
+++ b/template/en/default/account/auth/login-small.html.tmpl
@@ -47,6 +47,7 @@
id="mini_login[% qs_suffix FILTER html %]"
onsubmit="return check_mini_login_fields( '[% qs_suffix FILTER html %]' );"
>
+
<input id="Bugzilla_login[% qs_suffix FILTER html %]"
class="bz_login"
name="Bugzilla_login"
@@ -76,8 +77,8 @@
id="log_in[% qs_suffix %]">
<script type="text/javascript">
mini_login_constants = {
- "login" : "login",
- "warning" : "You must set the login and password before logging in."
+ "login" : "email address",
+ "warning" : "You must set the email address and password before logging in."
};
[%# We need this event to fire after autocomplete, because it does
# something different depending on whether or not there's already
@@ -109,7 +110,8 @@
});
}
</script>
- <a href="#" onclick="return hide_mini_login_form('[% qs_suffix %]')">[x]</a>
+ <a href="#" id="hide_mini_login[% qs_suffix FILTER html %]"
+ onclick="return hide_mini_login_form('[% qs_suffix %]')">[x]</a>
</form>
</li>
<li id="forgot_container[% qs_suffix %]">
diff --git a/template/en/default/account/auth/login.html.tmpl b/template/en/default/account/auth/login.html.tmpl
index 3de52b6a0..0aac403a5 100644
--- a/template/en/default/account/auth/login.html.tmpl
+++ b/template/en/default/account/auth/login.html.tmpl
@@ -37,14 +37,14 @@
[% USE Bugzilla %]
<p>
- I need a legitimate login and password to continue.
+ I need an email address and password to continue.
</p>
<form name="login" action="[% target FILTER html %]" method="POST"
[%- IF Bugzilla.cgi.param("data") %] enctype="multipart/form-data"[% END %]>
<table>
<tr>
- <th align="right"><label for="Bugzilla_login">Login:</label></th>
+ <th align="right"><label for="Bugzilla_login">Email Address:</label></th>
<td>
<input size="35" id="Bugzilla_login" name="Bugzilla_login">
[% Param('emailsuffix') FILTER html %]
@@ -64,7 +64,7 @@
<td>
<input type="checkbox" id="Bugzilla_remember" name="Bugzilla_remember" value="on"
[%+ "checked" IF Param('rememberlogin') == "defaulton" %]>
- <label for="Bugzilla_remember">Remember my Login</label>
+ <label for="Bugzilla_remember">Remember my email address</label>
</td>
</tr>
[% END %]
@@ -112,7 +112,7 @@
<form id="forgot" method="get" action="token.cgi">
<input type="hidden" name="a" value="reqpw">
If you have an account, but have forgotten your password,
- enter your login name below and submit a request
+ enter your email address below and submit a request
to change your password.<br>
<input size="35" name="loginname">
<input type="hidden" id="token" name="token" value="[% issue_hash_token(['reqpw']) FILTER html %]">
diff --git a/template/en/default/account/create.html.tmpl b/template/en/default/account/create.html.tmpl
index 5acd9f541..985a54841 100644
--- a/template/en/default/account/create.html.tmpl
+++ b/template/en/default/account/create.html.tmpl
@@ -77,4 +77,6 @@
<input type="submit" id="send" value="Send">
</form>
+[% Hook.process('additional_methods') %]
+
[% PROCESS global/footer.html.tmpl %]
diff --git a/template/en/default/account/prefs/email.html.tmpl b/template/en/default/account/prefs/email.html.tmpl
index 96a111bae..32d52fd8e 100644
--- a/template/en/default/account/prefs/email.html.tmpl
+++ b/template/en/default/account/prefs/email.html.tmpl
@@ -48,7 +48,7 @@ function SetCheckboxes(setting) {
var theinput = document.userprefsform.elements[count];
if (theinput.type == "checkbox" && !theinput.disabled) {
if (theinput.name.match("neg")) {
- theinput.checked = false;
+ theinput.checked = !setting;
}
else {
theinput.checked = setting;
@@ -119,6 +119,8 @@ document.write('<input type="button" value="Disable All Mail" onclick="SetCheckb
description = "A new $terms.bug is created" },
{ id = constants.EVT_OPENED_CLOSED,
description = "The $terms.bug is resolved or reopened" },
+ { id = constants.EVT_COMPONENT,
+ description = "The product or component changes" },
{ id = constants.EVT_PROJ_MANAGEMENT,
description = "The priority, status, severity, or milestone changes" },
{ id = constants.EVT_COMMENT,
diff --git a/template/en/default/account/prefs/permissions.html.tmpl b/template/en/default/account/prefs/permissions.html.tmpl
index 5e8dc9ca2..d3c787b07 100644
--- a/template/en/default/account/prefs/permissions.html.tmpl
+++ b/template/en/default/account/prefs/permissions.html.tmpl
@@ -65,9 +65,9 @@
There are no permission bits set on your account.
[% END %]
- [% IF user.in_group('editusers') %]
+ [% IF user.in_group('admin') %]
<br>
- You have editusers privileges. You can turn on and off
+ You have admin privileges. You can turn on and off
all permissions for all users.
[% ELSIF set_bits.size %]
<br>
diff --git a/template/en/default/account/prefs/saved-searches.html.tmpl b/template/en/default/account/prefs/saved-searches.html.tmpl
index 1b78592ca..ce9623372 100644
--- a/template/en/default/account/prefs/saved-searches.html.tmpl
+++ b/template/en/default/account/prefs/saved-searches.html.tmpl
@@ -67,6 +67,7 @@
Share With a Group
</th>
[% END %]
+ [% Hook.process('saved-header') %]
</tr>
<tr>
<td>My [% terms.Bugs %]</td>
@@ -145,6 +146,7 @@
[% END %]
</td>
[% END %]
+ [% Hook.process('saved-row') %]
</tr>
[% END %]
</table>
diff --git a/template/en/default/account/profile-activity.html.tmpl b/template/en/default/account/profile-activity.html.tmpl
index ee00875fe..aa6a63e85 100644
--- a/template/en/default/account/profile-activity.html.tmpl
+++ b/template/en/default/account/profile-activity.html.tmpl
@@ -35,7 +35,7 @@
#%]
[% title = BLOCK %]
- Account History for '[% otheruser.login FILTER html %]'
+ [% IF action == 'admin_activity' %]Admin[% ELSE %]Account[% END %] History for '[% otheruser.login FILTER html %]'
[% END %]
diff --git a/template/en/default/admin/params/advanced.html.tmpl b/template/en/default/admin/params/advanced.html.tmpl
index a8e8a297b..1cf0c344f 100644
--- a/template/en/default/admin/params/advanced.html.tmpl
+++ b/template/en/default/admin/params/advanced.html.tmpl
@@ -78,4 +78,12 @@
_ " use the <code>http://user:pass@proxy_url/</code> syntax.",
strict_transport_security => sts_desc,
+
+ disable_bug_updates =>
+ "When enabled, all updates to $terms.bugs will be blocked.",
+
+ arecibo_server =>
+ "When set, important errors and warnings will be sent to the"
+ _ " specified Arecibo server. Enter the Arecibo server's full URL;"
+ _ " eg <code>https://arecibo.example.com/v/1/</code>.",
} %]
diff --git a/template/en/default/admin/params/auth.html.tmpl b/template/en/default/admin/params/auth.html.tmpl
index 2e11dffbc..7a8d34791 100644
--- a/template/en/default/admin/params/auth.html.tmpl
+++ b/template/en/default/admin/params/auth.html.tmpl
@@ -107,6 +107,12 @@
"front page will require a login. No anonymous users will " _
"be permitted.",
+ webservice_email_filter => "Filter email addresses returned by the WebService API depending on " _
+ "if the user is logged in or not. This works similarly to how the " _
+ "web UI currently filters email addresses. If <tt>requirelogin</tt> " _
+ "is enabled, then this parameter has no effect as users must be logged " _
+ "in to use Bugzilla.",
+
emailregexp => "This defines the regexp to use for legal email addresses. The " _
"default tries to match fully qualified email addresses. Another " _
"popular value to put here is <tt>^[^@]+$</tt>, which means " _
diff --git a/template/en/default/admin/users/edit.html.tmpl b/template/en/default/admin/users/edit.html.tmpl
index 3efa4b8bf..010cacb73 100644
--- a/template/en/default/admin/users/edit.html.tmpl
+++ b/template/en/default/admin/users/edit.html.tmpl
@@ -116,9 +116,15 @@
<input type="hidden" name="token" value="[% token FILTER html %]">
[% INCLUDE listselectionhiddenfields %]
- or <a href="editusers.cgi?action=activity&amp;userid=[% otheruser.id %]"
- title="View Account History for '
- [%- otheruser.login FILTER html %]'">View Account History</a>
+ [% IF editusers %], [% ELSE %] or [% END %]
+ <a href="editusers.cgi?action=activity&amp;userid=[% otheruser.id %]"
+ title="View Account History for '
+ [%- otheruser.login FILTER html %]'">View Account History</a>
+ [% IF editusers %]
+ or <a href="editusers.cgi?action=admin_activity&amp;userid=[% otheruser.id %]"
+ title="View Account History for '
+ [%- otheruser.login FILTER html %]'">View Admin History</a>
+ [% END %]
</p>
</form>
<p>
diff --git a/template/en/default/admin/users/list.html.tmpl b/template/en/default/admin/users/list.html.tmpl
index 3f745a458..4d1d35c95 100644
--- a/template/en/default/admin/users/list.html.tmpl
+++ b/template/en/default/admin/users/list.html.tmpl
@@ -51,6 +51,17 @@
]
%]
+[% IF editusers %]
+ [% columns.push({
+ heading => 'Admin History'
+ content => 'View'
+ contentlink => 'editusers.cgi?action=admin_activity' _
+ '&amp;userid=%%userid%%' _
+ listselectionurlparams
+ })
+ %]
+[% END %]
+
[% IF Param('allowuserdeletion') && editusers %]
[% columns.push({heading => 'Action'
content => 'Delete'
diff --git a/template/en/default/attachment/createformcontents.html.tmpl b/template/en/default/attachment/createformcontents.html.tmpl
index 5b04382b6..7f738c07f 100644
--- a/template/en/default/attachment/createformcontents.html.tmpl
+++ b/template/en/default/attachment/createformcontents.html.tmpl
@@ -54,6 +54,7 @@
<th>Content Type:</th>
<td>
<em>If the attachment is a patch, check the box below.</em><br>
+ [% Hook.process("patch_notes") %]
<input type="checkbox" id="ispatch" name="ispatch" value="1"
onchange="setContentTypeDisabledState(this.form);">
<label for="ispatch">patch</label><br><br>
@@ -99,6 +100,7 @@
{type => "image/gif", desc => "GIF image"},
{type => "image/jpeg", desc => "JPEG image"},
{type => "image/png", desc => "PNG image"},
+ {type => "application/pdf", desc => "PDF document"},
{type => "application/octet-stream", desc => "binary file"}]
%]
[% Hook.process("mimetypes", "attachment/createformcontents.html.tmpl") %]
diff --git a/template/en/default/attachment/delete_reason.txt.tmpl b/template/en/default/attachment/delete_reason.txt.tmpl
index e4a1fc41f..87175c1a3 100644
--- a/template/en/default/attachment/delete_reason.txt.tmpl
+++ b/template/en/default/attachment/delete_reason.txt.tmpl
@@ -16,17 +16,10 @@
[%# INTERFACE:
# attachment: object of the attachment the user wants to delete.
# reason: string; The reason provided by the user.
- # date: the date when the request to delete the attachment was made.
#%]
-The content of attachment [% attachment.id %] has been deleted by
- [%+ user.identity %]
-[% IF reason %]
-who provided the following reason:
+The content of attachment [% attachment.id %] has been deleted
+[%~ IF reason %] for the following reason:
[%+ reason %]
-[% ELSE %]
-without providing any reason.
[% END %]
-
-The token used to delete this attachment was generated at [% date FILTER time %].
diff --git a/template/en/default/attachment/diff-footer.html.tmpl b/template/en/default/attachment/diff-footer.html.tmpl
index 49c662a98..e9965a9a8 100644
--- a/template/en/default/attachment/diff-footer.html.tmpl
+++ b/template/en/default/attachment/diff-footer.html.tmpl
@@ -20,6 +20,12 @@
</form>
+[% IF !file_count %]
+<div id="error_msg" class="throw_error">
+ No valid patch files were found in the attachment.
+</div>
+[% END %]
+
[% IF headers %]
<br>
diff --git a/template/en/default/attachment/edit.html.tmpl b/template/en/default/attachment/edit.html.tmpl
index 95ad4d335..530b2d04c 100644
--- a/template/en/default/attachment/edit.html.tmpl
+++ b/template/en/default/attachment/edit.html.tmpl
@@ -306,10 +306,17 @@
<div id="attachment_list">
Attachments on [% "$terms.bug ${attachment.bug_id}" FILTER bug_link(attachment.bug_id) FILTER none %]:
[% FOREACH a = attachments %]
- [% IF a == attachment.id %]
- [%+ a %]
+ [% IF a.isobsolete %]
+ <span class="bz_obsolete">
+ [% END %]
+ [% IF a.id == attachment.id %]
+ [%+ a.id FILTER html %]
[% ELSE %]
- <a href="attachment.cgi?id=[% a %]&amp;action=edit">[% a %]</a>
+ <a href="attachment.cgi?id=[% a.id FILTER uri %]&amp;action=edit"
+ title="[% a.description FILTER html %]">[% a.id FILTER html %]</a>
+ [% END %]
+ [% IF a.isobsolete %]
+ </span>
[% END %]
[% " |" UNLESS loop.last() %]
[% END %]
diff --git a/template/en/default/attachment/list.html.tmpl b/template/en/default/attachment/list.html.tmpl
index fa8e4774e..5079a0eec 100644
--- a/template/en/default/attachment/list.html.tmpl
+++ b/template/en/default/attachment/list.html.tmpl
@@ -64,6 +64,7 @@ function toggle_display(link) {
[% count = 0 %]
[% obsolete_attachments = 0 %]
+ [% user_cache = template_cache.users %]
[% FOREACH attachment = attachments %]
[% count = count + 1 %]
@@ -102,7 +103,14 @@ function toggle_display(link) {
title="Go to the comment associated with the attachment">
[%- attachment.attached FILTER time %]</a>,
- [% INCLUDE global/user.html.tmpl who = attachment.attacher %]
+ [%# No need to recreate the exact same template if we already have it. %]
+ [% attacher_id = attachment.attacher.id %]
+ [% UNLESS user_cache.$attacher_id %]
+ [% user_cache.$attacher_id = BLOCK %]
+ [% INCLUDE global/user.html.tmpl who = attachment.attacher %]
+ [% END %]
+ [% END %]
+ [% user_cache.$attacher_id FILTER none %]
</span>
</td>
diff --git a/template/en/default/bug/comments.html.tmpl b/template/en/default/bug/comments.html.tmpl
index 170c69349..23f024ae1 100644
--- a/template/en/default/bug/comments.html.tmpl
+++ b/template/en/default/bug/comments.html.tmpl
@@ -25,8 +25,65 @@
<script src="[% 'js/comments.js' FILTER mtime %]" type="text/javascript">
</script>
+<script type="text/javascript">
+<!--
+ /* Adds the reply text to the 'comment' textarea */
+ function replyToComment(id, real_id, name) {
+ var prefix = "(In reply to " + name + " from comment #" + id + ")\n";
+ var replytext = "";
+ [% IF user.settings.quote_replies.value == 'quoted_reply' %]
+ /* pre id="comment_name_N" */
+ var text_elem = document.getElementById('comment_text_'+id);
+ var text = getText(text_elem);
+ replytext = prefix + wrapReplyText(text);
+ [% ELSIF user.settings.quote_replies.value == 'simple_reply' %]
+ replytext = prefix;
+ [% END %]
+
+ [% IF user.is_insider %]
+ if (document.getElementById('isprivate_' + real_id).checked) {
+ document.getElementById('newcommentprivacy').checked = 'checked';
+ updateCommentTagControl(document.getElementById('newcommentprivacy'), 'comment');
+ }
+ [% END %]
+
+ /* Remove embedded links to attachment details */
+ replytext = replytext.replace(/(attachment\s+\d+)(\s+\[[^\[\n]+\])+/gi, '$1');
+
+ /* <textarea id="comment"> */
+ var textarea = document.getElementById('comment');
+ if (textarea.value != replytext) {
+ textarea.value += replytext;
+ }
+
+ textarea.focus();
+ }
+
+ function toggleCommentWrap(a, id) {
+ var spans = document.getElementById('comment_text_' + id).getElementsByTagName('span');
+ var old_class;
+ var new_class;
+ if (a.innerHTML == 'wrap') {
+ a.innerHTML = 'unwrap';
+ old_class = 'quote';
+ new_class = 'quote_wrapped';
+ } else {
+ a.innerHTML = 'wrap';
+ old_class = 'quote_wrapped';
+ new_class = 'quote';
+ }
+ for (var i = 0, l = spans.length; i < l; i++) {
+ if (spans[i].className == old_class)
+ spans[i].className = new_class;
+ }
+ return false;
+ }
+//-->
+</script>
+
[% DEFAULT start_at = 0 mode = "show" %]
[% sort_order = user.settings.comment_sort_order.value %]
+[% user_cache = template_cache.users %]
[%# NOTE: (start_at > 0) means we came here from a midair collision,
# in which case we don't care what the user's preference is.
@@ -52,6 +109,8 @@
[% END %]
[% END %]
+[% Hook.process("comment_banner") %]
+
<!-- This auto-sizes the comments and positions the collapse/expand links
to the right. -->
<table class="bz_comment_table" cellpadding="0" cellspacing="0"><tr>
@@ -65,14 +124,6 @@
[% count = count + increment %]
[% END %]
-[% IF user.settings.comment_box_position.value == "before_comments" && user.id %]
- <div class="bz_add_comment">
- <a href="#"
- onclick="return goto_add_comments();">
- Add Comment</a>
- </div>
-[% END %]
-
[%# Note: this template is used in multiple places; if you use this hook,
# make sure you are aware of this fact.
#%]
@@ -86,11 +137,6 @@
return false;">Collapse All Comments</a></li>
<li><a href="#" onclick="toggle_all_comments('expand');
return false;">Expand All Comments</a></li>
- [% IF user.settings.comment_box_position.value == "after_comments" && user.id %]
- <li class="bz_add_comment"><a href="#"
- onclick="return goto_add_comments('bug_status_bottom');">
- Add Comment</a></li>
- [% END %]
</ul>
[% END %]
</td>
@@ -101,7 +147,7 @@
[%############################################################################%]
[% BLOCK a_comment %]
- [% RETURN IF comment.is_private AND ! user.is_insider %]
+ [% RETURN IF comment.is_private AND NOT (user.is_insider || user.id == comment.author.id) %]
[% comment_text = comment.body_full %]
[% RETURN IF comment_text == '' AND (comment.work_time - 0) != 0 AND !user.is_timetracker %]
@@ -120,8 +166,16 @@
[% IF mode == "edit" %]
<span class="bz_comment_actions">
+ [% IF comment_text.search("(?:^>|\n>)") %]
+ [<a class="bz_wrap_link" href="#"
+ onclick="return toggleCommentWrap(this, [% count %])">wrap</a>]
+ [% END %]
+ [<a class="bz_reply_link" href="#add_comment"
+ [% IF user.settings.quote_replies.value != 'off' %]
+ onclick="replyToComment('[% count %]', '[% comment.id %]', '[% comment.author.name || comment.author.nick FILTER html FILTER js %]'); return false;"
+ [% END %]
+ >reply</a>]
<script type="text/javascript"><!--
- addReplyLink([% count %], [% comment.id %]);
addCollapseLink([% count %], 'Toggle comment display'); // -->
</script>
</span>
@@ -147,12 +201,19 @@
</span>
<span class="bz_comment_user">
- [% INCLUDE global/user.html.tmpl who = comment.author %]
- </span>
+ [%# No need to recreate the exact same template if we already have it. %]
+ [% commenter_id = comment.author.id %]
+ [% UNLESS user_cache.$commenter_id %]
+ [% user_cache.$commenter_id = BLOCK %]
+ [% INCLUDE global/user.html.tmpl who = comment.author %]
+ [% END %]
+ [% END %]
+ [% user_cache.$commenter_id FILTER none %]
+ [% Hook.process('user', 'bug/comments.html.tmpl') %]
+ </span>
<span class="bz_comment_user_images">
- [% FOREACH group = comment.author.direct_group_membership %]
- [% NEXT UNLESS group.icon_url %]
+ [% FOREACH group = comment.author.groups_with_icon %]
<img src="[% group.icon_url FILTER html %]"
alt="[% group.name FILTER html %]"
title="[% group.name FILTER html %] - [% group.description FILTER html %]">
@@ -179,4 +240,5 @@
[%- comment_text FILTER quoteUrls(bug, comment) -%]
</pre>
</div>
+ [% Hook.process('a_comment-end', 'bug/comments.html.tmpl') %]
[% END %]
diff --git a/template/en/default/bug/create/comment-guided.txt.tmpl b/template/en/default/bug/create/comment-guided.txt.tmpl
index df04d8fb5..67748e594 100644
--- a/template/en/default/bug/create/comment-guided.txt.tmpl
+++ b/template/en/default/bug/create/comment-guided.txt.tmpl
@@ -41,7 +41,7 @@ Steps to Reproduce:
[%+ cgi.param("reproduce_steps") %]
[% END %]
-[% IF cgi.param("actual_results") -%]
+[% IF cgi.param("actual_results") %]
Actual Results:
[%+ cgi.param("actual_results") %]
[% END %]
diff --git a/template/en/default/bug/create/create-guided.html.tmpl b/template/en/default/bug/create/create-guided.html.tmpl
index d10314628..43437bcd7 100644
--- a/template/en/default/bug/create/create-guided.html.tmpl
+++ b/template/en/default/bug/create/create-guided.html.tmpl
@@ -31,22 +31,12 @@
[% PROCESS global/header.html.tmpl
title = "Enter $terms.ABug"
onload = "PutDescription()"
- style = "#somebugs { width: 100%; height: 500px }"
+ style_urls = [ "skins/standard/guided.css" ]
%]
[% style = "" %]
-<p>
- <font color="red">
- This is a template used on mozilla.org. This template, and the
- comment-guided.txt.tmpl template that formats the data submitted via
- the form in this template, are included as a demo of what it's
- possible to do with custom templates in general, and custom [% terms.bug %]
- entry templates in particular. As much of the text will not apply,
- you should alter it
- if you want to use this form on your [% terms.Bugzilla %] installation.
- </font>
-</p>
+[% INCLUDE 'bug/create/user-message.html.tmpl' %]
[% tablecolour = "#FFFFCC" %]
@@ -80,15 +70,15 @@ function PutDescription() {
[%# Include other products if sensible %]
[% IF product.name == "Firefox" %]
- [% productstring = "product=Mozilla%20Application%20Suite&amp;product=Firefox" %]
+ [% productstring = "product=Toolkit&amp;product=Core&amp;product=Firefox" %]
[% ELSIF product.name == "Thunderbird" %]
- [% productstring = "product=Mozilla%20Application%20Suite&amp;product=Thunderbird" %]
+ [% productstring = "product=MailNews%20Core&amp;product=Thunderbird" %]
[% ELSE %]
[% productstring = BLOCK %]product=[% product.name FILTER uri %][% END %]
[% END %]
<p>
- <a href="duplicates.cgi?[% productstring %]&amp;format=simple" target="somebugs">All-time Top 100</a> (loaded initially) |
+ <a href="duplicates.cgi?[% productstring %]&amp;format=simple" target="somebugs">All-time Top 20</a> (loaded initially) |
<a href="duplicates.cgi?[% productstring %]&amp;format=simple&amp;sortby=delta&amp;reverse=1&amp;maxrows=100&amp;changedsince=14" target="somebugs">Hot in the last two weeks</a>
</p>
@@ -112,14 +102,14 @@ function PutDescription() {
<input type="hidden" name="product" value="[% product.name FILTER html %]">
[% IF product.name == "Firefox" OR
product.name == "Thunderbird" OR
- product.name == "Mozilla Application Suite" OR
+ product.name == "SeaMonkey" OR
product.name == "Camino" %]
<input type="hidden" name="product" value="Core">
<input type="hidden" name="product" value="Toolkit">
- <input type="hidden" name="product" value="PSM">
<input type="hidden" name="product" value="NSPR">
<input type="hidden" name="product" value="NSS">
- [% END %]
+ <input type="hidden" name="product" value="MailNews Core">
+ [% END %]
<input type="hidden" name="chfieldfrom" value="-6m">
<input type="hidden" name="chfieldto" value="Now">
<input type="hidden" name="chfield" value="[Bug creation]">
@@ -215,7 +205,7 @@ function PutDescription() {
[%# We override rep_platform and op_sys for simplicity. The values chosen
are based on which are most common in the b.m.o database %]
- [% rep_platform = [ "PC", "Macintosh", "All", "Other" ] %]
+ [% rep_platform = [ "x86", "x86_64", "PowerPC", "All", "Other" ] %]
<tr bgcolor="[% tablecolour %]">
<td align="right" valign="top">
@@ -238,7 +228,7 @@ function PutDescription() {
</td>
</tr>
- [% IF product.name.match("Firefox|Camino|Mozilla Application Suite") %]
+ [% IF product.name.match("Firefox|Camino|SeaMonkey") %]
[% matches = cgi.user_agent('Gecko/(\d+)') %]
[% buildid = cgi.user_agent() IF matches %]
[% END %]
@@ -257,8 +247,8 @@ function PutDescription() {
<p>
This should identify the exact version of the product you were using.
If the above field is blank or you know it is incorrect, copy the
- version text from the product's Help |
- About menu (for browsers this will begin with "Mozilla/5.0...").
+ user agent text from the product's Help | Troubleshooting Information menu
+ (for browsers this will begin with "Mozilla/5.0...").
If the product won't start, instead paste the complete URL you downloaded
it from.
</p>
@@ -275,7 +265,7 @@ function PutDescription() {
URL that demonstrates the problem you are seeing (optional).<br>
<b>IMPORTANT</b>: if the problem is with a broken web page, you need
to report it
- <a href="https://bugzilla.mozilla.org/page.cgi?id=broken-website.html">a different way</a>.
+ <a href="http://input.mozilla.com/feedback">a different way</a>.
</p>
</td>
</tr>
@@ -418,10 +408,7 @@ function PutDescription() {
%]
<p>
Add any additional information you feel may be
- relevant to this [% terms.bug %], such as the <b>theme</b> you were
- using (does the [% terms.bug %] still occur
- with the default theme?), a
- <b><a href="http://kb.mozillazine.org/Quality_Feedback_Agent">Talkback crash ID</a></b>, or special
+ relevant to this [% terms.bug %], such as special
information about <b>your computer's configuration</b>. Any information
longer than a few lines, such as a <b>stack trace</b> or <b>HTML
testcase</b>, should be added
@@ -431,13 +418,12 @@ function PutDescription() {
into your URL bar.
<br>
<br>
- If you are reporting a crash, note the module in
- which the software crashed (e.g., <tt>Application Violation in
- gkhtml.dll</tt>).
+ If you are reporting a crash, please <a href="https://developer.mozilla.org/En/How_to_get_a_stacktrace_for_a_bug_report
+">try and get a stack trace</a>, which tells us exactly where things went wrong.
</p>
</td>
</tr>
-
+
<tr>
<td valign="top" align="right">
<b>Severity</b>
diff --git a/template/en/default/bug/create/create.html.tmpl b/template/en/default/bug/create/create.html.tmpl
index f3dd680df..22ca4ebab 100644
--- a/template/en/default/bug/create/create.html.tmpl
+++ b/template/en/default/bug/create/create.html.tmpl
@@ -32,16 +32,37 @@
title = title
yui = [ 'autocomplete', 'calendar', 'datatable', 'button' ]
style_urls = [ 'skins/standard/attachment.css',
- 'skins/standard/enter_bug.css' ]
+ 'skins/standard/enter_bug.css',
+ 'skins/custom/create_bug.css' ]
javascript_urls = [ "js/attachment.js", "js/util.js",
- "js/field.js", "js/TUI.js", "js/bug.js" ]
- onload = "set_assign_to(); hideElementById('attachment_true');
- showElementById('attachment_false'); showElementById('btn_no_attachment');"
+ "js/field.js", "js/TUI.js", "js/bug.js",
+ "js/create_bug.js" ]
+ onload = "init();"
%]
<script type="text/javascript">
<!--
+function init() {
+ set_assign_to();
+ hideElementById('attachment_true');
+ showElementById('attachment_false');
+ showElementById('btn_no_attachment');
+ initCrashSignatureField();
+ init_take_handler('[% user.login FILTER js %]');
+}
+
+function initCrashSignatureField() {
+ var el = document.getElementById('cf_crash_signature');
+ if (!el) return;
+ [% IF cf_crash_signature.length %]
+ YAHOO.util.Dom.addClass('cf_crash_signature_container', 'bz_default_hidden');
+ [% ELSE %]
+ hideEditableField('cf_crash_signature_container','cf_crash_signature_input',
+ 'cf_crash_signature_action', 'cf_crash_signature');
+ [% END %]
+}
+
var initialowners = new Array([% product.components.size %]);
var last_initialowner;
var initialccs = new Array([% product.components.size %]);
@@ -60,11 +81,9 @@ var flags = new Array([% product.components.size %]);
initialowners[[% count %]] = "[% c.default_assignee.login FILTER js %]";
[% flag_list = [] %]
[% FOREACH f = c.flag_types.bug %]
- [% NEXT UNLESS f.is_active %]
[% flag_list.push(f.id) %]
[% END %]
[% FOREACH f = c.flag_types.attachment %]
- [% NEXT UNLESS f.is_active %]
[% flag_list.push(f.id) %]
[% END %]
flags[[% count %]] = [[% flag_list.join(",") FILTER js %]];
@@ -112,6 +131,14 @@ function set_assign_to() {
document.getElementById('initial_cc').innerHTML = initialccs[index];
document.getElementById('comp_desc').innerHTML = comp_desc[index];
+ if (initialccs[index]) {
+ showElementById('initial_cc_label');
+ showElementById('initial_cc');
+ } else {
+ hideElementById('initial_cc_label');
+ hideElementById('initial_cc');
+ }
+
[% IF Param("useqacontact") %]
var contact = initialqacontacts[index];
if (qa_contact == last_initialqacontact
@@ -122,30 +149,31 @@ function set_assign_to() {
}
[% END %]
- // First, we disable all flags. Then we re-enable those
- // which are available for the selected component.
- var inputElements = document.getElementsByTagName("select");
- var inputElement, flagField;
- for ( var i=0 ; i<inputElements.length ; i++ ) {
- inputElement = inputElements.item(i);
- if (inputElement.name.search(/^flag_type-(\d+)$/) != -1) {
- var id = inputElement.name.replace(/^flag_type-(\d+)$/, "$1");
- inputElement.disabled = true;
- // Also hide the requestee field, if it exists.
- inputElement = document.getElementById("requestee_type-" + id);
- if (inputElement)
- YAHOO.util.Dom.addClass(inputElement.parentNode, 'bz_default_hidden');
+ // We show or hide the available flags depending on the selected component.
+ var flag_rows = YAHOO.util.Dom.getElementsByClassName('bz_flag_type', 'tbody');
+ for (var i = 0; i < flag_rows.length; i++) {
+ // Each flag table row should have one flag form select element
+ // We get the flag type id from the id attribute of the select.
+ var flag_select = YAHOO.util.Dom.getElementsByClassName('flag_select',
+ 'select',
+ flag_rows[i])[0];
+ var type_id = flag_select.id.split('-')[1];
+ var can_set = flag_select.options.length > 1 ? 1 : 0;
+ var show = 0;
+ // Loop through the allowed flag ids for the selected component
+ // and if we match, then show the row, otherwise hide the row.
+ for (var j = 0; j < flags[index].length; j++) {
+ if (flags[index][j] == type_id) {
+ show = 1;
+ break;
+ }
}
- }
- // Now enable flags available for the selected component.
- for (var i = 0; i < flags[index].length; i++) {
- flagField = document.getElementById("flag_type-" + flags[index][i]);
- // Do not enable flags the user cannot set nor request.
- if (flagField && flagField.options.length > 1) {
- flagField.disabled = false;
- // Re-enabling the requestee field depends on the status
- // of the flag.
- toggleRequesteeField(flagField, 1);
+ if (show && can_set) {
+ flag_select.disabled = false;
+ YAHOO.util.Dom.removeClass(flag_rows[i], 'bz_default_hidden');
+ } else {
+ flag_select.disabled = true;
+ YAHOO.util.Dom.addClass(flag_rows[i], 'bz_default_hidden');
}
}
}
@@ -185,9 +213,8 @@ TUI_hide_default('attachment_text_field');
<tr>
<td colspan="2">
- <a id="expert_fields_controller" class="controller bz_default_hidden"
- href="javascript:TUI_toggle_class('expert_fields')">Hide
- Advanced Fields</a>
+ <input type="button" id="expert_fields_controller"
+ value="Hide Advanced Fields" onClick="toggleAdvancedFields()">
[%# Show the link if the browser supports JS %]
<script type="text/javascript">
YAHOO.util.Dom.removeClass('expert_fields_controller',
@@ -349,121 +376,78 @@ TUI_hide_default('attachment_text_field');
bug = default, field = bug_fields.bug_status,
editable = (bug_status.size > 1), value = default.bug_status
override_legal_values = bug_status %]
-
- <td>&nbsp;</td>
- [%# Calculate the number of rows we can use for flags %]
- [% num_rows = 6 + (Param("useqacontact") ? 1 : 0) +
- (user.is_timetracker ? 3 : 0) +
- (Param("usebugaliases") ? 1 : 0)
- %]
-
- <td rowspan="[% num_rows FILTER html %]">
- [% IF product.flag_types.bug.size > 0 %]
- [% display_flag_headers = 0 %]
- [% any_flags_requesteeble = 0 %]
-
- [% FOREACH flag_type = product.flag_types.bug %]
- [% NEXT UNLESS flag_type.is_active %]
- [% display_flag_headers = 1 %]
- [% SET any_flags_requesteeble = 1 IF flag_type.is_requestable && flag_type.is_requesteeble %]
- [% END %]
-
- [% IF display_flag_headers %]
- [% PROCESS "flag/list.html.tmpl" flag_types = product.flag_types.bug
- any_flags_requesteeble = any_flags_requesteeble
- flag_table_id = "bug_flags"
- %]
- [% END %]
- [% END %]
- </td>
</tr>
<tr>
[% INCLUDE "bug/field-label.html.tmpl"
field = bug_fields.assigned_to editable = 1
%]
- <td colspan="2">
+ <td>
[% INCLUDE global/userselect.html.tmpl
- id => "assigned_to"
- name => "assigned_to"
- value => assigned_to
+ id => "assigned_to"
+ name => "assigned_to"
+ value => assigned_to
disabled => assigned_to_disabled
- size => 30
- emptyok => 1
+ size => 30
+ emptyok => 1
custom_userlist => assignees_list
- %]
+ %]
+ [% UNLESS assigned_to_disabled %]
+ <span id="take_bug">
+ &nbsp;(<a title="Assign to yourself" href="#"
+ onclick="return take_bug('[% user.login FILTER js %]')">take</a>)
+ </span>
+ [% END %]
<noscript>(Leave blank to assign to component's default assignee)</noscript>
</td>
- </tr>
[% IF Param("useqacontact") %]
- <tr>
- [% INCLUDE "bug/field-label.html.tmpl"
- field = bug_fields.qa_contact editable = 1
- %]
- <td colspan="2">
- [% INCLUDE global/userselect.html.tmpl
- id => "qa_contact"
- name => "qa_contact"
- value => qa_contact
- disabled => qa_contact_disabled
- size => 30
- emptyok => 1
- custom_userlist => qa_contacts_list
- %]
- <noscript>(Leave blank to assign to default qa contact)</noscript>
- </td>
- </tr>
+ [% INCLUDE "bug/field-label.html.tmpl"
+ field = bug_fields.qa_contact editable = 1
+ %]
+ <td>
+ [% INCLUDE global/userselect.html.tmpl
+ id => "qa_contact"
+ name => "qa_contact"
+ value => qa_contact
+ disabled => qa_contact_disabled
+ size => 30
+ emptyok => 1
+ custom_userlist => qa_contacts_list
+ %]
+ <noscript>(Leave blank to assign to default qa contact)</noscript>
+ </td>
+ </tr>
[% END %]
<tr>
[% INCLUDE "bug/field-label.html.tmpl"
field = bug_fields.cc editable = 1
%]
- <td colspan="2">
+ <td>
[% INCLUDE global/userselect.html.tmpl
- id => "cc"
- name => "cc"
- value => cc
+ id => "cc"
+ name => "cc"
+ value => cc
disabled => cc_disabled
- size => 30
+ size => 30
multiple => 5
%]
+ </td>
+ <th>
+ <span id="initial_cc_label" class="bz_default_hidden">
+ Default [% field_descs.cc FILTER html %]:
+ </span>
+ </th>
+ <td>
+ <span id="initial_cc"></span>
</td>
</tr>
<tr>
- <th>Default [% field_descs.cc FILTER html %]:</th>
- <td colspan="2">
- <div id="initial_cc">
- </div>
- </td>
- </tr>
-
- <tr>
<td colspan="3">&nbsp;</td>
</tr>
-[% IF user.is_timetracker %]
- <tr>
- [% INCLUDE "bug/field-label.html.tmpl"
- field = bug_fields.estimated_time editable = 1
- %]
- <td colspan="2">
- <input name="estimated_time" size="6" maxlength="6" value="[% estimated_time FILTER html %]">
- </td>
- </tr>
- <tr>
- [% INCLUDE bug/field.html.tmpl
- bug = default, field = bug_fields.deadline, value = deadline,
- editable = 1, value_span = 2 %]
- </tr>
-
- <tr>
- <td colspan="3">&nbsp;</td>
- </tr>
-[% END %]
-
[% IF Param("usebugaliases") %]
<tr>
[% INCLUDE "bug/field-label.html.tmpl"
@@ -474,34 +458,9 @@ TUI_hide_default('attachment_text_field');
</td>
</tr>
[% END %]
-
- <tr>
- [% INCLUDE "bug/field-label.html.tmpl"
- field = bug_fields.bug_file_loc editable = 1
- %]
- <td colspan="2" class="field_value">
- <input name="bug_file_loc" id="bug_file_loc" class="text_input"
- size="40" value="[% bug_file_loc FILTER html %]">
- </td>
- </tr>
-</tbody>
-
-<tbody>
- [% USE Bugzilla %]
-
- [% FOREACH field = Bugzilla.active_custom_fields %]
- [% NEXT UNLESS field.enter_bug %]
- [% SET value = ${field.name}.defined ? ${field.name} : "" %]
- <tr [% 'class="expert_fields"' IF !field.is_mandatory %]>
- [% INCLUDE bug/field.html.tmpl
- bug = default, field = field, value = value, editable = 1,
- value_span = 3 %]
- </tr>
- [% END %]
</tbody>
<tbody>
-
<tr>
[% INCLUDE "bug/field-label.html.tmpl"
field = bug_fields.short_desc editable = 1
@@ -574,21 +533,17 @@ TUI_hide_default('attachment_text_field');
</td>
</tr>
- [% IF user.is_insider %]
- <tr class="expert_fields">
- <th>&nbsp;</th>
- <td colspan="3">
- &nbsp;&nbsp;
- <input type="checkbox" id="comment_is_private" name="comment_is_private"
- [% ' checked="checked"' IF comment_is_private %]
- onClick="updateCommentTagControl(this, 'comment')">
- <label for="comment_is_private">
- Make description and any new attachment private (visible only to members
- of the <strong>[% Param('insidergroup') FILTER html %]</strong> group)
- </label>
- </td>
- </tr>
- [% END %]
+<tbody class="expert_fields">
+ <tr>
+ [% INCLUDE "bug/field-label.html.tmpl"
+ field = bug_fields.bug_file_loc editable = 1
+ %]
+ <td colspan="3" class="field_value">
+ <input name="bug_file_loc" id="bug_file_loc" class="text_input"
+ size="40" value="[% bug_file_loc FILTER html %]">
+ </td>
+ </tr>
+</tbody>
[% IF Param("maxattachmentsize") || Param("maxlocalattachment") %]
<tr>
@@ -609,6 +564,16 @@ TUI_hide_default('attachment_text_field');
any_flags_requesteeble = 1
flag_table_id ="attachment_flags" %]
</table>
+
+ [% IF user.is_insider %]
+ <input type="checkbox" id="comment_is_private" name="comment_is_private"
+ [% ' checked="checked"' IF comment_is_private %]
+ onClick="updateCommentTagControl(this, 'comment')">
+ <label for="comment_is_private">
+ Make this attachment and [% terms.bug %] description private (visible only
+ to members of the <strong>[% Param('insidergroup') FILTER html %]</strong> group)
+ </label>
+ [% END %]
</fieldset>
</div>
</td>
@@ -618,41 +583,193 @@ TUI_hide_default('attachment_text_field');
<tbody class="expert_fields">
[% IF user.in_group('editbugs', product.id) %]
+ <tr>
+ [% INCLUDE "bug/field-label.html.tmpl"
+ field = bug_fields.dependson editable = 1
+ %]
+ <td>
+ <input name="dependson" accesskey="d" value="[% dependson FILTER html %]" size="30">
+ </td>
+ [% INCLUDE "bug/field-label.html.tmpl"
+ field = bug_fields.blocked editable = 1
+ %]
+ <td>
+ <input name="blocked" accesskey="b" value="[% blocked FILTER html %]" size="30">
+ </td>
+ </tr>
+
[% IF use_keywords %]
<tr>
[% INCLUDE bug/field.html.tmpl
bug = default, field = bug_fields.keywords, editable = 1,
value = keywords, desc_url = "describekeywords.cgi",
- value_span = 2
+ value_span = 3
%]
</tr>
[% END %]
<tr>
- [% INCLUDE "bug/field-label.html.tmpl"
- field = bug_fields.dependson editable = 1
- %]
- <td colspan="3">
- <input name="dependson" accesskey="d" value="[% dependson FILTER html %]">
+ <th>Status Whiteboard:</th>
+ <td colspan="3" class="field_value">
+ <input id="status_whiteboard" name="status_whiteboard" size="70"
+ value="[% status_whiteboard FILTER html %]" class="text_input">
</td>
</tr>
+ [% END %]
+
+ [% IF user.is_timetracker %]
<tr>
[% INCLUDE "bug/field-label.html.tmpl"
- field = bug_fields.blocked editable = 1
+ field = bug_fields.estimated_time editable = 1
%]
- <td colspan="3">
- <input name="blocked" accesskey="b" value="[% blocked FILTER html %]">
+ <td>
+ <input name="estimated_time" size="6" maxlength="6" value="[% estimated_time FILTER html %]">
</td>
+ [% INCLUDE bug/field.html.tmpl
+ bug = default, field = bug_fields.deadline, value = deadline, editable = 1
+ %]
</tr>
[% END %]
</tbody>
+<tbody>
+[%# non-tracking flags custom fields %]
+[% FOREACH field = Bugzilla.active_custom_fields(product=>product,type=>1) %]
+ [% NEXT UNLESS field.enter_bug %]
+ [%# crash-signature gets custom handling %]
+ [% IF field.name == 'cf_crash_signature' %]
+ [% show_crash_signature = 1 %]
+ [% NEXT %]
+ [% END %]
+ [% SET value = ${field.name}.defined ? ${field.name} : "" %]
+ <tr [% 'class="expert_fields"' IF !field.is_mandatory %]>
+ [% INCLUDE bug/field.html.tmpl
+ bug = default, field = field, value = value, editable = 1,
+ value_span = 3 %]
+ </tr>
+[% END %]
+</tbody>
+
+[%# crash-signature handling %]
+[% IF show_crash_signature %]
+<tbody class="expert_fields">
+ <tr>
+ <th id="field_label_cf_crash_signature" class="field_label">
+ <label for="cf_crash_signature"> Crash Signature: </label>
+ </th>
+ <td colspan="3">
+ <span id="cf_crash_signature_container">
+ <span id="cf_crash_signature_nonedit_display"><i>None</i></span>
+ (<a id="cf_crash_signature_action" href="#">edit</a>)
+ </span>
+ <span id="cf_crash_signature_input">
+ <textarea id="cf_crash_signature" name="cf_crash_signature" rows="4" cols="60"
+ >[% cf_crash_signature FILTER html %]</textarea>
+ </span>
+ </td>
+ </tr>
+</tbody>
+[% END %]
+
+[% tracking_flags = [] %]
+[% project_flags = [] %]
+[% FOREACH field = Bugzilla.active_custom_fields(product=>product,type=>2) %]
+ [% NEXT UNLESS field.enter_bug %]
+ [% IF cf_is_project_flag(field.name) %]
+ [% project_flags.push(field) %]
+ [% ELSE %]
+ [% tracking_flags.push(field) %]
+ [% END %]
+[% END %]
+
+[% display_flags = 0 %]
+[% any_flags_requesteeble = 0 %]
+[% FOREACH flag_type = product.flag_types.bug %]
+ [% display_flags = 1 %]
+ [% SET any_flags_requesteeble = 1 IF flag_type.is_requestable && flag_type.is_requesteeble %]
+ [% LAST IF display_flags && any_flags_requesteeable %]
+[% END %]
+
+[% IF project_flags.size || tracking_flags.size || display_flags %]
+ <tbody class="expert_fields">
+ <tr>
+ <th>Flags:</th>
+ <td colspan="3">
+ <div id="bug_flags_false" class="bz_default_hidden">
+ <input type="button" value="Set [% terms.bug FILTER html %] flags" onClick="handleWantsBugFlags(true)">
+ </div>
+
+ <div id="bug_flags_true">
+ <input type="button" id="btn_no_bug_flags" value="Don't set [% terms.bug %] flags"
+ class="bz_default_hidden" onClick="handleWantsBugFlags(false)">
+
+ <fieldset>
+ <legend>Set [% terms.bug %] flags</legend>
+
+ <table cellpadding="0" cellspacing="0">
+ <tr>
+ [% IF tracking_flags.size %]
+ <td [% IF project_flags.size %]rowspan="2"[% END %]>
+ <table id="bug_tracking_flags">
+ <tr>
+ <th colspan="2" style="text-align:left">Tracking Flags:</th>
+ </tr>
+ <tr>
+ [% FOREACH field = tracking_flags %]
+ [% SET value = ${field.name}.defined ? ${field.name} : "" %]
+ <tr>
+ [% INCLUDE bug/field.html.tmpl
+ bug = default, field = field, value = value, editable = 1,
+ value_span = 3 %]
+ </tr>
+ [% END %]
+ </tr>
+ </table>
+ </td>
+ [% END %]
+ [% IF project_flags.size %]
+ <td>
+ <table id="bug_project_flags">
+ <tr>
+ <th colspan="2" style="text-align:left">Project Flags:</th>
+ </tr>
+ <tr>
+ [% FOREACH field = project_flags %]
+ [% SET value = ${field.name}.defined ? ${field.name} : "" %]
+ <tr>
+ [% INCLUDE bug/field.html.tmpl
+ bug = default, field = field, value = value, editable = 1,
+ value_span = 3 %]
+ </tr>
+ [% END %]
+ </tr>
+ </table>
+ </td>
+ </tr>
+ <tr>
+ [% END %]
+ [% IF display_flags %]
+ <td>
+ [% PROCESS "flag/list.html.tmpl" flag_types = product.flag_types.bug
+ any_flags_requesteeble = any_flags_requesteeble
+ flag_table_id = "bug_flags"
+ %]
+ </td>
+ [% END %]
+ </tr>
+ </table>
+ </fieldset>
+ </div>
+ </td>
+ </tr>
+ </tbody>
+[% END %]
+
<tbody class="expert_fields">
[% IF product.groups_available.size %]
<tr>
<th>&nbsp;</th>
<td colspan="3">
- <br>
<strong>
Only users in all of the selected groups can view this
[%+ terms.bug %]:
@@ -662,7 +779,6 @@ TUI_hide_default('attachment_text_field');
(Leave all boxes unchecked to make this a public [% terms.bug %].)
</font>
<br>
- <br>
<!-- Checkboxes -->
<input type="hidden" name="defined_groups" value="1">
@@ -694,6 +810,13 @@ TUI_hide_default('attachment_text_field');
</td>
</tr>
</tbody>
+ [%# "status whiteboard" and "qa contact" are the longest labels
+ # add them here to avoid shifting the page when toggling advanced fields %]
+ <tr>
+ <th class="hidden_text">Status Whiteboard:</th>
+ <td>&nbsp;</td>
+ <th class="hidden_text">QA Contact:</th>
+ </tr>
</table>
<input type="hidden" name="form_name" value="enter_bug">
</form>
@@ -701,6 +824,13 @@ TUI_hide_default('attachment_text_field');
[%# Links or content with more information about the bug being created. %]
[% Hook.process("end") %]
+<div id="guided">
+ <a id="guided_img" href="enter_bug.cgi?format=guided&amp;product=[% product.name FILTER uri %]"><img
+ src="extensions/BMO/web/images/guided.png" width="16" height="16" border="0" align="absmiddle"></a>
+ <a id="guided_link" href="enter_bug.cgi?format=guided&amp;product=[% product.name FILTER uri %]"
+ >Switch to the [% terms.Bugzilla %] Helper</a>
+</div>
+
[% PROCESS global/footer.html.tmpl %]
[%############################################################################%]
diff --git a/template/en/default/bug/edit.html.tmpl b/template/en/default/bug/edit.html.tmpl
index fbc6e4a96..52e5865b8 100644
--- a/template/en/default/bug/edit.html.tmpl
+++ b/template/en/default/bug/edit.html.tmpl
@@ -32,71 +32,6 @@
<script type="text/javascript">
<!--
- /* Outputs a link to call replyToComment(); used to reduce HTML output */
- function addReplyLink(id, real_id) {
- /* XXX this should really be updated to use the DOM Core's
- * createElement, but finding a container isn't trivial.
- */
- [% IF user.settings.quote_replies.value != 'off' %]
- document.write('[<a href="#add_comment" onclick="replyToComment(' +
- id + ',' + real_id + '); return false;">reply<' + '/a>]');
- [% END %]
- }
-
- /* Adds the reply text to the `comment' textarea */
- function replyToComment(id, real_id) {
- var prefix = "(In reply to comment #" + id + ")\n";
- var replytext = "";
- [% IF user.settings.quote_replies.value == 'quoted_reply' %]
- /* pre id="comment_name_N" */
- var text_elem = document.getElementById('comment_text_'+id);
- var text = getText(text_elem);
- replytext = prefix + wrapReplyText(text);
- [% ELSIF user.settings.quote_replies.value == 'simple_reply' %]
- replytext = prefix;
- [% END %]
-
- [% IF user.is_insider %]
- if (document.getElementById('isprivate_' + real_id).checked) {
- document.getElementById('newcommentprivacy').checked = 'checked';
- updateCommentTagControl(document.getElementById('newcommentprivacy'), 'comment');
- }
- [% END %]
-
- /* <textarea id="comment"> */
- var textarea = document.getElementById('comment');
- textarea.value += replytext;
-
- textarea.focus();
- }
-
- if (typeof Node == 'undefined') {
- /* MSIE doesn't define Node, so provide a compatibility object */
- window.Node = {
- TEXT_NODE: 3,
- ENTITY_REFERENCE_NODE: 5
- };
- }
-
- /* Concatenates all text from element's childNodes. This is used
- * instead of innerHTML because we want the actual text (and
- * innerText is non-standard).
- */
- function getText(element) {
- var child, text = "";
- for (var i=0; i < element.childNodes.length; i++) {
- child = element.childNodes[i];
- var type = child.nodeType;
- if (type == Node.TEXT_NODE || type == Node.ENTITY_REFERENCE_NODE) {
- text += child.nodeValue;
- } else {
- /* recurse into nodes of other types */
- text += getText(child);
- }
- }
- return text;
- }
-
[% IF user.is_timetracker %]
var fRemainingTime = [% bug.remaining_time %]; // holds the original value
function adjustRemainingTime() {
@@ -115,7 +50,6 @@
// if the remaining time is changed manually, update fRemainingTime
fRemainingTime = document.changeform.remaining_time.value;
}
-
[% END %]
[% IF user.id %]
@@ -164,17 +98,29 @@
[% PROCESS section_url_keyword_whiteboard %]
[% PROCESS section_spacer %]
-
- [%# *** Dependencies *** %]
+
+ [%# *** Dependencies and duplicates *** %]
+ [% PROCESS section_duplicates %]
+
[% PROCESS section_dependson_blocks %]
-
+
+ [% IF user.id %]
+ <tr>
+ <td colspan="2">
+ <span style="float:left">
+ <a href="page.cgi?id=fields.html">What do these fields mean?</a>
+ </span>
+ [% PROCESS commit_button id="_top"%]
+ </td>
+ </tr>
+ [% END %]
</table>
</td>
<td>
<div class="bz_column_spacer">&nbsp;</div>
</td>
[%# 2nd Column %]
- <td id="bz_show_bug_column_2" class="bz_show_bug_column">
+ <td id="bz_show_bug_column_2" class="bz_show_bug_column_table" valign="top">
<table cellpadding="3" cellspacing="1">
[%# *** Reported and modified dates *** %]
[% PROCESS section_dates %]
@@ -182,16 +128,16 @@
[% PROCESS section_cclist %]
[% PROCESS section_spacer %]
-
- [% PROCESS section_see_also %]
+
+ [% PROCESS section_flags %]
- [% PROCESS section_customfields %]
+ [% PROCESS section_see_also %]
[% PROCESS section_spacer %]
+ [% PROCESS section_customfields %]
+
[% Hook.process("after_custom_fields") %]
-
- [% PROCESS section_flags %]
</table>
</td>
@@ -220,6 +166,8 @@
[% IF user.settings.comment_box_position.value == 'before_comments' %]
[% PROCESS comment_box %]
+ [% ELSE %]
+ [% PROCESS summon_comment_box %]
[% END %]
</td>
<td>
@@ -238,7 +186,10 @@
[% IF user.settings.comment_box_position.value == 'after_comments' %]
<hr>
[% PROCESS comment_box %]
- [% END %]
+ [% ELSE %]
+ [% PROCESS summon_comment_box %]
+ [% END %]
+
</form>
@@ -249,7 +200,10 @@
[% BLOCK section_title %]
[%# That's the main table, which contains all editable fields. %]
<div class="bz_alias_short_desc_container edit_form">
- [% PROCESS commit_button id="_top"%]
+ <span class="last_comment_link">
+ <a href="#c[% bug.comments.size - 1 %]"
+ accesskey="l"><b>L</b>ast Comment</a>
+ </span>
<a href="show_bug.cgi?id=[% bug.bug_id %]">
[%-# %]<b>[% terms.Bug %]&nbsp;[% bug.bug_id FILTER html %]</b>
[%-# %]</a> -<span id="summary_alias_container" class="bz_default_hidden">
@@ -351,9 +305,9 @@
%]
</tr>
<tr>
- <td class="field_label">
- <label for="version"><b>Version</b></label>:
- </td>
+ <th class="field_label">
+ <label for="version">Version</label>:
+ </th>
[% PROCESS select selname => "version" %]
</tr>
@@ -361,9 +315,9 @@
[%# PLATFORM #%]
[%############%]
<tr>
- <td class="field_label">
- <label for="rep_platform" accesskey="h"><b>Platform</b></label>:
- </td>
+ <th class="field_label">
+ <label for="rep_platform" accesskey="h">Platform</label>:
+ </th>
<td class="field_value">
[% INCLUDE bug/field.html.tmpl
bug = bug, field = bug_fields.rep_platform,
@@ -373,9 +327,6 @@
bug = bug, field = bug_fields.op_sys,
no_tds = 1, value = bug.op_sys
editable = bug.check_can_change_field('op_sys', 0, 1) %]
- <script type="text/javascript">
- assignToDefaultOnChange(['product', 'component']);
- </script>
</td>
</tr>
@@ -389,9 +340,9 @@
[% BLOCK section_status %]
<tr>
- <td class="field_label">
- <b><a href="page.cgi?id=fields.html#status">Status</a></b>:
- </td>
+ <th class="field_label">
+ <a href="page.cgi?id=fields.html#status">Status</a>:
+ </th>
<td id="bz_field_status">
<span id="static_bug_status">
[% display_value("bug_status", bug.bug_status) FILTER html %]
@@ -408,6 +359,30 @@
</span>
</td>
</tr>
+ [% IF Param('usestatuswhiteboard') %]
+ <tr>
+ <th class="field_label">
+ <label for="status_whiteboard" accesskey="w"><u>W</u>hiteboard</label>:
+ </th>
+ [% PROCESS input inputname => "status_whiteboard" size => "40" colspan => 2 %]
+ </tr>
+ [% END %]
+
+ [% IF use_keywords %]
+ <tr>
+ <th class="field_label">
+ <label for="keywords" accesskey="k">
+ <a href="describekeywords.cgi"><u>K</u>eywords</a></label>:
+ </th>
+ <td class="field_value" colspan="2">
+ [% INCLUDE bug/field.html.tmpl
+ bug = bug, field = bug_fields.keywords, value = bug.keywords
+ editable = bug.check_can_change_field("keywords", 0, 1),
+ no_tds = 1
+ %]
+ </td>
+ </tr>
+ [% END %]
[% END %]
[%############################################################################%]
@@ -420,10 +395,10 @@
[%# Importance (priority and severity) #%]
[%###############################################################%]
<tr>
- <td class="field_label">
+ <th class="field_label">
<label for="priority" accesskey="i">
- <b><a href="page.cgi?id=fields.html#importance"><u>I</u>mportance</a></b></label>:
- </td>
+ <a href="page.cgi?id=fields.html#importance"><u>I</u>mportance</a></label>:
+ </th>
<td>
[% INCLUDE bug/field.html.tmpl
bug = bug, field = bug_fields.priority,
@@ -439,11 +414,11 @@
[% IF Param("usetargetmilestone") && bug.target_milestone %]
<tr>
- <td class="field_label">
+ <th class="field_label">
<label for="target_milestone">
- <a href="page.cgi?id=fields.html#target_milestone">
+ <a href="page.cgi?id=fields.html#target_milestone">
Target&nbsp;Milestone</a></label>:
- </td>
+ </th>
[% PROCESS select selname = "target_milestone" %]
</tr>
[% END %]
@@ -457,9 +432,9 @@
[% BLOCK section_people %]
<tr>
- <td class="field_label">
- <b><a href="page.cgi?id=fields.html#assigned_to">Assigned To</a></b>:
- </td>
+ <th class="field_label">
+ <a href="page.cgi?id=fields.html#assigned_to">Assigned To</a>:
+ </th>
<td>
[% IF bug.check_can_change_field("assigned_to", 0, 1) %]
<div id="bz_assignee_edit_container" class="bz_default_hidden">
@@ -506,41 +481,46 @@
[% IF Param('useqacontact') %]
<tr>
- <td class="field_label">
- <label for="qa_contact" accesskey="q"><b><u>Q</u>A Contact</b></label>:
- </td>
+ <th class="field_label">
+ <label for="qa_contact" accesskey="q"><u>Q</u>A Contact</label>:
+ </th>
<td>
[% IF bug.check_can_change_field("qa_contact", 0, 1) %]
- [% IF bug.qa_contact != "" %]
- <div id="bz_qa_contact_edit_container" class="bz_default_hidden">
+ <div id="bz_qa_contact_edit_container" class="bz_default_hidden">
<span>
- <span id="bz_qa_contact_edit_display">
- [% INCLUDE global/user.html.tmpl who = bug.qa_contact %]</span>
+ [% INCLUDE global/user.html.tmpl who = bug.qa_contact %]
(<a href="#" id="bz_qa_contact_edit_action">edit</a>)
+ [% IF bug.qa_contact.id != user.id %]
+ (<a title="Change QA contact to yourself"
+ href="#" id="bz_qa_contact_take_action">take</a>)
+ [% END %]
</span>
</div>
- [% END %]
<div id="bz_qa_contact_input">
[% INCLUDE global/userselect.html.tmpl
- id => "qa_contact"
- name => "qa_contact"
- value => bug.qa_contact.login
- size => 30
- classes => ["bz_userfield"]
- emptyok => 1
+ id => "qa_contact"
+ name => "qa_contact"
+ value => bug.qa_contact.login
+ size => 30
+ classes => ["bz_userfield"]
+ emptyok => 1
%]
<br>
<input type="checkbox" id="set_default_qa_contact" name="set_default_qa_contact" value="1">
<label for="set_default_qa_contact" id="set_default_qa_contact_label">Reset QA Contact to default</label>
</div>
<script type="text/javascript">
- [% IF bug.qa_contact != "" %]
- hideEditableField('bz_qa_contact_edit_container',
- 'bz_qa_contact_input',
- 'bz_qa_contact_edit_action',
- 'qa_contact',
- '[% bug.qa_contact.login FILTER js %]');
- [% END %]
+ hideEditableField('bz_qa_contact_edit_container',
+ 'bz_qa_contact_input',
+ 'bz_qa_contact_edit_action',
+ 'qa_contact',
+ '[% bug.qa_contact.login FILTER js %]');
+ hideEditableField('bz_qa_contact_edit_container',
+ 'bz_qa_contact_input',
+ 'bz_qa_contact_take_action',
+ 'qa_contact',
+ '[% bug.qa_contact.login FILTER js %]',
+ '[% user.login FILTER js %]');
initDefaultCheckbox('qa_contact');
</script>
[% ELSE %]
@@ -549,6 +529,11 @@
</td>
</tr>
[% END %]
+ <script type="text/javascript">
+ assignToDefaultOnChange(['product', 'component'],
+ '[% bug.component_obj.default_assignee.login FILTER js %]',
+ '[% bug.component_obj.default_qa_contact.login FILTER js %]');
+ </script>
[% END %]
[%############################################################################%]
@@ -564,14 +549,17 @@
<td>
[% IF bug.check_can_change_field("bug_file_loc", 0, 1) %]
<span id="bz_url_edit_container" class="bz_default_hidden">
- [% IF is_safe_url(bug.bug_file_loc) %]
- <a href="[% bug.bug_file_loc FILTER html %]" target="_blank"
- title="[% bug.bug_file_loc FILTER html %]">
- [% bug.bug_file_loc FILTER truncate(40) FILTER html %]</a>
- [% ELSE %]
- [% bug.bug_file_loc FILTER html %]
- [% END %]
- (<a href="#" id="bz_url_edit_action">edit</a>)</span>
+ <a href="[% bug.bug_file_loc FILTER html %]" target="_blank"
+ title="[% bug.bug_file_loc FILTER html %]"
+ [% IF NOT is_safe_url(bug.bug_file_loc) %]
+ onclick="return confirm(
+ 'This is considered an unsafe URL and could possibly be harmful. '
+ + 'The full URL is:\n\n[% bug.bug_file_loc FILTER js FILTER html %]\n\n'
+ + 'Continue?')"
+ [% END %]>
+ [% bug.bug_file_loc FILTER truncate(40) FILTER html %]</a>
+ (<a href="#" id="bz_url_edit_action">edit</a>)
+ </span>
[% END %]
<span id="bz_url_input_area">
[% url_output = PROCESS input no_td=1 inputname => "bug_file_loc" size => "40" colspan => 2 %]
@@ -593,36 +581,34 @@
[% END %]
</td>
</tr>
-
- [% IF Param('usestatuswhiteboard') %]
- <tr>
- <td class="field_label">
- <label for="status_whiteboard" accesskey="w"><b><u>W</u>hiteboard</b></label>:
- </td>
- [% PROCESS input inputname => "status_whiteboard" size => "40" colspan => 2 %]
- </tr>
- [% END %]
-
- [% IF use_keywords %]
- <tr>
- <td class="field_label">
- <label for="keywords" accesskey="k">
- <b><a href="describekeywords.cgi"><u>K</u>eywords</a></b></label>:
- </td>
- <td class="field_value" colspan="2">
- [% INCLUDE bug/field.html.tmpl
- bug = bug, field = bug_fields.keywords, value = bug.keywords
- editable = bug.check_can_change_field("keywords", 0, 1),
- no_tds = 1
- %]
- </td>
- </tr>
- [% END %]
[% END %]
[%############################################################################%]
-[%# Block for Depends On / Blocks #%]
+[%# Block for Duplicates #%]
[%############################################################################%]
+
+[% BLOCK section_duplicates %]
+ [% RETURN UNLESS bug.duplicates.size %]
+ <tr>
+ <th class="field_label">
+ <label for="duplicates">Duplicates</label>:
+ </th>
+ <td class="field_value" colspan="2">
+ <span id="duplicates">
+ [% FOREACH dupe = bug.duplicates %]
+ [% dupe.id FILTER bug_link(dupe, use_alias => 1) FILTER none %][% " " %]
+ [% END %]
+ </span>
+ (<a href="buglist.cgi?bug_id=[% bug.duplicate_ids.join(",") FILTER html %]">
+ [%-%]view as [% terms.bug %] list</a>)
+ </td>
+ </tr>
+[% END %]
+
+[%############################################################################%]
+[%# Block for Depends On / Blocks #%]
+[%############################################################################%]
+
[% BLOCK section_dependson_blocks %]
<tr>
[% INCLUDE dependencies
@@ -749,18 +735,18 @@
[% BLOCK section_dates %]
<tr>
- <td class="field_label">
- <b>Reported</b>:
- </td>
+ <th class="field_label">
+ Reported:
+ </th>
<td>
[% bug.creation_ts FILTER time %] by [% INCLUDE global/user.html.tmpl who = bug.reporter %]
</td>
</tr>
<tr>
- <td class="field_label">
- <b> Modified</b>:
- </td>
+ <th class="field_label">
+ Modified:
+ </th>
<td>
[% bug.delta_ts FILTER time FILTER replace(':\d\d$', '') FILTER replace(':\d\d ', ' ')%]
(<a href="show_activity.cgi?id=[% bug.bug_id %]">[%# terms.Bug %]History</a>)
@@ -774,9 +760,9 @@
[%############################################################################%]
[% BLOCK section_cclist %]
<tr>
- <td class="field_label">
- <label for="newcc" accesskey="a"><b>CC List</b>:</label>
- </td>
+ <th class="field_label">
+ <label for="newcc" accesskey="a">CC List:</label>
+ </th>
<td>
[% IF user.id %]
[% IF NOT bug.cc || NOT bug.cc.contains(user.login) %]
@@ -808,10 +794,17 @@
[% IF user.id || bug.cc.size %]
<span id="cc_edit_area_showhide_container" class="bz_default_hidden">
(<a href="#" id="cc_edit_area_showhide">[% IF user.id %]edit[% ELSE %]show[% END %]</a>)
- </span>
+ [% IF user.id && bug.cc.size %]
+ <br>
+ <ul class="cc_list_display">
+ [% FOREACH c = bug.cc %]
+ <li>[% c FILTER email FILTER html %]</li>
+ [% END %]
+ </ul>
+ [% END %]
+ </span>
[% END %]
<div id="cc_edit_area">
- <br>
[% IF user.id %]
<div>
<div><label for="cc"><b>Add</b></label></div>
@@ -885,26 +878,52 @@
[% BLOCK section_flags %]
[%# *** Flags *** %]
[% show_bug_flags = 0 %]
+ [% bug_flags_set = 0 %]
+ [% show_more_flags = 0 %]
[% FOREACH type = bug.flag_types %]
[% IF (type.flags && type.flags.size > 0) || (user.id && type.is_active) %]
[% show_bug_flags = 1 %]
- [% LAST %]
[% END %]
+ [% IF user.id && type.is_active && (type.flags.size == 0 || type.is_multiplicable) %]
+ [% show_more_flags = 1 %]
+ [% END %]
+ [% IF type.flags && type.flags.size > 0 %]
+ [% bug_flags_set = 1 %]
+ [% END %]
+ [% LAST IF show_bug_flags && show_more_flags && bug_flags_set %]
[% END %]
[% IF show_bug_flags %]
<tr>
- <td class="field_label flags_label">
- <label><b>Flags:</b></label>
- </td>
- <td></td>
- </tr>
- <tr>
- <td colspan="2">
+ <th class="field_label">
+ <label>Flags:</label>
+ </th>
+ <td>
[% IF bug.flag_types.size > 0 %]
[% PROCESS "flag/list.html.tmpl" flag_no_header = 1
flag_types = bug.flag_types
any_flags_requesteeble = bug.any_flags_requesteeble %]
[% END %]
+ [% IF show_more_flags %]
+ <span id="bz_flags_more_container" class="bz_default_hidden">
+ [% IF !bug_flags_set %]<em>None yet set</em>[% END %]
+ (<a href="#" id="bz_flags_more_action">[% IF !bug_flags_set %]set[% ELSE %]more[% END %] flags</a>)
+ </span>
+ <script type="text/javascript">
+ YAHOO.util.Dom.removeClass('bz_flags_more_container', 'bz_default_hidden');
+ var table = YAHOO.util.Dom.get("flags");
+ var rows = YAHOO.util.Dom.getElementsByClassName('bz_flag_type', 'tbody', table);
+ for (var i = 0; i < rows.length; i++) {
+ YAHOO.util.Dom.addClass(rows[i], 'bz_default_hidden');
+ }
+ YAHOO.util.Event.addListener('bz_flags_more_action', 'click', function (e) {
+ YAHOO.util.Dom.addClass('bz_flags_more_container', 'bz_default_hidden');
+ for (var i = 0; i < rows.length; i++) {
+ YAHOO.util.Dom.removeClass(rows[i], 'bz_default_hidden');
+ }
+ YAHOO.util.Event.preventDefault(e);
+ });
+ </script>
+ [% END %]
</td>
</tr>
[% END %]
@@ -917,7 +936,10 @@
[% BLOCK section_customfields %]
[%# *** Custom Fields *** %]
[% USE Bugzilla %]
- [% FOREACH field = Bugzilla.active_custom_fields %]
+ [% FOREACH field = Bugzilla.active_custom_fields(product=>bug.product_obj,component=>bug.component_obj,type=>1) %]
+ [% NEXT IF NOT user.id AND field.value == "---" %]
+ [% Hook.process('custom_field', 'bug/edit.html.tmpl') %]
+ [% NEXT IF field.hidden %]
<tr>
[% PROCESS bug/field.html.tmpl value = bug.${field.name}
editable = bug.check_can_change_field(field.name, 0, 1)
@@ -1091,12 +1113,14 @@
<br>
[% PROCESS commit_button id=""%]
+ [% Hook.process("after_comment_commit_button", 'bug/edit.html.tmpl') %]
+
<table id="bug_status_bottom"
class="status" cellspacing="0" cellpadding="0">
<tr>
- <td class="field_label">
- <b><a href="page.cgi?id=fields.html#status">Status</a></b>:
- </td>
+ <th class="field_label">
+ <a href="page.cgi?id=fields.html#status">Status</a>:
+ </th>
<td>
[% PROCESS bug/knob.html.tmpl %]
</td>
@@ -1123,6 +1147,21 @@
</div>
[% END %]
+[% BLOCK summon_comment_box %]
+<div id="comment_top_hat">
+ <script type="text/javascript">
+ function summonCommentBox() {
+ var commentbox = document.getElementById('add_comment');
+ document.getElementById('comment_top_hat').appendChild(commentbox);
+ document.getElementById('wave_wand').style.display = 'none';
+ }
+ </script>
+ <p id="wave_wand">
+ <a href="javascript:summonCommentBox()"><i>Summon comment box</i></a>
+ </p>
+</div>
+[% END %]
+
[%############################################################################%]
[%# Block for SELECT fields #%]
[%############################################################################%]
@@ -1131,6 +1170,7 @@
<td>
[% IF bug.check_can_change_field(selname, 0, 1)
AND bug.choices.${selname}.size > 1 %]
+ <input type="hidden" id="[% selname %]_dirty">
<select id="[% selname %]" name="[% selname %]">
[% FOREACH x = bug.choices.${selname} %]
[% NEXT IF NOT x.is_active AND x.name != bug.${selname} %]
diff --git a/template/en/default/bug/field.html.tmpl b/template/en/default/bug/field.html.tmpl
index 58f1b0ccc..2447a240e 100644
--- a/template/en/default/bug/field.html.tmpl
+++ b/template/en/default/bug/field.html.tmpl
@@ -97,6 +97,7 @@
</script>
[% CASE [ constants.FIELD_TYPE_SINGLE_SELECT
constants.FIELD_TYPE_MULTI_SELECT ] %]
+ <input type="hidden" id="[% field.name FILTER html %]_dirty">
<select id="[% field.name FILTER html %]"
name="[% field.name FILTER html %]"
[% IF field.type == constants.FIELD_TYPE_MULTI_SELECT %]
@@ -121,6 +122,30 @@
[% END %]
[% FOREACH legal_value = legal_values %]
[% NEXT IF NOT legal_value.is_active AND NOT value.contains(legal_value.name).size %]
+
+ [%# Purpose: hide field values from those who can't change them %]
+ [% IF field.name.match("^cf_blocking_") OR
+ field.name.match("^cf_status_") OR
+ field.name.match("^cf_tracking_") OR
+ field.name == "resolution" %]
+ [% NEXT UNLESS bug.check_can_change_field(field.name, '---', legal_value.name) OR
+ value.contains(legal_value.name).size %]
+ [% END %]
+
+ [% IF field.name == "resolution" &&
+ legal_value.name != bug.resolution %]
+ [% r = legal_value.name %]
+ [% IF bug.user.canconfirm &&
+ !(bug.user.canedit || bug.user.isreporter) %]
+ [% NEXT IF r != "WORKSFORME" && r != "INCOMPLETE" %]
+ [% END %]
+ [% IF bug.user.isreporter &&
+ !(bug.user.canconfirm || bug.user.canedit) %]
+ [% NEXT IF r == "INCOMPLETE" %]
+ [% END %]
+ [% NEXT IF r == "EXPIRED" %]
+ [% END %]
+
<option value="[% legal_value.name FILTER html %]"
id="v[% legal_value.id FILTER html %]_
[%- field.name FILTER html %]"
@@ -177,7 +202,7 @@
</span>
<div id="container_[% field.name FILTER html %]">
<label for="[% field.name FILTER html %]">
- <strong>Add [% terms.Bug %] URLs:</strong>
+ Add [% terms.Bug %] URLs:
</label><br>
<input type="text" id="[% field.name FILTER html %]" size="40"
class="text_input" name="[% field.name FILTER html %]">
diff --git a/template/en/default/bug/navigate.html.tmpl b/template/en/default/bug/navigate.html.tmpl
index 46b92aec4..56150bec3 100644
--- a/template/en/default/bug/navigate.html.tmpl
+++ b/template/en/default/bug/navigate.html.tmpl
@@ -29,12 +29,22 @@
<li>&nbsp;-&nbsp;<a href="show_bug.cgi?ctype=xml&amp;id=
[% bug.bug_id FILTER uri %]">XML</a></li>
<li>&nbsp;-&nbsp;<a href="enter_bug.cgi?cloned_bug_id=
- [% bug.bug_id FILTER uri %]">Clone This
+ [% bug.bug_id FILTER uri %]"
+ id="clone_bug">Clone This
[% terms.Bug %]</a></li>
[%# Links to more things users can do with this bug. %]
[% Hook.process("links") %]
<li>&nbsp;-&nbsp;<a href="#">Top of page </a></li>
- </ul>
+ </ul>
+ <script type="text/javascript">
+ YAHOO.util.Event.onDOMReady(function() {
+ init_clone_bug_menu(
+ YAHOO.util.Dom.get('clone_bug'),
+ '[% bug.bug_id FILTER js %]',
+ '[% bug.product FILTER js %]',
+ '[% bug.component FILTER js %]');
+ });
+ </script>
[% END %]
diff --git a/template/en/default/bug/process/bugmail.html.tmpl b/template/en/default/bug/process/bugmail.html.tmpl
index b0132a2fe..50f6e7aa8 100644
--- a/template/en/default/bug/process/bugmail.html.tmpl
+++ b/template/en/default/bug/process/bugmail.html.tmpl
@@ -26,7 +26,15 @@
[% PROCESS global/variables.none.tmpl %]
-<dl>
+[%# hide the recipient list by default from new users %]
+[% show_recipients =
+ user.settings.post_bug_submit_action.value == 'nothing'
+ || user.in_group('canconfirm')
+ || !user.can_see_bug(mailing_bugid)
+%]
+
+<dl id="bugmail_summary_[% mailing_bugid FILTER none %]"
+ [%~ ' class="bz_default_hidden"' UNLESS show_recipients %]>
[% PROCESS emails
description = "Email sent to"
names = sent_bugmail.sent
@@ -38,6 +46,27 @@
%]
</dl>
+[% IF !show_recipients %]
+ [% recipient_count = sent_bugmail.sent.size %]
+ <div id="bugmail_summary_placeholder_[% mailing_bugid FILTER none %]"
+ [%~ ' class="bz_default_hidden"' IF show_recipients %]>
+ [% IF recipient_count > 0 %]
+ Email sent to [% recipient_count FILTER html %]
+ recipient[% 's' UNLESS recipient_count == 1 %].
+ [% ELSE %]
+ No emails were sent.
+ [% END %]
+ (<a href="#" onclick="
+ YAHOO.util.Dom.removeClass(
+ 'bugmail_summary_[% mailing_bugid FILTER none %]',
+ 'bz_default_hidden');
+ YAHOO.util.Dom.addClass(
+ 'bugmail_summary_placeholder_[% mailing_bugid FILTER none %]',
+ 'bz_default_hidden');
+ return false;">show</a>)
+ </div>
+[% END %]
+
[%############################################################################%]
[%# Block for a set of email addresses #%]
[%############################################################################%]
diff --git a/template/en/default/bug/process/updates-disabled.html.tmpl b/template/en/default/bug/process/updates-disabled.html.tmpl
new file mode 100644
index 000000000..5ea84d476
--- /dev/null
+++ b/template/en/default/bug/process/updates-disabled.html.tmpl
@@ -0,0 +1,73 @@
+[%# 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): Byron Jones <glob@mozilla.com>
+ #
+ #%]
+[% PROCESS global/variables.none.tmpl %]
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
+<html>
+<head>
+<title>[% terms.Bugzilla %] - [% terms.Bug %] Updates Temporarily Suspended</title>
+<style type="text/css">
+body {
+ margin: 2em;
+ background-color: #455372;
+ color: #fff;
+ font-family: verdana, sans-serif;
+ font-size: small;
+}
+a {
+ color: #fff;
+ text-decoration: underline;
+}
+#buggie {
+ float: left;
+}
+#content {
+ margin-left: 100px;
+ max-width: 600px;
+}
+</style>
+</head>
+<body>
+<img src="images/buggie.png" id="buggie" alt="buggie">
+<div id="content">
+<h1>[% terms.Bug %] Updates Temporarily Suspended</h1>
+
+<p>
+We are currently adding a field to [% terms.Bugzilla %]. This requires us to
+prevent updates to [% terms.bugs %] for the duration of the database schema
+change to add the field (usually 3 to 5 minutes).
+</p>
+
+<p>
+<b>You should be able to leave this page open, wait a minute or two, then hit
+reload or refresh in your browser</b> (and OK any request to re-send the form
+data) to complete your [% terms.bug %] change. Once this maintenance is
+complete, your change will succeed and you won't get this page any more.
+</p>
+
+<p>
+Only updates to [% terms.bugs %] are being blocked by this page, any other
+activities in [% terms.Bugzilla %] are still fair game. <a href="index.cgi"
+target="_blank">Open [% terms.Bugzilla %] in a new tab/window</a> if you'd
+like, to continue working on other things while waiting.
+</p>
+</div>
+</body>
+</html>
diff --git a/template/en/default/bug/show-header.html.tmpl b/template/en/default/bug/show-header.html.tmpl
index 54570911d..40f35ba7c 100644
--- a/template/en/default/bug/show-header.html.tmpl
+++ b/template/en/default/bug/show-header.html.tmpl
@@ -39,16 +39,27 @@
[% IF bug.defined %]
[% unfiltered_title = "$terms.Bug $bug.bug_id – $bug.short_desc" %]
[% javascript = BLOCK %]
- if( !document.location.href.match(/show_bug\.cgi/) && history && history.replaceState ) {
- history.replaceState( null,
- "[% unfiltered_title FILTER js %]",
- "show_bug.cgi?id=[% bug.bug_id FILTER js %]" );
- document.title = "[% unfiltered_title FILTER js %]";
+ if (history && history.replaceState) {
+ if(!document.location.href.match(/show_bug\.cgi/)) {
+ history.replaceState( null,
+ "[% unfiltered_title FILTER js %]",
+ "show_bug.cgi?id=[% bug.bug_id FILTER js %]" );
+ document.title = "[% unfiltered_title FILTER js %]";
+ }
+ if (document.location.href.match(/show_bug\.cgi\?.*list_id=/)) {
+ var href = document.location.href;
+ href = href.replace(/[\?&]+list_id=(\d+|cookie)/, '');
+ history.replaceState(null, "[% unfiltered_title FILTER js %]", href);
+ }
}
+ YAHOO.util.Event.onDOMReady(function() {
+ initDirtyFieldTracking();
+ });
[% javascript FILTER none %]
[% END %]
[% END %]
-[% style_urls = [ "skins/standard/show_bug.css" ] %]
+[% style_urls = [ "skins/standard/show_bug.css",
+ "skins/custom/bug_groups.css" ] %]
[% doc_section = "bug_page.html" %]
[% bodyclasses = ['bz_bug',
"bz_status_$bug.bug_status",
diff --git a/template/en/default/bug/show-multiple.html.tmpl b/template/en/default/bug/show-multiple.html.tmpl
index 7c2b5345e..207b3ed86 100644
--- a/template/en/default/bug/show-multiple.html.tmpl
+++ b/template/en/default/bug/show-multiple.html.tmpl
@@ -192,6 +192,8 @@
[% USE Bugzilla %]
[% field_counter = 0 %]
[% FOREACH field = Bugzilla.active_custom_fields %]
+ [% NEXT IF cf_hidden_in_product(field.name, bug.product, bug.component) %]
+ [% NEXT IF cf_flag_disabled(field.name, bug) %]
[% field_counter = field_counter + 1 %]
[%# Odd-numbered fields get an opening <tr> %]
[% '<tr>' IF field_counter % 2 %]
diff --git a/template/en/default/bug/show.xml.tmpl b/template/en/default/bug/show.xml.tmpl
index dae207f26..cb323d229 100644
--- a/template/en/default/bug/show.xml.tmpl
+++ b/template/en/default/bug/show.xml.tmpl
@@ -20,8 +20,10 @@
#
#%]
[% PROCESS bug/time.html.tmpl %]
+[% USE Bugzilla %]
+[% cgi = Bugzilla.cgi %]
<?xml version="1.0" [% IF Param('utf8') %]encoding="UTF-8" [% END %]standalone="yes" ?>
-<!DOCTYPE bugzilla SYSTEM "[% urlbase FILTER html %]bugzilla.dtd">
+<!DOCTYPE bugzilla [% IF cgi.param('dtd') %][[% PROCESS pages/bugzilla.dtd.tmpl %]][% ELSE %]SYSTEM "[% urlbase FILTER xml %]page.cgi?id=bugzilla.dtd"[% END %]>
<bugzilla version="[% constants.BUGZILLA_VERSION %]"
urlbase="[% urlbase FILTER xml %]"
@@ -142,6 +144,7 @@
[% ELSIF field == "see_also" %]
[% val = val.name %]
[% END %]
+ [% NEXT IF cf_hidden_in_product(field.name, bug.product, bug.component) %]
<[% field %][% IF name != '' %] name="[% name FILTER xml %]"[% END -%]>
[%- val FILTER xml %]</[% field %]>
[% END %]
diff --git a/template/en/default/config.rdf.tmpl b/template/en/default/config.rdf.tmpl
index 15f784ce8..5686d138b 100644
--- a/template/en/default/config.rdf.tmpl
+++ b/template/en/default/config.rdf.tmpl
@@ -168,12 +168,12 @@
<bz:component rdf:about="[% escaped_urlbase %]component.cgi?name=[% component.name FILTER uri
%]&amp;product=[% product.name FILTER uri %]">
<bz:name>[% component.name FILTER html %]</bz:name>
+ <bz:is_active>[% component.is_active FILTER html %]</bz:is_active>
[% IF show_flags %]
<bz:flag_types>
<Seq>
[% flag_types = component.flag_types.bug.merge(component.flag_types.attachment) %]
[% FOREACH flag_type = flag_types %]
- [% NEXT UNLESS flag_type.is_active %]
[% all_visible_flag_types.${flag_type.id} = flag_type %]
<li resource="[% escaped_urlbase %]flag.cgi?id=[% flag_type.id FILTER uri
%]&amp;name=[% flag_type.name FILTER uri %]" />
@@ -195,6 +195,7 @@
<li>
<bz:version rdf:about="[% escaped_urlbase %]version.cgi?name=[% version.name FILTER uri %]">
<bz:name>[% version.name FILTER html %]</bz:name>
+ <bz:is_active>[% version.is_active FILTER html %]</bz:is_active>
</bz:version>
</li>
[% END %]
@@ -210,6 +211,7 @@
<li>
<bz:target_milestone rdf:about="[% escaped_urlbase %]milestone.cgi?name=[% milestone.name FILTER uri %]">
<bz:name>[% milestone.name FILTER html %]</bz:name>
+ <bz:is_active>[% milestone.is_active FILTER html %]</bz:is_active>
</bz:target_milestone>
</li>
[% END %]
diff --git a/template/en/default/email/bugmail-header.txt.tmpl b/template/en/default/email/bugmail-header.txt.tmpl
index 94559a942..1cbc8864b 100644
--- a/template/en/default/email/bugmail-header.txt.tmpl
+++ b/template/en/default/email/bugmail-header.txt.tmpl
@@ -23,10 +23,12 @@
[% PROCESS "global/field-descs.none.tmpl" %]
[% PROCESS "global/reason-descs.none.tmpl" %]
[% isnew = bug.lastdiffed ? 0 : 1 %]
+[% show_new = isnew
+ && (to_user.settings.bugmail_new_prefix.value == 'on') %]
From: [% Param('mailfrom') %]
To: [% to_user.email %]
-Subject: [[% terms.Bug %] [%+ bug.id %]] [% 'New: ' IF isnew %][%+ bug.short_desc %]
+Subject: [[% terms.Bug %] [%+ bug.id %]] [% 'New: ' IF show_new %][%+ bug.short_desc %]
Date: [% date %]
X-Bugzilla-Reason: [% reasonsheader %]
X-Bugzilla-Type: [% isnew ? 'new' : 'changed' %]
@@ -40,8 +42,10 @@ X-Bugzilla-Keywords: [% bug.keywords %]
X-Bugzilla-Severity: [% bug.bug_severity %]
X-Bugzilla-Who: [% changer.login %]
X-Bugzilla-Status: [% bug.bug_status %]
+X-Bugzilla-Resolution: [% bug.resolution %]
X-Bugzilla-Priority: [% bug.priority %]
X-Bugzilla-Assigned-To: [% bug.assigned_to.login %]
X-Bugzilla-Target-Milestone: [% bug.target_milestone %]
X-Bugzilla-Changed-Fields: [% changedfields.join(" ") %]
+X-Bugzilla-Changed-Field-Names: [% changedfieldnames.join(" ") %]
[%+ threadingmarker %]
diff --git a/template/en/default/email/bugmail.html.tmpl b/template/en/default/email/bugmail.html.tmpl
index d52fe6306..88c935d87 100644
--- a/template/en/default/email/bugmail.html.tmpl
+++ b/template/en/default/email/bugmail.html.tmpl
@@ -40,9 +40,24 @@
</div>
[% END %]
</p>
+
+ [% IF referenced_bugs.size %]
+ <hr>
+ <span>Referenced [% terms.Bugs %]:</span>
+
+ <ul>
+ [% FOREACH ref = referenced_bugs %]
+ <li>
+ [<a href="[% urlbase FILTER html %]show_bug.cgi?id=[% ref.id FILTER none %]">
+ [% terms.Bug %]&nbsp;[% ref.id FILTER none %]</a>] [% ref.short_desc FILTER html %]
+ </li>
+ [% END %]
+ </ul>
+ [% END %]
+
<hr>
<span>You are receiving this mail because:</span>
-
+
<ul>
[% FOREACH reason = reasons %]
[% IF reason_descs.$reason %]
diff --git a/template/en/default/email/bugmail.txt.tmpl b/template/en/default/email/bugmail.txt.tmpl
index 0b349fb15..fed0565c7 100644
--- a/template/en/default/email/bugmail.txt.tmpl
+++ b/template/en/default/email/bugmail.txt.tmpl
@@ -34,6 +34,15 @@
[% END %]
[%+ comment.body_full({ is_bugmail => 1, wrap => 1 }) %]
[% END %]
+[% IF referenced_bugs.size %]
+
+Referenced [% terms.Bugs %]:
+
+[% FOREACH ref = referenced_bugs %]
+[%+ urlbase %]show_bug.cgi?id=[% ref.id %]
+[%+ "[" _ terms.Bug _ " " _ ref.id _ "] " _ ref.short_desc FILTER wrap_comment(76) %]
+[% END %]
+[% END %]
-- [%# Protect the trailing space of the signature marker %]
You are receiving this mail because:
diff --git a/template/en/default/email/lockout.txt.tmpl b/template/en/default/email/lockout.txt.tmpl
index ac6525779..94e9c74cb 100644
--- a/template/en/default/email/lockout.txt.tmpl
+++ b/template/en/default/email/lockout.txt.tmpl
@@ -22,10 +22,10 @@
From: [% Param('mailfrom') %]
To: [% Param('maintainer') %]
-Subject: [[% terms.Bugzilla %]] Account Lock-Out: [% locked_user.login %] ([% attempts.0.ip_addr %])
+Subject: [[% terms.Bugzilla %]] Account Lock-Out: [% locked_user.login %] ([% address %])
X-Bugzilla-Type: admin
-The IP address [% attempts.0.ip_addr %] failed too many login attempts (
+The address [% address %] failed too many login attempts (
[%- constants.MAX_LOGIN_ATTEMPTS +%]) for
the account [% locked_user.login %].
diff --git a/template/en/default/filterexceptions.pl b/template/en/default/filterexceptions.pl
index d804ad8fa..917dc85ae 100644
--- a/template/en/default/filterexceptions.pl
+++ b/template/en/default/filterexceptions.pl
@@ -52,7 +52,6 @@
],
'flag/list.html.tmpl' => [
- 'flag.id',
'flag.status',
'type.id',
],
@@ -320,7 +319,6 @@
'attachment/edit.html.tmpl' => [
'attachment.id',
'attachment.bug_id',
- 'a',
'editable_or_hide',
],
diff --git a/template/en/default/flag/list.html.tmpl b/template/en/default/flag/list.html.tmpl
index 4467e81ce..e670515e0 100644
--- a/template/en/default/flag/list.html.tmpl
+++ b/template/en/default/flag/list.html.tmpl
@@ -51,73 +51,13 @@
[%-# Step 1a: Display existing flag(s). %]
[% FOREACH flag = type.flags %]
- <tr>
- <td>
- <span title="[% flag.setter.identity FILTER html %]">[% flag.setter.nick FILTER html %]</span>:
- </td>
- <td>
- <label title="[% type.description FILTER html %]"
- for="flag-[% flag.id %]">
- [%- type.name FILTER html FILTER no_break -%]</label>
- </td>
- <td>
- <select id="flag-[% flag.id %]" name="flag-[% flag.id %]"
- title="[% type.description FILTER html %]"
- onchange="toggleRequesteeField(this);"
- class="flag_select flag_type-[% type.id %]">
- [%# Only display statuses the user is allowed to set. %]
- [% IF user.can_request_flag(type) || flag.setter_id == user.id %]
- <option value="X"></option>
- [% END %]
- [% IF type.is_active %]
- [% IF (type.is_requestable && user.can_request_flag(type)) || flag.status == "?" %]
- <option value="?" [% "selected" IF flag.status == "?" %]>?</option>
- [% END %]
- [% IF user.can_set_flag(type) || flag.status == "+" %]
- <option value="+" [% "selected" IF flag.status == "+" %]>+</option>
- [% END %]
- [% IF user.can_set_flag(type) || flag.status == "-" %]
- <option value="-" [% "selected" IF flag.status == "-" %]>-</option>
- [% END %]
- [% ELSE %]
- <option value="[% flag.status %]" selected="selected">[% flag.status %]</option>
- [% END %]
- </select>
- </td>
- [% IF any_flags_requesteeble %]
- <td>
- [% IF (type.is_active && type.is_requestable && type.is_requesteeble) || flag.requestee %]
- <span style="white-space: nowrap;">
- [% SET flag_custom_list = [] %]
- [% IF Param('usemenuforusers') %]
- [% flag_custom_list = flag.type.grant_list %]
- [% IF !(type.is_active && type.is_requestable && type.is_requesteeble) %]
- [%# We are here only because there was already a requestee. In this case,
- the only valid action is to remove the requestee or leave it alone;
- nothing else. %]
- [% flag_custom_list = [flag.requestee] %]
- [% END %]
- [% END %]
- [% INCLUDE global/userselect.html.tmpl
- name => "requestee-$flag.id"
- id => "requestee-$flag.id"
- value => flag.requestee.login
- multiple => 0
- emptyok => 1
- classes => ["requestee"]
- custom_userlist => flag_custom_list
- %]
- </span>
- [% END %]
- </td>
- [% END %]
- </tr>
+ [% PROCESS flag_row flag = flag type = type %]
[% END -%]
+ [% SET flag = "" %]
[%-# Step 1b: Display UI for setting flag. %]
[% IF (!type.flags || type.flags.size == 0) && type.is_active %]
-
- [% PROCESS flag_row first_cell_empty = 1 addl_text = "" %]
+ [% PROCESS flag_row type = type %]
[% END %]
[% END %]
@@ -125,11 +65,12 @@
[% FOREACH type = flag_types %]
[% NEXT UNLESS type.flags && type.flags.size > 0 && type.is_multiplicable && type.is_active %]
[% IF !separator_displayed %]
+ <tbody class="bz_flag_type">
<tr><td colspan="3"><hr></td></tr>
- [% separator_displayed = 1 %]
+ </tbody>
+ [% separator_displayed = 1 %]
[% END %]
-
- [% PROCESS flag_row first_cell_empty = 0 addl_text = "addl." %]
+ [% PROCESS flag_row type = type addl_text = "addl." %]
[% END %]
</table>
@@ -159,58 +100,82 @@
[% END %]
[% END %]
-[%# Display a table row for unset flags %]
+[%# Display a table row for flags %]
[% BLOCK flag_row %]
- <tr>
- [% IF first_cell_empty %]
- <td>&nbsp;</td>
- <td>
- [% ELSE %]
- <td colspan="2">
- [% END %]
-
- [% addl_text FILTER html %]
- <label title="[% type.description FILTER html %]" for="flag_type-[% type.id %]">
- [%- type.name FILTER html FILTER no_break %]</label>
- </td>
- <td>
- <select id="flag_type-[% type.id %]" name="flag_type-[% type.id %]"
- title="[% type.description FILTER html %]"
- [% " disabled=\"disabled\"" UNLESS (type.is_requestable && user.can_request_flag(type)) || user.can_set_flag(type) %]
- onchange="toggleRequesteeField(this);"
- class="flag_select flag_type-[% type.id %]">
- <option value="X"></option>
- [% IF type.is_requestable && user.can_request_flag(type) %]
- <option value="?">?</option>
- [% END %]
- [% IF user.can_set_flag(type) %]
- <option value="+">+</option>
- <option value="-">-</option>
+ [% SET fid = flag ? "flag-$flag.id" : "flag_type-$type.id" %]
+ <tbody[% ' class="bz_flag_type"' IF !flag %]>
+ <tr>
+ <td>
+ [% IF flag %]
+ <span title="[% flag.setter.identity FILTER html %]">[% flag.setter.nick FILTER html %]</span>:
+ [% ELSE %]
+ [% addl_text FILTER html %]
[% END %]
- </select>
- </td>
- [% IF any_flags_requesteeble %]
+ </td>
<td>
- [% IF type.is_requestable && type.is_requesteeble %]
- <span style="white-space: nowrap;">
- [% SET grant_list = [] %]
- [% IF Param('usemenuforusers') %]
- [% grant_list = type.grant_list %]
- [% END %]
- [% INCLUDE global/userselect.html.tmpl
- name => "requestee_type-$type.id"
- id => "requestee_type-$type.id"
- multiple => type.is_multiplicable * 3
- emptyok => !type.is_multiplicable
- value => ""
- custom_userlist => grant_list
- classes => ["requestee"]
- %]
-
- </span>
+ <label title="[% type.description FILTER html %]" for="[% fid FILTER html %]">
+ [%- type.name FILTER html FILTER no_break -%]</label>
+ </td>
+ <td>
+ <input type="hidden" id="[% fid FILTER html %]_dirty">
+ <select id="[% fid FILTER html %]" name="[% fid FILTER html %]"
+ [% IF !flag && !((type.is_requestable && user.can_request_flag(type)) || user.can_set_flag(type)) %]
+ disabled="disabled"
+ [% END %]
+ title="[% type.description FILTER html %]"
+ onchange="toggleRequesteeField(this);"
+ class="flag_select flag_type-[% type.id %]">
+ [%# Only display statuses the user is allowed to set. %]
+ [% IF !flag || user.can_request_flag(type) || flag.setter_id == user.id %]
+ <option value="X"></option>
+ [% END %]
+ [% IF type.is_active %]
+ [% IF (type.is_requestable && user.can_request_flag(type)) || (flag && flag.status == "?") %]
+ <option value="?" [% "selected" IF flag && flag.status == "?" %]>?</option>
+ [% END %]
+ [% IF user.can_set_flag(type) || (flag && flag.status == "+") %]
+ <option value="+" [% "selected" IF flag && flag.status == "+" %]>+</option>
+ [% END %]
+ [% IF user.can_set_flag(type) || (flag && flag.status == "-") %]
+ <option value="-" [% "selected" IF flag && flag.status == "-" %]>-</option>
+ [% END %]
+ [% ELSE %]
+ <option value="[% flag.status %]" selected="selected">[% flag.status %]</option>
[% END %]
+ </select>
</td>
- [% END %]
- </tr>
+ [% IF any_flags_requesteeble %]
+ <td>
+ [% IF (type.is_active && type.is_requestable && type.is_requesteeble) || (flag && flag.requestee) %]
+ <span style="white-space: nowrap;">
+ [% SET grant_list = [] %]
+ [% IF Param('usemenuforusers') %]
+ [% grant_list = type.grant_list %]
+ [% IF flag && !(type.is_active && type.is_requestable && type.is_requesteeble) %]
+ [%# We are here only because there was already a requestee. In this case,
+ the only valid action is to remove the requestee or leave it alone;
+ nothing else. %]
+ [% grant_list = [flag.requestee] %]
+ [% END %]
+ [% END %]
+ [% SET flag_name = flag ? "requestee-$flag.id" : "requestee_type-$type.id" %]
+ [% SET flag_requestee = (flag && flag.requestee) ? flag.requestee.login : '' %]
+ [% SET flag_multiple = flag ? 0 : type.is_multiplicable * 3 %]
+ [% SET flag_empty_ok = flag ? 1 : !type.is_multiplicable %]
+ [% INCLUDE global/userselect.html.tmpl
+ name => flag_name
+ id => flag_name
+ value => flag_requestee
+ multiple => flag_multiple
+ emptyok => flag_empty_ok
+ classes => ["requestee"]
+ custom_userlist => grant_list
+ %]
+ </span>
+ [% END %]
+ </td>
+ [% END %]
+ </tr>
+ </tbody>
[% END %]
diff --git a/template/en/default/global/code-error.html.tmpl b/template/en/default/global/code-error.html.tmpl
index 24e46fb14..ffb39c160 100644
--- a/template/en/default/global/code-error.html.tmpl
+++ b/template/en/default/global/code-error.html.tmpl
@@ -506,31 +506,23 @@
admindocslinks = admindocslinks
%]
-<tt>
- <p>
- [% terms.Bugzilla %] has suffered an internal error. Please save this page and send
- it to [% Param("maintainer") %] with details of what you were doing at
- the time this message appeared.
- </p>
- <script type="text/javascript"> <!--
- document.write("<p>URL: " +
- document.location.href.replace(/&/g,"&amp;")
- .replace(/</g,"&lt;")
- .replace(/>/g,"&gt;") + "</p>");
- // -->
- </script>
-</tt>
-
-<table cellpadding="20">
- <tr>
- <td id="error_msg" class="throw_error">
- [% error_message FILTER none %]
- </td>
- </tr>
-</table>
-
-<p>Traceback:</p>
-<pre>[% traceback FILTER html %]</pre>
+[%# return the generated error_message for arecibo %]
+[% processed.error_message = error_message %]
+
+<p>
+ [% terms.Bugzilla %] has suffered an internal error:
+</p>
+
+<p class="throw_error">
+ [% error_message FILTER none %]
+</p>
+
+[% IF maintainers_notified %]
+<p>
+ The [% terms.Bugzilla %] maintainers have been notified of this error
+ [#[% uid FILTER html %]].
+</p>
+[% END %]
[% IF variables %]
<pre>
diff --git a/template/en/default/global/common-links.html.tmpl b/template/en/default/global/common-links.html.tmpl
index 769d41e7e..ec8608eed 100644
--- a/template/en/default/global/common-links.html.tmpl
+++ b/template/en/default/global/common-links.html.tmpl
@@ -55,6 +55,8 @@
[% END %]
[%-# Work around FF bug: keep this on one line %]</li>
+ [% Hook.process('action-links') %]
+
[% IF user.login %]
<li><span class="separator">| </span><a href="userprefs.cgi">Preferences</a></li>
[% IF user.in_group('tweakparams') || user.in_group('editusers') || user.can_bless
diff --git a/template/en/default/global/header.html.tmpl b/template/en/default/global/header.html.tmpl
index a7449883f..480197431 100644
--- a/template/en/default/global/header.html.tmpl
+++ b/template/en/default/global/header.html.tmpl
@@ -239,8 +239,7 @@
[%# Required for the 'Autodiscovery' feature in Firefox 2 and IE 7. %]
<link rel="search" type="application/opensearchdescription+xml"
- title="[% terms.Bugzilla %]" href="./search_plugin.cgi">
- <link rel="shortcut icon" href="images/favicon.ico" >
+ title="[% terms.BugzillaTitle %]" href="./search_plugin.cgi">
[% Hook.process("additional_header") %]
</head>
@@ -265,7 +264,7 @@
<table border="0" cellspacing="0" cellpadding="0" id="titles">
<tr>
<td id="title">
- <p>[% terms.Bugzilla %]
+ <p>[% terms.BugzillaTitle %]
[% " &ndash; $header" IF header %]</p>
</td>
diff --git a/template/en/default/global/setting-descs.none.tmpl b/template/en/default/global/setting-descs.none.tmpl
index a0b11f048..37d81039e 100644
--- a/template/en/default/global/setting-descs.none.tmpl
+++ b/template/en/default/global/setting-descs.none.tmpl
@@ -52,6 +52,8 @@
"email_format" => "Preferred email format",
"html" => "HTML",
"text_only" => "Text Only",
+ "bugmail_new_prefix" => "Add 'New:' to subject line of email sent when a new $terms.bug is filed",
+ "requestee_cc" => "Automatically add me to the CC list of $terms.bugs I am requested to review",
}
%]
diff --git a/template/en/default/global/user-error.html.tmpl b/template/en/default/global/user-error.html.tmpl
index 4269d693d..c2b2ceb28 100644
--- a/template/en/default/global/user-error.html.tmpl
+++ b/template/en/default/global/user-error.html.tmpl
@@ -160,6 +160,8 @@
use
[% ELSIF action == "approve" %]
approve
+ [% ELSIF action == "admin_activity" %]
+ view admin activity for
[% ELSE %]
[%+ Hook.process('auth_failure_action') %]
[% END %]
@@ -270,6 +272,7 @@
<li>A ticket in a Trac installation.</li>
<li>A b[% %]ug in a MantisBT installation.</li>
<li>A b[% %]ug on sourceforge.net.</li>
+ <li>An issue on github.com.</li>
</ul>
[% ELSIF reason == 'id' %]
There is no valid [% terms.bug %] id in that URL.
@@ -1350,6 +1353,40 @@
[% END %]
</ul>
+ [% ELSIF error == "password_not_complex" %]
+ [% title = "Password Fails Requirements" %]
+ [% passregex = Param('password_complexity') %]
+ Password must contain at least one:
+ <ul>
+ [% IF passregex.search('letters') %]
+ <li>UPPERCASE letter</li>
+ <li>lowercase letter</li>
+ [% END %]
+ [% IF passregex.search('numbers') %]
+ <li>digit</li>
+ [% END %]
+ [% IF passregex.search('specialchars') %]
+ <li>special character</li>
+ [% END %]
+ </ul>
+
+ [% ELSIF error == "password_not_complex" %]
+ [% title = "Password Fails Requirements" %]
+ [% passregex = Param('password_complexity') %]
+ Password must contain at least one:
+ <ul>
+ [% IF passregex.search('letters') %]
+ <li>UPPERCASE letter</li>
+ <li>lowercase letter</li>
+ [% END %]
+ [% IF passregex.search('numbers') %]
+ <li>digit</li>
+ [% END %]
+ [% IF passregex.search('specialchars') %]
+ <li>special character</li>
+ [% END %]
+ </ul>
+
[% ELSIF error == "product_access_denied" %]
[% title = "Product Access Denied" %]
Either the product
@@ -1759,6 +1796,8 @@
[% error_message FILTER none %]
[% END %]
[% END %]
+
+ [% Hook.process('error_message') %]
[% END %]
[%# We only want HTML error messages for ERROR_MODE_WEBPAGE %]
diff --git a/template/en/default/global/user.html.tmpl b/template/en/default/global/user.html.tmpl
index df902b451..4f9b8a41b 100644
--- a/template/en/default/global/user.html.tmpl
+++ b/template/en/default/global/user.html.tmpl
@@ -27,6 +27,10 @@
[% FILTER collapse %]
[% IF user.id %]
<a class="email" href="mailto:[% who.email FILTER html %]"
+ [% IF who.id && user.in_group('canconfirm') %]
+ onclick="return show_usermenu(event, [% who.id FILTER none %], '[% who.email FILTER js %]',
+ [% IF (user.in_group('editusers') || user.bless_groups.size > 0) %]true[% ELSE %]false[% END %]);"
+ [% END %]
title="[% who.identity FILTER html %]">
[%- END -%]
[% IF who.name %]
diff --git a/template/en/default/index.html.tmpl b/template/en/default/index.html.tmpl
index 5b9237aa1..29bc9adb6 100644
--- a/template/en/default/index.html.tmpl
+++ b/template/en/default/index.html.tmpl
@@ -125,30 +125,20 @@ YAHOO.util.Event.onDOMReady(onLoadActions);
<td>
<h1 id="welcome"> Welcome to [% terms.Bugzilla %]</h1>
<div class="intro">[% Hook.process('intro') %]</div>
-
- <div class="bz_common_actions">
- <ul>
- <li>
- <a id="enter_bug" href="enter_bug.cgi"><span>File
- [%= terms.aBug %]</span></a>
- </li>
- <li>
- <a id="query" href="query.cgi"><span>Search</span></a>
- </li>
- <li>
- <a id="account"
- [% IF user.id %]
- href="userprefs.cgi"><span>User Preferences</span></a>
- [% ELSIF Param('createemailregexp')
- && user.authorizer.user_can_create_account
- %]
- href="createaccount.cgi"><span>Open a New Account</span></a>
- [% ELSE %]
- href="?GoAheadAndLogIn=1"><span>Log In</span></a>
- [% END %]
- </li>
- </ul>
- </div>
+ <a id="enter_bug" class="bz_common_actions"
+ href="enter_bug.cgi"><span>File [% terms.aBug %]</span></a>
+ <a id="query" class="bz_common_actions"
+ href="query.cgi"><span>Search</span></a>
+ <a id="account" class="bz_common_actions"
+ [% IF user.id %]
+ href="userprefs.cgi"><span>User Preferences</span></a>
+ [% ELSIF Param('createemailregexp')
+ && user.authorizer.user_can_create_account
+ %]
+ href="createaccount.cgi"><span>Open a New Account</span></a>
+ [% ELSE %]
+ href="?GoAheadAndLogIn=1"><span>Log In</span></a>
+ [% END %]
<form id="quicksearchForm" name="quicksearchForm" action="buglist.cgi"
onsubmit="return checkQuicksearch(this);">
diff --git a/template/en/default/list/edit-multiple.html.tmpl b/template/en/default/list/edit-multiple.html.tmpl
index 92e578e8f..7c7d99408 100644
--- a/template/en/default/list/edit-multiple.html.tmpl
+++ b/template/en/default/list/edit-multiple.html.tmpl
@@ -282,8 +282,9 @@
[% USE Bugzilla %]
[%# Show all legal values and all fields, ignoring visibility controls. %]
- [% bug = 0 %]
+ [% bug = default.defined ? default : 0 %]
[% FOREACH field = Bugzilla.active_custom_fields %]
+ [% NEXT IF cf_hidden_in_product(field.name, one_product, components) %]
<tr>
[% PROCESS bug/field.html.tmpl value = dontchange
editable = 1
@@ -427,6 +428,7 @@
[% FOREACH r = resolutions %]
[% NEXT IF !r %]
[% NEXT IF r == "DUPLICATE" || r == "MOVED" %]
+ [% NEXT IF r == "EXPIRED" AND user.login != "gerv@mozilla.org" %]
<option value="[% r FILTER html %]">[% display_value("resolution", r) FILTER html %]</option>
[% END %]
</select>
diff --git a/template/en/default/list/list.html.tmpl b/template/en/default/list/list.html.tmpl
index 4eeff5e64..a21117d34 100644
--- a/template/en/default/list/list.html.tmpl
+++ b/template/en/default/list/list.html.tmpl
@@ -42,10 +42,11 @@
[%# Page Header #%]
[%############################################################################%]
+[% url_filtered_title = title FILTER uri %]
[% PROCESS global/header.html.tmpl
title = title
style = style
- atomlink = "buglist.cgi?$urlquerypart&title=$title&ctype=atom"
+ atomlink = "buglist.cgi?$urlquerypart&title=$url_filtered_title&ctype=atom"
yui = [ 'autocomplete', 'calendar' ]
javascript_urls = [ "js/util.js", "js/field.js" ]
style_urls = [ "skins/standard/buglist.css" ]
@@ -58,10 +59,13 @@
</span>
[% IF debug %]
- <p class="bz_query">[% query FILTER html %]</p>
- [% IF query_explain.defined %]
- <pre class="bz_query_explain">[% query_explain FILTER html %]</pre>
- [% END %]
+ <div class="bz_query_debug">
+ <p>[% query FILTER html %]</p>
+ <p>Execution time: [% query_time FILTER html %] seconds</p>
+ [% IF query_explain.defined %]
+ <pre>[% query_explain FILTER html %]</pre>
+ [% END %]
+ </div>
[% END %]
[% IF user.settings.display_quips.value == 'on' %]
@@ -205,7 +209,7 @@
[% urlquerypart FILTER html %]&amp;ctype=csv&amp;human=1">CSV</a> |
<a href="buglist.cgi?
[% urlquerypart FILTER html %]&amp;title=
- [%- title FILTER html %]&amp;ctype=atom">Feed</a> |
+ [%- title FILTER uri %]&amp;ctype=atom">Feed</a> |
<a href="buglist.cgi?
[% urlquerypart FILTER html %]&amp;ctype=ics">iCalendar</a> |
<a href="colchange.cgi?
diff --git a/template/en/default/list/table.html.tmpl b/template/en/default/list/table.html.tmpl
index a074fcbd0..547a9cbe3 100644
--- a/template/en/default/list/table.html.tmpl
+++ b/template/en/default/list/table.html.tmpl
@@ -80,12 +80,15 @@
[%############################################################################%]
[% tableheader = BLOCK %]
- <table class="bz_buglist" cellspacing="0" cellpadding="4" width="100%">
+ <table class="bz_buglist sortable" cellspacing="0" cellpadding="4" width="100%">
+ <thead>
<tr class="bz_buglist_header bz_first_buglist_header">
[% IF dotweak %]
<th>&nbsp;</th>
[% END %]
- <th colspan="[% splitheader ? 2 : 1 %]" class="first-child">
+ <th colspan="[% splitheader ? 2 : 1 %]" class="first-child
+ sortable_column_0
+ sorted_[% lsearch(order_columns, 'bug_id') FILTER html %]">
<a href="buglist.cgi?
[% urlquerypart FILTER html %]&amp;order=
[% PROCESS new_order id='bug_id' %]
@@ -100,7 +103,7 @@
[% FOREACH id = displaycolumns %]
[% NEXT UNLESS loop.count() % 2 == 0 %]
[% column = columns.$id %]
- [% PROCESS columnheader %]
+ [% PROCESS columnheader key=loop.count() %]
[% END %]
</tr><tr class="bz_buglist_header">
@@ -112,7 +115,7 @@
[% FOREACH id = displaycolumns %]
[% NEXT IF loop.count() % 2 == 0 %]
[% column = columns.$id %]
- [% PROCESS columnheader %]
+ [% PROCESS columnheader key=loop.count() %]
[% END %]
[% ELSE %]
@@ -125,10 +128,13 @@
[% END %]
</tr>
+ </thead>
[% END %]
[% BLOCK columnheader %]
- <th colspan="[% splitheader ? 2 : 1 %]">
+ <th colspan="[% splitheader ? 2 : 1 %]"
+ class="sortable_column_[% key FILTER html %]
+ sorted_[% lsearch(order_columns, id) FILTER html %]">
<a href="buglist.cgi?[% urlquerypart FILTER html %]&amp;order=
[% PROCESS new_order %]
[%-#%]&amp;query_based_on=
@@ -168,6 +174,7 @@
[% tableheader %]
+<tbody class="sorttable_body">
[% FOREACH bug = bugs %]
[% count = loop.count() %]
@@ -193,7 +200,17 @@
[% FOREACH column = displaycolumns %]
<td [% 'style="white-space: nowrap"' IF NOT abbrev.$column.wrap %]
- class="bz_[% column FILTER css_class_quote %]_column">
+ class="bz_[% column FILTER css_class_quote %]_column"
+ [% SWITCH column %]
+ [% CASE 'opendate' %]
+ sorttable_customkey="[% bug.opentime FILTER html %]"
+ [% CASE 'changeddate' %]
+ sorttable_customkey="[% bug.changedtime FILTER html %]"
+ [% CASE columns_sortkey.keys %]
+ [% SET sortkey = columns_sortkey.$column.${bug.$column} %]
+ sorttable_customkey="[% sortkey FILTER html %]"
+ [% END %]
+ >
[% IF abbrev.$column.maxlength %]
<span title="[%- display_value(column, bug.$column) FILTER html %]">
[% END %]
@@ -228,6 +245,7 @@
[% END %]
[% END %]
+</tbody>
</table>
diff --git a/template/en/default/pages/bugzilla.dtd.tmpl b/template/en/default/pages/bugzilla.dtd.tmpl
new file mode 100644
index 000000000..f7fc1b4ad
--- /dev/null
+++ b/template/en/default/pages/bugzilla.dtd.tmpl
@@ -0,0 +1,179 @@
+[%# 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): Dawn Endico <endico@mozilla.org>
+ # Dave Miller <justdave@syndicomm.com>
+ # Bradley Baetz <bbaetz@student.usyd.edu.au>
+ # Myk Mylez <myk@mozilla.org>
+ # Colin Ogilvie <mozilla@colinogilvie.co.uk>
+ # Joel Peshkin <bugreport@peshkin.net>
+ # Frédéric Buclin <LpSolit@gmail.com>
+ # Gervase Markham <gerv@gerv.net>
+ # Max Kanat-Alexander <mkanat@bugzilla.org>
+ # David Lawrence <dkl@mozilla.com>
+ #
+ #%]
+[% USE Bugzilla %]
+<!ELEMENT [% "bugzilla" %] (bug+)>
+<!ATTLIST [% "bugzilla" %]
+ version CDATA #REQUIRED
+ urlbase CDATA #REQUIRED
+ maintainer CDATA #REQUIRED
+ exporter CDATA #IMPLIED
+>
+<!ELEMENT [% "bug" %] (bug_id,
+ (alias?,
+ creation_ts,
+ short_desc,
+ delta_ts,
+ reporter_accessible,
+ cclist_accessible,
+ classification_id,
+ classification,
+ product,
+ component,
+ version,
+ rep_platform,
+ op_sys,
+ bug_status,
+ resolution?,
+ dup_id?,
+ see_also*,
+ bug_file_loc?,
+ status_whiteboard?,
+ keywords*,
+ priority,
+ bug_severity,
+ target_milestone?,
+ dependson*,
+ blocked*,
+ everconfirmed,
+ reporter,
+ assigned_to,
+ cc*,
+ (estimated_time,
+ remaining_time,
+ actual_time,
+ deadline?)?,
+ qa_contact?,
+[% FOREACH field = Bugzilla.active_custom_fields %]
+ [%+ field.name FILTER xml -%]
+ [%- IF field.type == constants.FIELD_TYPE_MULTI_SELECT %]*[% ELSE %]?[% END %],
+[% END %]
+ votes?,
+ token?,
+ group*,
+ flag*,
+ long_desc*,
+ attachment*)?)>
+<!ATTLIST [% "bug" %]
+ error (NotFound | NotPermitted | InvalidBugId) #IMPLIED
+>
+<!ELEMENT bug_id (#PCDATA)>
+<!ELEMENT alias (#PCDATA)>
+<!ELEMENT reporter_accessible (#PCDATA)>
+<!ELEMENT cclist_accessible (#PCDATA)>
+<!ELEMENT exporter (#PCDATA)>
+<!ELEMENT urlbase (#PCDATA)>
+<!ELEMENT bug_status (#PCDATA)>
+<!ELEMENT classification_id (#PCDATA)>
+<!ELEMENT classification (#PCDATA)>
+<!ELEMENT product (#PCDATA)>
+<!ELEMENT priority (#PCDATA)>
+<!ELEMENT version (#PCDATA)>
+<!ELEMENT rep_platform (#PCDATA)>
+<!ELEMENT assigned_to (#PCDATA)>
+<!ATTLIST assigned_to
+ name CDATA #REQUIRED
+>
+<!ELEMENT delta_ts (#PCDATA)>
+<!ELEMENT component (#PCDATA)>
+<!ELEMENT reporter (#PCDATA)>
+<!ATTLIST reporter
+ name CDATA #REQUIRED
+>
+<!ELEMENT target_milestone (#PCDATA)>
+<!ELEMENT bug_severity (#PCDATA)>
+<!ELEMENT creation_ts (#PCDATA)>
+<!ELEMENT qa_contact (#PCDATA)>
+<!ATTLIST qa_contact
+ name CDATA #REQUIRED
+>
+<!ELEMENT status_whiteboard (#PCDATA)>
+<!ELEMENT op_sys (#PCDATA)>
+<!ELEMENT resolution (#PCDATA)>
+<!ELEMENT dup_id (#PCDATA)>
+<!ELEMENT bug_file_loc (#PCDATA)>
+<!ELEMENT short_desc (#PCDATA)>
+<!ELEMENT keywords (#PCDATA)>
+<!ELEMENT dependson (#PCDATA)>
+<!ELEMENT blocked (#PCDATA)>
+<!ELEMENT everconfirmed (#PCDATA)>
+<!ELEMENT cc (#PCDATA)>
+<!ELEMENT see_also (#PCDATA)>
+<!ELEMENT votes (#PCDATA)>
+<!ELEMENT token (#PCDATA)>
+<!ELEMENT group (#PCDATA)>
+<!ATTLIST group
+ id CDATA #REQUIRED
+>
+<!ELEMENT estimated_time (#PCDATA)>
+<!ELEMENT remaining_time (#PCDATA)>
+<!ELEMENT actual_time (#PCDATA)>
+<!ELEMENT deadline (#PCDATA)>
+[% FOREACH field = Bugzilla.active_custom_fields %]
+<!ELEMENT [% field.name FILTER xml %] (#PCDATA)>
+[% END %]
+<!ELEMENT long_desc (commentid, attachid?, who, bug_when, work_time?, thetext)>
+<!ATTLIST long_desc
+ isprivate (0|1) #REQUIRED
+>
+<!ELEMENT commentid (#PCDATA)>
+<!ELEMENT who (#PCDATA)>
+<!ATTLIST who
+ name CDATA #REQUIRED
+>
+<!ELEMENT bug_when (#PCDATA)>
+<!ELEMENT work_time (#PCDATA)>
+<!ELEMENT thetext (#PCDATA)>
+<!ELEMENT attachment (attachid, date, delta_ts, desc, filename, type, size, attacher, token?, data?, flag*)>
+<!ATTLIST attachment
+ isobsolete (0|1) #REQUIRED
+ ispatch (0|1) #REQUIRED
+ isprivate (0|1) #REQUIRED
+ isurl (0|1) #REQUIRED
+>
+<!ELEMENT attacher (#PCDATA)>
+<!ELEMENT attachid (#PCDATA)>
+<!ELEMENT date (#PCDATA)>
+<!ELEMENT desc (#PCDATA)>
+<!ELEMENT filename (#PCDATA)>
+<!ELEMENT type (#PCDATA)>
+<!ELEMENT size (#PCDATA)>
+<!ELEMENT data (#PCDATA)>
+<!ATTLIST data
+ encoding (base64) #IMPLIED
+>
+<!ELEMENT flag EMPTY>
+<!ATTLIST flag
+ name CDATA #REQUIRED
+ id CDATA #REQUIRED
+ type_id CDATA #REQUIRED
+ status CDATA #REQUIRED
+ setter CDATA #REQUIRED
+ requestee CDATA #IMPLIED
+>
diff --git a/template/en/default/pages/fields.html.tmpl b/template/en/default/pages/fields.html.tmpl
index 2794e1cc4..568245653 100644
--- a/template/en/default/pages/fields.html.tmpl
+++ b/template/en/default/pages/fields.html.tmpl
@@ -62,34 +62,41 @@
</dt>
<dd class="unconfirmed">
This [% terms.bug %] has recently been added to the database.
- Nobody has confirmed that this [% terms.bug %] is valid. Users
+ Nobody has validated that this [% terms.bug %] is true. Users
who have the "canconfirm" permission set may confirm
- this [% terms.bug %], changing its state to
- <b>[% display_value("bug_status", "CONFIRMED") FILTER html %]</b>.
- Or, it may be directly resolved and marked
+ this [% terms.bug %], changing its state to [% display_value("bug_status", "NEW") FILTER html %]. Or, it may be
+ directly resolved and marked [% display_value("bug_status", "RESOLVED") FILTER html %].
+ </dd>
+ <dt>
+ <b>[% display_value("bug_status", "NEW") FILTER html %]</b>
+ </dt>
+ <dd>
+ This [% terms.bug %] has recently been added to the assignee's
+ list of [% terms.bugs %] and must be processed. [% terms.Bugs %] in
+ this state may be accepted, and become <b>[% display_value("bug_status", "ASSIGNED") FILTER html %]</b>, passed
+ on to someone else, and remain <b>[% display_value("bug_status", "NEW") FILTER html %]</b>, or resolved and marked
<b>[% display_value("bug_status", "RESOLVED") FILTER html %]</b>.
</dd>
- <dt class="confirmed">
- [% display_value("bug_status", "CONFIRMED") FILTER html %]
+ <dt>
+ <b>[% display_value("bug_status", "ASSIGNED") FILTER html %]</b>
</dt>
- <dd class="confirmed">
- This [% terms.bug %] is valid and has recently been filed.
- [%+ terms.Bugs %] in this state become
- <b>[% display_value("bug_status", "IN_PROGRESS") FILTER html %]</b>
- when somebody is working on them, or become resolved and marked
- <b>[% display_value("bug_status", "RESOLVED") FILTER html %]</b>.
+ <dd>
+ This [% terms.bug %] is not yet resolved, but is assigned to the
+ proper person. From here [% terms.bugs %] can be given to another
+ person and become <b>[% display_value("bug_status", "NEW") FILTER html %]</b>, or
+ resolved and become <b>[% display_value("bug_status", "RESOLVED") FILTER html %]</b>.
</dd>
- <dt class="in_progress">
- [% display_value("bug_status", "IN_PROGRESS") FILTER html %]
+ <dt>
+ <b>[% display_value("bug_status", "REOPENED") FILTER html %]</b>
</dt>
- <dd class="in_progress">
- This [% terms.bug %] is not yet resolved, but is assigned to the
- proper person who is working on the [% terms.bug %]. From here,
- [%+ terms.bugs %] can be given to another person and become
- <b>[% display_value("bug_status", "CONFIRMED") FILTER html %]</b>, or
- resolved and become
+ <dd>
+ This [% terms.bug %] was once resolved, but the resolution was
+ deemed incorrect. For example, a <b>[% display_value("resolution", "WORKSFORME") FILTER html %]</b> [% terms.bug %] is
+ <b>[% display_value("bug_status", "REOPENED") FILTER html %]</b> when more information shows up and
+ the [% terms.bug %] is now reproducible. From here [% terms.bugs %] are
+ either marked <b>[% display_value("bug_status", "ASSIGNED") FILTER html %]</b> or
<b>[% display_value("bug_status", "RESOLVED") FILTER html %]</b>.
</dd>
@@ -124,9 +131,10 @@
[% display_value("bug_status", "VERIFIED") FILTER html %]
</dt>
<dd class="verified">
- QA has looked at the [% terms.bug %] and the resolution and
- agrees that the appropriate resolution has been taken. This is
- the final status for [% terms.bugs %].
+ QA has looked at the [% terms.bug %] and the resolution and
+ agrees that the appropriate resolution has been taken.
+ Any zombie [% terms.bugs %] who choose to walk the earth again must
+ do so by becoming <b>[% display_value("bug_status", "REOPENED") FILTER html %]</b>.
</dd>
[% Hook.process('closed-status') %]
@@ -163,10 +171,9 @@
</dt>
<dd class="duplicate">
The problem is a duplicate of an existing [% terms.bug %].
- When [% terms.abug %] is marked as a
- <b>[% display_value("resolution", "DUPLICATE") FILTER html %]</b>,
- you will see which [% terms.bug %] it is a duplicate of,
- next to the resolution.
+ Marking [% terms.abug %] duplicate requires the [% terms.bug %]#
+ of the duplicating [% terms.bug %] and will at least put
+ that [% terms.bug %] number in the description field.
</dd>
<dt class="worksforme">
diff --git a/template/en/default/pages/quicksearch.html.tmpl b/template/en/default/pages/quicksearch.html.tmpl
index 901f05467..c43047d9f 100644
--- a/template/en/default/pages/quicksearch.html.tmpl
+++ b/template/en/default/pages/quicksearch.html.tmpl
@@ -303,6 +303,14 @@
<strong>#</strong><em>value</em>
</td>
</tr>
+ <tr>
+ <td class="field_name">Comment Searching</td>
+ <td class="field_nickname">
+ Allows overriding of the comment searching preference.<br>
+ "<strong>++comments</strong>" will always enable comment searching.<br>
+ "<strong>--comments</strong>" will always disable searching.<br>
+ </td>
+ </tr>
[% IF Param('usestatuswhiteboard') %]
<tr>
<td class="field_name">[% field_descs.short_desc FILTER html %]
diff --git a/template/en/default/reports/components.html.tmpl b/template/en/default/reports/components.html.tmpl
index ef7d5ae6d..b2a21ccc1 100644
--- a/template/en/default/reports/components.html.tmpl
+++ b/template/en/default/reports/components.html.tmpl
@@ -22,6 +22,7 @@
[%# INTERFACE:
# product: object. The product for which we want to display component
# descriptions.
+ # component: string. The name of the component to hilight in the browser
#%]
[% title = BLOCK %]
@@ -39,6 +40,8 @@
[% numcols = 2 %]
[% END %]
+<h2>[% mark FILTER html %]</h2>
+
<table cellpadding="0" cellspacing="0" id="components_header_table">
<tr>
<td class="instructions">
@@ -81,9 +84,11 @@
[%############################################################################%]
[% BLOCK describe_comp %]
- <tr id="[% comp.name FILTER html %]">
+ <tr id="[% comp.name FILTER html %]"
+ [%- IF comp.name == component_mark %] class="component_hilite"[% END %]>
<td rowspan="2" class="component_name">
- <a href="buglist.cgi?product=
+ <a name="[% comp.name FILTER html %]"
+ href="buglist.cgi?product=
[%- product.name FILTER uri %]&amp;component=
[%- comp.name FILTER uri %]&amp;resolution=---">
[% comp.name FILTER html %]</a>
@@ -97,7 +102,7 @@
</td>
[% END %]
</tr>
- <tr>
+ <tr[% IF comp.name == component_mark %] class="component_hilite"[% END %]>
<td colspan="[% numcols - 1 %]" class="component_description">
[% comp.description FILTER html_light %]
</td>
diff --git a/template/en/default/request/email.txt.tmpl b/template/en/default/request/email.txt.tmpl
index fb957484b..510741eed 100644
--- a/template/en/default/request/email.txt.tmpl
+++ b/template/en/default/request/email.txt.tmpl
@@ -25,7 +25,8 @@
[% bugidsummary = bug.bug_id _ ': ' _ bug.short_desc %]
[% attidsummary = attachment.id _ ': ' _ attachment.description %]
[% flagtype_name = flag ? flag.type.name : old_flag.type.name %]
-[% statuses = { '+' => "granted" , '-' => 'denied' , 'X' => "canceled" ,
+[%# Upstreaming: denied (bug 621883) %]
+[% statuses = { '+' => "granted" , '-' => 'not granted' , 'X' => "canceled" ,
'?' => "asked" } %]
[% to_identity = "" %]
@@ -53,6 +54,9 @@ Subject: [% flagtype_name %] [%+ subject_status %]: [[% terms.Bug %] [%+ bug.bug
[Attachment [% attachment.id %]] [% attachment.description FILTER clean_text %][% END %]
Date: [% date %]
X-Bugzilla-Type: request
+[%- IF flag.requestee %]
+X-Bugzilla-Flag-Requestee: [% flag.requestee.email %]
+[% END %]
[%+ threadingmarker %]
[%+ USE wrap -%]
diff --git a/template/en/default/request/queue.html.tmpl b/template/en/default/request/queue.html.tmpl
index 57650de55..a1f670158 100644
--- a/template/en/default/request/queue.html.tmpl
+++ b/template/en/default/request/queue.html.tmpl
@@ -198,7 +198,10 @@ to some group are shown by default.
[% PROCESS start_new_table %]
[% END %]
[% buglist.${request.bug_id} = 1 %]
- <tr>
+
+ <tr class="bz_bugitem bz_[% request.bug_severity FILTER css_class_quote -%]
+ bz_[% request.priority FILTER css_class_quote -%]
+ bz_[% request.bug_status FILTER css_class_quote %]">
[% FOREACH column = display_columns %]
[% NEXT IF column == group_field || excluded_columns.contains(column) %]
<td>
@@ -238,7 +241,7 @@ to some group are shown by default.
[% BLOCK display_bug %]
<a href="show_bug.cgi?id=[% request.bug_id %]"
[%- ' class="bz_secure"' IF request.restricted %]>
- [% request.bug_id %]: [%+ request.bug_summary FILTER html %]</a>
+ [% request.bug_id %] ([% request.priority FILTER html %]/[% request.bug_severity FILTER html %]): [%+ request.bug_summary FILTER html %]</a>
[% END %]
[% BLOCK display_attachment %]
diff --git a/template/en/default/search/field.html.tmpl b/template/en/default/search/field.html.tmpl
index defc94cc3..19f199692 100644
--- a/template/en/default/search/field.html.tmpl
+++ b/template/en/default/search/field.html.tmpl
@@ -115,7 +115,7 @@
<select name="[% field.name FILTER html%]"
id="[% field.name FILTER html %]"
[% IF onchange %] onchange="[% onchange FILTER html %]"[% END %]
- multiple="multiple" size="7">
+ multiple="multiple" size="9">
[% legal_values = ${field.name} %]
[% IF field.name == "component" %]
[% legal_values = ${"component_"} %]
diff --git a/template/en/default/search/form.html.tmpl b/template/en/default/search/form.html.tmpl
index 41e116518..93c81689f 100644
--- a/template/en/default/search/form.html.tmpl
+++ b/template/en/default/search/form.html.tmpl
@@ -333,6 +333,7 @@ TUI_hide_default('information_query');
<select name="emailtype[% n %]">
[% FOREACH qv = [
{ name => "substring", description => "contains" },
+ { name => "notsubstring", description => "doesn't contain" },
{ name => "exact", description => "is" },
{ name => "notequals", description => "is not" },
{ name => "regexp", description => "matches regexp" },
diff --git a/template/en/default/search/search-google.html.tmpl b/template/en/default/search/search-google.html.tmpl
new file mode 100644
index 000000000..080887abb
--- /dev/null
+++ b/template/en/default/search/search-google.html.tmpl
@@ -0,0 +1,57 @@
+[%# 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 Developers are Copyright (C) 2011 the
+ # Initial Developer. All Rights Reserved.
+ #
+ # Contributor(s):
+ # Dave Lawrence <dkl@mozilla.com>
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+
+[% PROCESS global/header.html.tmpl
+ title = "Search " _ terms.Bugs _ " using Google"
+%]
+
+[% WRAPPER search/tabs.html.tmpl %]
+
+<p>
+ Use the <a href="http://www.google.com">Google</a> search engine to search
+ for [% terms.Bugzilla +%] [%+ terms.bugs %]. Find the [% terms.bugs %] you are
+ looking for by entering words that best describe it.
+</p>
+
+<p>
+ For example, if the [% terms.bug %] you are looking for is a browser crash when
+ you go to a secure web site with an embedded Flash animation, you might search
+ for "crash secure SSL flash".
+</p>
+
+<p>
+ <span style="color:red;">*</span>
+ Google only indexes publicly viewable [% terms.bugs %] and all may not be represented.
+<p>
+
+<form method="get" action="http://www.google.com/search">
+<input type="hidden" name="sitesearch" value="bugzilla.mozilla.org">
+ <nobr>
+ <input type="text" name="q" size="60" maxlength="255" value="">
+ <input type="submit" value="Search">
+ </nobr>
+</form>
+
+[% END %]
+
+[% PROCESS global/footer.html.tmpl %]
+
diff --git a/template/en/default/search/search-instant.html.tmpl b/template/en/default/search/search-instant.html.tmpl
new file mode 100644
index 000000000..5d75d1996
--- /dev/null
+++ b/template/en/default/search/search-instant.html.tmpl
@@ -0,0 +1,85 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+ # License, v. 2.0. If a copy of the MPL was not distributed with this
+ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ #
+ # This Source Code Form is "Incompatible With Secondary Licenses", as
+ # defined by the Mozilla Public License, v. 2.0.
+ #%]
+
+[% PROCESS global/variables.none.tmpl %]
+
+[% PROCESS global/header.html.tmpl
+ title = "Instant Search"
+ javascript_urls = [ 'extensions/GuidedBugEntry/web/js/products.js',
+ 'js/instant-search.js', ]
+ yui = [ 'datatable', 'container' ]
+%]
+
+[% UNLESS default.exists('product') && default.product.size %]
+ [% default.product = [ 'Firefox' ] %]
+[% END %]
+
+<script>
+YAHOO.bugzilla.instantSearch.setLabels( {
+ id: "[% field_descs.bug_id FILTER js %]",
+ summary: "[% field_descs.short_desc FILTER js %]",
+ component: "[% field_descs.component FILTER js %]",
+ status: "[% field_descs.bug_status FILTER js %]",
+});
+</script>
+
+[% WRAPPER search/tabs.html.tmpl %]
+
+<p>
+ This page provides instant results; however, only the [% terms.bug %]'s summary
+ is searched. Products related to the selected product may also be searched.
+</p>
+
+<table>
+ <tr>
+ <td align="right" valign="baseline">
+ <b><label for="product">Product:</label></b>
+ </td>
+ <td>
+ <select name="product" id="product">
+ [% IF Param('useclassification') %]
+ [% FOREACH c = classification %]
+ <optgroup label="[% c.name FILTER html %]">
+ [% FOREACH p = user.get_selectable_products(c.id) %]
+ [% IF p.components.size %]
+ <option value="[% p.name FILTER html %]"
+ [% " selected" IF lsearch(default.product, p.name) != -1 %]>
+ [% p.name FILTER html %]
+ </option>
+ [% END %]
+ [% END %]
+ </optgroup>
+ [% END %]
+ [% ELSE %]
+ [% FOREACH p = product %]
+ <option value="[% p.name FILTER html %]"
+ [% " selected" IF lsearch(default.product, p.name) != -1 %]>
+ [% p.name FILTER html %]
+ </option>
+ [% END %]
+ [% END %]
+ </select>
+ </td>
+ </tr>
+ <tr>
+ <td align="right" valign="baseline">
+ <b><label for="content">Words:</label></b>
+ </td>
+ <td>
+ <input id="content" spellcheck="true" size="60"
+ value="[% default.content.0 FILTER html %]">
+ </td>
+ </tr>
+</table>
+<br>
+
+<div id="results"></div>
+
+[% END %]
+
+[% PROCESS global/footer.html.tmpl %]
diff --git a/template/en/default/search/search-specific.html.tmpl b/template/en/default/search/search-specific.html.tmpl
index 9ef299425..7e5de2c4a 100644
--- a/template/en/default/search/search-specific.html.tmpl
+++ b/template/en/default/search/search-specific.html.tmpl
@@ -98,7 +98,7 @@ for "crash secure SSL flash".
<label for="content">Words:</label>
</th>
<td>
- <input name="content" size="40" id="content"
+ <input name="content" size="60" id="content"
value="[% default.content.0 FILTER html %]">
<script type="text/javascript"> <!--
document.forms['queryform'].content.focus();
@@ -107,6 +107,15 @@ for "crash secure SSL flash".
</td>
</tr>
<tr>
+ <td>&nbsp;</td>
+ <td>
+ <input type="hidden" name="comments" value="0">
+ <input type="checkbox" id="comments" name="comments"
+ value="1" [% 'checked' IF cgi.param("comments") %]>
+ <label for="comments">Search comments</label>
+ </td>
+ </tr>
+ <tr>
<td></td>
<td>
diff --git a/template/en/default/search/tabs.html.tmpl b/template/en/default/search/tabs.html.tmpl
index 119b30fde..26ad4f39b 100644
--- a/template/en/default/search/tabs.html.tmpl
+++ b/template/en/default/search/tabs.html.tmpl
@@ -24,10 +24,14 @@
#%]
[% WRAPPER global/tabs.html.tmpl
- tabs = [ { name => 'specific', label => "Simple Search",
+ tabs = [ { name => 'instant', label => "Instant Search",
+ link => "query.cgi?format=instant" },
+ { name => 'specific', label => "Simple Search",
link => "query.cgi?format=specific" },
{ name => 'advanced', label => "Advanced Search",
- link => "query.cgi?format=advanced" } ]
+ link => "query.cgi?format=advanced" },
+ { name => 'google', label => 'Google Search',
+ link => "query.cgi?format=google" } ]
current_tab_name = query_format || format || "advanced"
%]