From 353af77714fd29aeab330255808636a9088eecc2 Mon Sep 17 00:00:00 2001 From: Jeff Fearn Date: Thu, 7 May 2020 11:41:39 +1000 Subject: Bug 478886 - Open Source Red Hat Bugzilla Import Red Hat Bugzilla changes. --- .gitignore | 41 + .htaccess | 43 +- .perltidyrc | 17 + Bugzilla.pm | 1105 +- Bugzilla/Attachment.pm | 1008 +- Bugzilla/Attachment/PatchReader.pm | 494 +- Bugzilla/Auth.pm | 371 +- Bugzilla/Auth/Login.pm | 25 +- Bugzilla/Auth/Login/APIKey.pm | 42 +- Bugzilla/Auth/Login/CGI.pm | 121 +- Bugzilla/Auth/Login/Cookie.pm | 197 +- Bugzilla/Auth/Login/Env.pm | 26 +- Bugzilla/Auth/Login/Stack.pm | 122 +- Bugzilla/Auth/Persist/Cookie.pm | 275 +- Bugzilla/Auth/Verify.pm | 200 +- Bugzilla/Auth/Verify/DB.pm | 146 +- Bugzilla/Auth/Verify/LDAP.pm | 253 +- Bugzilla/Auth/Verify/RADIUS.pm | 58 +- Bugzilla/Auth/Verify/RedHat.pm | 207 + Bugzilla/Auth/Verify/Stack.pm | 107 +- Bugzilla/Bug.pm | 8824 +++--- Bugzilla/BugMail.pm | 1197 +- Bugzilla/BugUrl.pm | 233 +- Bugzilla/BugUrl/Bugzilla.pm | 52 +- Bugzilla/BugUrl/Bugzilla/Local.pm | 110 +- Bugzilla/BugUrl/Debian.pm | 34 +- Bugzilla/BugUrl/GitHub.pm | 26 +- Bugzilla/BugUrl/Google.pm | 30 +- Bugzilla/BugUrl/JIRA.pm | 37 +- Bugzilla/BugUrl/Launchpad.pm | 31 +- Bugzilla/BugUrl/MantisBT.pm | 18 +- Bugzilla/BugUrl/SourceForge.pm | 30 +- Bugzilla/BugUrl/Trac.pm | 25 +- Bugzilla/BugUserLastVisit.pm | 22 +- Bugzilla/CGI.pm | 1163 +- Bugzilla/Chart.pm | 697 +- Bugzilla/Classification.pm | 198 +- Bugzilla/Comment.pm | 664 +- Bugzilla/Comment/TagWeights.pm | 10 +- Bugzilla/Component.pm | 650 +- Bugzilla/Config.pm | 526 +- Bugzilla/Config/Admin.pm | 39 +- Bugzilla/Config/Advanced.pm | 29 +- Bugzilla/Config/Attachment.pm | 75 +- Bugzilla/Config/Auth.pm | 182 +- Bugzilla/Config/BugChange.pm | 70 +- Bugzilla/Config/BugFields.pm | 111 +- Bugzilla/Config/Common.pm | 610 +- Bugzilla/Config/Core.pm | 32 +- Bugzilla/Config/DependencyGraph.pm | 22 +- Bugzilla/Config/General.pm | 43 +- Bugzilla/Config/GroupSecurity.pm | 141 +- Bugzilla/Config/LDAP.pm | 45 +- Bugzilla/Config/MTA.pm | 97 +- Bugzilla/Config/Memcached.pm | 12 +- Bugzilla/Config/Query.pm | 78 +- Bugzilla/Config/RADIUS.pm | 32 +- Bugzilla/Config/ShadowDB.pm | 44 +- Bugzilla/Config/UserMatch.pm | 39 +- Bugzilla/Constants.pm | 854 +- Bugzilla/DB.pm | 2040 +- Bugzilla/DB/Mysql.pm | 1602 +- Bugzilla/DB/Oracle.pm | 1019 +- Bugzilla/DB/Pg.pm | 843 +- Bugzilla/DB/Schema.pm | 4155 +-- Bugzilla/DB/Schema/Mysql.pm | 571 +- Bugzilla/DB/Schema/Oracle.pm | 782 +- Bugzilla/DB/Schema/Pg.pm | 291 +- Bugzilla/DB/Schema/Sqlite.pm | 423 +- Bugzilla/DB/Sqlite.pm | 328 +- Bugzilla/Error.pm | 359 +- Bugzilla/Extension.pm | 300 +- Bugzilla/Field.pm | 1568 +- Bugzilla/Field/Choice.pm | 311 +- Bugzilla/Field/ChoiceInterface.pm | 229 +- Bugzilla/Flag.pm | 1817 +- Bugzilla/FlagType.pm | 884 +- Bugzilla/Group.pm | 734 +- Bugzilla/Hook.pm | 56 +- Bugzilla/Install.pm | 716 +- Bugzilla/Install/CPAN.pm | 426 +- Bugzilla/Install/DB.pm | 6747 ++--- Bugzilla/Install/Filesystem.pm | 1435 +- Bugzilla/Install/Localconfig.pm | 405 +- Bugzilla/Install/Requirements.pm | 1183 +- Bugzilla/Install/Util.pm | 1076 +- Bugzilla/Job/BugMail.pm | 24 +- Bugzilla/Job/Mailer.pm | 34 +- Bugzilla/JobQueue.pm | 239 +- Bugzilla/JobQueue/Runner.pm | 340 +- Bugzilla/Keyword.pm | 120 +- Bugzilla/MIME.pm | 160 +- Bugzilla/Mailer.pm | 484 +- Bugzilla/Memcached.pm | 417 +- Bugzilla/Migrate.pm | 1232 +- Bugzilla/Migrate/Gnats.pm | 1034 +- Bugzilla/Milestone.pm | 296 +- Bugzilla/ModPerl.pm | 115 + Bugzilla/Object.pm | 1366 +- Bugzilla/Product.pm | 1496 +- Bugzilla/RNG.pm | 247 +- Bugzilla/Release.pm | 399 + Bugzilla/Report.pm | 74 +- Bugzilla/Search.pm | 5633 ++-- Bugzilla/Search/Clause.pm | 170 +- Bugzilla/Search/ClauseGroup.pm | 138 +- Bugzilla/Search/Condition.pm | 70 +- Bugzilla/Search/Quicksearch.pm | 1101 +- Bugzilla/Search/Recent.pm | 117 +- Bugzilla/Search/Saved.pm | 388 +- Bugzilla/Sender/Transport/Sendmail.pm | 145 +- Bugzilla/Series.pm | 421 +- Bugzilla/Status.pm | 246 +- Bugzilla/Template.pm | 2199 +- Bugzilla/Template/Context.pm | 126 +- Bugzilla/Template/Plugin/Bugzilla.pm | 14 +- Bugzilla/Template/Plugin/Hook.pm | 111 +- Bugzilla/Template/PreloadProvider.pm | 140 + Bugzilla/Token.pm | 648 +- Bugzilla/Update.pm | 274 +- Bugzilla/User.pm | 3971 +-- Bugzilla/User/APIKey.pm | 143 +- Bugzilla/User/Session.pm | 70 + Bugzilla/User/Setting.pm | 454 +- Bugzilla/User/Setting/Lang.pm | 6 +- Bugzilla/User/Setting/Skin.pm | 28 +- Bugzilla/User/Setting/Timezone.pm | 22 +- Bugzilla/UserAgent.pm | 352 +- Bugzilla/Util.pm | 1437 +- Bugzilla/Version.pm | 321 +- Bugzilla/WebService.pm | 58 +- Bugzilla/WebService/Bug.pm | 3282 ++- Bugzilla/WebService/BugUserLastVisit.pm | 113 +- Bugzilla/WebService/Bugzilla.pm | 237 +- Bugzilla/WebService/Classification.pm | 89 +- Bugzilla/WebService/Component.pm | 225 +- Bugzilla/WebService/Constants.pm | 522 +- Bugzilla/WebService/FlagType.pm | 534 +- Bugzilla/WebService/Group.pm | 334 +- Bugzilla/WebService/Product.pm | 873 +- Bugzilla/WebService/Server.pm | 115 +- Bugzilla/WebService/Server/JSONRPC.pm | 719 +- Bugzilla/WebService/Server/REST.pm | 781 +- Bugzilla/WebService/Server/REST/Resources/Bug.pm | 274 +- .../Server/REST/Resources/BugUserLastVisit.pm | 35 +- .../WebService/Server/REST/Resources/Bugzilla.pm | 46 +- .../Server/REST/Resources/Classification.pm | 27 +- .../WebService/Server/REST/Resources/Component.pm | 18 +- .../WebService/Server/REST/Resources/FlagType.pm | 67 +- Bugzilla/WebService/Server/REST/Resources/Group.pm | 41 +- .../WebService/Server/REST/Resources/Product.pm | 78 +- Bugzilla/WebService/Server/REST/Resources/User.pm | 76 +- Bugzilla/WebService/Server/XMLRPC.pm | 498 +- Bugzilla/WebService/User.pm | 668 +- Bugzilla/WebService/Util.pm | 439 +- Bugzilla/Whine.pm | 22 +- Bugzilla/Whine/Query.pm | 24 +- Bugzilla/Whine/Schedule.pm | 78 +- Build.PL | 7 +- MANIFEST.SKIP | 3 + README.md | 107 + admin.cgi | 7 +- attachment.cgi | 1256 +- buglist.cgi | 1534 +- chart.cgi | 410 +- checksetup.pl | 62 +- clean-bug-user-last-visit.pl | 6 +- colchange.cgi | 239 +- collectstats.pl | 658 +- config.cgi | 158 +- contrib/jb2bz.py | 3 +- contrib/sendunsentbugmail.pl | 53 +- createaccount.cgi | 25 +- describecomponents.cgi | 66 +- describekeywords.cgi | 6 +- docs/en/images/redhat_logo.png | Bin 0 -> 5278 bytes docs/en/rst/_static/bugzilla.css | 89 + docs/en/rst/administering/extensions.rst | 7 +- docs/en/rst/administering/parameters.rst | 9 + docs/en/rst/api/core/v1/bug.rst | 16 +- docs/en/rst/api/index.rst | 3 +- docs/en/rst/conf.py | 12 +- docs/en/rst/integrating/extensions.rst | 2 +- docs/en/rst/integrating/index.rst | 1 + docs/en/rst/integrating/templates.rst | 2 +- docs/en/rst/using/extensions.rst | 7 +- docs/en/rst/using/filing.rst | 6 +- docs/en/rst/using/finding.rst | 23 + docs/makedocs.pl | 239 +- duplicates.cgi | 268 +- editclassifications.cgi | 212 +- editcomponents.cgi | 387 +- editfields.cgi | 292 +- editflagtypes.cgi | 903 +- editgroups.cgi | 702 +- editkeywords.cgi | 144 +- editmilestones.cgi | 209 +- editparams.cgi | 201 +- editproducts.cgi | 629 +- editreleases.cgi | 212 + editsettings.cgi | 51 +- editusers.cgi | 1208 +- editvalues.cgi | 136 +- editversions.cgi | 188 +- editwhines.cgi | 563 +- editworkflow.cgi | 195 +- email_in.pl | 870 +- enter_bug.cgi | 513 +- errors/401.html | 34 + errors/403.html | 30 + errors/404.html | 30 + errors/500.html | 30 + extensions/ActivityReport/Config.pm | 36 + extensions/ActivityReport/Extension.pm | 154 + extensions/ActivityReport/lib/Reports.pm | 702 + extensions/ActivityReport/lib/WebService.pm | 88 + .../hook/global/user-error-errors.html.tmpl | 29 + .../en/default/hook/reports/menu-end.html.tmpl | 46 + .../en/default/pages/email_queue.html.tmpl | 99 + .../en/default/pages/group_admins.html.tmpl | 54 + .../en/default/pages/group_membership.html.tmpl | 75 + .../en/default/pages/group_membership.txt.tmpl | 16 + .../en/default/pages/user_activity.html.tmpl | 226 + .../en/default/pages/user_admin_activity.html.tmpl | 151 + extensions/ActivityReport/web/styles/reports.css | 62 + extensions/AgileTools/Config.pm | 24 + extensions/AgileTools/Extension.pm | 1385 + extensions/AgileTools/README.md | 35 + extensions/AgileTools/lib/Backlog.pm | 238 + extensions/AgileTools/lib/Burn.pm | 237 + extensions/AgileTools/lib/Constants.pm | 81 + extensions/AgileTools/lib/Pages.pm | 86 + extensions/AgileTools/lib/Pages/Scrum.pm | 83 + extensions/AgileTools/lib/Pages/Team.pm | 176 + extensions/AgileTools/lib/Params.pm | 180 + extensions/AgileTools/lib/Pool.pm | 521 + extensions/AgileTools/lib/Role.pm | 259 + extensions/AgileTools/lib/Schema.pm | 691 + extensions/AgileTools/lib/Sprint.pm | 583 + extensions/AgileTools/lib/Team.pm | 932 + extensions/AgileTools/lib/Util.pm | 237 + extensions/AgileTools/lib/WebService/Backlog.pm | 127 + extensions/AgileTools/lib/WebService/Pool.pm | 200 + extensions/AgileTools/lib/WebService/Sprint.pm | 277 + extensions/AgileTools/lib/WebService/Team.pm | 458 + extensions/AgileTools/lib/WebService/Util.pm | 118 + extensions/AgileTools/set_current_sprint.pl | 58 + .../en/default/admin/params/agiletools.html.tmpl | 17 + .../template/en/default/agiletools/README | 16 + .../en/default/agiletools/blitem.html.tmpl | 24 + .../en/default/agiletools/burn-chart.html.tmpl | 35 + .../en/default/agiletools/burn-init.html.tmpl | 40 + .../agiletools/process/1_summary_links.html.tmpl | 62 + .../template/en/default/filterexceptions.pl | 35 + .../AgileTools/template/en/default/hook/README | 5 + .../hook/admin/admin-end_links_left.html.tmpl | 11 + .../admin/components/edit-common-rows.html.tmpl | 38 + .../hook/admin/products/edit-common-rows.html.tmpl | 15 + .../hook/admin/products/updated-changes.html.tmpl | 11 + .../admin/sanitycheck/messages-statuses.html.tmpl | 37 + .../default/hook/bug/edit-after_people.html.tmpl | 65 + .../en/default/hook/bug/field-help-end.none.tmpl | 23 + .../hook/global/code-error-errors.html.tmpl | 14 + .../hook/global/common-links-end-of-menu.html.tmpl | 14 + .../hook/global/header-additional_header.html.tmpl | 38 + .../messages-component_updated_fields.html.tmpl | 22 + .../hook/global/messages-messages.html.tmpl | 20 + .../global/user-error-end_object_name.html.tmpl | 17 + .../hook/global/user-error-errors.html.tmpl | 110 + .../en/default/hook/global/variables-end.none.tmpl | 12 + .../edit-multiple-after_custom_fields.html.tmpl | 30 + .../en/default/hook/list/list-links.html.tmpl | 11 + .../template/en/default/list/list-burn.html.tmpl | 28 + .../default/pages/agile_component_mapping.csv.tmpl | 15 + .../pages/agile_component_mapping.html.tmpl | 101 + .../pages/agile_component_mapping.json.tmpl | 9 + .../default/pages/agile_team_membership.csv.tmpl | 14 + .../default/pages/agile_team_membership.html.tmpl | 68 + .../default/pages/agile_team_membership.json.tmpl | 9 + .../en/default/pages/agiletools/admin.html.tmpl | 51 + .../pages/agiletools/scrum/planning.html.tmpl | 137 + .../pages/agiletools/scrum/sprints.html.tmpl | 132 + .../default/pages/agiletools/team/create.html.tmpl | 81 + .../default/pages/agiletools/team/list.html.tmpl | 68 + .../default/pages/agiletools/team/show.html.tmpl | 253 + extensions/AgileTools/web/css/base.css | 76 + extensions/AgileTools/web/css/buglist.css | 99 + extensions/AgileTools/web/css/colorbox.css | 60 + extensions/AgileTools/web/css/images/border1.png | Bin 0 -> 896 bytes extensions/AgileTools/web/css/images/border2.png | Bin 0 -> 183 bytes extensions/AgileTools/web/css/images/loading.gif | Bin 0 -> 9427 bytes extensions/AgileTools/web/css/planning.css | 49 + extensions/AgileTools/web/js/buglist.js | 570 + extensions/AgileTools/web/js/burn.js | 198 + .../AgileTools/web/js/jquery.colorbox-min.js | 6 + .../AgileTools/web/js/jquery.flot.axislabels.js | 412 + extensions/AgileTools/web/js/jquery.flot.min.js | 6 + extensions/AgileTools/web/js/scrum.js | 568 + extensions/AgileTools/web/js/team.js | 379 + extensions/AntiSpam/Config.pm | 20 + extensions/AntiSpam/Extension.pm | 495 + extensions/AntiSpam/lib/Config.pm | 144 + .../en/default/admin/params/antispam.html.tmpl | 45 + .../hook/admin/admin-end_links_right.html.tmpl | 16 + .../params/editparams-current_panel.html.tmpl | 19 + .../hook/global/user-error-errors.html.tmpl | 12 + extensions/AuthJWT/Config.pm | 18 + extensions/AuthJWT/Extension.pm | 140 + extensions/AuthJWT/lib/Login.pm | 130 + extensions/AuthJWT/lib/Source.pm | 178 + extensions/AuthJWT/lib/Util.pm | 47 + .../AuthJWT/template/en/default/authjwt/README | 16 + extensions/AuthJWT/template/en/default/hook/README | 5 + .../hook/admin/admin-end_links_left.html.tmpl | 8 + .../hook/global/user-error-errors.html.tmpl | 23 + .../en/default/pages/authjwt/settings.html.tmpl | 110 + extensions/BayotBase/Config.pm | 31 + extensions/BayotBase/Extension.pm | 242 + extensions/BayotBase/README.rst | 44 + extensions/BayotBase/buglist_format-4.2.4.patch | 86 + extensions/BayotBase/lib/Config.pm | 59 + extensions/BayotBase/lib/Util.pm | 543 + .../en/default/admin/params/bayotbase.html.tmpl | 25 + .../template/en/default/bb/wraplist.html.tmpl | 324 + .../hook/global/common-links-link-row.html.tmpl | 35 + .../en/default/hook/global/header-start.html.tmpl | 32 + .../en/default/pages/bayotbase/fielddefs.js.tmpl | 26 + extensions/BayotBase/web/css/base.css | 61 + extensions/BayotBase/web/css/images/spinner.gif | Bin 0 -> 1849 bytes .../web/css/images/ui-bg_flat_0_aaaaaa_40x100.png | Bin 0 -> 180 bytes .../web/css/images/ui-bg_flat_75_ffffff_40x100.png | Bin 0 -> 178 bytes .../web/css/images/ui-bg_glass_55_fbf9ee_1x400.png | Bin 0 -> 335 bytes .../web/css/images/ui-bg_glass_65_ffffff_1x400.png | Bin 0 -> 207 bytes .../web/css/images/ui-bg_glass_75_dadada_1x400.png | Bin 0 -> 262 bytes .../web/css/images/ui-bg_glass_75_e6e6e6_1x400.png | Bin 0 -> 262 bytes .../web/css/images/ui-bg_glass_95_fef1ec_1x400.png | Bin 0 -> 332 bytes .../ui-bg_highlight-soft_75_cccccc_1x100.png | Bin 0 -> 280 bytes .../web/css/images/ui-icons_222222_256x240.png | Bin 0 -> 6922 bytes .../web/css/images/ui-icons_2e83ff_256x240.png | Bin 0 -> 4549 bytes .../web/css/images/ui-icons_454545_256x240.png | Bin 0 -> 6992 bytes .../web/css/images/ui-icons_888888_256x240.png | Bin 0 -> 6999 bytes .../web/css/images/ui-icons_cd0a0a_256x240.png | Bin 0 -> 4549 bytes .../BayotBase/web/css/jquery-ui-1.9.2.custom.css | 414 + .../web/css/jquery-ui-1.9.2.custom.min.css | 7 + extensions/BayotBase/web/js/Base.js | 139 + extensions/BayotBase/web/js/README_JQUERY_UI | 18 + extensions/BayotBase/web/js/bayot.util.js | 1363 + extensions/BayotBase/web/js/es5-shim.js | 1102 + extensions/BayotBase/web/js/es5-shim.min.js | 16 + extensions/BayotBase/web/js/jquery-1.12.4.js | 11008 +++++++ extensions/BayotBase/web/js/jquery-1.12.4.min.js | 5 + .../BayotBase/web/js/jquery-ui-1.9.2.custom.js | 10107 +++++++ .../BayotBase/web/js/jquery-ui-1.9.2.custom.min.js | 7 + extensions/BayotBase/web/js/jquery.cookie.js | 47 + extensions/BayotBase/web/js/jquery.jsonrpc.js | 256 + extensions/BmpConvert/Config.pm | 9 +- extensions/BmpConvert/Extension.pm | 49 +- extensions/BugViewPlus/Config.pm | 21 + extensions/BugViewPlus/Extension.pm | 381 + extensions/BugViewPlus/Makefile | 55 + extensions/BugViewPlus/README.md | 30 + extensions/BugViewPlus/lib/Params.pm | 124 + extensions/BugViewPlus/lib/Template.pm | 147 + .../en/default/admin/params/bugviewplus.html.tmpl | 17 + .../template/en/default/filterexceptions.pl | 16 + .../hook/admin/admin-end_links_right.html.tmpl | 11 + .../hook/bug/comments-aftercomments.html.tmpl | 60 + .../hook/bug/edit-after_custom_fields.html.tmpl | 34 + .../en/default/hook/bug/show-header-end.html.tmpl | 4 + .../hook/global/header-additional_header.html.tmpl | 34 + .../hook/global/messages-messages.html.tmpl | 10 + .../hook/global/setting-descs-settings.none.tmpl | 11 + .../hook/search/form-after_selects_top.html.tmpl | 17 + .../en/default/pages/bvp_template.html.tmpl | 90 + extensions/BugViewPlus/web/css/editor_toggle.css | 7 + extensions/BugViewPlus/web/css/inline_editor.css | 19 + extensions/BugViewPlus/web/css/templates.css | 17 + extensions/BugViewPlus/web/js/editor_toggle.js | 65 + extensions/BugViewPlus/web/js/inline_editor.js | 236 + extensions/BugViewPlus/web/js/templates.js | 36 + extensions/ComponentWatching/Config.pm | 14 + extensions/ComponentWatching/Extension.pm | 473 + .../account/prefs/component_watch.html.tmpl | 175 + .../account/prefs/email-relationships.html.tmpl | 10 + .../hook/account/prefs/prefs-tabs.html.tmpl | 14 + .../admin/sanitycheck/messages-statuses.html.tmpl | 23 + .../hook/global/code-error-errors.html.tmpl | 12 + .../messages-component_updated_fields.html.tmpl | 18 + .../default/hook/global/reason-descs-end.none.tmpl | 10 + .../hook/global/user-error-errors.html.tmpl | 17 + extensions/DependentProducts/Config.pm | 44 + extensions/DependentProducts/Extension.pm | 767 + extensions/DependentProducts/README | 10 + extensions/DependentProducts/lib/Bug.pm | 180 + extensions/DependentProducts/lib/Contact.pm | 195 + extensions/DependentProducts/lib/Record.pm | 368 + extensions/DependentProducts/lib/Util.pm | 37 + extensions/DependentProducts/lib/WebService.pm | 464 + .../template/en/default/filterexceptions.pl | 21 + .../hook/admin/products/edit-common-rows.html.tmpl | 98 + .../hook/admin/products/updated-changes.html.tmpl | 63 + .../default/hook/bug/create/create-form.html.tmpl | 24 + .../hook/bug/edit-after_custom_fields.html.tmpl | 36 + .../en/default/hook/bug/field-help-end.none.tmpl | 15 + .../default/hook/global/field-descs-end.none.tmpl | 6 + .../default/hook/global/reason-descs-end.none.tmpl | 17 + .../hook/global/user-error-errors.html.tmpl | 12 + .../edit-multiple-after_custom_fields.html.tmpl | 8 + .../template/en/filterexceptions.pl | 20 + extensions/EditTable/Config.pm | 17 + extensions/EditTable/Extension.pm | 179 + .../user-error-auth_failure_object.html.tmpl | 11 + .../hook/global/user-error-errors.html.tmpl | 17 + .../template/en/default/pages/edit_table.html.tmpl | 45 + extensions/EditTable/web/js/edit_table.js | 131 + extensions/EditTable/web/styles/edit_table.css | 39 + extensions/Example/Config.pm | 21 +- extensions/Example/Extension.pm | 1505 +- extensions/Example/lib/Auth/Login.pm | 2 +- extensions/Example/lib/Auth/Verify.pm | 2 +- extensions/Example/lib/Config.pm | 13 +- extensions/Example/lib/WebService.pm | 4 +- .../template/en/default/setup/strings.txt.pl | 4 +- extensions/ExternalBugs/Config.pm | 24 + extensions/ExternalBugs/Extension.pm | 1558 + extensions/ExternalBugs/lib/Bug.pm | 404 + extensions/ExternalBugs/lib/Config.pm | 134 + extensions/ExternalBugs/lib/Job/JIRA.pm | 111 + extensions/ExternalBugs/lib/Job/SFDC.pm | 136 + extensions/ExternalBugs/lib/Regex.pm | 111 + extensions/ExternalBugs/lib/Type.pm | 675 + extensions/ExternalBugs/lib/Type/Bugzilla.pm | 95 + extensions/ExternalBugs/lib/Type/Gerrit.pm | 68 + extensions/ExternalBugs/lib/Type/GitHub.pm | 152 + extensions/ExternalBugs/lib/Type/JIRA.pm | 372 + extensions/ExternalBugs/lib/Type/KBase.pm | 90 + extensions/ExternalBugs/lib/Type/Redmine.pm | 75 + extensions/ExternalBugs/lib/Type/SFDC.pm | 178 + extensions/ExternalBugs/lib/Type/SFDC_DEV.pm | 157 + extensions/ExternalBugs/lib/Type/SFDC_QA.pm | 157 + extensions/ExternalBugs/lib/Type/SFDC_Sandbox.pm | 389 + extensions/ExternalBugs/lib/Type/SFDC_Stage.pm | 346 + extensions/ExternalBugs/lib/Util.pm | 191 + extensions/ExternalBugs/lib/WebService.pm | 1205 + .../en/default/admin/params/externalbugs.html.tmpl | 24 + .../template/en/default/bug/extbz-check.js.tmpl | 67 + .../template/en/default/filterexceptions.pl | 21 + .../hook/admin/admin-end_links_left.html.tmpl | 9 + .../default/hook/bug/create/create-end.html.tmpl | 6 + .../default/hook/bug/create/create-form.html.tmpl | 31 + .../en/default/hook/bug/edit-table-data.html.tmpl | 177 + .../hook/global/code-error-errors.html.tmpl | 33 + .../default/hook/global/field-descs-end.none.tmpl | 7 + .../en/default/hook/global/header-start.html.tmpl | 7 + .../hook/global/messages-messages.html.tmpl | 24 + .../global/user-error-end_object_name.html.tmpl | 12 + .../hook/global/user-error-errors.html.tmpl | 83 + .../edit-multiple-after_custom_fields.html.tmpl | 30 + .../hook/list/table-list_table_abbrev.html.tmpl | 7 + .../pages/externalbugs/confirm-delete.html.tmpl | 50 + .../en/default/pages/externalbugs/edit.html.tmpl | 125 + .../default/pages/externalbugs/extbug_js.html.tmpl | 73 + .../en/default/pages/externalbugs/list.html.tmpl | 126 + .../externalbugs/regexs/confirm-delete.html.tmpl | 42 + .../pages/externalbugs/regexs/edit.html.tmpl | 42 + .../pages/externalbugs/regexs/list.html.tmpl | 68 + .../template/en/default/setup/strings.txt.pl | 20 + .../ExternalBugs/template/en/filterexceptions.pl | 20 + extensions/ExternalBugs/web/css/global.css | 15 + extensions/ExternalBugs/web/images/wait20trans.gif | Bin 0 -> 4035 bytes extensions/ExternalBugs/web/js/external_bugs.js | 85 + extensions/FontAwesome/Config.pm | 16 + extensions/FontAwesome/Extension.pm | 24 + extensions/FontAwesome/lib/Util.pm | 19 + .../en/default/hook/global/header-start.html.tmpl | 6 + extensions/FontAwesome/web/LICENSE.txt | 34 + extensions/FontAwesome/web/css/all.css | 4450 +++ extensions/FontAwesome/web/css/all.min.css | 5 + extensions/FontAwesome/web/css/brands.css | 14 + extensions/FontAwesome/web/css/brands.min.css | 5 + extensions/FontAwesome/web/css/fontawesome.css | 4417 +++ extensions/FontAwesome/web/css/fontawesome.min.css | 5 + extensions/FontAwesome/web/css/regular.css | 15 + extensions/FontAwesome/web/css/regular.min.css | 5 + extensions/FontAwesome/web/css/solid.css | 16 + extensions/FontAwesome/web/css/solid.min.css | 5 + extensions/FontAwesome/web/css/svg-with-js.css | 371 + extensions/FontAwesome/web/css/svg-with-js.min.css | 5 + extensions/FontAwesome/web/css/v4-shims.css | 2172 ++ extensions/FontAwesome/web/css/v4-shims.min.css | 5 + .../FontAwesome/web/webfonts/fa-brands-400.eot | Bin 0 -> 131930 bytes .../FontAwesome/web/webfonts/fa-brands-400.svg | 3535 +++ .../FontAwesome/web/webfonts/fa-brands-400.ttf | Bin 0 -> 131624 bytes .../FontAwesome/web/webfonts/fa-brands-400.woff | Bin 0 -> 89100 bytes .../FontAwesome/web/webfonts/fa-brands-400.woff2 | Bin 0 -> 75936 bytes .../FontAwesome/web/webfonts/fa-regular-400.eot | Bin 0 -> 34390 bytes .../FontAwesome/web/webfonts/fa-regular-400.svg | 803 + .../FontAwesome/web/webfonts/fa-regular-400.ttf | Bin 0 -> 34092 bytes .../FontAwesome/web/webfonts/fa-regular-400.woff | Bin 0 -> 16800 bytes .../FontAwesome/web/webfonts/fa-regular-400.woff2 | Bin 0 -> 13576 bytes .../FontAwesome/web/webfonts/fa-solid-900.eot | Bin 0 -> 194066 bytes .../FontAwesome/web/webfonts/fa-solid-900.svg | 4700 +++ .../FontAwesome/web/webfonts/fa-solid-900.ttf | Bin 0 -> 193780 bytes .../FontAwesome/web/webfonts/fa-solid-900.woff | Bin 0 -> 98996 bytes .../FontAwesome/web/webfonts/fa-solid-900.woff2 | Bin 0 -> 76084 bytes extensions/InlineHistory/Config.pm | 15 + extensions/InlineHistory/Extension.pm | 219 + extensions/InlineHistory/README | 10 + extensions/InlineHistory/lib/Util.pm | 65 + .../hook/bug/comments-aftercomments.html.tmpl | 175 + .../hook/bug/comments-comment_banner.html.tmpl | 13 + .../en/default/hook/bug/show-header-end.html.tmpl | 12 + .../hook/global/setting-descs-settings.none.tmpl | 11 + extensions/InlineHistory/web/inline-history.js | 409 + extensions/InlineHistory/web/style.css | 35 + extensions/ListOfBugs/Config.pm | 17 + extensions/ListOfBugs/Extension.pm | 440 + extensions/ListOfBugs/lib/BugList.pm | 404 + extensions/ListOfBugs/lib/Util.pm | 21 + extensions/ListOfBugs/lib/WebService.pm | 555 + .../default/account/prefs/user_bug_lists.html.tmpl | 62 + .../ListOfBugs/template/en/default/hook/README | 5 + .../hook/account/prefs/prefs-tabs.html.tmpl | 11 + .../hook/admin/admin-end_links_left.html.tmpl | 8 + .../default/hook/global/field-descs-end.none.tmpl | 6 + .../hook/global/user-error-errors.html.tmpl | 33 + .../en/default/hook/list/list-links.html.tmpl | 11 + .../en/default/pages/listofbugs/create.html.tmpl | 93 + .../en/default/pages/listofbugs/delete.html.tmpl | 30 + .../en/default/pages/listofbugs/manage.html.tmpl | 62 + .../en/default/pages/listofbugs/save.html.tmpl | 31 + extensions/ListOfBugs/web/README | 7 + extensions/MoreBugUrl/Config.pm | 6 +- extensions/MoreBugUrl/Extension.pm | 45 +- extensions/MoreBugUrl/lib/BitBucket.pm | 20 +- extensions/MoreBugUrl/lib/GetSatisfaction.pm | 22 +- extensions/MoreBugUrl/lib/PHP.pm | 26 +- extensions/MoreBugUrl/lib/RT.pm | 25 +- extensions/MoreBugUrl/lib/Redmine.pm | 23 +- extensions/MoreBugUrl/lib/ReviewBoard.pm | 31 +- extensions/MoreBugUrl/lib/Rietveld.pm | 48 +- extensions/MoreBugUrl/lib/Savane.pm | 20 +- extensions/MultipleValues/Config.pm | 12 + extensions/MultipleValues/Extension.pm | 599 + .../template/en/default/filterexceptions.pl | 21 + .../MultipleValues/template/en/default/hook/README | 5 + .../hook/global/code-error-errors.html.tmpl | 3 + .../hook/global/user-error-errors.html.tmpl | 20 + .../template/en/default/multiples/README | 16 + .../MultipleValues/template/en/filterexceptions.pl | 20 + extensions/MultipleValues/web/README | 7 + extensions/OldBugMove/Extension.pm | 248 +- extensions/OldBugMove/lib/Params.pm | 22 +- extensions/PlotlyReports/Config.pm | 19 + extensions/PlotlyReports/Extension.pm | 389 + extensions/PlotlyReports/lib/Graphs/BugSeverity.pm | 673 + extensions/PlotlyReports/lib/Graphs/BugStatus.pm | 1252 + extensions/PlotlyReports/lib/Graphs/NonDefaults.pm | 297 + extensions/PlotlyReports/lib/Util.pm | 38 + extensions/PlotlyReports/lib/WebService.pm | 331 + .../hook/admin/products/edit-common-rows.html.tmpl | 53 + .../hook/admin/products/updated-changes.html.tmpl | 11 + .../hook/global/user-error-errors.html.tmpl | 12 + .../en/default/hook/reports/menu-end.html.tmpl | 24 + .../en/default/pages/bug_severity.html.tmpl | 151 + .../template/en/default/pages/bug_status.html.tmpl | 162 + .../en/default/pages/non_defaults.html.tmpl | 189 + extensions/PlotlyReports/web/README | 6 + extensions/PlotlyReports/web/plotly/plotly.min.js | 7 + extensions/ProductDashboard/Config.pm | 16 + extensions/ProductDashboard/Extension.pm | 225 + extensions/ProductDashboard/lib/Queries.pm | 594 + extensions/ProductDashboard/lib/Util.pm | 121 + .../global/common-links-action-links.html.tmpl | 9 + .../hook/global/user-error-errors.html.tmpl | 12 + .../en/default/pages/productdashboard.html.tmpl | 251 + .../pages/productdashboard/charts.html.tmpl | 251 + .../pages/productdashboard/components.html.tmpl | 234 + .../pages/productdashboard/duplicates.html.tmpl | 55 + .../pages/productdashboard/popularity.html.tmpl | 61 + .../pages/productdashboard/recents.html.tmpl | 121 + .../pages/productdashboard/roadmap.html.tmpl | 49 + .../pages/productdashboard/summary.html.tmpl | 129 + extensions/ProductDashboard/web/images/spacer.gif | Bin 0 -> 43 bytes .../web/styles/productdashboard.css | 50 + extensions/Push/Config.pm | 31 + extensions/Push/Extension.pm | 853 + extensions/Push/bin/bugzilla-pushd.pl | 54 + extensions/Push/bin/nagios_push_checker.pl | 54 + extensions/Push/lib/Admin.pm | 135 + extensions/Push/lib/BacklogMessage.pm | 156 + extensions/Push/lib/BacklogQueue.pm | 126 + extensions/Push/lib/Backoff.pm | 118 + extensions/Push/lib/Config.pm | 231 + extensions/Push/lib/Connector.disabled/AMQP.pm | 224 + .../Push/lib/Connector.disabled/ServiceNow.pm | 457 + extensions/Push/lib/Connector/Base.pm | 121 + extensions/Push/lib/Connector/File.pm | 68 + extensions/Push/lib/Connector/ReviewBoard.pm | 179 + extensions/Push/lib/Connector/STOMP_SSL.pm | 331 + extensions/Push/lib/Connector/Spark.pm | 179 + extensions/Push/lib/Connector/TCL.pm | 367 + extensions/Push/lib/Connectors.pm | 123 + extensions/Push/lib/Constants.pm | 46 + extensions/Push/lib/Daemon.pm | 133 + extensions/Push/lib/Log.pm | 43 + extensions/Push/lib/LogEntry.pm | 70 + extensions/Push/lib/Logger.pm | 75 + extensions/Push/lib/Message.pm | 107 + extensions/Push/lib/Option.pm | 65 + extensions/Push/lib/Push.pm | 271 + extensions/Push/lib/Queue.pm | 68 + extensions/Push/lib/Serialise.pm | 330 + extensions/Push/lib/Util.pm | 172 + extensions/Push/t/ReviewBoard.t | 234 + .../hook/admin/admin-end_links_right.html.tmpl | 18 + .../hook/global/code-error-errors.html.tmpl | 25 + .../hook/global/messages-messages.html.tmpl | 16 + .../hook/global/user-error-errors.html.tmpl | 11 + .../en/default/pages/push_config.html.tmpl | 135 + .../template/en/default/pages/push_log.html.tmpl | 45 + .../en/default/pages/push_queues.html.tmpl | 102 + .../en/default/pages/push_queues_view.html.tmpl | 88 + .../Push/template/en/default/setup/strings.txt.pl | 15 + extensions/Push/web/admin.css | 75 + extensions/Push/web/admin.js | 44 + extensions/RedHat/Config.pm | 24 + extensions/RedHat/Extension.pm | 4375 +++ extensions/RedHat/Makefile | 96 + extensions/RedHat/bin/bz-expire-cookies.pl | 65 + extensions/RedHat/bin/bz-schema-audit.pl | 650 + extensions/RedHat/bin/changes.pl | 20 + extensions/RedHat/bin/jobdelta.pl | 36 + extensions/RedHat/bin/no_attach_data.pl | 21 + extensions/RedHat/bin/rebuild_fts_index.sql | 63 + extensions/RedHat/bin/release.pl | 78 + extensions/RedHat/bin/sitemap.pl | 196 + extensions/RedHat/bin/unshare_old_queries.pl | 120 + extensions/RedHat/bugzilla.spec.in | 103 + extensions/RedHat/custom_fields.yaml | 535 + extensions/RedHat/lib/BugUrl/SFDC.pm | 37 + extensions/RedHat/lib/Config.pm | 191 + extensions/RedHat/lib/DocTypeText.pm | 100 + extensions/RedHat/lib/Schema.pm | 1010 + extensions/RedHat/lib/WebService/Bugzilla.pm | 1455 + extensions/RedHat/lib/WebService/Component.pm | 763 + extensions/RedHat/lib/WebService/Field.pm | 82 + extensions/RedHat/lib/WebService/Flag.pm | 366 + .../en/default/account/auth/login-small.html.tmpl | 70 + .../default/admin/products/private-group.html.tmpl | 34 + .../bug/create/create-core-review.html.tmpl | 145 + ...reate-fedora-nonresponsive-maintainer.html.tmpl | 128 + .../bug/create/create-fedora-review.html.tmpl | 151 + .../create/create-fedora-systemd-request.html.tmpl | 152 + .../bug/create/create-partner-majrel.html.tmpl | 222 + .../bug/create/create-partner-minrel.html.tmpl | 225 + .../bug/create/create-rhel-review.html.tmpl | 155 + .../create/create-swcert-symbol-review.html.tmpl | 148 + .../en/default/bug/private_fields.none.tmpl | 15 + .../default/bug/process/warn_duplicate.html.tmpl | 63 + .../en/default/bug/process/warn_merge.html.tmpl | 105 + .../template/en/default/email/long-query.txt.tmpl | 19 + .../en/default/email/unshare-search.txt.tmpl | 28 + .../RedHat/template/en/default/filterexceptions.pl | 21 + .../en/default/global/common-links.html.tmpl | 118 + .../account/prefs/email-relationships.html.tmpl | 8 + .../hook/admin/admin-end_links_left.html.tmpl | 14 + .../admin/components/edit-common-rows.html.tmpl | 27 + .../hook/admin/groups/create-field.html.tmpl | 20 + .../default/hook/admin/groups/edit-field.html.tmpl | 20 + .../params/editparams-current_panel.html.tmpl | 104 + .../hook/admin/products/edit-common-rows.html.tmpl | 129 + .../hook/admin/products/updated-changes.html.tmpl | 100 + .../attachment/create-form_before_submit.html.tmpl | 24 + .../hook/bug/comments-aftercomments.html.tmpl | 18 + .../hook/bug/edit-after_custom_fields.html.tmpl | 53 + .../en/default/hook/bug/edit-before_form.html.tmpl | 8 + .../hook/bug/field-end_field_column.html.tmpl | 50 + .../en/default/hook/bug/field-help-end.none.tmpl | 287 + .../hook/bug/field-start_field_column.html.tmpl | 37 + .../en/default/hook/bug/navigate-links.html.tmpl | 32 + .../process/verify-new-product-exclude.html.tmpl | 9 + .../bug/process/verify-new-product-field.html.tmpl | 15 + .../en/default/hook/email/bugmail-start.html.tmpl | 13 + .../en/default/hook/email/bugmail-start.txt.tmpl | 8 + .../hook/global/code-error-errors.html.tmpl | 70 + .../global/common-links-action-links.html.tmpl | 11 + .../default/hook/global/field-descs-end.none.tmpl | 24 + .../en/default/hook/global/footer-end.html.tmpl | 53 + .../en/default/hook/global/footer-outro.html.tmpl | 4 + .../hook/global/header-additional_header.html.tmpl | 34 + .../en/default/hook/global/header-start.html.tmpl | 24 + .../messages-component_updated_fields.html.tmpl | 31 + .../hook/global/messages-messages.html.tmpl | 75 + .../default/hook/global/reason-descs-end.none.tmpl | 27 + .../hook/global/setting-descs-settings.none.tmpl | 39 + .../user-error-auth_failure_object.html.tmpl | 14 + .../user-error-bug_url_invalid_tracker.html.tmpl | 10 + .../hook/global/user-error-errors.html.tmpl | 403 + .../en/default/hook/global/variables-end.none.tmpl | 9 + .../template/en/default/hook/index-intro.html.tmpl | 31 + .../en/default/hook/list/list-links.html.tmpl | 36 + .../hook/list/table-list_table_abbrev.html.tmpl | 54 + .../default/hook/pages/fields-resolution.html.tmpl | 12 + .../hook/pages/release-notes-updates_top.html.tmpl | 1577 + .../hook/search/form-before_selects_top.html.tmpl | 24 + .../RedHat/template/en/default/pages/faq.html.tmpl | 867 + .../pages/redhat/admin_user_sessions.html.tmpl | 232 + .../en/default/pages/redhat/ban_apikey.html.tmpl | 34 + .../en/default/pages/redhat/contact.html.tmpl | 32 + .../redhat/doctypetext/confirm-delete.html.tmpl | 39 + .../pages/redhat/doctypetext/edit.html.tmpl | 41 + .../pages/redhat/doctypetext/list.html.tmpl | 69 + .../pages/redhat/groupgraphs/select.html.tmpl | 61 + .../en/default/pages/terms-conditions.html.tmpl | 33 + .../template/en/default/setup/strings.txt.pl | 13 + .../template/en/default/whine/disabled.txt.tmpl | 33 + .../template/en/default/whine/timeout.txt.tmpl | 34 + extensions/RedHat/template/en/filterexceptions.pl | 20 + extensions/RedHat/web/DataTables/datatables.css | 1267 + extensions/RedHat/web/DataTables/datatables.js | 28799 +++++++++++++++++++ .../RedHat/web/DataTables/datatables.min.css | 143 + extensions/RedHat/web/DataTables/datatables.min.js | 571 + .../RedHat/web/DataTables/images/sort_asc.png | Bin 0 -> 160 bytes .../web/DataTables/images/sort_asc_disabled.png | Bin 0 -> 148 bytes .../RedHat/web/DataTables/images/sort_both.png | Bin 0 -> 201 bytes .../RedHat/web/DataTables/images/sort_desc.png | Bin 0 -> 158 bytes .../web/DataTables/images/sort_desc_disabled.png | Bin 0 -> 146 bytes extensions/RedHat/web/alertify/alertify.js | 3660 +++ extensions/RedHat/web/alertify/alertify.min.js | 3 + extensions/RedHat/web/alertify/css/alertify.css | 968 + .../RedHat/web/alertify/css/alertify.min.css | 6 + .../RedHat/web/alertify/css/alertify.rtl.css | 968 + .../RedHat/web/alertify/css/alertify.rtl.min.css | 6 + .../RedHat/web/alertify/css/themes/bootstrap.css | 61 + .../web/alertify/css/themes/bootstrap.min.css | 6 + .../web/alertify/css/themes/bootstrap.rtl.css | 61 + .../web/alertify/css/themes/bootstrap.rtl.min.css | 6 + .../RedHat/web/alertify/css/themes/default.css | 69 + .../RedHat/web/alertify/css/themes/default.min.css | 6 + .../RedHat/web/alertify/css/themes/default.rtl.css | 69 + .../web/alertify/css/themes/default.rtl.min.css | 6 + .../RedHat/web/alertify/css/themes/semantic.css | 89 + .../web/alertify/css/themes/semantic.min.css | 6 + .../web/alertify/css/themes/semantic.rtl.css | 89 + .../web/alertify/css/themes/semantic.rtl.min.css | 6 + extensions/RedHat/web/css/Icons.md | 23 + extensions/RedHat/web/css/favicon.ico | Bin 0 -> 318 bytes extensions/RedHat/web/css/favicons/devel-1.ico | Bin 0 -> 4286 bytes extensions/RedHat/web/css/favicons/devel-2.ico | Bin 0 -> 67646 bytes extensions/RedHat/web/css/favicons/partner.ico | Bin 0 -> 67646 bytes extensions/RedHat/web/css/favicons/production.ico | Bin 0 -> 67646 bytes extensions/RedHat/web/css/favicons/qe.ico | Bin 0 -> 67646 bytes extensions/RedHat/web/css/l_redhat-lg.png | Bin 0 -> 3702 bytes extensions/RedHat/web/css/redhat.css | 1325 + extensions/RedHat/web/js/redhat.js | 114 + extensions/Releases/Config.pm | 13 + extensions/Releases/Extension.pm | 850 + extensions/Releases/lib/ACKCustomSearch.pm | 262 + extensions/Releases/lib/ACKList.pm | 4981 ++++ extensions/Releases/lib/ACKView.pm | 1445 + extensions/Releases/lib/Components.pm | 701 + extensions/Releases/lib/Release.pm | 269 + extensions/Releases/lib/Schedule.pm | 436 + extensions/Releases/lib/Util.pm | 477 + extensions/Releases/lib/WebService.pm | 336 + .../components/confirm_empty_list.html.tmpl | 37 + .../template/en/default/filterexceptions.pl | 21 + .../hook/admin/admin-end_links_right.html.tmpl | 21 + .../hook/global/code-error-errors.html.tmpl | 14 + .../default/hook/global/field-descs-end.none.tmpl | 9 + .../hook/global/messages-messages.html.tmpl | 15 + .../hook/global/user-error-errors.html.tmpl | 35 + .../pages/releases/ack_edit_columns.html.tmpl | 71 + .../en/default/pages/releases/ack_list.html.tmpl | 867 + .../en/default/pages/releases/ack_view.html.tmpl | 424 + .../en/default/pages/releases/activity.html.tmpl | 54 + .../pages/releases/activity_table.html.tmpl | 63 + .../pages/releases/components/edit.html.tmpl | 97 + .../pages/releases/components/list.html.tmpl | 39 + .../en/default/pages/releases/midair.html.tmpl | 47 + .../en/default/pages/releases/releases.html.tmpl | 105 + .../default/pages/releases/schedule/edit.html.tmpl | 119 + .../default/pages/releases/schedule/list.html.tmpl | 38 + .../Releases/template/en/filterexceptions.pl | 20 + extensions/Releases/web/css/metrics.css | 219 + extensions/Releases/web/images/blue.png | Bin 0 -> 171 bytes extensions/Releases/web/images/green.png | Bin 0 -> 156 bytes extensions/Releases/web/images/icon_yes.png | Bin 0 -> 583 bytes extensions/Releases/web/images/no.png | Bin 0 -> 425 bytes extensions/Releases/web/images/pink.png | Bin 0 -> 163 bytes extensions/Releases/web/images/question.png | Bin 0 -> 501 bytes extensions/Releases/web/images/red.png | Bin 0 -> 180 bytes extensions/Releases/web/images/yes.png | Bin 0 -> 547 bytes extensions/Releases/web/js/configview.js | 177 + extensions/Releases/web/js/jquery.validate.min.js | 51 + extensions/RequestWhiner/Config.pm | 33 + extensions/RequestWhiner/Extension.pm | 47 + extensions/RequestWhiner/bin/whineatrequests.pl | 174 + extensions/RequestWhiner/lib/Constants.pm | 34 + .../en/default/requestwhiner/mail-header.tmpl | 14 + .../en/default/requestwhiner/mail.html.tmpl | 66 + .../en/default/requestwhiner/mail.txt.tmpl | 51 + extensions/RuleEngine/Config.pm | 19 + extensions/RuleEngine/Extension.pm | 1670 ++ extensions/RuleEngine/add_bugs_to_queue.pl | 70 + extensions/RuleEngine/lib/FlagGroup.pm | 246 + extensions/RuleEngine/lib/FlagGroupDetail.pm | 281 + extensions/RuleEngine/lib/Job.pm | 1608 ++ extensions/RuleEngine/lib/Pages.pm | 1631 ++ extensions/RuleEngine/lib/Rule.pm | 755 + extensions/RuleEngine/lib/RuleDetail.pm | 312 + extensions/RuleEngine/lib/RuleGroup.pm | 861 + extensions/RuleEngine/lib/RuleState.pm | 439 + extensions/RuleEngine/lib/Util.pm | 32 + extensions/RuleEngine/lib/WebService.pm | 671 + extensions/RuleEngine/lib/WebService/RuleGroup.pm | 428 + extensions/RuleEngine/lib/WebService/Util.pm | 141 + .../en/default/email/dryrun-header.txt.tmpl | 14 + .../template/en/default/email/dryrun.html.tmpl | 122 + .../template/en/default/email/dryrun.txt.tmpl | 39 + .../en/default/email/rule-change-header.txt.tmpl | 13 + .../en/default/email/rule-change.html.tmpl | 69 + .../template/en/default/email/rule-change.txt.tmpl | 40 + .../email/rule-killswitch-disabled.txt.tmpl | 33 + .../default/email/rule-killswitch-enabled.txt.tmpl | 31 + .../template/en/default/email/rule-notify.txt.tmpl | 21 + .../hook/admin/admin-end_links_right.html.tmpl | 11 + .../hook/admin/products/edit-common-rows.html.tmpl | 48 + .../hook/global/code-error-errors.html.tmpl | 32 + .../hook/global/messages-messages.html.tmpl | 62 + .../user-error-auth_failure_object.html.tmpl | 8 + .../global/user-error-end_object_name.html.tmpl | 12 + .../hook/global/user-error-errors.html.tmpl | 172 + .../pages/ruleengine/details/change.html.tmpl | 30 + .../pages/ruleengine/details/history.html.tmpl | 26 + .../pages/ruleengine/details/index.html.tmpl | 165 + .../en/default/pages/ruleengine/edit.html.tmpl | 312 + .../default/pages/ruleengine/edit/page1.html.tmpl | 146 + .../default/pages/ruleengine/edit/page2.html.tmpl | 331 + .../default/pages/ruleengine/edit/page3.html.tmpl | 301 + .../default/pages/ruleengine/edit/page4.html.tmpl | 139 + .../default/pages/ruleengine/edit/page5.html.tmpl | 21 + .../pages/ruleengine/flaggroup/edit.html.tmpl | 87 + .../pages/ruleengine/flaggroup/history.html.tmpl | 35 + .../pages/ruleengine/flaggroup/list.html.tmpl | 61 + .../default/pages/ruleengine/group/del.html.tmpl | 33 + .../default/pages/ruleengine/group/edit.html.tmpl | 150 + .../default/pages/ruleengine/group/list.html.tmpl | 55 + .../en/default/pages/ruleengine/index.html.tmpl | 196 + .../default/pages/ruleengine/killswitch.html.tmpl | 115 + .../en/default/ruleengine/explain.html.tmpl | 187 + .../en/default/ruleengine/footer.html.tmpl | 12 + extensions/RuleEngine/web/admin.css | 29 + extensions/RuleEngine/web/admin.js | 190 + extensions/RuleEngine/web/component-columns.js | 125 + extensions/SAML2Auth/Config.pm | 62 + extensions/SAML2Auth/Extension.pm | 314 + extensions/SAML2Auth/README | 3 + extensions/SAML2Auth/bin/import-metadata.pl | 45 + extensions/SAML2Auth/lib/IDP.pm | 381 + extensions/SAML2Auth/lib/Login.pm | 351 + extensions/SAML2Auth/lib/Params.pm | 63 + extensions/SAML2Auth/lib/Util.pm | 122 + extensions/SAML2Auth/lib/Verify.pm | 41 + .../auth/saml2auth-verify-account.html.tmpl | 45 + .../en/default/admin/params/saml2auth.html.tmpl | 14 + .../auth/login-additional_methods.html.tmpl | 15 + .../auth/login-small-additional_methods.html.tmpl | 18 + .../hook/admin/admin-end_links_left.html.tmpl | 8 + .../hook/global/user-error-errors.html.tmpl | 67 + .../en/default/pages/saml2auth/settings.html.tmpl | 96 + .../en/default/saml2auth/metadata.xml.tmpl | 32 + extensions/SAML2Auth/web/README | 7 + extensions/SecureMail/Config.pm | 47 + extensions/SecureMail/Extension.pm | 905 + extensions/SecureMail/README | 8 + extensions/SecureMail/lib/Params.pm | 61 + .../account/email/encryption-required.txt.tmpl | 20 + .../default/account/email/securemail-test.txt.tmpl | 24 + .../en/default/account/prefs/securemail.html.tmpl | 46 + .../en/default/admin/params/securemail.html.tmpl | 13 + .../hook/account/prefs/prefs-tabs.html.tmpl | 28 + .../account/prefs/securemail-moreinfo.html.tmpl | 7 + .../hook/admin/groups/create-field.html.tmpl | 25 + .../default/hook/admin/groups/edit-field.html.tmpl | 27 + .../hook/global/code-error-errors.html.tmpl | 26 + .../global/messages-group_updated_fields.html.tmpl | 7 + .../hook/global/user-error-errors.html.tmpl | 27 + .../en/default/pages/securemail/help.html.tmpl | 130 + extensions/SelectizeJS/Config.pm | 16 + extensions/SelectizeJS/Extension.pm | 34 + extensions/SelectizeJS/lib/Util.pm | 19 + extensions/SelectizeJS/lib/WebService/Search.pm | 413 + .../en/default/bug/component-input.html.tmpl | 58 + .../SelectizeJS/template/en/default/hook/README | 5 + .../default/hook/bug/create/create-end.html.tmpl | 15 + .../en/default/hook/global/footer-end.html.tmpl | 32 + .../en/default/hook/global/header-start.html.tmpl | 21 + .../en/default/list/change-columns.html.tmpl | 104 + extensions/SelectizeJS/web/README | 7 + extensions/SelectizeJS/web/css/SelectizeJS.css | 230 + .../SelectizeJS/web/css/selectize.bootstrap2.css | 503 + .../SelectizeJS/web/css/selectize.bootstrap3.css | 417 + extensions/SelectizeJS/web/css/selectize.css | 333 + .../SelectizeJS/web/css/selectize.default.css | 403 + .../SelectizeJS/web/css/selectize.legacy.css | 380 + extensions/SelectizeJS/web/js/SelectizeJS.js | 1204 + .../SelectizeJS/web/js/standalone/selectize.js | 3935 +++ .../SelectizeJS/web/js/standalone/selectize.min.js | 166 + extensions/SubComponents/Config.pm | 16 + extensions/SubComponents/Extension.pm | 1497 + extensions/SubComponents/lib/Config.pm | 34 + extensions/SubComponents/lib/RHSubComponent.pm | 911 + extensions/SubComponents/lib/WebService.pm | 816 + extensions/SubComponents/rh-subcomp-report.pl | 142 + .../en/default/bug/sub_component.html.tmpl | 84 + .../en/default/email/sub_comp_change.txt.tmpl | 25 + .../admin/components/edit-common-rows.html.tmpl | 15 + .../admin/components/list-before_table.html.tmpl | 20 + .../params/editparams-current_panel.html.tmpl | 12 + .../admin/sanitycheck/messages-statuses.html.tmpl | 22 + ...ilities-sub_component_responsibilites.html.tmpl | 53 + .../en/default/hook/bug/show-header-end.html.tmpl | 10 + .../hook/global/code-error-errors.html.tmpl | 10 + .../hook/global/messages-messages.html.tmpl | 15 + .../global/user-error-end_object_name.html.tmpl | 6 + .../hook/global/user-error-errors.html.tmpl | 72 + .../en/default/pages/subcomponents/del.html.tmpl | 59 + .../en/default/pages/subcomponents/edit.html.tmpl | 211 + .../en/default/pages/subcomponents/list.html.tmpl | 166 + .../en/default/search/sub_components.html.tmpl | 27 + extensions/TreeViewPlus/Config.pm | 21 + extensions/TreeViewPlus/Extension.pm | 99 + extensions/TreeViewPlus/README.md | 45 + extensions/TreeViewPlus/lib/Util.pm | 118 + .../search_include_dependencies_5.0.patch | 130 + .../TreeViewPlus/template/en/default/hook/README | 5 + .../hook/bug/edit-after_bug_fields.html.tmpl | 18 + .../hook/bug/field-end_field_column.html.tmpl | 11 + .../en/default/hook/bug/navigate-links.html.tmpl | 13 + .../default/hook/global/field-descs-end.none.tmpl | 5 + .../en/default/hook/list/list-links.html.tmpl | 16 + .../template/en/default/list/list-tvp.html.tmpl | 97 + extensions/TreeViewPlus/web/css/bugtree/icons.gif | Bin 0 -> 4681 bytes .../TreeViewPlus/web/css/bugtree/loading.gif | Bin 0 -> 570 bytes extensions/TreeViewPlus/web/css/bugtree/tree.css | 462 + extensions/TreeViewPlus/web/css/bugtree/vline.gif | Bin 0 -> 844 bytes .../TreeViewPlus/web/js/jquery.dynatree-1.2.8.js | 3457 +++ .../web/js/jquery.dynatree-1.2.8.min.js | 4 + extensions/TreeViewPlus/web/js/tvp.js | 381 + extensions/Voting/Config.pm | 6 +- extensions/Voting/Extension.pm | 1414 +- extensions/Voting/lib/WebService.pm | 95 + .../hook/bug/edit-after_importance.html.tmpl | 6 +- .../template/en/default/pages/voting.html.tmpl | 3 - extensions/Voting/web/style.css | 12 +- extensions/Workflows/Config.pm | 19 + extensions/Workflows/Extension.pm | 348 + extensions/Workflows/lib/GroupApproval.pm | 784 + extensions/Workflows/lib/GroupRequest.pm | 455 + extensions/Workflows/lib/ManageComponents.pm | 328 + extensions/Workflows/lib/Util.pm | 115 + .../Workflows/lib/WebService/GroupRequest.pm | 193 + .../Workflows/lib/WebService/ManageComponents.pm | 396 + .../hook/admin/admin-end_links_left.html.tmpl | 10 + .../global/common-links-action-links.html.tmpl | 16 + .../hook/global/messages-messages.html.tmpl | 30 + .../hook/global/user-error-errors.html.tmpl | 70 + .../pages/workflows/group_req_admin.html.tmpl | 199 + .../pages/workflows/group_request.html.tmpl | 88 + .../pages/workflows/manage_components.html.tmpl | 814 + extensions/Workflows/web/README | 7 + extensions/create.pl | 45 +- importxml.pl | 2087 +- index.cgi | 58 +- install-module.pl | 72 +- jobqueue.pl | 22 +- js/TUI.js | 1 + js/attachment.js | 15 + js/bug.js | 8 +- js/comment-tagging.js | 3 + js/comments.js | 8 +- js/custom-search.js | 2 +- js/diff2html/README | 9 + js/diff2html/diff2html-ui.js | 366 + js/diff2html/diff2html-ui.min.js | 1 + js/diff2html/diff2html.js | 7361 +++++ js/diff2html/diff2html.min.js | 1 + js/diff2html/highlight.pack.js | 2 + js/field.js | 50 +- js/global.js | 4 + jsonrpc.cgi | 7 +- migrate.pl | 15 +- mod_perl.pl | 159 +- page.cgi | 79 +- ping.html | 1 + post_bug.cgi | 254 +- process_bug.cgi | 713 +- query.cgi | 399 +- quips.cgi | 199 +- relogin.cgi | 329 +- report.cgi | 515 +- reports.cgi | 313 +- request.cgi | 494 +- rest.cgi | 9 +- runtests.pl | 12 +- sanitycheck.cgi | 1022 +- sanitycheck.pl | 60 +- search_plugin.cgi | 12 +- show_activity.cgi | 11 +- show_bug.cgi | 135 +- show_comment_activity.cgi | 57 + showdependencygraph.cgi | 420 +- showdependencytree.cgi | 122 +- skins/standard/admin.css | 6 +- skins/standard/bug.css | 3 + skins/standard/buglist.css | 8 +- skins/standard/diff2html/diff2html.css | 369 + skins/standard/diff2html/diff2html.min.css | 1 + skins/standard/diff2html/github.css | 99 + skins/standard/global.css | 4 +- summarize_time.cgi | 503 +- t/001compile.t | 118 +- t/002goodperl.t | 273 +- t/003safesys.t | 63 +- t/004template.t | 184 +- t/005whitespace.t | 68 +- t/006spellcheck.t | 106 +- t/007util.t | 70 +- t/008filter.t | 312 +- t/009bugwords.t | 86 +- t/010dependencies.t | 76 +- t/011pod.t | 162 +- t/012throwables.t | 280 +- t/013dbschema.t | 80 +- t/020ext-versions.t | 33 + t/050CERT.t | 72 + t/090checksetup.t | 23 + t/100Push.t | 51 + t/300rpc.t | 266 + t/310BRE.t | 133 + t/320rpc.t | 125 + t/340GPGin.t | 180 + t/350GroupWorkflows.t | 282 + t/Support/Files.pm | 40 +- t/Support/Templates.pm | 91 +- t/test.conf | 44 + template/en/default/account/auth/login.html.tmpl | 10 +- template/en/default/account/create.html.tmpl | 2 +- template/en/default/account/prefs/apikey.html.tmpl | 38 +- template/en/default/account/prefs/email.html.tmpl | 8 + .../en/default/account/prefs/permissions.html.tmpl | 10 + template/en/default/account/prefs/prefs.html.tmpl | 87 +- .../default/account/prefs/saved-searches.html.tmpl | 15 +- .../en/default/account/prefs/sessions.html.tmpl | 56 + .../en/default/account/prefs/settings.html.tmpl | 3 + .../en/default/account/profile-activity.html.tmpl | 1 + template/en/default/admin/admin.html.tmpl | 13 +- .../admin/components/confirm-delete.html.tmpl | 10 + .../en/default/admin/components/create.html.tmpl | 4 +- .../default/admin/components/edit-common.html.tmpl | 26 +- .../en/default/admin/components/edit.html.tmpl | 4 +- .../en/default/admin/components/footer.html.tmpl | 5 + .../en/default/admin/components/list.html.tmpl | 42 +- .../default/admin/custom_fields/cf-js-rh.js.tmpl | 37 + .../en/default/admin/custom_fields/cf-js.js.tmpl | 3 +- .../default/admin/custom_fields/create.html.tmpl | 10 +- .../admin/custom_fields/edit-common.html.tmpl | 94 +- .../en/default/admin/custom_fields/edit.html.tmpl | 2 +- .../en/default/admin/custom_fields/list.html.tmpl | 47 +- .../admin/flag-type/confirm-delete.html.tmpl | 7 + template/en/default/admin/flag-type/edit.html.tmpl | 61 +- template/en/default/admin/flag-type/list.html.tmpl | 20 +- template/en/default/admin/groups/edit.html.tmpl | 46 +- template/en/default/admin/groups/list.html.tmpl | 22 +- template/en/default/admin/keywords/list.html.tmpl | 14 + .../en/default/admin/params/editparams.html.tmpl | 3 + .../default/admin/params/groupsecurity.html.tmpl | 7 +- .../en/default/admin/products/create.html.tmpl | 1 + template/en/default/admin/products/edit.html.tmpl | 59 +- .../admin/products/groupcontrol/edit.html.tmpl | 26 +- template/en/default/admin/products/list.html.tmpl | 15 + .../admin/releases/confirm-delete.html.tmpl | 82 + .../en/default/admin/releases/create.html.tmpl | 43 + template/en/default/admin/releases/edit.html.tmpl | 52 + .../en/default/admin/releases/footer.html.tmpl | 52 + template/en/default/admin/releases/list.html.tmpl | 97 + .../admin/releases/select-product.html.tmpl | 53 + template/en/default/admin/table.html.tmpl | 19 +- .../default/admin/users/confirm-delete.html.tmpl | 4 +- template/en/default/admin/users/edit.html.tmpl | 67 +- .../default/admin/users/responsibilities.html.tmpl | 29 +- template/en/default/admin/users/userdata.html.tmpl | 4 +- template/en/default/attachment/create.html.tmpl | 16 +- .../attachment/createformcontents.html.tmpl | 25 +- template/en/default/attachment/diff2html.html.tmpl | 85 + template/en/default/attachment/edit.html.tmpl | 6 + template/en/default/attachment/list.html.tmpl | 7 +- .../en/default/bug/activity/comments.html.tmpl | 135 + template/en/default/bug/activity/table.html.tmpl | 19 + template/en/default/bug/comments.html.tmpl | 59 +- .../en/default/bug/create/create-guided.html.tmpl | 508 +- template/en/default/bug/create/create.html.tmpl | 336 +- template/en/default/bug/create/created.html.tmpl | 4 + .../en/default/bug/create/user-message.html.tmpl | 13 +- template/en/default/bug/dependency-tree.html.tmpl | 21 +- template/en/default/bug/edit.html.tmpl | 589 +- template/en/default/bug/field-events.js.tmpl | 3 + template/en/default/bug/field-label.html.tmpl | 7 +- template/en/default/bug/field.html.tmpl | 66 +- template/en/default/bug/knob.html.tmpl | 43 +- template/en/default/bug/link.html.tmpl | 11 +- template/en/default/bug/navigate.html.tmpl | 54 +- template/en/default/bug/needinfo.html.tmpl | 234 + template/en/default/bug/process/midair.html.tmpl | 8 + .../bug/process/verify-new-product.html.tmpl | 78 +- template/en/default/bug/show-header.html.tmpl | 7 +- template/en/default/bug/show-multiple.html.tmpl | 45 +- template/en/default/bug/show.html.tmpl | 1 + template/en/default/bug/show.xml.tmpl | 32 +- template/en/default/config.js.tmpl | 12 + template/en/default/email/bugmail-common.txt.tmpl | 12 +- template/en/default/email/bugmail-header.txt.tmpl | 30 +- template/en/default/email/bugmail.html.tmpl | 18 + template/en/default/email/bugmail.txt.tmpl | 19 + template/en/default/email/flagmail.txt.tmpl | 12 + template/en/default/email/header-common.txt.tmpl | 26 +- template/en/default/email/lockout.txt.tmpl | 2 +- template/en/default/filterexceptions.pl | 9 +- template/en/default/flag/list.html.tmpl | 33 +- template/en/default/global/banner.html.tmpl | 43 + template/en/default/global/code-error.html.tmpl | 10 + .../en/default/global/confirm-user-match.html.tmpl | 5 + template/en/default/global/field-descs.none.tmpl | 10 + template/en/default/global/header.html.tmpl | 38 +- template/en/default/global/messages.html.tmpl | 108 +- .../en/default/global/product-select.html.tmpl | 24 +- template/en/default/global/select-menu.html.tmpl | 7 +- template/en/default/global/user-error.html.tmpl | 118 +- template/en/default/global/user.html.tmpl | 2 +- template/en/default/list/change-columns.html.tmpl | 3 + template/en/default/list/edit-multiple.html.tmpl | 168 +- template/en/default/list/list.atom.tmpl | 2 +- template/en/default/list/list.csv.tmpl | 11 +- template/en/default/list/list.html.tmpl | 132 +- template/en/default/list/list.ics.tmpl | 4 +- template/en/default/list/table.html.tmpl | 116 +- template/en/default/pages/bug-writing.html.tmpl | 20 + template/en/default/pages/bugzilla.dtd.tmpl | 5 + template/en/default/pages/fields.html.tmpl | 348 +- template/en/default/pages/release-notes.html.tmpl | 5 +- template/en/default/pages/whats-new.html.tmpl | 180 + template/en/default/reports/components.html.tmpl | 29 +- .../en/default/reports/duplicates-table.html.tmpl | 2 + template/en/default/reports/keywords.html.tmpl | 2 +- template/en/default/reports/report-table.csv.tmpl | 6 +- template/en/default/reports/report-table.html.tmpl | 56 +- template/en/default/request/queue.html.tmpl | 56 +- .../en/default/search/boolean-charts.html.tmpl | 8 +- template/en/default/search/field.html.tmpl | 45 +- template/en/default/search/form.html.tmpl | 68 +- template/en/default/search/knob.html.tmpl | 1 + .../en/default/search/search-advanced.html.tmpl | 2 +- .../default/search/search-report-select.html.tmpl | 4 + template/en/default/setup/strings.txt.pl | 1 + template/en/default/whine/mail.txt.tmpl | 2 + testserver.pl | 360 +- token.cgi | 422 +- userprefs.cgi | 979 +- votes.cgi | 18 +- whine.pl | 923 +- whineatnews.pl | 71 +- xmlrpc.cgi | 16 +- 1174 files changed, 274307 insertions(+), 54053 deletions(-) create mode 100644 .perltidyrc create mode 100644 Bugzilla/Auth/Verify/RedHat.pm create mode 100644 Bugzilla/ModPerl.pm create mode 100644 Bugzilla/Release.pm create mode 100644 Bugzilla/Template/PreloadProvider.pm create mode 100644 Bugzilla/User/Session.pm create mode 100644 README.md create mode 100644 docs/en/images/redhat_logo.png create mode 100755 editreleases.cgi create mode 100644 errors/401.html create mode 100644 errors/403.html create mode 100644 errors/404.html create mode 100644 errors/500.html create mode 100644 extensions/ActivityReport/Config.pm create mode 100644 extensions/ActivityReport/Extension.pm create mode 100644 extensions/ActivityReport/lib/Reports.pm create mode 100644 extensions/ActivityReport/lib/WebService.pm create mode 100644 extensions/ActivityReport/template/en/default/hook/global/user-error-errors.html.tmpl create mode 100644 extensions/ActivityReport/template/en/default/hook/reports/menu-end.html.tmpl create mode 100644 extensions/ActivityReport/template/en/default/pages/email_queue.html.tmpl create mode 100644 extensions/ActivityReport/template/en/default/pages/group_admins.html.tmpl create mode 100644 extensions/ActivityReport/template/en/default/pages/group_membership.html.tmpl create mode 100644 extensions/ActivityReport/template/en/default/pages/group_membership.txt.tmpl create mode 100644 extensions/ActivityReport/template/en/default/pages/user_activity.html.tmpl create mode 100644 extensions/ActivityReport/template/en/default/pages/user_admin_activity.html.tmpl create mode 100644 extensions/ActivityReport/web/styles/reports.css create mode 100644 extensions/AgileTools/Config.pm create mode 100644 extensions/AgileTools/Extension.pm create mode 100644 extensions/AgileTools/README.md create mode 100644 extensions/AgileTools/lib/Backlog.pm create mode 100644 extensions/AgileTools/lib/Burn.pm create mode 100644 extensions/AgileTools/lib/Constants.pm create mode 100644 extensions/AgileTools/lib/Pages.pm create mode 100644 extensions/AgileTools/lib/Pages/Scrum.pm create mode 100644 extensions/AgileTools/lib/Pages/Team.pm create mode 100644 extensions/AgileTools/lib/Params.pm create mode 100644 extensions/AgileTools/lib/Pool.pm create mode 100644 extensions/AgileTools/lib/Role.pm create mode 100644 extensions/AgileTools/lib/Schema.pm create mode 100644 extensions/AgileTools/lib/Sprint.pm create mode 100644 extensions/AgileTools/lib/Team.pm create mode 100644 extensions/AgileTools/lib/Util.pm create mode 100644 extensions/AgileTools/lib/WebService/Backlog.pm create mode 100644 extensions/AgileTools/lib/WebService/Pool.pm create mode 100644 extensions/AgileTools/lib/WebService/Sprint.pm create mode 100644 extensions/AgileTools/lib/WebService/Team.pm create mode 100644 extensions/AgileTools/lib/WebService/Util.pm create mode 100755 extensions/AgileTools/set_current_sprint.pl create mode 100644 extensions/AgileTools/template/en/default/admin/params/agiletools.html.tmpl create mode 100644 extensions/AgileTools/template/en/default/agiletools/README create mode 100644 extensions/AgileTools/template/en/default/agiletools/blitem.html.tmpl create mode 100644 extensions/AgileTools/template/en/default/agiletools/burn-chart.html.tmpl create mode 100644 extensions/AgileTools/template/en/default/agiletools/burn-init.html.tmpl create mode 100644 extensions/AgileTools/template/en/default/agiletools/process/1_summary_links.html.tmpl create mode 100644 extensions/AgileTools/template/en/default/filterexceptions.pl create mode 100644 extensions/AgileTools/template/en/default/hook/README create mode 100644 extensions/AgileTools/template/en/default/hook/admin/admin-end_links_left.html.tmpl create mode 100644 extensions/AgileTools/template/en/default/hook/admin/components/edit-common-rows.html.tmpl create mode 100644 extensions/AgileTools/template/en/default/hook/admin/products/edit-common-rows.html.tmpl create mode 100644 extensions/AgileTools/template/en/default/hook/admin/products/updated-changes.html.tmpl create mode 100644 extensions/AgileTools/template/en/default/hook/admin/sanitycheck/messages-statuses.html.tmpl create mode 100644 extensions/AgileTools/template/en/default/hook/bug/edit-after_people.html.tmpl create mode 100644 extensions/AgileTools/template/en/default/hook/bug/field-help-end.none.tmpl create mode 100644 extensions/AgileTools/template/en/default/hook/global/code-error-errors.html.tmpl create mode 100644 extensions/AgileTools/template/en/default/hook/global/common-links-end-of-menu.html.tmpl create mode 100644 extensions/AgileTools/template/en/default/hook/global/header-additional_header.html.tmpl create mode 100644 extensions/AgileTools/template/en/default/hook/global/messages-component_updated_fields.html.tmpl create mode 100644 extensions/AgileTools/template/en/default/hook/global/messages-messages.html.tmpl create mode 100644 extensions/AgileTools/template/en/default/hook/global/user-error-end_object_name.html.tmpl create mode 100644 extensions/AgileTools/template/en/default/hook/global/user-error-errors.html.tmpl create mode 100644 extensions/AgileTools/template/en/default/hook/global/variables-end.none.tmpl create mode 100644 extensions/AgileTools/template/en/default/hook/list/edit-multiple-after_custom_fields.html.tmpl create mode 100644 extensions/AgileTools/template/en/default/hook/list/list-links.html.tmpl create mode 100644 extensions/AgileTools/template/en/default/list/list-burn.html.tmpl create mode 100644 extensions/AgileTools/template/en/default/pages/agile_component_mapping.csv.tmpl create mode 100644 extensions/AgileTools/template/en/default/pages/agile_component_mapping.html.tmpl create mode 100644 extensions/AgileTools/template/en/default/pages/agile_component_mapping.json.tmpl create mode 100644 extensions/AgileTools/template/en/default/pages/agile_team_membership.csv.tmpl create mode 100644 extensions/AgileTools/template/en/default/pages/agile_team_membership.html.tmpl create mode 100644 extensions/AgileTools/template/en/default/pages/agile_team_membership.json.tmpl create mode 100644 extensions/AgileTools/template/en/default/pages/agiletools/admin.html.tmpl create mode 100644 extensions/AgileTools/template/en/default/pages/agiletools/scrum/planning.html.tmpl create mode 100644 extensions/AgileTools/template/en/default/pages/agiletools/scrum/sprints.html.tmpl create mode 100644 extensions/AgileTools/template/en/default/pages/agiletools/team/create.html.tmpl create mode 100644 extensions/AgileTools/template/en/default/pages/agiletools/team/list.html.tmpl create mode 100644 extensions/AgileTools/template/en/default/pages/agiletools/team/show.html.tmpl create mode 100644 extensions/AgileTools/web/css/base.css create mode 100644 extensions/AgileTools/web/css/buglist.css create mode 100644 extensions/AgileTools/web/css/colorbox.css create mode 100644 extensions/AgileTools/web/css/images/border1.png create mode 100644 extensions/AgileTools/web/css/images/border2.png create mode 100644 extensions/AgileTools/web/css/images/loading.gif create mode 100644 extensions/AgileTools/web/css/planning.css create mode 100644 extensions/AgileTools/web/js/buglist.js create mode 100644 extensions/AgileTools/web/js/burn.js create mode 100644 extensions/AgileTools/web/js/jquery.colorbox-min.js create mode 100644 extensions/AgileTools/web/js/jquery.flot.axislabels.js create mode 100644 extensions/AgileTools/web/js/jquery.flot.min.js create mode 100644 extensions/AgileTools/web/js/scrum.js create mode 100644 extensions/AgileTools/web/js/team.js create mode 100644 extensions/AntiSpam/Config.pm create mode 100644 extensions/AntiSpam/Extension.pm create mode 100644 extensions/AntiSpam/lib/Config.pm create mode 100644 extensions/AntiSpam/template/en/default/admin/params/antispam.html.tmpl create mode 100644 extensions/AntiSpam/template/en/default/hook/admin/admin-end_links_right.html.tmpl create mode 100644 extensions/AntiSpam/template/en/default/hook/admin/params/editparams-current_panel.html.tmpl create mode 100644 extensions/AntiSpam/template/en/default/hook/global/user-error-errors.html.tmpl create mode 100644 extensions/AuthJWT/Config.pm create mode 100644 extensions/AuthJWT/Extension.pm create mode 100644 extensions/AuthJWT/lib/Login.pm create mode 100644 extensions/AuthJWT/lib/Source.pm create mode 100644 extensions/AuthJWT/lib/Util.pm create mode 100644 extensions/AuthJWT/template/en/default/authjwt/README create mode 100644 extensions/AuthJWT/template/en/default/hook/README create mode 100644 extensions/AuthJWT/template/en/default/hook/admin/admin-end_links_left.html.tmpl create mode 100644 extensions/AuthJWT/template/en/default/hook/global/user-error-errors.html.tmpl create mode 100644 extensions/AuthJWT/template/en/default/pages/authjwt/settings.html.tmpl create mode 100644 extensions/BayotBase/Config.pm create mode 100644 extensions/BayotBase/Extension.pm create mode 100644 extensions/BayotBase/README.rst create mode 100644 extensions/BayotBase/buglist_format-4.2.4.patch create mode 100644 extensions/BayotBase/lib/Config.pm create mode 100644 extensions/BayotBase/lib/Util.pm create mode 100644 extensions/BayotBase/template/en/default/admin/params/bayotbase.html.tmpl create mode 100644 extensions/BayotBase/template/en/default/bb/wraplist.html.tmpl create mode 100644 extensions/BayotBase/template/en/default/hook/global/common-links-link-row.html.tmpl create mode 100644 extensions/BayotBase/template/en/default/hook/global/header-start.html.tmpl create mode 100644 extensions/BayotBase/template/en/default/pages/bayotbase/fielddefs.js.tmpl create mode 100644 extensions/BayotBase/web/css/base.css create mode 100644 extensions/BayotBase/web/css/images/spinner.gif create mode 100644 extensions/BayotBase/web/css/images/ui-bg_flat_0_aaaaaa_40x100.png create mode 100644 extensions/BayotBase/web/css/images/ui-bg_flat_75_ffffff_40x100.png create mode 100644 extensions/BayotBase/web/css/images/ui-bg_glass_55_fbf9ee_1x400.png create mode 100644 extensions/BayotBase/web/css/images/ui-bg_glass_65_ffffff_1x400.png create mode 100644 extensions/BayotBase/web/css/images/ui-bg_glass_75_dadada_1x400.png create mode 100644 extensions/BayotBase/web/css/images/ui-bg_glass_75_e6e6e6_1x400.png create mode 100644 extensions/BayotBase/web/css/images/ui-bg_glass_95_fef1ec_1x400.png create mode 100644 extensions/BayotBase/web/css/images/ui-bg_highlight-soft_75_cccccc_1x100.png create mode 100644 extensions/BayotBase/web/css/images/ui-icons_222222_256x240.png create mode 100644 extensions/BayotBase/web/css/images/ui-icons_2e83ff_256x240.png create mode 100644 extensions/BayotBase/web/css/images/ui-icons_454545_256x240.png create mode 100644 extensions/BayotBase/web/css/images/ui-icons_888888_256x240.png create mode 100644 extensions/BayotBase/web/css/images/ui-icons_cd0a0a_256x240.png create mode 100644 extensions/BayotBase/web/css/jquery-ui-1.9.2.custom.css create mode 100644 extensions/BayotBase/web/css/jquery-ui-1.9.2.custom.min.css create mode 100644 extensions/BayotBase/web/js/Base.js create mode 100644 extensions/BayotBase/web/js/README_JQUERY_UI create mode 100644 extensions/BayotBase/web/js/bayot.util.js create mode 100644 extensions/BayotBase/web/js/es5-shim.js create mode 100644 extensions/BayotBase/web/js/es5-shim.min.js create mode 100644 extensions/BayotBase/web/js/jquery-1.12.4.js create mode 100644 extensions/BayotBase/web/js/jquery-1.12.4.min.js create mode 100644 extensions/BayotBase/web/js/jquery-ui-1.9.2.custom.js create mode 100644 extensions/BayotBase/web/js/jquery-ui-1.9.2.custom.min.js create mode 100644 extensions/BayotBase/web/js/jquery.cookie.js create mode 100644 extensions/BayotBase/web/js/jquery.jsonrpc.js create mode 100644 extensions/BugViewPlus/Config.pm create mode 100644 extensions/BugViewPlus/Extension.pm create mode 100644 extensions/BugViewPlus/Makefile create mode 100644 extensions/BugViewPlus/README.md create mode 100644 extensions/BugViewPlus/lib/Params.pm create mode 100644 extensions/BugViewPlus/lib/Template.pm create mode 100644 extensions/BugViewPlus/template/en/default/admin/params/bugviewplus.html.tmpl create mode 100644 extensions/BugViewPlus/template/en/default/filterexceptions.pl create mode 100644 extensions/BugViewPlus/template/en/default/hook/admin/admin-end_links_right.html.tmpl create mode 100644 extensions/BugViewPlus/template/en/default/hook/bug/comments-aftercomments.html.tmpl create mode 100644 extensions/BugViewPlus/template/en/default/hook/bug/edit-after_custom_fields.html.tmpl create mode 100644 extensions/BugViewPlus/template/en/default/hook/bug/show-header-end.html.tmpl create mode 100644 extensions/BugViewPlus/template/en/default/hook/global/header-additional_header.html.tmpl create mode 100644 extensions/BugViewPlus/template/en/default/hook/global/messages-messages.html.tmpl create mode 100644 extensions/BugViewPlus/template/en/default/hook/global/setting-descs-settings.none.tmpl create mode 100644 extensions/BugViewPlus/template/en/default/hook/search/form-after_selects_top.html.tmpl create mode 100644 extensions/BugViewPlus/template/en/default/pages/bvp_template.html.tmpl create mode 100644 extensions/BugViewPlus/web/css/editor_toggle.css create mode 100644 extensions/BugViewPlus/web/css/inline_editor.css create mode 100644 extensions/BugViewPlus/web/css/templates.css create mode 100644 extensions/BugViewPlus/web/js/editor_toggle.js create mode 100644 extensions/BugViewPlus/web/js/inline_editor.js create mode 100644 extensions/BugViewPlus/web/js/templates.js create mode 100644 extensions/ComponentWatching/Config.pm create mode 100644 extensions/ComponentWatching/Extension.pm create mode 100644 extensions/ComponentWatching/template/en/default/account/prefs/component_watch.html.tmpl create mode 100644 extensions/ComponentWatching/template/en/default/hook/account/prefs/email-relationships.html.tmpl create mode 100644 extensions/ComponentWatching/template/en/default/hook/account/prefs/prefs-tabs.html.tmpl create mode 100644 extensions/ComponentWatching/template/en/default/hook/admin/sanitycheck/messages-statuses.html.tmpl create mode 100644 extensions/ComponentWatching/template/en/default/hook/global/code-error-errors.html.tmpl create mode 100644 extensions/ComponentWatching/template/en/default/hook/global/messages-component_updated_fields.html.tmpl create mode 100644 extensions/ComponentWatching/template/en/default/hook/global/reason-descs-end.none.tmpl create mode 100644 extensions/ComponentWatching/template/en/default/hook/global/user-error-errors.html.tmpl create mode 100644 extensions/DependentProducts/Config.pm create mode 100644 extensions/DependentProducts/Extension.pm create mode 100644 extensions/DependentProducts/README create mode 100644 extensions/DependentProducts/lib/Bug.pm create mode 100644 extensions/DependentProducts/lib/Contact.pm create mode 100644 extensions/DependentProducts/lib/Record.pm create mode 100644 extensions/DependentProducts/lib/Util.pm create mode 100644 extensions/DependentProducts/lib/WebService.pm create mode 100644 extensions/DependentProducts/template/en/default/filterexceptions.pl create mode 100644 extensions/DependentProducts/template/en/default/hook/admin/products/edit-common-rows.html.tmpl create mode 100644 extensions/DependentProducts/template/en/default/hook/admin/products/updated-changes.html.tmpl create mode 100644 extensions/DependentProducts/template/en/default/hook/bug/create/create-form.html.tmpl create mode 100644 extensions/DependentProducts/template/en/default/hook/bug/edit-after_custom_fields.html.tmpl create mode 100644 extensions/DependentProducts/template/en/default/hook/bug/field-help-end.none.tmpl create mode 100644 extensions/DependentProducts/template/en/default/hook/global/field-descs-end.none.tmpl create mode 100644 extensions/DependentProducts/template/en/default/hook/global/reason-descs-end.none.tmpl create mode 100644 extensions/DependentProducts/template/en/default/hook/global/user-error-errors.html.tmpl create mode 100644 extensions/DependentProducts/template/en/default/hook/list/edit-multiple-after_custom_fields.html.tmpl create mode 100644 extensions/DependentProducts/template/en/filterexceptions.pl create mode 100644 extensions/EditTable/Config.pm create mode 100644 extensions/EditTable/Extension.pm create mode 100644 extensions/EditTable/template/en/default/hook/global/user-error-auth_failure_object.html.tmpl create mode 100644 extensions/EditTable/template/en/default/hook/global/user-error-errors.html.tmpl create mode 100644 extensions/EditTable/template/en/default/pages/edit_table.html.tmpl create mode 100644 extensions/EditTable/web/js/edit_table.js create mode 100644 extensions/EditTable/web/styles/edit_table.css create mode 100644 extensions/ExternalBugs/Config.pm create mode 100644 extensions/ExternalBugs/Extension.pm create mode 100644 extensions/ExternalBugs/lib/Bug.pm create mode 100644 extensions/ExternalBugs/lib/Config.pm create mode 100644 extensions/ExternalBugs/lib/Job/JIRA.pm create mode 100644 extensions/ExternalBugs/lib/Job/SFDC.pm create mode 100644 extensions/ExternalBugs/lib/Regex.pm create mode 100644 extensions/ExternalBugs/lib/Type.pm create mode 100644 extensions/ExternalBugs/lib/Type/Bugzilla.pm create mode 100644 extensions/ExternalBugs/lib/Type/Gerrit.pm create mode 100644 extensions/ExternalBugs/lib/Type/GitHub.pm create mode 100644 extensions/ExternalBugs/lib/Type/JIRA.pm create mode 100644 extensions/ExternalBugs/lib/Type/KBase.pm create mode 100644 extensions/ExternalBugs/lib/Type/Redmine.pm create mode 100644 extensions/ExternalBugs/lib/Type/SFDC.pm create mode 100644 extensions/ExternalBugs/lib/Type/SFDC_DEV.pm create mode 100644 extensions/ExternalBugs/lib/Type/SFDC_QA.pm create mode 100644 extensions/ExternalBugs/lib/Type/SFDC_Sandbox.pm create mode 100644 extensions/ExternalBugs/lib/Type/SFDC_Stage.pm create mode 100644 extensions/ExternalBugs/lib/Util.pm create mode 100644 extensions/ExternalBugs/lib/WebService.pm create mode 100644 extensions/ExternalBugs/template/en/default/admin/params/externalbugs.html.tmpl create mode 100644 extensions/ExternalBugs/template/en/default/bug/extbz-check.js.tmpl create mode 100644 extensions/ExternalBugs/template/en/default/filterexceptions.pl create mode 100644 extensions/ExternalBugs/template/en/default/hook/admin/admin-end_links_left.html.tmpl create mode 100644 extensions/ExternalBugs/template/en/default/hook/bug/create/create-end.html.tmpl create mode 100644 extensions/ExternalBugs/template/en/default/hook/bug/create/create-form.html.tmpl create mode 100644 extensions/ExternalBugs/template/en/default/hook/bug/edit-table-data.html.tmpl create mode 100644 extensions/ExternalBugs/template/en/default/hook/global/code-error-errors.html.tmpl create mode 100644 extensions/ExternalBugs/template/en/default/hook/global/field-descs-end.none.tmpl create mode 100644 extensions/ExternalBugs/template/en/default/hook/global/header-start.html.tmpl create mode 100644 extensions/ExternalBugs/template/en/default/hook/global/messages-messages.html.tmpl create mode 100644 extensions/ExternalBugs/template/en/default/hook/global/user-error-end_object_name.html.tmpl create mode 100644 extensions/ExternalBugs/template/en/default/hook/global/user-error-errors.html.tmpl create mode 100644 extensions/ExternalBugs/template/en/default/hook/list/edit-multiple-after_custom_fields.html.tmpl create mode 100644 extensions/ExternalBugs/template/en/default/hook/list/table-list_table_abbrev.html.tmpl create mode 100644 extensions/ExternalBugs/template/en/default/pages/externalbugs/confirm-delete.html.tmpl create mode 100644 extensions/ExternalBugs/template/en/default/pages/externalbugs/edit.html.tmpl create mode 100644 extensions/ExternalBugs/template/en/default/pages/externalbugs/extbug_js.html.tmpl create mode 100644 extensions/ExternalBugs/template/en/default/pages/externalbugs/list.html.tmpl create mode 100644 extensions/ExternalBugs/template/en/default/pages/externalbugs/regexs/confirm-delete.html.tmpl create mode 100644 extensions/ExternalBugs/template/en/default/pages/externalbugs/regexs/edit.html.tmpl create mode 100644 extensions/ExternalBugs/template/en/default/pages/externalbugs/regexs/list.html.tmpl create mode 100644 extensions/ExternalBugs/template/en/default/setup/strings.txt.pl create mode 100644 extensions/ExternalBugs/template/en/filterexceptions.pl create mode 100644 extensions/ExternalBugs/web/css/global.css create mode 100644 extensions/ExternalBugs/web/images/wait20trans.gif create mode 100644 extensions/ExternalBugs/web/js/external_bugs.js create mode 100644 extensions/FontAwesome/Config.pm create mode 100644 extensions/FontAwesome/Extension.pm create mode 100644 extensions/FontAwesome/lib/Util.pm create mode 100644 extensions/FontAwesome/template/en/default/hook/global/header-start.html.tmpl create mode 100644 extensions/FontAwesome/web/LICENSE.txt create mode 100644 extensions/FontAwesome/web/css/all.css create mode 100644 extensions/FontAwesome/web/css/all.min.css create mode 100644 extensions/FontAwesome/web/css/brands.css create mode 100644 extensions/FontAwesome/web/css/brands.min.css create mode 100644 extensions/FontAwesome/web/css/fontawesome.css create mode 100644 extensions/FontAwesome/web/css/fontawesome.min.css create mode 100644 extensions/FontAwesome/web/css/regular.css create mode 100644 extensions/FontAwesome/web/css/regular.min.css create mode 100644 extensions/FontAwesome/web/css/solid.css create mode 100644 extensions/FontAwesome/web/css/solid.min.css create mode 100644 extensions/FontAwesome/web/css/svg-with-js.css create mode 100644 extensions/FontAwesome/web/css/svg-with-js.min.css create mode 100644 extensions/FontAwesome/web/css/v4-shims.css create mode 100644 extensions/FontAwesome/web/css/v4-shims.min.css create mode 100644 extensions/FontAwesome/web/webfonts/fa-brands-400.eot create mode 100644 extensions/FontAwesome/web/webfonts/fa-brands-400.svg create mode 100644 extensions/FontAwesome/web/webfonts/fa-brands-400.ttf create mode 100644 extensions/FontAwesome/web/webfonts/fa-brands-400.woff create mode 100644 extensions/FontAwesome/web/webfonts/fa-brands-400.woff2 create mode 100644 extensions/FontAwesome/web/webfonts/fa-regular-400.eot create mode 100644 extensions/FontAwesome/web/webfonts/fa-regular-400.svg create mode 100644 extensions/FontAwesome/web/webfonts/fa-regular-400.ttf create mode 100644 extensions/FontAwesome/web/webfonts/fa-regular-400.woff create mode 100644 extensions/FontAwesome/web/webfonts/fa-regular-400.woff2 create mode 100644 extensions/FontAwesome/web/webfonts/fa-solid-900.eot create mode 100644 extensions/FontAwesome/web/webfonts/fa-solid-900.svg create mode 100644 extensions/FontAwesome/web/webfonts/fa-solid-900.ttf create mode 100644 extensions/FontAwesome/web/webfonts/fa-solid-900.woff create mode 100644 extensions/FontAwesome/web/webfonts/fa-solid-900.woff2 create mode 100644 extensions/InlineHistory/Config.pm create mode 100644 extensions/InlineHistory/Extension.pm create mode 100644 extensions/InlineHistory/README create mode 100644 extensions/InlineHistory/lib/Util.pm create mode 100644 extensions/InlineHistory/template/en/default/hook/bug/comments-aftercomments.html.tmpl create mode 100644 extensions/InlineHistory/template/en/default/hook/bug/comments-comment_banner.html.tmpl create mode 100644 extensions/InlineHistory/template/en/default/hook/bug/show-header-end.html.tmpl create mode 100644 extensions/InlineHistory/template/en/default/hook/global/setting-descs-settings.none.tmpl create mode 100644 extensions/InlineHistory/web/inline-history.js create mode 100644 extensions/InlineHistory/web/style.css create mode 100644 extensions/ListOfBugs/Config.pm create mode 100644 extensions/ListOfBugs/Extension.pm create mode 100644 extensions/ListOfBugs/lib/BugList.pm create mode 100644 extensions/ListOfBugs/lib/Util.pm create mode 100644 extensions/ListOfBugs/lib/WebService.pm create mode 100644 extensions/ListOfBugs/template/en/default/account/prefs/user_bug_lists.html.tmpl create mode 100644 extensions/ListOfBugs/template/en/default/hook/README create mode 100644 extensions/ListOfBugs/template/en/default/hook/account/prefs/prefs-tabs.html.tmpl create mode 100644 extensions/ListOfBugs/template/en/default/hook/admin/admin-end_links_left.html.tmpl create mode 100644 extensions/ListOfBugs/template/en/default/hook/global/field-descs-end.none.tmpl create mode 100644 extensions/ListOfBugs/template/en/default/hook/global/user-error-errors.html.tmpl create mode 100644 extensions/ListOfBugs/template/en/default/hook/list/list-links.html.tmpl create mode 100644 extensions/ListOfBugs/template/en/default/pages/listofbugs/create.html.tmpl create mode 100644 extensions/ListOfBugs/template/en/default/pages/listofbugs/delete.html.tmpl create mode 100644 extensions/ListOfBugs/template/en/default/pages/listofbugs/manage.html.tmpl create mode 100644 extensions/ListOfBugs/template/en/default/pages/listofbugs/save.html.tmpl create mode 100644 extensions/ListOfBugs/web/README create mode 100644 extensions/MultipleValues/Config.pm create mode 100644 extensions/MultipleValues/Extension.pm create mode 100644 extensions/MultipleValues/template/en/default/filterexceptions.pl create mode 100644 extensions/MultipleValues/template/en/default/hook/README create mode 100644 extensions/MultipleValues/template/en/default/hook/global/code-error-errors.html.tmpl create mode 100644 extensions/MultipleValues/template/en/default/hook/global/user-error-errors.html.tmpl create mode 100644 extensions/MultipleValues/template/en/default/multiples/README create mode 100644 extensions/MultipleValues/template/en/filterexceptions.pl create mode 100644 extensions/MultipleValues/web/README create mode 100644 extensions/PlotlyReports/Config.pm create mode 100644 extensions/PlotlyReports/Extension.pm create mode 100644 extensions/PlotlyReports/lib/Graphs/BugSeverity.pm create mode 100644 extensions/PlotlyReports/lib/Graphs/BugStatus.pm create mode 100644 extensions/PlotlyReports/lib/Graphs/NonDefaults.pm create mode 100644 extensions/PlotlyReports/lib/Util.pm create mode 100644 extensions/PlotlyReports/lib/WebService.pm create mode 100644 extensions/PlotlyReports/template/en/default/hook/admin/products/edit-common-rows.html.tmpl create mode 100644 extensions/PlotlyReports/template/en/default/hook/admin/products/updated-changes.html.tmpl create mode 100644 extensions/PlotlyReports/template/en/default/hook/global/user-error-errors.html.tmpl create mode 100644 extensions/PlotlyReports/template/en/default/hook/reports/menu-end.html.tmpl create mode 100644 extensions/PlotlyReports/template/en/default/pages/bug_severity.html.tmpl create mode 100644 extensions/PlotlyReports/template/en/default/pages/bug_status.html.tmpl create mode 100644 extensions/PlotlyReports/template/en/default/pages/non_defaults.html.tmpl create mode 100644 extensions/PlotlyReports/web/README create mode 100644 extensions/PlotlyReports/web/plotly/plotly.min.js create mode 100644 extensions/ProductDashboard/Config.pm create mode 100644 extensions/ProductDashboard/Extension.pm create mode 100644 extensions/ProductDashboard/lib/Queries.pm create mode 100644 extensions/ProductDashboard/lib/Util.pm create mode 100644 extensions/ProductDashboard/template/en/default/hook/global/common-links-action-links.html.tmpl create mode 100644 extensions/ProductDashboard/template/en/default/hook/global/user-error-errors.html.tmpl create mode 100644 extensions/ProductDashboard/template/en/default/pages/productdashboard.html.tmpl create mode 100644 extensions/ProductDashboard/template/en/default/pages/productdashboard/charts.html.tmpl create mode 100644 extensions/ProductDashboard/template/en/default/pages/productdashboard/components.html.tmpl create mode 100644 extensions/ProductDashboard/template/en/default/pages/productdashboard/duplicates.html.tmpl create mode 100644 extensions/ProductDashboard/template/en/default/pages/productdashboard/popularity.html.tmpl create mode 100644 extensions/ProductDashboard/template/en/default/pages/productdashboard/recents.html.tmpl create mode 100644 extensions/ProductDashboard/template/en/default/pages/productdashboard/roadmap.html.tmpl create mode 100644 extensions/ProductDashboard/template/en/default/pages/productdashboard/summary.html.tmpl create mode 100644 extensions/ProductDashboard/web/images/spacer.gif create mode 100644 extensions/ProductDashboard/web/styles/productdashboard.css create mode 100644 extensions/Push/Config.pm create mode 100644 extensions/Push/Extension.pm create mode 100755 extensions/Push/bin/bugzilla-pushd.pl create mode 100644 extensions/Push/bin/nagios_push_checker.pl create mode 100644 extensions/Push/lib/Admin.pm create mode 100644 extensions/Push/lib/BacklogMessage.pm create mode 100644 extensions/Push/lib/BacklogQueue.pm create mode 100644 extensions/Push/lib/Backoff.pm create mode 100644 extensions/Push/lib/Config.pm create mode 100644 extensions/Push/lib/Connector.disabled/AMQP.pm create mode 100644 extensions/Push/lib/Connector.disabled/ServiceNow.pm create mode 100644 extensions/Push/lib/Connector/Base.pm create mode 100644 extensions/Push/lib/Connector/File.pm create mode 100644 extensions/Push/lib/Connector/ReviewBoard.pm create mode 100644 extensions/Push/lib/Connector/STOMP_SSL.pm create mode 100644 extensions/Push/lib/Connector/Spark.pm create mode 100644 extensions/Push/lib/Connector/TCL.pm create mode 100644 extensions/Push/lib/Connectors.pm create mode 100644 extensions/Push/lib/Constants.pm create mode 100644 extensions/Push/lib/Daemon.pm create mode 100644 extensions/Push/lib/Log.pm create mode 100644 extensions/Push/lib/LogEntry.pm create mode 100644 extensions/Push/lib/Logger.pm create mode 100644 extensions/Push/lib/Message.pm create mode 100644 extensions/Push/lib/Option.pm create mode 100644 extensions/Push/lib/Push.pm create mode 100644 extensions/Push/lib/Queue.pm create mode 100644 extensions/Push/lib/Serialise.pm create mode 100644 extensions/Push/lib/Util.pm create mode 100644 extensions/Push/t/ReviewBoard.t create mode 100644 extensions/Push/template/en/default/hook/admin/admin-end_links_right.html.tmpl create mode 100644 extensions/Push/template/en/default/hook/global/code-error-errors.html.tmpl create mode 100644 extensions/Push/template/en/default/hook/global/messages-messages.html.tmpl create mode 100644 extensions/Push/template/en/default/hook/global/user-error-errors.html.tmpl create mode 100644 extensions/Push/template/en/default/pages/push_config.html.tmpl create mode 100644 extensions/Push/template/en/default/pages/push_log.html.tmpl create mode 100644 extensions/Push/template/en/default/pages/push_queues.html.tmpl create mode 100644 extensions/Push/template/en/default/pages/push_queues_view.html.tmpl create mode 100644 extensions/Push/template/en/default/setup/strings.txt.pl create mode 100644 extensions/Push/web/admin.css create mode 100644 extensions/Push/web/admin.js create mode 100644 extensions/RedHat/Config.pm create mode 100644 extensions/RedHat/Extension.pm create mode 100644 extensions/RedHat/Makefile create mode 100755 extensions/RedHat/bin/bz-expire-cookies.pl create mode 100755 extensions/RedHat/bin/bz-schema-audit.pl create mode 100755 extensions/RedHat/bin/changes.pl create mode 100755 extensions/RedHat/bin/jobdelta.pl create mode 100755 extensions/RedHat/bin/no_attach_data.pl create mode 100755 extensions/RedHat/bin/rebuild_fts_index.sql create mode 100755 extensions/RedHat/bin/release.pl create mode 100755 extensions/RedHat/bin/sitemap.pl create mode 100755 extensions/RedHat/bin/unshare_old_queries.pl create mode 100644 extensions/RedHat/bugzilla.spec.in create mode 100644 extensions/RedHat/custom_fields.yaml create mode 100644 extensions/RedHat/lib/BugUrl/SFDC.pm create mode 100644 extensions/RedHat/lib/Config.pm create mode 100644 extensions/RedHat/lib/DocTypeText.pm create mode 100644 extensions/RedHat/lib/Schema.pm create mode 100644 extensions/RedHat/lib/WebService/Bugzilla.pm create mode 100644 extensions/RedHat/lib/WebService/Component.pm create mode 100644 extensions/RedHat/lib/WebService/Field.pm create mode 100644 extensions/RedHat/lib/WebService/Flag.pm create mode 100644 extensions/RedHat/template/en/default/account/auth/login-small.html.tmpl create mode 100644 extensions/RedHat/template/en/default/admin/products/private-group.html.tmpl create mode 100644 extensions/RedHat/template/en/default/bug/create/create-core-review.html.tmpl create mode 100644 extensions/RedHat/template/en/default/bug/create/create-fedora-nonresponsive-maintainer.html.tmpl create mode 100644 extensions/RedHat/template/en/default/bug/create/create-fedora-review.html.tmpl create mode 100644 extensions/RedHat/template/en/default/bug/create/create-fedora-systemd-request.html.tmpl create mode 100644 extensions/RedHat/template/en/default/bug/create/create-partner-majrel.html.tmpl create mode 100644 extensions/RedHat/template/en/default/bug/create/create-partner-minrel.html.tmpl create mode 100644 extensions/RedHat/template/en/default/bug/create/create-rhel-review.html.tmpl create mode 100644 extensions/RedHat/template/en/default/bug/create/create-swcert-symbol-review.html.tmpl create mode 100644 extensions/RedHat/template/en/default/bug/private_fields.none.tmpl create mode 100644 extensions/RedHat/template/en/default/bug/process/warn_duplicate.html.tmpl create mode 100644 extensions/RedHat/template/en/default/bug/process/warn_merge.html.tmpl create mode 100644 extensions/RedHat/template/en/default/email/long-query.txt.tmpl create mode 100644 extensions/RedHat/template/en/default/email/unshare-search.txt.tmpl create mode 100644 extensions/RedHat/template/en/default/filterexceptions.pl create mode 100644 extensions/RedHat/template/en/default/global/common-links.html.tmpl create mode 100644 extensions/RedHat/template/en/default/hook/account/prefs/email-relationships.html.tmpl create mode 100644 extensions/RedHat/template/en/default/hook/admin/admin-end_links_left.html.tmpl create mode 100644 extensions/RedHat/template/en/default/hook/admin/components/edit-common-rows.html.tmpl create mode 100644 extensions/RedHat/template/en/default/hook/admin/groups/create-field.html.tmpl create mode 100644 extensions/RedHat/template/en/default/hook/admin/groups/edit-field.html.tmpl create mode 100644 extensions/RedHat/template/en/default/hook/admin/params/editparams-current_panel.html.tmpl create mode 100644 extensions/RedHat/template/en/default/hook/admin/products/edit-common-rows.html.tmpl create mode 100644 extensions/RedHat/template/en/default/hook/admin/products/updated-changes.html.tmpl create mode 100644 extensions/RedHat/template/en/default/hook/attachment/create-form_before_submit.html.tmpl create mode 100644 extensions/RedHat/template/en/default/hook/bug/comments-aftercomments.html.tmpl create mode 100644 extensions/RedHat/template/en/default/hook/bug/edit-after_custom_fields.html.tmpl create mode 100644 extensions/RedHat/template/en/default/hook/bug/edit-before_form.html.tmpl create mode 100644 extensions/RedHat/template/en/default/hook/bug/field-end_field_column.html.tmpl create mode 100644 extensions/RedHat/template/en/default/hook/bug/field-help-end.none.tmpl create mode 100644 extensions/RedHat/template/en/default/hook/bug/field-start_field_column.html.tmpl create mode 100644 extensions/RedHat/template/en/default/hook/bug/navigate-links.html.tmpl create mode 100644 extensions/RedHat/template/en/default/hook/bug/process/verify-new-product-exclude.html.tmpl create mode 100644 extensions/RedHat/template/en/default/hook/bug/process/verify-new-product-field.html.tmpl create mode 100644 extensions/RedHat/template/en/default/hook/email/bugmail-start.html.tmpl create mode 100644 extensions/RedHat/template/en/default/hook/email/bugmail-start.txt.tmpl create mode 100644 extensions/RedHat/template/en/default/hook/global/code-error-errors.html.tmpl create mode 100644 extensions/RedHat/template/en/default/hook/global/common-links-action-links.html.tmpl create mode 100644 extensions/RedHat/template/en/default/hook/global/field-descs-end.none.tmpl create mode 100644 extensions/RedHat/template/en/default/hook/global/footer-end.html.tmpl create mode 100644 extensions/RedHat/template/en/default/hook/global/footer-outro.html.tmpl create mode 100644 extensions/RedHat/template/en/default/hook/global/header-additional_header.html.tmpl create mode 100644 extensions/RedHat/template/en/default/hook/global/header-start.html.tmpl create mode 100644 extensions/RedHat/template/en/default/hook/global/messages-component_updated_fields.html.tmpl create mode 100644 extensions/RedHat/template/en/default/hook/global/messages-messages.html.tmpl create mode 100644 extensions/RedHat/template/en/default/hook/global/reason-descs-end.none.tmpl create mode 100644 extensions/RedHat/template/en/default/hook/global/setting-descs-settings.none.tmpl create mode 100644 extensions/RedHat/template/en/default/hook/global/user-error-auth_failure_object.html.tmpl create mode 100644 extensions/RedHat/template/en/default/hook/global/user-error-bug_url_invalid_tracker.html.tmpl create mode 100644 extensions/RedHat/template/en/default/hook/global/user-error-errors.html.tmpl create mode 100644 extensions/RedHat/template/en/default/hook/global/variables-end.none.tmpl create mode 100644 extensions/RedHat/template/en/default/hook/index-intro.html.tmpl create mode 100644 extensions/RedHat/template/en/default/hook/list/list-links.html.tmpl create mode 100644 extensions/RedHat/template/en/default/hook/list/table-list_table_abbrev.html.tmpl create mode 100644 extensions/RedHat/template/en/default/hook/pages/fields-resolution.html.tmpl create mode 100644 extensions/RedHat/template/en/default/hook/pages/release-notes-updates_top.html.tmpl create mode 100644 extensions/RedHat/template/en/default/hook/search/form-before_selects_top.html.tmpl create mode 100644 extensions/RedHat/template/en/default/pages/faq.html.tmpl create mode 100644 extensions/RedHat/template/en/default/pages/redhat/admin_user_sessions.html.tmpl create mode 100644 extensions/RedHat/template/en/default/pages/redhat/ban_apikey.html.tmpl create mode 100644 extensions/RedHat/template/en/default/pages/redhat/contact.html.tmpl create mode 100644 extensions/RedHat/template/en/default/pages/redhat/doctypetext/confirm-delete.html.tmpl create mode 100644 extensions/RedHat/template/en/default/pages/redhat/doctypetext/edit.html.tmpl create mode 100644 extensions/RedHat/template/en/default/pages/redhat/doctypetext/list.html.tmpl create mode 100644 extensions/RedHat/template/en/default/pages/redhat/groupgraphs/select.html.tmpl create mode 100644 extensions/RedHat/template/en/default/pages/terms-conditions.html.tmpl create mode 100644 extensions/RedHat/template/en/default/setup/strings.txt.pl create mode 100644 extensions/RedHat/template/en/default/whine/disabled.txt.tmpl create mode 100644 extensions/RedHat/template/en/default/whine/timeout.txt.tmpl create mode 100644 extensions/RedHat/template/en/filterexceptions.pl create mode 100644 extensions/RedHat/web/DataTables/datatables.css create mode 100644 extensions/RedHat/web/DataTables/datatables.js create mode 100644 extensions/RedHat/web/DataTables/datatables.min.css create mode 100644 extensions/RedHat/web/DataTables/datatables.min.js create mode 100644 extensions/RedHat/web/DataTables/images/sort_asc.png create mode 100644 extensions/RedHat/web/DataTables/images/sort_asc_disabled.png create mode 100644 extensions/RedHat/web/DataTables/images/sort_both.png create mode 100644 extensions/RedHat/web/DataTables/images/sort_desc.png create mode 100644 extensions/RedHat/web/DataTables/images/sort_desc_disabled.png create mode 100644 extensions/RedHat/web/alertify/alertify.js create mode 100644 extensions/RedHat/web/alertify/alertify.min.js create mode 100644 extensions/RedHat/web/alertify/css/alertify.css create mode 100644 extensions/RedHat/web/alertify/css/alertify.min.css create mode 100644 extensions/RedHat/web/alertify/css/alertify.rtl.css create mode 100644 extensions/RedHat/web/alertify/css/alertify.rtl.min.css create mode 100644 extensions/RedHat/web/alertify/css/themes/bootstrap.css create mode 100644 extensions/RedHat/web/alertify/css/themes/bootstrap.min.css create mode 100644 extensions/RedHat/web/alertify/css/themes/bootstrap.rtl.css create mode 100644 extensions/RedHat/web/alertify/css/themes/bootstrap.rtl.min.css create mode 100644 extensions/RedHat/web/alertify/css/themes/default.css create mode 100644 extensions/RedHat/web/alertify/css/themes/default.min.css create mode 100644 extensions/RedHat/web/alertify/css/themes/default.rtl.css create mode 100644 extensions/RedHat/web/alertify/css/themes/default.rtl.min.css create mode 100644 extensions/RedHat/web/alertify/css/themes/semantic.css create mode 100644 extensions/RedHat/web/alertify/css/themes/semantic.min.css create mode 100644 extensions/RedHat/web/alertify/css/themes/semantic.rtl.css create mode 100644 extensions/RedHat/web/alertify/css/themes/semantic.rtl.min.css create mode 100644 extensions/RedHat/web/css/Icons.md create mode 100644 extensions/RedHat/web/css/favicon.ico create mode 100644 extensions/RedHat/web/css/favicons/devel-1.ico create mode 100644 extensions/RedHat/web/css/favicons/devel-2.ico create mode 100644 extensions/RedHat/web/css/favicons/partner.ico create mode 100644 extensions/RedHat/web/css/favicons/production.ico create mode 100644 extensions/RedHat/web/css/favicons/qe.ico create mode 100644 extensions/RedHat/web/css/l_redhat-lg.png create mode 100644 extensions/RedHat/web/css/redhat.css create mode 100644 extensions/RedHat/web/js/redhat.js create mode 100644 extensions/Releases/Config.pm create mode 100644 extensions/Releases/Extension.pm create mode 100644 extensions/Releases/lib/ACKCustomSearch.pm create mode 100644 extensions/Releases/lib/ACKList.pm create mode 100644 extensions/Releases/lib/ACKView.pm create mode 100644 extensions/Releases/lib/Components.pm create mode 100644 extensions/Releases/lib/Release.pm create mode 100644 extensions/Releases/lib/Schedule.pm create mode 100644 extensions/Releases/lib/Util.pm create mode 100644 extensions/Releases/lib/WebService.pm create mode 100644 extensions/Releases/template/en/default/admin/releases/components/confirm_empty_list.html.tmpl create mode 100644 extensions/Releases/template/en/default/filterexceptions.pl create mode 100644 extensions/Releases/template/en/default/hook/admin/admin-end_links_right.html.tmpl create mode 100644 extensions/Releases/template/en/default/hook/global/code-error-errors.html.tmpl create mode 100644 extensions/Releases/template/en/default/hook/global/field-descs-end.none.tmpl create mode 100644 extensions/Releases/template/en/default/hook/global/messages-messages.html.tmpl create mode 100644 extensions/Releases/template/en/default/hook/global/user-error-errors.html.tmpl create mode 100644 extensions/Releases/template/en/default/pages/releases/ack_edit_columns.html.tmpl create mode 100644 extensions/Releases/template/en/default/pages/releases/ack_list.html.tmpl create mode 100644 extensions/Releases/template/en/default/pages/releases/ack_view.html.tmpl create mode 100644 extensions/Releases/template/en/default/pages/releases/activity.html.tmpl create mode 100644 extensions/Releases/template/en/default/pages/releases/activity_table.html.tmpl create mode 100644 extensions/Releases/template/en/default/pages/releases/components/edit.html.tmpl create mode 100644 extensions/Releases/template/en/default/pages/releases/components/list.html.tmpl create mode 100644 extensions/Releases/template/en/default/pages/releases/midair.html.tmpl create mode 100644 extensions/Releases/template/en/default/pages/releases/releases.html.tmpl create mode 100644 extensions/Releases/template/en/default/pages/releases/schedule/edit.html.tmpl create mode 100644 extensions/Releases/template/en/default/pages/releases/schedule/list.html.tmpl create mode 100644 extensions/Releases/template/en/filterexceptions.pl create mode 100644 extensions/Releases/web/css/metrics.css create mode 100644 extensions/Releases/web/images/blue.png create mode 100644 extensions/Releases/web/images/green.png create mode 100644 extensions/Releases/web/images/icon_yes.png create mode 100644 extensions/Releases/web/images/no.png create mode 100644 extensions/Releases/web/images/pink.png create mode 100644 extensions/Releases/web/images/question.png create mode 100644 extensions/Releases/web/images/red.png create mode 100644 extensions/Releases/web/images/yes.png create mode 100644 extensions/Releases/web/js/configview.js create mode 100644 extensions/Releases/web/js/jquery.validate.min.js create mode 100644 extensions/RequestWhiner/Config.pm create mode 100644 extensions/RequestWhiner/Extension.pm create mode 100755 extensions/RequestWhiner/bin/whineatrequests.pl create mode 100644 extensions/RequestWhiner/lib/Constants.pm create mode 100644 extensions/RequestWhiner/template/en/default/requestwhiner/mail-header.tmpl create mode 100644 extensions/RequestWhiner/template/en/default/requestwhiner/mail.html.tmpl create mode 100644 extensions/RequestWhiner/template/en/default/requestwhiner/mail.txt.tmpl create mode 100644 extensions/RuleEngine/Config.pm create mode 100644 extensions/RuleEngine/Extension.pm create mode 100755 extensions/RuleEngine/add_bugs_to_queue.pl create mode 100644 extensions/RuleEngine/lib/FlagGroup.pm create mode 100644 extensions/RuleEngine/lib/FlagGroupDetail.pm create mode 100644 extensions/RuleEngine/lib/Job.pm create mode 100644 extensions/RuleEngine/lib/Pages.pm create mode 100644 extensions/RuleEngine/lib/Rule.pm create mode 100644 extensions/RuleEngine/lib/RuleDetail.pm create mode 100644 extensions/RuleEngine/lib/RuleGroup.pm create mode 100644 extensions/RuleEngine/lib/RuleState.pm create mode 100644 extensions/RuleEngine/lib/Util.pm create mode 100644 extensions/RuleEngine/lib/WebService.pm create mode 100644 extensions/RuleEngine/lib/WebService/RuleGroup.pm create mode 100644 extensions/RuleEngine/lib/WebService/Util.pm create mode 100644 extensions/RuleEngine/template/en/default/email/dryrun-header.txt.tmpl create mode 100644 extensions/RuleEngine/template/en/default/email/dryrun.html.tmpl create mode 100644 extensions/RuleEngine/template/en/default/email/dryrun.txt.tmpl create mode 100644 extensions/RuleEngine/template/en/default/email/rule-change-header.txt.tmpl create mode 100644 extensions/RuleEngine/template/en/default/email/rule-change.html.tmpl create mode 100644 extensions/RuleEngine/template/en/default/email/rule-change.txt.tmpl create mode 100644 extensions/RuleEngine/template/en/default/email/rule-killswitch-disabled.txt.tmpl create mode 100644 extensions/RuleEngine/template/en/default/email/rule-killswitch-enabled.txt.tmpl create mode 100644 extensions/RuleEngine/template/en/default/email/rule-notify.txt.tmpl create mode 100644 extensions/RuleEngine/template/en/default/hook/admin/admin-end_links_right.html.tmpl create mode 100644 extensions/RuleEngine/template/en/default/hook/admin/products/edit-common-rows.html.tmpl create mode 100644 extensions/RuleEngine/template/en/default/hook/global/code-error-errors.html.tmpl create mode 100644 extensions/RuleEngine/template/en/default/hook/global/messages-messages.html.tmpl create mode 100644 extensions/RuleEngine/template/en/default/hook/global/user-error-auth_failure_object.html.tmpl create mode 100644 extensions/RuleEngine/template/en/default/hook/global/user-error-end_object_name.html.tmpl create mode 100644 extensions/RuleEngine/template/en/default/hook/global/user-error-errors.html.tmpl create mode 100644 extensions/RuleEngine/template/en/default/pages/ruleengine/details/change.html.tmpl create mode 100644 extensions/RuleEngine/template/en/default/pages/ruleengine/details/history.html.tmpl create mode 100644 extensions/RuleEngine/template/en/default/pages/ruleengine/details/index.html.tmpl create mode 100644 extensions/RuleEngine/template/en/default/pages/ruleengine/edit.html.tmpl create mode 100644 extensions/RuleEngine/template/en/default/pages/ruleengine/edit/page1.html.tmpl create mode 100644 extensions/RuleEngine/template/en/default/pages/ruleengine/edit/page2.html.tmpl create mode 100644 extensions/RuleEngine/template/en/default/pages/ruleengine/edit/page3.html.tmpl create mode 100644 extensions/RuleEngine/template/en/default/pages/ruleengine/edit/page4.html.tmpl create mode 100644 extensions/RuleEngine/template/en/default/pages/ruleengine/edit/page5.html.tmpl create mode 100644 extensions/RuleEngine/template/en/default/pages/ruleengine/flaggroup/edit.html.tmpl create mode 100644 extensions/RuleEngine/template/en/default/pages/ruleengine/flaggroup/history.html.tmpl create mode 100644 extensions/RuleEngine/template/en/default/pages/ruleengine/flaggroup/list.html.tmpl create mode 100644 extensions/RuleEngine/template/en/default/pages/ruleengine/group/del.html.tmpl create mode 100644 extensions/RuleEngine/template/en/default/pages/ruleengine/group/edit.html.tmpl create mode 100644 extensions/RuleEngine/template/en/default/pages/ruleengine/group/list.html.tmpl create mode 100644 extensions/RuleEngine/template/en/default/pages/ruleengine/index.html.tmpl create mode 100644 extensions/RuleEngine/template/en/default/pages/ruleengine/killswitch.html.tmpl create mode 100644 extensions/RuleEngine/template/en/default/ruleengine/explain.html.tmpl create mode 100644 extensions/RuleEngine/template/en/default/ruleengine/footer.html.tmpl create mode 100644 extensions/RuleEngine/web/admin.css create mode 100644 extensions/RuleEngine/web/admin.js create mode 100644 extensions/RuleEngine/web/component-columns.js create mode 100644 extensions/SAML2Auth/Config.pm create mode 100644 extensions/SAML2Auth/Extension.pm create mode 100644 extensions/SAML2Auth/README create mode 100644 extensions/SAML2Auth/bin/import-metadata.pl create mode 100644 extensions/SAML2Auth/lib/IDP.pm create mode 100644 extensions/SAML2Auth/lib/Login.pm create mode 100644 extensions/SAML2Auth/lib/Params.pm create mode 100644 extensions/SAML2Auth/lib/Util.pm create mode 100644 extensions/SAML2Auth/lib/Verify.pm create mode 100644 extensions/SAML2Auth/template/en/default/account/auth/saml2auth-verify-account.html.tmpl create mode 100644 extensions/SAML2Auth/template/en/default/admin/params/saml2auth.html.tmpl create mode 100644 extensions/SAML2Auth/template/en/default/hook/account/auth/login-additional_methods.html.tmpl create mode 100644 extensions/SAML2Auth/template/en/default/hook/account/auth/login-small-additional_methods.html.tmpl create mode 100644 extensions/SAML2Auth/template/en/default/hook/admin/admin-end_links_left.html.tmpl create mode 100644 extensions/SAML2Auth/template/en/default/hook/global/user-error-errors.html.tmpl create mode 100644 extensions/SAML2Auth/template/en/default/pages/saml2auth/settings.html.tmpl create mode 100644 extensions/SAML2Auth/template/en/default/saml2auth/metadata.xml.tmpl create mode 100644 extensions/SAML2Auth/web/README create mode 100644 extensions/SecureMail/Config.pm create mode 100644 extensions/SecureMail/Extension.pm create mode 100644 extensions/SecureMail/README create mode 100644 extensions/SecureMail/lib/Params.pm create mode 100644 extensions/SecureMail/template/en/default/account/email/encryption-required.txt.tmpl create mode 100644 extensions/SecureMail/template/en/default/account/email/securemail-test.txt.tmpl create mode 100644 extensions/SecureMail/template/en/default/account/prefs/securemail.html.tmpl create mode 100644 extensions/SecureMail/template/en/default/admin/params/securemail.html.tmpl create mode 100644 extensions/SecureMail/template/en/default/hook/account/prefs/prefs-tabs.html.tmpl create mode 100644 extensions/SecureMail/template/en/default/hook/account/prefs/securemail-moreinfo.html.tmpl create mode 100644 extensions/SecureMail/template/en/default/hook/admin/groups/create-field.html.tmpl create mode 100644 extensions/SecureMail/template/en/default/hook/admin/groups/edit-field.html.tmpl create mode 100644 extensions/SecureMail/template/en/default/hook/global/code-error-errors.html.tmpl create mode 100644 extensions/SecureMail/template/en/default/hook/global/messages-group_updated_fields.html.tmpl create mode 100644 extensions/SecureMail/template/en/default/hook/global/user-error-errors.html.tmpl create mode 100644 extensions/SecureMail/template/en/default/pages/securemail/help.html.tmpl create mode 100644 extensions/SelectizeJS/Config.pm create mode 100644 extensions/SelectizeJS/Extension.pm create mode 100644 extensions/SelectizeJS/lib/Util.pm create mode 100644 extensions/SelectizeJS/lib/WebService/Search.pm create mode 100644 extensions/SelectizeJS/template/en/default/bug/component-input.html.tmpl create mode 100644 extensions/SelectizeJS/template/en/default/hook/README create mode 100644 extensions/SelectizeJS/template/en/default/hook/bug/create/create-end.html.tmpl create mode 100644 extensions/SelectizeJS/template/en/default/hook/global/footer-end.html.tmpl create mode 100644 extensions/SelectizeJS/template/en/default/hook/global/header-start.html.tmpl create mode 100644 extensions/SelectizeJS/template/en/default/list/change-columns.html.tmpl create mode 100644 extensions/SelectizeJS/web/README create mode 100644 extensions/SelectizeJS/web/css/SelectizeJS.css create mode 100644 extensions/SelectizeJS/web/css/selectize.bootstrap2.css create mode 100644 extensions/SelectizeJS/web/css/selectize.bootstrap3.css create mode 100644 extensions/SelectizeJS/web/css/selectize.css create mode 100644 extensions/SelectizeJS/web/css/selectize.default.css create mode 100644 extensions/SelectizeJS/web/css/selectize.legacy.css create mode 100644 extensions/SelectizeJS/web/js/SelectizeJS.js create mode 100644 extensions/SelectizeJS/web/js/standalone/selectize.js create mode 100644 extensions/SelectizeJS/web/js/standalone/selectize.min.js create mode 100644 extensions/SubComponents/Config.pm create mode 100644 extensions/SubComponents/Extension.pm create mode 100644 extensions/SubComponents/lib/Config.pm create mode 100644 extensions/SubComponents/lib/RHSubComponent.pm create mode 100644 extensions/SubComponents/lib/WebService.pm create mode 100755 extensions/SubComponents/rh-subcomp-report.pl create mode 100644 extensions/SubComponents/template/en/default/bug/sub_component.html.tmpl create mode 100644 extensions/SubComponents/template/en/default/email/sub_comp_change.txt.tmpl create mode 100644 extensions/SubComponents/template/en/default/hook/admin/components/edit-common-rows.html.tmpl create mode 100644 extensions/SubComponents/template/en/default/hook/admin/components/list-before_table.html.tmpl create mode 100644 extensions/SubComponents/template/en/default/hook/admin/params/editparams-current_panel.html.tmpl create mode 100644 extensions/SubComponents/template/en/default/hook/admin/sanitycheck/messages-statuses.html.tmpl create mode 100644 extensions/SubComponents/template/en/default/hook/admin/users/responsibilities-sub_component_responsibilites.html.tmpl create mode 100644 extensions/SubComponents/template/en/default/hook/bug/show-header-end.html.tmpl create mode 100644 extensions/SubComponents/template/en/default/hook/global/code-error-errors.html.tmpl create mode 100644 extensions/SubComponents/template/en/default/hook/global/messages-messages.html.tmpl create mode 100644 extensions/SubComponents/template/en/default/hook/global/user-error-end_object_name.html.tmpl create mode 100644 extensions/SubComponents/template/en/default/hook/global/user-error-errors.html.tmpl create mode 100644 extensions/SubComponents/template/en/default/pages/subcomponents/del.html.tmpl create mode 100644 extensions/SubComponents/template/en/default/pages/subcomponents/edit.html.tmpl create mode 100644 extensions/SubComponents/template/en/default/pages/subcomponents/list.html.tmpl create mode 100644 extensions/SubComponents/template/en/default/search/sub_components.html.tmpl create mode 100644 extensions/TreeViewPlus/Config.pm create mode 100644 extensions/TreeViewPlus/Extension.pm create mode 100644 extensions/TreeViewPlus/README.md create mode 100644 extensions/TreeViewPlus/lib/Util.pm create mode 100644 extensions/TreeViewPlus/search_include_dependencies_5.0.patch create mode 100644 extensions/TreeViewPlus/template/en/default/hook/README create mode 100644 extensions/TreeViewPlus/template/en/default/hook/bug/edit-after_bug_fields.html.tmpl create mode 100644 extensions/TreeViewPlus/template/en/default/hook/bug/field-end_field_column.html.tmpl create mode 100644 extensions/TreeViewPlus/template/en/default/hook/bug/navigate-links.html.tmpl create mode 100644 extensions/TreeViewPlus/template/en/default/hook/global/field-descs-end.none.tmpl create mode 100644 extensions/TreeViewPlus/template/en/default/hook/list/list-links.html.tmpl create mode 100644 extensions/TreeViewPlus/template/en/default/list/list-tvp.html.tmpl create mode 100644 extensions/TreeViewPlus/web/css/bugtree/icons.gif create mode 100644 extensions/TreeViewPlus/web/css/bugtree/loading.gif create mode 100644 extensions/TreeViewPlus/web/css/bugtree/tree.css create mode 100644 extensions/TreeViewPlus/web/css/bugtree/vline.gif create mode 100644 extensions/TreeViewPlus/web/js/jquery.dynatree-1.2.8.js create mode 100644 extensions/TreeViewPlus/web/js/jquery.dynatree-1.2.8.min.js create mode 100644 extensions/TreeViewPlus/web/js/tvp.js create mode 100644 extensions/Voting/lib/WebService.pm create mode 100644 extensions/Workflows/Config.pm create mode 100644 extensions/Workflows/Extension.pm create mode 100644 extensions/Workflows/lib/GroupApproval.pm create mode 100644 extensions/Workflows/lib/GroupRequest.pm create mode 100644 extensions/Workflows/lib/ManageComponents.pm create mode 100644 extensions/Workflows/lib/Util.pm create mode 100644 extensions/Workflows/lib/WebService/GroupRequest.pm create mode 100644 extensions/Workflows/lib/WebService/ManageComponents.pm create mode 100644 extensions/Workflows/template/en/default/hook/admin/admin-end_links_left.html.tmpl create mode 100644 extensions/Workflows/template/en/default/hook/global/common-links-action-links.html.tmpl create mode 100644 extensions/Workflows/template/en/default/hook/global/messages-messages.html.tmpl create mode 100644 extensions/Workflows/template/en/default/hook/global/user-error-errors.html.tmpl create mode 100644 extensions/Workflows/template/en/default/pages/workflows/group_req_admin.html.tmpl create mode 100644 extensions/Workflows/template/en/default/pages/workflows/group_request.html.tmpl create mode 100644 extensions/Workflows/template/en/default/pages/workflows/manage_components.html.tmpl create mode 100644 extensions/Workflows/web/README create mode 100644 js/diff2html/README create mode 100644 js/diff2html/diff2html-ui.js create mode 100644 js/diff2html/diff2html-ui.min.js create mode 100644 js/diff2html/diff2html.js create mode 100644 js/diff2html/diff2html.min.js create mode 100644 js/diff2html/highlight.pack.js create mode 100644 ping.html create mode 100755 show_comment_activity.cgi create mode 100644 skins/standard/diff2html/diff2html.css create mode 100644 skins/standard/diff2html/diff2html.min.css create mode 100644 skins/standard/diff2html/github.css create mode 100644 t/020ext-versions.t create mode 100644 t/050CERT.t create mode 100644 t/090checksetup.t create mode 100644 t/100Push.t create mode 100644 t/300rpc.t create mode 100644 t/310BRE.t create mode 100644 t/320rpc.t create mode 100644 t/340GPGin.t create mode 100644 t/350GroupWorkflows.t create mode 100644 t/test.conf create mode 100644 template/en/default/account/prefs/sessions.html.tmpl create mode 100644 template/en/default/admin/custom_fields/cf-js-rh.js.tmpl create mode 100644 template/en/default/admin/releases/confirm-delete.html.tmpl create mode 100644 template/en/default/admin/releases/create.html.tmpl create mode 100644 template/en/default/admin/releases/edit.html.tmpl create mode 100644 template/en/default/admin/releases/footer.html.tmpl create mode 100644 template/en/default/admin/releases/list.html.tmpl create mode 100644 template/en/default/admin/releases/select-product.html.tmpl create mode 100644 template/en/default/attachment/diff2html.html.tmpl create mode 100644 template/en/default/bug/activity/comments.html.tmpl create mode 100644 template/en/default/bug/needinfo.html.tmpl create mode 100644 template/en/default/pages/whats-new.html.tmpl diff --git a/.gitignore b/.gitignore index ba98f70c2..2844be714 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,44 @@ /skins/contrib/Dusk/admin.css /skins/contrib/Dusk/bug.css + +# Don't include files that patch may generate +*.orig +*.rej + +# These files come from making bugzilla +/rh-bugzilla* +/bugzilla.spec +/tmp +/dev +/logs +ansible/ansible.log +ansible/report.txt + +*.swp +*.swo + +# ignore perl files +Build +MANIFEST +MANIFEST.bak +MYMETA.json +MYMETA.yml +_build/ +cover_db/ +nytprof* + +docs/en/rst/extensions/ +docs/en/rst/integrating/api/ + +/localconfig.tests + +saml2*.cgi + +ansible/playbooks/roles/ +*.retry +*.pyc +.vstags +.vscode/ +.tidyall.d/ + diff --git a/.htaccess b/.htaccess index 2f009697c..1f92060ac 100644 --- a/.htaccess +++ b/.htaccess @@ -15,6 +15,13 @@ Options -Indexes +ErrorDocument 401 /errors/401.html +ErrorDocument 403 /errors/403.html +ErrorDocument 404 /errors/404.html +ErrorDocument 500 /errors/500.html + +AddType text/plain .md + @@ -30,9 +37,42 @@ Options -Indexes Header append Cache-Control "public" + Header set Connection keep-alive + + ExpiresActive On + + # 480 weeks - 290304000 + # 2 WEEKS + + Header set Cache-Control "max-age=1209600, public" + + + # 1 DAY + + Header set Cache-Control "max-age=86400, public" + #Header set Cache-Control "max-age=0, public, must-revalidate" + + + # 2 DAYS + + Header set Cache-Control "max-age=172800, public, must-revalidate" + + + # Do not cache most of these as they are generated. + + + Header set Cache-Control "max-age=172800, private, must-revalidate" + + + Header set Cache-Control "no-cache, no-store" + + + + FileETag none + # This lets Bugzilla know that we are properly sending Cache-Control # and Expires headers for CSS and JS files. - SetEnv BZ_CACHE_CONTROL 1 + SetEnv BZ_CACHE_CONTROL 0 @@ -41,4 +81,5 @@ Options -Indexes RewriteEngine On RewriteOptions inherit RewriteRule ^rest/(.*)$ rest.cgi/$1 [NE] + RewriteRule ^jsonrpc - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] diff --git a/.perltidyrc b/.perltidyrc new file mode 100644 index 000000000..95c897374 --- /dev/null +++ b/.perltidyrc @@ -0,0 +1,17 @@ +-pbp # Start with Perl Best Practices +-w # Show all warnings +-iob # Ignore old breakpoints +-l=80 # 80 characters per line +-vmll +-ibc +-iscl +-hsc +-mbl=2 # No more than 2 blank lines +-i=2 # Indentation is 2 columns +-ci=2 # Continuation indentation is 2 columns +-vt=0 # Less vertical tightness +-pt=2 # High parenthesis tightness +-bt=2 # High brace tightness +-sbt=2 # High square bracket tightness +-wn # Weld nested containers +-isbc # Don't indent comments without leading space diff --git a/Bugzilla.pm b/Bugzilla.pm index e4772e08b..40e1d103f 100644 --- a/Bugzilla.pm +++ b/Bugzilla.pm @@ -13,11 +13,11 @@ use warnings; # We want any compile errors to get to the browser, if possible. BEGIN { - # This makes sure we're in a CGI. - if ($ENV{SERVER_SOFTWARE} && !$ENV{MOD_PERL}) { - require CGI::Carp; - CGI::Carp->import('fatalsToBrowser'); - } + # This makes sure we're in a CGI. + if ($ENV{SERVER_SOFTWARE} && !$ENV{MOD_PERL}) { + require CGI::Carp; + CGI::Carp->import('fatalsToBrowser'); + } } use Bugzilla::Auth; @@ -45,22 +45,50 @@ use DateTime::TimeZone; use Date::Parse; use Safe; +## REDHAT EXTENSION BEGIN 406301 +# add Log4perl support +use Log::Log4perl; +use Log::Log4perl::MDC; +use Time::HiRes; +## REDHAT EXTENSION END 406301 + +## REDHAT EXTENSION BEGIN 822622 +use YAML::Syck qw(LoadFile); +## REDHAT EXTENSION END 822622 + +sub logger { + Log::Log4perl::get_logger(""); +} + +sub log_filename { + Log::Log4perl::appender_by_name('LOGFILE')->filename; +} + +my $t0; +## REDHAT EXTENSION END 406301 + ##################################################################### # Constants ##################################################################### # Scripts that are not stopped by shutdownhtml being in effect. use constant SHUTDOWNHTML_EXEMPT => qw( - editparams.cgi - checksetup.pl - migrate.pl - recode.pl + editparams.cgi + checksetup.pl + migrate.pl + recode.pl ); # Non-cgi scripts that should silently exit. use constant SHUTDOWNHTML_EXIT_SILENTLY => qw( - whine.pl + whine.pl +); + +## REDHAT START EXTENSION 1245411 - Make the job queue die when shutdownhtml is set +use constant SHUTDOWNHTML_DIE_NOISY => qw( + jobqueue.pl ); +## REDHAT END EXTENSION 1245411 # shutdownhtml pages are sent as an HTTP 503. After how many seconds # should search engines attempt to index the page again? @@ -70,603 +98,762 @@ use constant SHUTDOWNHTML_RETRY_AFTER => 3600; # Global Code ##################################################################### -#$::SIG{__DIE__} = i_am_cgi() ? \&CGI::Carp::confess : \&Carp::confess; - # Note that this is a raw subroutine, not a method, so $class isn't available. sub init_page { - if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) { - init_console(); - } - elsif (Bugzilla->params->{'utf8'}) { - binmode STDOUT, ':utf8'; + if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) { + init_console(); + } + elsif (Bugzilla->params->{'utf8'}) { + binmode STDOUT, ':utf8'; + } + + if (${^TAINT}) { + my $path = ''; + if (ON_WINDOWS) { + + # On Windows, these paths are tainted, preventing + # File::Spec::Win32->tmpdir from using them. But we need + # a place to temporary store attachments which are uploaded. + foreach my $temp (qw(TMPDIR TMP TEMP WINDIR)) { + trick_taint($ENV{$temp}) if $ENV{$temp}; + } + + # Some DLLs used by Strawberry Perl are also in c\bin, + # see https://rt.cpan.org/Public/Bug/Display.html?id=99104 + if (!ON_ACTIVESTATE) { + my $c_path = $path = dirname($^X); + $c_path =~ s/\bperl\b(?=\\bin)/c/; + $path .= ";$c_path"; + trick_taint($path); + } } - if (${^TAINT}) { - my $path = ''; - if (ON_WINDOWS) { - # On Windows, these paths are tainted, preventing - # File::Spec::Win32->tmpdir from using them. But we need - # a place to temporary store attachments which are uploaded. - foreach my $temp (qw(TMPDIR TMP TEMP WINDIR)) { - trick_taint($ENV{$temp}) if $ENV{$temp}; - } - # Some DLLs used by Strawberry Perl are also in c\bin, - # see https://rt.cpan.org/Public/Bug/Display.html?id=99104 - if (!ON_ACTIVESTATE) { - my $c_path = $path = dirname($^X); - $c_path =~ s/\bperl\b(?=\\bin)/c/; - $path .= ";$c_path"; - trick_taint($path); - } - } - # Some environment variables are not taint safe - delete @::ENV{'PATH', 'IFS', 'CDPATH', 'ENV', 'BASH_ENV'}; - # Some modules throw undefined errors (notably File::Spec::Win32) if - # PATH is undefined. - $ENV{'PATH'} = $path; + # Some environment variables are not taint safe + delete @::ENV{'PATH', 'IFS', 'CDPATH', 'ENV', 'BASH_ENV'}; + + # Some modules throw undefined errors (notably File::Spec::Win32) if + # PATH is undefined. + $ENV{'PATH'} = $path; + } + + # Because this function is run live from perl "use" commands of + # other scripts, we're skipping the rest of this function if we get here + # during a perl syntax check (perl -c, like we do during the + # 001compile.t test). + return if $^C; + + # IIS prints out warnings to the webpage, so ignore them, or log them + # to a file if the file exists. + if ($ENV{SERVER_SOFTWARE} && $ENV{SERVER_SOFTWARE} =~ /microsoft-iis/i) { + $SIG{__WARN__} = sub { + my ($msg) = @_; + my $datadir = bz_locations()->{'datadir'}; + if (-w "$datadir/errorlog") { + my $warning_log = new IO::File(">>$datadir/errorlog"); + print $warning_log $msg; + $warning_log->close(); + } + }; + } + + my $script = basename($0); + + # Because of attachment_base, attachment.cgi handles this itself. + if ($script ne 'attachment.cgi') { + do_ssl_redirect_if_required(); + } + + # If Bugzilla is shut down, do not allow anything to run, just display a + # message to the user about the downtime and log out. Scripts listed in + # SHUTDOWNHTML_EXEMPT are exempt from this message. + # + # This code must go here. It cannot go anywhere in Bugzilla::CGI, because + # it uses Template, and that causes various dependency loops. + if ( + Bugzilla->params->{"shutdownhtml"} + ## REDHAT EXTENSION: Allow scripts that begin with rh- to always run + && $script !~ m#(^|/)rh-# + && !grep { $_ eq $script } SHUTDOWNHTML_EXEMPT + ) + { + # Allow non-cgi scripts to exit silently (without displaying any + # message), if desired. At this point, no DBI call has been made + # yet, and no error will be returned if the DB is inaccessible. + if (!i_am_cgi() && grep { $_ eq $script } SHUTDOWNHTML_EXIT_SILENTLY) { + exit; } - # Because this function is run live from perl "use" commands of - # other scripts, we're skipping the rest of this function if we get here - # during a perl syntax check (perl -c, like we do during the - # 001compile.t test). - return if $^C; - - # IIS prints out warnings to the webpage, so ignore them, or log them - # to a file if the file exists. - if ($ENV{SERVER_SOFTWARE} && $ENV{SERVER_SOFTWARE} =~ /microsoft-iis/i) { - $SIG{__WARN__} = sub { - my ($msg) = @_; - my $datadir = bz_locations()->{'datadir'}; - if (-w "$datadir/errorlog") { - my $warning_log = new IO::File(">>$datadir/errorlog"); - print $warning_log $msg; - $warning_log->close(); - } - }; +## REDHAT START EXTENSION 1245411 + if (!i_am_cgi() && grep { $_ eq $script } SHUTDOWNHTML_DIE_NOISY) { + Bugzilla->template->process("global/message.txt.tmpl", {message => 'shutdown'}) + || ThrowTemplateError(Bugzilla->template->error); + exit(1); } +## REDHAT END EXTENSION 1245411 - my $script = basename($0); + # For security reasons, log out users when Bugzilla is down. + # Bugzilla->login() is required to catch the logincookie, if any. + my $user; + eval { $user = Bugzilla->login(LOGIN_OPTIONAL); }; + if ($@) { - # Because of attachment_base, attachment.cgi handles this itself. - if ($script ne 'attachment.cgi') { - do_ssl_redirect_if_required(); + # The DB is not accessible. Use the default user object. + $user = Bugzilla->user; + $user->{settings} = {}; } - - # If Bugzilla is shut down, do not allow anything to run, just display a - # message to the user about the downtime and log out. Scripts listed in - # SHUTDOWNHTML_EXEMPT are exempt from this message. - # - # This code must go here. It cannot go anywhere in Bugzilla::CGI, because - # it uses Template, and that causes various dependency loops. - if (!grep { $_ eq $script } SHUTDOWNHTML_EXEMPT - and Bugzilla->params->{'shutdownhtml'}) + my $userid = $user->id; + Bugzilla->logout(); + + my $template = Bugzilla->template; + my $vars = {}; + $vars->{'message'} = 'shutdown'; + $vars->{'userid'} = $userid; + + # Generate and return a message about the downtime, appropriately + # for if we're a command-line script or a CGI script. + my $extension; + ## REDHAT EXTENSION BEGIN 745975 + # We cannot use Bugzilla->usage_mode, since it has not been set + if (basename($0) eq 'xmlrpc.cgi' or basename($0) eq 'jsonrpc.cgi') { + + # Simply throw a 503 error + my $status = "503 Service unavailable. " . Bugzilla->params->{"shutdownhtml"}; + + # Replace newlines with spaces + $status =~ s/\s+/ /g; + print Bugzilla->cgi->header(-status => $status); + exit; + } + ## REDHAT EXTENSION END 745975 + if (i_am_cgi() + && (!Bugzilla->cgi->param('ctype') || Bugzilla->cgi->param('ctype') eq 'html')) { - # Allow non-cgi scripts to exit silently (without displaying any - # message), if desired. At this point, no DBI call has been made - # yet, and no error will be returned if the DB is inaccessible. - if (!i_am_cgi() - && grep { $_ eq $script } SHUTDOWNHTML_EXIT_SILENTLY) - { - exit; - } + $extension = 'html'; + } + else { + $extension = 'txt'; + } + if (i_am_cgi()) { + + # Set the HTTP status to 503 when Bugzilla is down to avoid pages + # being indexed by search engines. + print Bugzilla->cgi->header( + -status => 503, + -retry_after => SHUTDOWNHTML_RETRY_AFTER + ); + } + $template->process("global/message.$extension.tmpl", $vars) + || ThrowTemplateError($template->error); + exit; + } - # For security reasons, log out users when Bugzilla is down. - # Bugzilla->login() is required to catch the logincookie, if any. - my $user; - eval { $user = Bugzilla->login(LOGIN_OPTIONAL); }; - if ($@) { - # The DB is not accessible. Use the default user object. - $user = Bugzilla->user; - $user->{settings} = {}; - } - my $userid = $user->id; - Bugzilla->logout(); - - my $template = Bugzilla->template; - my $vars = {}; - $vars->{'message'} = 'shutdown'; - $vars->{'userid'} = $userid; - # Generate and return a message about the downtime, appropriately - # for if we're a command-line script or a CGI script. - my $extension; - if (i_am_cgi() && (!Bugzilla->cgi->param('ctype') - || Bugzilla->cgi->param('ctype') eq 'html')) { - $extension = 'html'; - } - else { - $extension = 'txt'; - } - if (i_am_cgi()) { - # Set the HTTP status to 503 when Bugzilla is down to avoid pages - # being indexed by search engines. - print Bugzilla->cgi->header(-status => 503, - -retry_after => SHUTDOWNHTML_RETRY_AFTER); - } - $template->process("global/message.$extension.tmpl", $vars) - || ThrowTemplateError($template->error); - exit; + ## REDHAT EXTENSION START 1275877 + Bugzilla->request_cache->{_rh_start_time} = time; + ## REDHAT EXTENSION END 1275877 + + configure_log4perl(); +} + +## REDHAT EXTENSION 406301 1220244 BEGIN +sub configure_log4perl { + my $remote_addr; + my $forwarded_for; + + if ($ENV{MOD_PERL}) { + + # use require as we may not be running in mod_perl mode. + require Apache2::RequestUtil; + require Apache2::Connection; + + my $req = Apache2::RequestUtil->request; + my $conn = $req->connection; + if ($conn->can('remote_ip')) { + $remote_addr = $conn->remote_ip; } + else { + $remote_addr = $conn->client_ip; + } + $forwarded_for = $req->headers_in->{'X-Forwarded-For'}; + } + else { + $forwarded_for = $ENV{HTTP_X_FORWARDED_FOR}; + $remote_addr = $ENV{REMOTE_ADDR}; + } + + # Set the remote IP addresses etc + Log::Log4perl::MDC->put(script => $0); + Log::Log4perl::MDC->put(remote_addr => $remote_addr // ''); + Log::Log4perl::MDC->put(http_x_forwarded_for => $forwarded_for // ''); + + # Clear out old data from a previous run. + Log::Log4perl::MDC->put(userid => 0); + Log::Log4perl::MDC->put(username => ''); + Log::Log4perl::MDC->put(userlogin => ''); + + $t0 = [Time::HiRes::gettimeofday]; + + unless (Log::Log4perl->initialized) { + eval { + my $conf = Bugzilla->params->{log4perl_config}; + Log::Log4perl::init_once(\$conf); + }; + if (my $e = $@) { + warn "config failed to load: $e"; + my $conf = Bugzilla::Constants::LOG4PERL_DEFAULT_CONFIG; + Log::Log4perl::init_once(\$conf); # should _always_ work + } + } + + # start a log message. the begin section should ensure + # that the config is loaded + Log::Log4perl::get_logger("")->debug("START"); + +# Record the script, query url and current process size for performance debugging purposes +# if we are running under CGI and/or mod_perl + if ($ENV{SERVER_SOFTWARE}) { + Log::Log4perl::get_logger("")->debug("SCRIPT_NAME: " . $ENV{SCRIPT_NAME}); + Log::Log4perl::get_logger("")->debug("QUERY_STRING: " . redact($ENV{QUERY_STRING})); + } +} + +sub configure_log4perl_user { + my $user = shift // Bugzilla->user; + + Log::Log4perl::MDC->put(userid => $user->id); + Log::Log4perl::MDC->put(username => $user->name); + Log::Log4perl::MDC->put(userlogin => $user->login); } +## REDHAT EXTENSION 406301 1220244 END ##################################################################### # Subroutines and Methods ##################################################################### sub template { - return $_[0]->request_cache->{template} ||= Bugzilla::Template->create(); + return $_[0]->request_cache->{template} ||= Bugzilla::Template->create(); } sub template_inner { - my ($class, $lang) = @_; - my $cache = $class->request_cache; - my $current_lang = $cache->{template_current_lang}->[0]; - $lang ||= $current_lang || ''; - return $cache->{"template_inner_$lang"} ||= Bugzilla::Template->create(language => $lang); + my ($class, $lang) = @_; + my $cache = $class->request_cache; + my $current_lang = $cache->{template_current_lang}->[0]; + $lang ||= $current_lang || ''; + return $cache->{"template_inner_$lang"} + ||= Bugzilla::Template->create(language => $lang); } our $extension_packages; + sub extensions { - my ($class) = @_; - my $cache = $class->request_cache; - if (!$cache->{extensions}) { - # Under mod_perl, mod_perl.pl populates $extension_packages for us. - if (!$extension_packages) { - $extension_packages = Bugzilla::Extension->load_all(); - } - my @extensions; - foreach my $package (@$extension_packages) { - my $extension = $package->new(); - if ($extension->enabled) { - push(@extensions, $extension); - } - } - $cache->{extensions} = \@extensions; + my ($class) = @_; + my $cache = $class->request_cache; + if (!$cache->{extensions}) { + + # Under mod_perl, mod_perl.pl populates $extension_packages for us. + if (!$extension_packages) { + $extension_packages = Bugzilla::Extension->load_all(); } - return $cache->{extensions}; + my @extensions; + foreach my $package (@$extension_packages) { + my $extension = $package->new(); + if ($extension->enabled) { + push(@extensions, $extension); + } + } + $cache->{extensions} = \@extensions; + } + return $cache->{extensions}; } sub feature { - my ($class, $feature) = @_; - my $cache = $class->request_cache; - return $cache->{feature}->{$feature} - if exists $cache->{feature}->{$feature}; - - my $feature_map = $cache->{feature_map}; - if (!$feature_map) { - foreach my $package (@{ OPTIONAL_MODULES() }) { - foreach my $f (@{ $package->{feature} }) { - $feature_map->{$f} ||= []; - push(@{ $feature_map->{$f} }, $package); - } - } - $cache->{feature_map} = $feature_map; + my ($class, $feature) = @_; + my $cache = $class->request_cache; + return $cache->{feature}->{$feature} if exists $cache->{feature}->{$feature}; + + my $feature_map = $cache->{feature_map}; + if (!$feature_map) { + foreach my $package (@{OPTIONAL_MODULES()}) { + foreach my $f (@{$package->{feature}}) { + $feature_map->{$f} ||= []; + push(@{$feature_map->{$f}}, $package); + } } + $cache->{feature_map} = $feature_map; + } - if (!$feature_map->{$feature}) { - ThrowCodeError('invalid_feature', { feature => $feature }); - } + if (!$feature_map->{$feature}) { + ThrowCodeError('invalid_feature', {feature => $feature}); + } - my $success = 1; - foreach my $package (@{ $feature_map->{$feature} }) { - have_vers($package) or $success = 0; - } - $cache->{feature}->{$feature} = $success; - return $success; + my $success = 1; + foreach my $package (@{$feature_map->{$feature}}) { + have_vers($package) or $success = 0; + } + + ## REDHAT EXTENSION BEGIN 1254054 + $success = 0 if (($feature eq 'old_charts') || ($feature eq 'new_charts')); + ## REDHAT EXTENSION END 1254054 + + $cache->{feature}->{$feature} = $success; + return $success; } sub cgi { - return $_[0]->request_cache->{cgi} ||= new Bugzilla::CGI(); + return $_[0]->request_cache->{cgi} ||= new Bugzilla::CGI(); } sub input_params { - my ($class, $params) = @_; - my $cache = $class->request_cache; - # This is how the WebService and other places set input_params. - if (defined $params) { - $cache->{input_params} = $params; - } - return $cache->{input_params} if defined $cache->{input_params}; + my ($class, $params) = @_; + my $cache = $class->request_cache; - # Making this scalar makes it a tied hash to the internals of $cgi, - # so if a variable is changed, then it actually changes the $cgi object - # as well. - $cache->{input_params} = $class->cgi->Vars; - return $cache->{input_params}; + # This is how the WebService and other places set input_params. + if (defined $params) { + $cache->{input_params} = $params; + } + return $cache->{input_params} if defined $cache->{input_params}; + + # Making this scalar makes it a tied hash to the internals of $cgi, + # so if a variable is changed, then it actually changes the $cgi object + # as well. + $cache->{input_params} = $class->cgi->Vars; + return $cache->{input_params}; } sub localconfig { - return $_[0]->process_cache->{localconfig} ||= read_localconfig(); + return $_[0]->process_cache->{localconfig} ||= read_localconfig(); } sub params { - return $_[0]->request_cache->{params} ||= Bugzilla::Config::read_param_file(); + return $_[0]->request_cache->{params} ||= Bugzilla::Config::read_param_file(); } sub user { - return $_[0]->request_cache->{user} ||= new Bugzilla::User; + return $_[0]->request_cache->{user} ||= new Bugzilla::User; } sub set_user { - my ($class, $user) = @_; - $class->request_cache->{user} = $user; + my ($class, $user) = @_; + + $class->request_cache->{user} = $user; } sub sudoer { - return $_[0]->request_cache->{sudoer}; + return $_[0]->request_cache->{sudoer}; } sub sudo_request { - my ($class, $new_user, $new_sudoer) = @_; - $class->request_cache->{user} = $new_user; - $class->request_cache->{sudoer} = $new_sudoer; - # NOTE: If you want to log the start of an sudo session, do it here. + my ($class, $new_user, $new_sudoer) = @_; + $class->request_cache->{user} = $new_user; + $class->request_cache->{sudoer} = $new_sudoer; + + # NOTE: If you want to log the start of an sudo session, do it here. } sub page_requires_login { - return $_[0]->request_cache->{page_requires_login}; + return $_[0]->request_cache->{page_requires_login}; } sub login { - my ($class, $type) = @_; + my ($class, $type) = @_; - return $class->user if $class->user->id; + if ($class->user->id) { + configure_log4perl_user($class->user); + return $class->user; + } - my $authorizer = new Bugzilla::Auth(); - $type = LOGIN_REQUIRED if $class->cgi->param('GoAheadAndLogIn'); + my $authorizer = new Bugzilla::Auth(); + $type = LOGIN_REQUIRED if $class->cgi->param('GoAheadAndLogIn'); - if (!defined $type || $type == LOGIN_NORMAL) { - $type = $class->params->{'requirelogin'} ? LOGIN_REQUIRED : LOGIN_NORMAL; - } + if (!defined $type || $type == LOGIN_NORMAL) { + $type = $class->params->{'requirelogin'} ? LOGIN_REQUIRED : LOGIN_NORMAL; + } + + # Allow templates to know that we're in a page that always requires + # login. + if ($type == LOGIN_REQUIRED) { + $class->request_cache->{page_requires_login} = 1; + } - # Allow templates to know that we're in a page that always requires - # login. - if ($type == LOGIN_REQUIRED) { - $class->request_cache->{page_requires_login} = 1; + my $authenticated_user = $authorizer->login($type); + + # At this point, we now know if a real person is logged in. + # We must now check to see if an sudo session is in progress. + # For a session to be in progress, the following must be true: + # 1: There must be a logged in user + # 2: That user must be in the 'bz_sudoer' group + # 3: There must be a valid value in the 'sudo' cookie + # 4: A Bugzilla::User object must exist for the given cookie value + # 5: That user must NOT be in the 'bz_sudo_protect' group + my $token = $class->cgi->cookie('sudo'); + if (defined $authenticated_user && $token) { + my ($user_id, $date, $sudo_target_id) = Bugzilla::Token::GetTokenData($token); + if (!$user_id + || $user_id != $authenticated_user->id + || !detaint_natural($sudo_target_id) + || (time() - str2time($date) > MAX_SUDO_TOKEN_AGE)) + { + $class->cgi->remove_cookie('sudo'); + ThrowUserError('sudo_invalid_cookie'); } - my $authenticated_user = $authorizer->login($type); - - # At this point, we now know if a real person is logged in. - # We must now check to see if an sudo session is in progress. - # For a session to be in progress, the following must be true: - # 1: There must be a logged in user - # 2: That user must be in the 'bz_sudoer' group - # 3: There must be a valid value in the 'sudo' cookie - # 4: A Bugzilla::User object must exist for the given cookie value - # 5: That user must NOT be in the 'bz_sudo_protect' group - my $token = $class->cgi->cookie('sudo'); - if (defined $authenticated_user && $token) { - my ($user_id, $date, $sudo_target_id) = Bugzilla::Token::GetTokenData($token); - if (!$user_id - || $user_id != $authenticated_user->id - || !detaint_natural($sudo_target_id) - || (time() - str2time($date) > MAX_SUDO_TOKEN_AGE)) - { - $class->cgi->remove_cookie('sudo'); - ThrowUserError('sudo_invalid_cookie'); - } + my $sudo_target = new Bugzilla::User($sudo_target_id); + if ( $authenticated_user->in_group('bz_sudoers') + && defined $sudo_target + && !$sudo_target->in_group('bz_sudo_protect')) + { + $class->set_user($sudo_target); + $class->request_cache->{sudoer} = $authenticated_user; - my $sudo_target = new Bugzilla::User($sudo_target_id); - if ($authenticated_user->in_group('bz_sudoers') - && defined $sudo_target - && !$sudo_target->in_group('bz_sudo_protect')) - { - $class->set_user($sudo_target); - $class->request_cache->{sudoer} = $authenticated_user; - # And make sure that both users have the same Auth object, - # since we never call Auth::login for the sudo target. - $sudo_target->set_authorizer($authenticated_user->authorizer); - - # NOTE: If you want to do any special logging, do it here. - } - else { - delete_token($token); - $class->cgi->remove_cookie('sudo'); - ThrowUserError('sudo_illegal_action', { sudoer => $authenticated_user, - target_user => $sudo_target }); - } + # And make sure that both users have the same Auth object, + # since we never call Auth::login for the sudo target. + $sudo_target->set_authorizer($authenticated_user->authorizer); + + # NOTE: If you want to do any special logging, do it here. } else { - $class->set_user($authenticated_user); + delete_token($token); + $class->cgi->remove_cookie('sudo'); + ThrowUserError('sudo_illegal_action', + {sudoer => $authenticated_user, target_user => $sudo_target}); } + } + else { + $class->set_user($authenticated_user); + } - if ($class->sudoer) { - $class->sudoer->update_last_seen_date(); - } else { - $class->user->update_last_seen_date(); - } + if ($class->sudoer) { + $class->sudoer->update_last_seen_date(); + } + else { + $class->user->update_last_seen_date(); + } - return $class->user; + ## REDHAT EXTENSION BEGIN 406301 1220244 + # A user has logged in, set their user details in the logger. + configure_log4perl_user($class->user); + ## REDHAT EXTENSION END 406301 1220244 + + return $class->user; } sub logout { - my ($class, $option) = @_; + my ($class, $option) = @_; - # If we're not logged in, go away - return unless $class->user->id; + # If we're not logged in, go away + return unless $class->user->id; - $option = LOGOUT_CURRENT unless defined $option; - Bugzilla::Auth::Persist::Cookie->logout({type => $option}); - $class->logout_request() unless $option eq LOGOUT_KEEP_CURRENT; + $option = LOGOUT_CURRENT unless defined $option; + Bugzilla::Auth::Persist::Cookie->logout({type => $option}); + $class->logout_request() unless $option eq LOGOUT_KEEP_CURRENT; } sub logout_user { - my ($class, $user) = @_; - # When we're logging out another user we leave cookies alone, and - # therefore avoid calling Bugzilla->logout() directly. - Bugzilla::Auth::Persist::Cookie->logout({user => $user}); + my ($class, $user) = @_; + + # When we're logging out another user we leave cookies alone, and + # therefore avoid calling Bugzilla->logout() directly. + Bugzilla::Auth::Persist::Cookie->logout({user => $user}); } # just a compatibility front-end to logout_user that gets a user by id sub logout_user_by_id { - my ($class, $id) = @_; - my $user = new Bugzilla::User($id); - $class->logout_user($user); + my ($class, $id) = @_; + my $user = new Bugzilla::User($id); + $class->logout_user($user); } # hack that invalidates credentials for a single request sub logout_request { - my $class = shift; - delete $class->request_cache->{user}; - delete $class->request_cache->{sudoer}; - # We can't delete from $cgi->cookie, so logincookie data will remain - # there. Don't rely on it: use Bugzilla->user->login instead! + my $class = shift; + delete $class->request_cache->{user}; + delete $class->request_cache->{sudoer}; + + # We can't delete from $cgi->cookie, so logincookie data will remain + # there. Don't rely on it: use Bugzilla->user->login instead! } sub job_queue { - require Bugzilla::JobQueue; - return $_[0]->request_cache->{job_queue} ||= Bugzilla::JobQueue->new(); + require Bugzilla::JobQueue; + return $_[0]->request_cache->{job_queue} ||= Bugzilla::JobQueue->new(); +} + +## REDHAT EXTENSION START 1561831 +# Sometimes connections can timeout at the most inopportune times +sub check_dbh { + my $class = shift; + my $dbh = $class->dbh; + my $ping; + eval { + # Can't use ping because DBD::Pg uses a non-query which pgpool can't parse. + $ping = $dbh->do('select 1'); + }; + + unless ($ping) { + $dbh->bz_rollback_transaction() if $dbh->bz_in_transaction; + $dbh->disconnect; + $class->request_cache->{dbh} = $class->request_cache->{dbh_main} + = Bugzilla::DB::connect_main(); + } + return; } +## REDHAT EXTENSION END 1561831 sub dbh { - # If we're not connected, then we must want the main db - return $_[0]->request_cache->{dbh} ||= $_[0]->dbh_main; + + # If we're not connected, then we must want the main db + return $_[0]->request_cache->{dbh} ||= $_[0]->dbh_main; } sub dbh_main { - return $_[0]->request_cache->{dbh_main} ||= Bugzilla::DB::connect_main(); + return $_[0]->request_cache->{dbh_main} ||= Bugzilla::DB::connect_main(); } sub languages { - return Bugzilla::Install::Util::supported_languages(); + return Bugzilla::Install::Util::supported_languages(); } sub current_language { - return $_[0]->request_cache->{current_language} ||= (include_languages())[0]; + return $_[0]->request_cache->{current_language} ||= (include_languages())[0]; } sub error_mode { - my ($class, $newval) = @_; - if (defined $newval) { - $class->request_cache->{error_mode} = $newval; - } + my ($class, $newval) = @_; + if (defined $newval) { + $class->request_cache->{error_mode} = $newval; + } - # XXX - Once we require Perl 5.10.1, this test can be replaced by //. - if (exists $class->request_cache->{error_mode}) { - return $class->request_cache->{error_mode}; - } - else { - return (i_am_cgi() ? ERROR_MODE_WEBPAGE : ERROR_MODE_DIE); - } + # XXX - Once we require Perl 5.10.1, this test can be replaced by //. + if (exists $class->request_cache->{error_mode}) { + return $class->request_cache->{error_mode}; + } + else { + return (i_am_cgi() ? ERROR_MODE_WEBPAGE : ERROR_MODE_DIE); + } } # This is used only by Bugzilla::Error to throw errors. sub _json_server { - my ($class, $newval) = @_; - if (defined $newval) { - $class->request_cache->{_json_server} = $newval; - } - return $class->request_cache->{_json_server}; + my ($class, $newval) = @_; + if (defined $newval) { + $class->request_cache->{_json_server} = $newval; + } + return $class->request_cache->{_json_server}; } sub usage_mode { - my ($class, $newval) = @_; - if (defined $newval) { - if ($newval == USAGE_MODE_BROWSER) { - $class->error_mode(ERROR_MODE_WEBPAGE); - } - elsif ($newval == USAGE_MODE_CMDLINE) { - $class->error_mode(ERROR_MODE_DIE); - } - elsif ($newval == USAGE_MODE_XMLRPC) { - $class->error_mode(ERROR_MODE_DIE_SOAP_FAULT); - } - elsif ($newval == USAGE_MODE_JSON) { - $class->error_mode(ERROR_MODE_JSON_RPC); - } - elsif ($newval == USAGE_MODE_EMAIL) { - $class->error_mode(ERROR_MODE_DIE); - } - elsif ($newval == USAGE_MODE_TEST) { - $class->error_mode(ERROR_MODE_TEST); - } - elsif ($newval == USAGE_MODE_REST) { - $class->error_mode(ERROR_MODE_REST); - } - else { - ThrowCodeError('usage_mode_invalid', - {'invalid_usage_mode', $newval}); - } - $class->request_cache->{usage_mode} = $newval; + my ($class, $newval) = @_; + if (defined $newval) { + if ($newval == USAGE_MODE_BROWSER) { + $class->error_mode(ERROR_MODE_WEBPAGE); } - - # XXX - Once we require Perl 5.10.1, this test can be replaced by //. - if (exists $class->request_cache->{usage_mode}) { - return $class->request_cache->{usage_mode}; + elsif ($newval == USAGE_MODE_CMDLINE) { + $class->error_mode(ERROR_MODE_DIE); + } + elsif ($newval == USAGE_MODE_XMLRPC) { + $class->error_mode(ERROR_MODE_DIE_SOAP_FAULT); + } + elsif ($newval == USAGE_MODE_JSON) { + $class->error_mode(ERROR_MODE_JSON_RPC); + } + elsif ($newval == USAGE_MODE_EMAIL) { + $class->error_mode(ERROR_MODE_DIE); + } + elsif ($newval == USAGE_MODE_TEST) { + $class->error_mode(ERROR_MODE_TEST); + } + elsif ($newval == USAGE_MODE_REST) { + $class->error_mode(ERROR_MODE_REST); } else { - return (i_am_cgi()? USAGE_MODE_BROWSER : USAGE_MODE_CMDLINE); + ThrowCodeError('usage_mode_invalid', {'invalid_usage_mode', $newval}); } + $class->request_cache->{usage_mode} = $newval; + } + + # XXX - Once we require Perl 5.10.1, this test can be replaced by //. + if (exists $class->request_cache->{usage_mode}) { + return $class->request_cache->{usage_mode}; + } + else { + return (i_am_cgi() ? USAGE_MODE_BROWSER : USAGE_MODE_CMDLINE); + } } sub installation_mode { - my ($class, $newval) = @_; - ($class->request_cache->{installation_mode} = $newval) if defined $newval; - return $class->request_cache->{installation_mode} - || INSTALLATION_MODE_INTERACTIVE; + my ($class, $newval) = @_; + ($class->request_cache->{installation_mode} = $newval) if defined $newval; + return $class->request_cache->{installation_mode} + || INSTALLATION_MODE_INTERACTIVE; } sub installation_answers { - my ($class, $filename) = @_; - if ($filename) { - my $s = new Safe; - $s->rdo($filename); + my ($class, $filename) = @_; + if ($filename) { + my $s = new Safe; + $s->rdo($filename); - die "Error reading $filename: $!" if $!; - die "Error evaluating $filename: $@" if $@; + die "Error reading $filename: $!" if $!; + die "Error evaluating $filename: $@" if $@; - # Now read the param back out from the sandbox - $class->request_cache->{installation_answers} = $s->varglob('answer'); - } - return $class->request_cache->{installation_answers} || {}; + # Now read the param back out from the sandbox + $class->request_cache->{installation_answers} = $s->varglob('answer'); + } + return $class->request_cache->{installation_answers} || {}; } sub switch_to_shadow_db { - my $class = shift; + my $class = shift; - if (!$class->request_cache->{dbh_shadow}) { - if ($class->params->{'shadowdb'}) { - $class->request_cache->{dbh_shadow} = Bugzilla::DB::connect_shadow(); - } else { - $class->request_cache->{dbh_shadow} = $class->dbh_main; - } + if (!$class->request_cache->{dbh_shadow}) { + if ($class->params->{'shadowdb'}) { + $class->request_cache->{dbh_shadow} = Bugzilla::DB::connect_shadow(); + } + else { + $class->request_cache->{dbh_shadow} = $class->dbh_main; } + } + + $class->request_cache->{dbh} = $class->request_cache->{dbh_shadow}; - $class->request_cache->{dbh} = $class->request_cache->{dbh_shadow}; - # we have to return $class->dbh instead of {dbh} as - # {dbh_shadow} may be undefined if no shadow DB is used - # and no connection to the main DB has been established yet. - return $class->dbh; + # we have to return $class->dbh instead of {dbh} as + # {dbh_shadow} may be undefined if no shadow DB is used + # and no connection to the main DB has been established yet. + return $class->dbh; } sub switch_to_main_db { - my $class = shift; + my $class = shift; - $class->request_cache->{dbh} = $class->dbh_main; - return $class->dbh_main; + $class->request_cache->{dbh} = $class->dbh_main; + return $class->dbh_main; } sub is_shadow_db { - my $class = shift; - return $class->request_cache->{dbh} != $class->dbh_main; + my $class = shift; + return $class->request_cache->{dbh} != $class->dbh_main; } sub fields { - my ($class, $criteria) = @_; - $criteria ||= {}; - my $cache = $class->request_cache; - - # We create an advanced cache for fields by type, so that we - # can avoid going back to the database for every fields() call. - # (And most of our fields() calls are for getting fields by type.) - # - # We also cache fields by name, because calling $field->name a few - # million times can be slow in calling code, but if we just do it - # once here, that makes things a lot faster for callers. - if (!defined $cache->{fields}) { - my @all_fields = Bugzilla::Field->get_all; - my (%by_name, %by_type); - foreach my $field (@all_fields) { - my $name = $field->name; - $by_type{$field->type}->{$name} = $field; - $by_name{$name} = $field; - } - $cache->{fields} = { by_type => \%by_type, by_name => \%by_name }; + my ($class, $criteria) = @_; + $criteria ||= {}; + my $cache = $class->request_cache; + + # We create an advanced cache for fields by type, so that we + # can avoid going back to the database for every fields() call. + # (And most of our fields() calls are for getting fields by type.) + # + # We also cache fields by name, because calling $field->name a few + # million times can be slow in calling code, but if we just do it + # once here, that makes things a lot faster for callers. + if (!defined $cache->{fields}) { + my @all_fields = Bugzilla::Field->get_all; + + ## REDHAT EXTENSION START 406151 + Bugzilla::Hook::process('bug_filter_fields', {fields => \@all_fields}); + ## REDHAT EXTENSION END 406151 + + my (%by_name, %by_type); + foreach my $field (@all_fields) { + my $name = $field->name; + $by_type{$field->type}->{$name} = $field; + $by_name{$name} = $field; } + $cache->{fields} = {by_type => \%by_type, by_name => \%by_name}; + } - my $fields = $cache->{fields}; - my %requested; - if (my $types = delete $criteria->{type}) { - $types = ref($types) ? $types : [$types]; - %requested = map { %{ $fields->{by_type}->{$_} || {} } } @$types; - } - else { - %requested = %{ $fields->{by_name} }; - } + my $fields = $cache->{fields}; + my %requested; + if (my $types = delete $criteria->{type}) { + $types = ref($types) ? $types : [$types]; + %requested = map { %{$fields->{by_type}->{$_} || {}} } @$types; + } + else { + %requested = %{$fields->{by_name}}; + } + + my $do_by_name = delete $criteria->{by_name}; - my $do_by_name = delete $criteria->{by_name}; + my $user = Bugzilla->login(LOGIN_OPTIONAL); - # Filtering before returning the fields based on - # the criterias. - foreach my $filter (keys %$criteria) { - foreach my $field (keys %requested) { - if ($requested{$field}->$filter != $criteria->{$filter}) { - delete $requested{$field}; - } + # Filtering before returning the fields based on + # the criterias. + foreach my $filter (keys %$criteria) { + foreach my $field (keys %requested) { + if ($filter =~ m/^user_can/) { + unless ($requested{$field}->$filter($user)) { + delete $requested{$field}; } + } + elsif ($requested{$field}->$filter != $criteria->{$filter}) { + delete $requested{$field}; + } } + } - return $do_by_name ? \%requested - : [sort { $a->sortkey <=> $b->sortkey || $a->name cmp $b->name } values %requested]; + return $do_by_name + ? \%requested + : [sort { $a->sortkey <=> $b->sortkey || $a->name cmp $b->name } + values %requested]; } 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 }); - } - return @{$class->request_cache->{active_custom_fields}}; + my $class = shift; + if (!exists $class->request_cache->{active_custom_fields}) { + my $custom_fields = Bugzilla::Field->match({custom => 1, obsolete => 0}); + my @custom_fields = grep {$_->user_can_view($class->user)} @$custom_fields; + ## REDHAT EXTENSION START 406151 + Bugzilla::Hook::process('bug_filter_fields', {fields => \@custom_fields}); + ## REDHAT EXTENSION END 406151 + $class->request_cache->{active_custom_fields} = \@custom_fields; + } + return @{$class->request_cache->{active_custom_fields}}; } sub has_flags { - my $class = shift; + my $class = shift; - if (!defined $class->request_cache->{has_flags}) { - $class->request_cache->{has_flags} = Bugzilla::Flag->any_exist; - } - return $class->request_cache->{has_flags}; + if (!defined $class->request_cache->{has_flags}) { + $class->request_cache->{has_flags} = Bugzilla::Flag->any_exist; + } + return $class->request_cache->{has_flags}; } sub local_timezone { - return $_[0]->process_cache->{local_timezone} - ||= DateTime::TimeZone->new(name => 'local'); + return $_[0]->process_cache->{local_timezone} + ||= DateTime::TimeZone->new(name => 'local'); } -# This creates the request cache for non-mod_perl installations. -# This is identical to Install::Util::_cache so that things loaded -# into Install::Util::_cache during installation can be read out -# of request_cache later in installation. -our $_request_cache = $Bugzilla::Install::Util::_cache; - -sub request_cache { - if ($ENV{MOD_PERL}) { - require Apache2::RequestUtil; - # Sometimes (for example, during mod_perl.pl), the request - # object isn't available, and we should use $_request_cache instead. - my $request = eval { Apache2::RequestUtil->request }; - return $_request_cache if !$request; - return $request->pnotes(); - } - return $_request_cache; +## REDHAT EXTENSION START 1171556 +sub is_pg { + return lc($_[0]->localconfig()->{db_driver}) eq 'pg'; +} + +sub is_mysql { + return lc($_[0]->localconfig()->{db_driver}) eq 'mysql'; } +## REDHAT EXTENSION END 1171556 + +my $request_cache = Bugzilla::Install::Util::_cache(); + +sub request_cache { return $request_cache } sub clear_request_cache { - $_request_cache = {}; - if ($ENV{MOD_PERL}) { - require Apache2::RequestUtil; - my $request = eval { Apache2::RequestUtil->request }; - if ($request) { - my $pnotes = $request->pnotes; - delete @$pnotes{(keys %$pnotes)}; - } - } + %$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 = {}; +my $process_cache = {}; sub process_cache { - return $_process_cache; + return $process_cache; } # This is a memcached wrapper, which provides cross-process and cross-system # caching. sub memcached { - return $_[0]->process_cache->{memcached} ||= Bugzilla::Memcached->_new(); + return $_[0]->process_cache->{memcached} ||= Bugzilla::Memcached->_new(); } # Private methods @@ -674,28 +861,40 @@ sub memcached { # Per-process cleanup. Note that this is a plain subroutine, not a method, # so we don't have $class available. sub _cleanup { - my $cache = Bugzilla->request_cache; - my $main = $cache->{dbh_main}; - my $shadow = $cache->{dbh_shadow}; - foreach my $dbh ($main, $shadow) { - next if !$dbh; - $dbh->bz_rollback_transaction() if $dbh->bz_in_transaction; - $dbh->disconnect; - } - my $smtp = $cache->{smtp}; - $smtp->disconnect if $smtp; - clear_request_cache(); - - # These are both set by CGI.pm but need to be undone so that - # Apache can actually shut down its children if it needs to. - foreach my $signal (qw(TERM PIPE)) { - $SIG{$signal} = 'DEFAULT' if $SIG{$signal} && $SIG{$signal} eq 'IGNORE'; - } + require Bugzilla::Bug; + my $cache = Bugzilla->request_cache; + my $main = $cache->{dbh_main}; + my $shadow = $cache->{dbh_shadow}; + foreach my $dbh ($main, $shadow) { + next if !$dbh; + $dbh->bz_rollback_transaction() if $dbh->bz_in_transaction; + $dbh->disconnect; + } + + ## REDHAT EXTENSION BEGIN 406301 + # Log4perl support + if ($t0) { + my $elapsed = Time::HiRes::tv_interval($t0, [Time::HiRes::gettimeofday]); + Log::Log4perl::get_logger("")->debug(sprintf("FINISH ELAPSED=%8.4f", $elapsed)); + } + ## REDHAT EXTENSION END 406301 + + my $smtp = $cache->{smtp}; + $smtp->disconnect if $smtp; + clear_request_cache(); + Bugzilla::Bug->CLEANUP(); + + # These are both set by CGI.pm but need to be undone so that + # Apache can actually shut down its children if it needs to. + foreach my $signal (qw(TERM PIPE)) { + $SIG{$signal} = 'DEFAULT' if $SIG{$signal} && $SIG{$signal} eq 'IGNORE'; + } } sub END { - # Bugzilla.pm cannot compile in mod_perl.pl if this runs. - _cleanup() unless $ENV{MOD_PERL}; + + # Bugzilla.pm cannot compile in mod_perl.pl if this runs. + _cleanup() unless $ENV{MOD_PERL}; } init_page() if !$ENV{MOD_PERL}; @@ -1062,6 +1261,10 @@ Memcached is not available. See the documentation for the C module for more information. +=item C + +Check if the database handles are still valid, if not then reconnect. + =back =back @@ -1082,4 +1285,16 @@ information. =item has_flags +=item configure_log4perl + +=item is_mysql + +=item log_filename + +=item is_pg + +=item logger + +=item configure_log4perl_user + =back diff --git a/Bugzilla/Attachment.pm b/Bugzilla/Attachment.pm index 33183797b..04e51e5d1 100644 --- a/Bugzilla/Attachment.pm +++ b/Bugzilla/Attachment.pm @@ -46,6 +46,7 @@ use Bugzilla::Hook; use File::Copy; use List::Util qw(max); +use Scalar::Util qw(weaken isweak); use Storable qw(dclone); use parent qw(Bugzilla::Object); @@ -57,55 +58,51 @@ use parent qw(Bugzilla::Object); use constant DB_TABLE => 'attachments'; use constant ID_FIELD => 'attach_id'; use constant LIST_ORDER => ID_FIELD; + # Attachments are tracked in bugs_activity. use constant AUDIT_CREATES => 0; use constant AUDIT_UPDATES => 0; use constant DB_COLUMNS => qw( - attach_id - bug_id - creation_ts - description - filename - isobsolete - ispatch - isprivate - mimetype - modification_time - submitter_id + attach_id + bug_id + creation_ts + description + filename + isobsolete + ispatch + isprivate + mimetype + modification_time + submitter_id ); -use constant REQUIRED_FIELD_MAP => { - bug_id => 'bug', -}; +use constant REQUIRED_FIELD_MAP => {bug_id => 'bug',}; use constant EXTRA_REQUIRED_FIELDS => qw(data); use constant UPDATE_COLUMNS => qw( - description - filename - isobsolete - ispatch - isprivate - mimetype + description + filename + isobsolete + ispatch + isprivate + mimetype ); use constant VALIDATORS => { - bug => \&_check_bug, - description => \&_check_description, - filename => \&_check_filename, - ispatch => \&Bugzilla::Object::check_boolean, - isprivate => \&_check_is_private, - mimetype => \&_check_content_type, + bug => \&_check_bug, + description => \&_check_description, + filename => \&_check_filename, + ispatch => \&Bugzilla::Object::check_boolean, + isprivate => \&_check_is_private, + mimetype => \&_check_content_type, }; -use constant VALIDATOR_DEPENDENCIES => { - content_type => ['ispatch'], - mimetype => ['ispatch'], -}; +use constant VALIDATOR_DEPENDENCIES => + {content_type => ['ispatch'], mimetype => ['ispatch'],}; -use constant UPDATE_VALIDATORS => { - isobsolete => \&Bugzilla::Object::check_boolean, -}; +use constant UPDATE_VALIDATORS => + {isobsolete => \&Bugzilla::Object::check_boolean,}; ############################### #### Accessors ###### @@ -126,7 +123,7 @@ the ID of the bug to which the attachment is attached =cut sub bug_id { - return $_[0]->{bug_id}; + return $_[0]->{bug_id}; } =over @@ -140,8 +137,14 @@ the bug object to which the attachment is attached =cut sub bug { - require Bugzilla::Bug; - return $_[0]->{bug} //= Bugzilla::Bug->new({ id => $_[0]->bug_id, cache => 1 }); + my ($self) = @_; + require Bugzilla::Bug; + return $self->{bug} if defined $self->{bug}; + +# note $bug exists as a strong reference to keep $self->{bug} defined until the end of this method + my $bug = $self->{bug} = Bugzilla::Bug->new({id => $_[0]->bug_id, cache => 1}); + weaken($self->{bug}) unless isweak($self->{bug}); + return $bug; } =over @@ -155,7 +158,7 @@ user-provided text describing the attachment =cut sub description { - return $_[0]->{description}; + return $_[0]->{description}; } =over @@ -169,7 +172,7 @@ the attachment's MIME media type =cut sub contenttype { - return $_[0]->{mimetype}; + return $_[0]->{mimetype}; } =over @@ -183,8 +186,8 @@ the user who attached the attachment =cut sub attacher { - return $_[0]->{attacher} - //= new Bugzilla::User({ id => $_[0]->{submitter_id}, cache => 1 }); + return $_[0]->{attacher} + //= new Bugzilla::User({id => $_[0]->{submitter_id}, cache => 1}); } =over @@ -198,7 +201,7 @@ the date and time on which the attacher attached the attachment =cut sub attached { - return $_[0]->{creation_ts}; + return $_[0]->{creation_ts}; } =over @@ -212,7 +215,7 @@ the date and time on which the attachment was last modified. =cut sub modification_time { - return $_[0]->{modification_time}; + return $_[0]->{modification_time}; } =over @@ -226,7 +229,7 @@ the name of the file the attacher attached =cut sub filename { - return $_[0]->{filename}; + return $_[0]->{filename}; } =over @@ -240,7 +243,7 @@ whether or not the attachment is a patch =cut sub ispatch { - return $_[0]->{ispatch}; + return $_[0]->{ispatch}; } =over @@ -254,7 +257,7 @@ whether or not the attachment is obsolete =cut sub isobsolete { - return $_[0]->{isobsolete}; + return $_[0]->{isobsolete}; } =over @@ -268,7 +271,7 @@ whether or not the attachment is private =cut sub isprivate { - return $_[0]->{isprivate}; + return $_[0]->{isprivate}; } =over @@ -285,23 +288,24 @@ matches, because this will return a value even if it's matched by the generic =cut sub is_viewable { - my $contenttype = $_[0]->contenttype; - my $cgi = Bugzilla->cgi; + my $contenttype = $_[0]->contenttype; + my $cgi = Bugzilla->cgi; - # We assume we can view all text and image types. - return 1 if ($contenttype =~ /^(text|image)\//); + # We assume we can view all text and image types. + return 1 if ($contenttype =~ /^(text|image)\//); - # Mozilla can view XUL. Note the trailing slash on the Gecko detection to - # avoid sending XUL to Safari. - return 1 if (($contenttype =~ /^application\/vnd\.mozilla\./) - && ($cgi->user_agent() =~ /Gecko\//)); + # Mozilla can view XUL. Note the trailing slash on the Gecko detection to + # avoid sending XUL to Safari. + return 1 + if (($contenttype =~ /^application\/vnd\.mozilla\./) + && ($cgi->user_agent() =~ /Gecko\//)); - # If it's not one of the above types, we check the Accept: header for any - # types mentioned explicitly. - my $accept = join(",", $cgi->Accept()); - return 1 if ($accept =~ /^(.*,)?\Q$contenttype\E(,.*)?$/); + # If it's not one of the above types, we check the Accept: header for any + # types mentioned explicitly. + my $accept = join(",", $cgi->Accept()); + return 1 if ($accept =~ /^(.*,)?\Q$contenttype\E(,.*)?$/); - return 0; + return 0; } =over @@ -315,28 +319,29 @@ the content of the attachment =cut sub data { - my $self = shift; - return $self->{data} if exists $self->{data}; + my $self = shift; + return $self->{data} if exists $self->{data}; - # First try to get the attachment data from the database. - ($self->{data}) = Bugzilla->dbh->selectrow_array("SELECT thedata + # First try to get the attachment data from the database. + ($self->{data}) = Bugzilla->dbh->selectrow_array( + "SELECT thedata FROM attach_data - WHERE id = ?", - undef, - $self->id); - - # If there's no attachment data in the database, the attachment is stored - # in a local file, so retrieve it from there. - if (length($self->{data}) == 0) { - if (open(AH, '<', $self->_get_local_filename())) { - local $/; - binmode AH; - $self->{data} = ; - close(AH); - } + WHERE id = ?", undef, + $self->id + ); + + # If there's no attachment data in the database, the attachment is stored + # in a local file, so retrieve it from there. + if (!$self->{data} || length($self->{data}) == 0) { + if (open(AH, '<', $self->_get_local_filename())) { + local $/; + binmode AH; + $self->{data} = ; + close(AH); } + } - return $self->{data}; + return $self->{data}; } =over @@ -358,37 +363,37 @@ the length (in bytes) of the attachment content # LENGTH() function or stat()ing the file instead. I've left it in for now. sub datasize { - my $self = shift; - return $self->{datasize} if defined $self->{datasize}; + my $self = shift; + return $self->{datasize} if defined $self->{datasize}; - # If we have already retrieved the data, return its size. - return length($self->{data}) if exists $self->{data}; + # If we have already retrieved the data, return its size. + return length($self->{data}) if exists $self->{data}; - $self->{datasize} = - Bugzilla->dbh->selectrow_array("SELECT LENGTH(thedata) + $self->{datasize} = Bugzilla->dbh->selectrow_array( + "SELECT LENGTH(thedata) FROM attach_data - WHERE id = ?", - undef, $self->id) || 0; - - # If there's no attachment data in the database, either the attachment - # is stored in a local file, and so retrieve its size from the file, - # or the attachment has been deleted. - unless ($self->{datasize}) { - if (open(AH, '<', $self->_get_local_filename())) { - binmode AH; - $self->{datasize} = (stat(AH))[7]; - close(AH); - } + WHERE id = ?", undef, $self->id + ) || 0; + + # If there's no attachment data in the database, either the attachment + # is stored in a local file, and so retrieve its size from the file, + # or the attachment has been deleted. + unless ($self->{datasize}) { + if (open(AH, '<', $self->_get_local_filename())) { + binmode AH; + $self->{datasize} = (stat(AH))[7]; + close(AH); } + } - return $self->{datasize}; + return $self->{datasize}; } sub _get_local_filename { - my $self = shift; - my $hash = ($self->id % 100) + 100; - $hash =~ s/.*(\d\d)$/group.$1/; - return bz_locations()->{'attachdir'} . "/$hash/attachment." . $self->id; + my $self = shift; + my $hash = ($self->id % 100) + 100; + $hash =~ s/.*(\d\d)$/group.$1/; + return bz_locations()->{'attachdir'} . "/$hash/attachment." . $self->id; } =over @@ -402,8 +407,9 @@ flags that have been set on the attachment =cut sub flags { - # Don't cache it as it must be in sync with ->flag_types. - return $_[0]->{flags} = [map { @{$_->{flags}} } @{$_[0]->flag_types}]; + + # Don't cache it as it must be in sync with ->flag_types. + return $_[0]->{flags} = [map { @{$_->{flags}} } @{$_[0]->flag_types}]; } =over @@ -418,202 +424,220 @@ already set, grouped by flag type. =cut sub flag_types { - my $self = shift; - return $self->{flag_types} if exists $self->{flag_types}; + my $self = shift; + return $self->{flag_types} if exists $self->{flag_types}; - my $vars = { target_type => 'attachment', - product_id => $self->bug->product_id, - component_id => $self->bug->component_id, - attach_id => $self->id }; + my $vars = { + target_type => 'attachment', + product_id => $self->bug->product_id, + component_id => $self->bug->component_id, + attach_id => $self->id + }; - return $self->{flag_types} = Bugzilla::Flag->_flag_types($vars); + return $self->{flag_types} = Bugzilla::Flag->_flag_types($vars); } ############################### #### Validators ###### ############################### -sub set_content_type { $_[0]->set('mimetype', $_[1]); } +sub set_content_type { $_[0]->set('mimetype', $_[1]); } sub set_description { $_[0]->set('description', $_[1]); } -sub set_filename { $_[0]->set('filename', $_[1]); } -sub set_is_patch { $_[0]->set('ispatch', $_[1]); } -sub set_is_private { $_[0]->set('isprivate', $_[1]); } - -sub set_is_obsolete { - my ($self, $obsolete) = @_; - - my $old = $self->isobsolete; - $self->set('isobsolete', $obsolete); - my $new = $self->isobsolete; - - # If the attachment is being marked as obsolete, cancel pending requests. - if ($new && $old != $new) { - my @requests = grep { $_->status eq '?' } @{$self->flags}; - return unless scalar @requests; - - my %flag_ids = map { $_->id => 1 } @requests; - foreach my $flagtype (@{$self->flag_types}) { - @{$flagtype->{flags}} = grep { !$flag_ids{$_->id} } @{$flagtype->{flags}}; - } +sub set_filename { $_[0]->set('filename', $_[1]); } +sub set_is_patch { $_[0]->set('ispatch', $_[1]); } +sub set_is_private { $_[0]->set('isprivate', $_[1]); } + +sub set_is_obsolete { + my ($self, $obsolete) = @_; + + my $old = $self->isobsolete; + $self->set('isobsolete', $obsolete); + my $new = $self->isobsolete; + + # If the attachment is being marked as obsolete, cancel pending requests. + if ($new && $old != $new) { + my @requests = grep { $_->status eq '?' } @{$self->flags}; + return unless scalar @requests; + + my %flag_ids = map { $_->id => 1 } @requests; + foreach my $flagtype (@{$self->flag_types}) { + @{$flagtype->{flags}} = grep { !$flag_ids{$_->id} } @{$flagtype->{flags}}; } + } } sub set_flags { - my ($self, $flags, $new_flags) = @_; + my ($self, $flags, $new_flags) = @_; - Bugzilla::Flag->set_flag($self, $_) foreach (@$flags, @$new_flags); + Bugzilla::Flag->set_flag($self, $_) foreach (@$flags, @$new_flags); } sub _check_bug { - my ($invocant, $bug) = @_; - my $user = Bugzilla->user; + my ($invocant, $bug) = @_; + my $user = Bugzilla->user; - $bug = ref $invocant ? $invocant->bug : $bug; + $bug = ref $invocant ? $invocant->bug : $bug; - $bug || ThrowCodeError('param_required', - { function => "$invocant->create", param => 'bug' }); + $bug + || ThrowCodeError('param_required', + {function => "$invocant->create", param => 'bug'}); - ($user->can_see_bug($bug->id) && $user->can_edit_product($bug->product_id)) - || ThrowUserError("illegal_attachment_edit_bug", { bug_id => $bug->id }); + ($user->can_see_bug($bug->id) && $user->can_edit_product($bug->product_id)) + || ThrowUserError("illegal_attachment_edit_bug", {bug_id => $bug->id}); - return $bug; + return $bug; } sub _check_content_type { - my ($invocant, $content_type, undef, $params) = @_; - - my $is_patch = ref($invocant) ? $invocant->ispatch : $params->{ispatch}; - $content_type = 'text/plain' if $is_patch; - $content_type = clean_text($content_type); - # The subsets below cover all existing MIME types and charsets registered by IANA. - # (MIME type: RFC 2045 section 5.1; charset: RFC 2278 section 3.3) - my $legal_types = join('|', LEGAL_CONTENT_TYPES); - if (!$content_type - || $content_type !~ /^($legal_types)\/[a-z0-9_\-\+\.]+(;\s*charset=[a-z0-9_\-\+]+)?$/i) - { - ThrowUserError("invalid_content_type", { contenttype => $content_type }); + my ($invocant, $content_type, undef, $params) = @_; + + my $is_patch = ref($invocant) ? $invocant->ispatch : $params->{ispatch}; + $content_type = 'text/plain' if $is_patch; + $content_type = clean_text($content_type); + +# The subsets below cover all existing MIME types and charsets registered by IANA. +# (MIME type: RFC 2045 section 5.1; charset: RFC 2278 section 3.3) + my $legal_types = join('|', LEGAL_CONTENT_TYPES); + if (!$content_type + || $content_type + !~ /^($legal_types)\/[a-z0-9_\-\+\.]+(;\s*charset=[a-z0-9_\-\+]+)?$/i) + { + ThrowUserError("invalid_content_type", {contenttype => $content_type}); + } + trick_taint($content_type); + + # $ENV{HOME} must be defined when using File::MimeInfo::Magic, + # see https://rt.cpan.org/Public/Bug/Display.html?id=41744. + local $ENV{HOME} = $ENV{HOME} || File::Spec->rootdir(); + + # If we have autodetected application/octet-stream from the Content-Type + # header, let's have a better go using a sniffer if available. + if ( + defined Bugzilla->input_params->{contenttypemethod} + && Bugzilla->input_params->{contenttypemethod} eq 'autodetect' +## REDHAT EXTENSION BEGIN 1121741 + && $content_type ne 'text/plain' +## REDHAT EXTENSION END 1121741 + && Bugzilla->feature('typesniffer') + ) + { + import File::MimeInfo::Magic qw(mimetype); + require IO::Scalar; + + # data is either a filehandle, or the data itself. + my $fh = $params->{data}; + if (!ref($fh)) { + $fh = new IO::Scalar \$fh; } - trick_taint($content_type); - - # $ENV{HOME} must be defined when using File::MimeInfo::Magic, - # see https://rt.cpan.org/Public/Bug/Display.html?id=41744. - local $ENV{HOME} = $ENV{HOME} || File::Spec->rootdir(); - - # If we have autodetected application/octet-stream from the Content-Type - # header, let's have a better go using a sniffer if available. - if (defined Bugzilla->input_params->{contenttypemethod} - && Bugzilla->input_params->{contenttypemethod} eq 'autodetect' - && $content_type eq 'application/octet-stream' - && Bugzilla->feature('typesniffer')) - { - import File::MimeInfo::Magic qw(mimetype); - require IO::Scalar; - - # data is either a filehandle, or the data itself. - my $fh = $params->{data}; - if (!ref($fh)) { - $fh = new IO::Scalar \$fh; - } - elsif (!$fh->isa('IO::Handle')) { - # 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. - $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); - $fh->seek(0, 0); - $content_type = $mimetype if $mimetype; + elsif (!$fh->isa('IO::Handle')) { + + # 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. + $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'; + } } - # Make sure patches are viewable in the browser - if (!ref($invocant) - && defined Bugzilla->input_params->{contenttypemethod} - && Bugzilla->input_params->{contenttypemethod} eq 'autodetect' - && $content_type =~ m{text/x-(?:diff|patch)}) - { - $params->{ispatch} = 1; - $content_type = 'text/plain'; - } - - return $content_type; + my $mimetype = mimetype($fh); + $fh->seek(0, 0); + $content_type = $mimetype if $mimetype; + } + + # Make sure patches are viewable in the browser + if (!ref($invocant) + && defined Bugzilla->input_params->{contenttypemethod} + && Bugzilla->input_params->{contenttypemethod} eq 'autodetect' + && $content_type =~ m{text/x-(?:diff|patch)}) + { + $params->{ispatch} = 1; + $content_type = 'text/plain'; + } + + return $content_type; } sub _check_data { - my ($invocant, $params) = @_; + my ($invocant, $params) = @_; - my $data = $params->{data}; - $params->{filesize} = ref $data ? -s $data : length($data); + my $data = $params->{data}; + $params->{filesize} = ref $data ? -s $data : length($data); - Bugzilla::Hook::process('attachment_process_data', { data => \$data, - attributes => $params }); + Bugzilla::Hook::process('attachment_process_data', + {data => \$data, attributes => $params}); - $params->{filesize} || ThrowUserError('zero_length_file'); - # Make sure the attachment does not exceed the maximum permitted size. - my $max_size = max(Bugzilla->params->{'maxlocalattachment'} * 1048576, - Bugzilla->params->{'maxattachmentsize'} * 1024); + $params->{filesize} || ThrowUserError('zero_length_file'); - if ($params->{filesize} > $max_size) { - my $vars = { filesize => sprintf("%.0f", $params->{filesize}/1024) }; - ThrowUserError('file_too_large', $vars); - } - return $data; + # Make sure the attachment does not exceed the maximum permitted size. + my $max_size = max( + Bugzilla->params->{'maxlocalattachment'} * 1048576, + Bugzilla->params->{'maxattachmentsize'} * 1024 + ); + + if ($params->{filesize} > $max_size) { + my $vars = {filesize => sprintf("%.0f", $params->{filesize} / 1024)}; + ThrowUserError('file_too_large', $vars); + } + return $data; } sub _check_description { - my ($invocant, $description) = @_; + my ($invocant, $description) = @_; - $description = trim($description); - $description || ThrowUserError('missing_attachment_description'); - return $description; + $description = trim($description); + $description || ThrowUserError('missing_attachment_description'); + return $description; } sub _check_filename { - my ($invocant, $filename) = @_; - - $filename = clean_text($filename); - if (!$filename) { - if (ref $invocant) { - ThrowUserError('filename_not_specified'); - } - else { - ThrowUserError('file_not_specified'); - } - } + my ($invocant, $filename) = @_; - # Remove path info (if any) from the file name. The browser should do this - # for us, but some are buggy. This may not work on Mac file names and could - # mess up file names with slashes in them, but them's the breaks. We only - # use this as a hint to users downloading attachments anyway, so it's not - # a big deal if it munges incorrectly occasionally. - $filename =~ s/^.*[\/\\]//; - - # Truncate the filename to MAX_ATTACH_FILENAME_LENGTH characters, counting - # from the end of the string to make sure we keep the filename extension. - $filename = substr($filename, - -&MAX_ATTACH_FILENAME_LENGTH, - MAX_ATTACH_FILENAME_LENGTH); - trick_taint($filename); - - return $filename; + $filename = clean_text($filename); + if (!$filename) { + if (ref $invocant) { + ThrowUserError('filename_not_specified'); + } + else { + ThrowUserError('file_not_specified'); + } + } + + # Remove path info (if any) from the file name. The browser should do this + # for us, but some are buggy. This may not work on Mac file names and could + # mess up file names with slashes in them, but them's the breaks. We only + # use this as a hint to users downloading attachments anyway, so it's not + # a big deal if it munges incorrectly occasionally. + $filename =~ s/^.*[\/\\]//; + + # Truncate the filename to MAX_ATTACH_FILENAME_LENGTH characters, counting + # from the end of the string to make sure we keep the filename extension. + $filename + = substr($filename, -&MAX_ATTACH_FILENAME_LENGTH, MAX_ATTACH_FILENAME_LENGTH); + trick_taint($filename); + + return $filename; } sub _check_is_private { - my ($invocant, $is_private) = @_; - - $is_private = $is_private ? 1 : 0; - if (((!ref $invocant && $is_private) - || (ref $invocant && $invocant->isprivate != $is_private)) - && !Bugzilla->user->is_insider) { - ThrowUserError('user_not_insider'); - } - return $is_private; + my ($invocant, $is_private) = @_; + + $is_private = $is_private ? 1 : 0; + if ( + ( + (!ref $invocant && $is_private) + || (ref $invocant && $invocant->isprivate != $is_private) + ) + && !Bugzilla->user->is_insider + ) + { + ThrowUserError('user_not_insider'); + } + return $is_private; } =pod @@ -635,69 +659,74 @@ Returns: a reference to an array of attachment objects. =cut sub get_attachments_by_bug { - my ($class, $bug, $vars) = @_; - my $user = Bugzilla->user; - my $dbh = Bugzilla->dbh; - - # By default, private attachments are not accessible, unless the user - # is in the insider group or submitted the attachment. - my $and_restriction = ''; - my @values = ($bug->id); - - unless ($user->is_insider) { - $and_restriction = 'AND (isprivate = 0 OR submitter_id = ?)'; - push(@values, $user->id); + my ($class, $bug, $vars) = @_; + my $user = Bugzilla->user; + my $dbh = Bugzilla->dbh; + + # By default, private attachments are not accessible, unless the user + # is in the insider group or submitted the attachment. + my $and_restriction = ''; + my @values = ($bug->id); + + unless ($user->is_insider) { + $and_restriction = 'AND (isprivate = 0 OR submitter_id = ?)'; + push(@values, $user->id); + } + + my $attach_ids = $dbh->selectcol_arrayref( + "SELECT attach_id FROM attachments + WHERE bug_id = ? $and_restriction", + undef, @values + ); + + my $attachments = Bugzilla::Attachment->new_from_list($attach_ids); + $_->{bug} = $bug foreach @$attachments; + + # To avoid $attachment->flags and $attachment->flag_types running SQL queries + # themselves for each attachment listed here, we collect all the data at once and + # populate $attachment->{flag_types} ourselves. We also load all attachers and + # datasizes at once for the same reason. + if ($vars->{preload}) { + + # Preload flag types and flags + my $vars = { + target_type => 'attachment', + product_id => $bug->product_id, + component_id => $bug->component_id, + attach_id => $attach_ids + }; + my $flag_types = Bugzilla::Flag->_flag_types($vars); + + foreach my $attachment (@$attachments) { + $attachment->{flag_types} = []; + my $new_types = dclone($flag_types); + foreach my $new_type (@$new_types) { + $new_type->{flags} + = [grep($_->attach_id == $attachment->id, @{$new_type->{flags}})]; + push(@{$attachment->{flag_types}}, $new_type); + } } - my $attach_ids = $dbh->selectcol_arrayref("SELECT attach_id FROM attachments - WHERE bug_id = ? $and_restriction", - undef, @values); - - my $attachments = Bugzilla::Attachment->new_from_list($attach_ids); - $_->{bug} = $bug foreach @$attachments; - - # To avoid $attachment->flags and $attachment->flag_types running SQL queries - # themselves for each attachment listed here, we collect all the data at once and - # populate $attachment->{flag_types} ourselves. We also load all attachers and - # datasizes at once for the same reason. - if ($vars->{preload}) { - # Preload flag types and flags - my $vars = { target_type => 'attachment', - product_id => $bug->product_id, - component_id => $bug->component_id, - attach_id => $attach_ids }; - my $flag_types = Bugzilla::Flag->_flag_types($vars); - - foreach my $attachment (@$attachments) { - $attachment->{flag_types} = []; - my $new_types = dclone($flag_types); - foreach my $new_type (@$new_types) { - $new_type->{flags} = [ grep($_->attach_id == $attachment->id, - @{ $new_type->{flags} }) ]; - push(@{ $attachment->{flag_types} }, $new_type); - } - } - - # Preload attachers. - my %user_ids = map { $_->{submitter_id} => 1 } @$attachments; - my $users = Bugzilla::User->new_from_list([keys %user_ids]); - my %user_map = map { $_->id => $_ } @$users; - foreach my $attachment (@$attachments) { - $attachment->{attacher} = $user_map{$attachment->{submitter_id}}; - } - - # Preload datasizes. - my $sizes = - $dbh->selectall_hashref('SELECT attach_id, LENGTH(thedata) AS datasize + # Preload attachers. + my %user_ids = map { $_->{submitter_id} => 1 } @$attachments; + my $users = Bugzilla::User->new_from_list([keys %user_ids]); + my %user_map = map { $_->id => $_ } @$users; + foreach my $attachment (@$attachments) { + $attachment->{attacher} = $user_map{$attachment->{submitter_id}}; + } + + # Preload datasizes. + my $sizes = $dbh->selectall_hashref( + 'SELECT attach_id, LENGTH(thedata) AS datasize FROM attachments LEFT JOIN attach_data ON attach_id = id - WHERE bug_id = ?', - 'attach_id', undef, $bug->id); + WHERE bug_id = ?', 'attach_id', undef, $bug->id + ); - # Force the size of attachments not in the DB to be recalculated. - $_->{datasize} = $sizes->{$_->id}->{datasize} || undef foreach @$attachments; - } + # Force the size of attachments not in the DB to be recalculated. + $_->{datasize} = $sizes->{$_->id}->{datasize} || undef foreach @$attachments; + } - return $attachments; + return $attachments; } =pod @@ -716,13 +745,15 @@ Returns: 1 on success, 0 otherwise. =cut sub validate_can_edit { - my $attachment = shift; - my $user = Bugzilla->user; - - # The submitter can edit their attachments. - return ($attachment->attacher->id == $user->id - || ((!$attachment->isprivate || $user->is_insider) - && $user->in_group('editbugs', $attachment->bug->product_id))) ? 1 : 0; + my $attachment = shift; + my $user = Bugzilla->user; + + # The submitter can edit their attachments. + return ( + $attachment->attacher->id == $user->id + || ((!$attachment->isprivate || $user->is_insider) + && $user->in_group('editbugs', $attachment->bug->product_id)) + ) ? 1 : 0; } =item C @@ -741,37 +772,36 @@ Returns: The list of attachment objects to mark as obsolete. =cut sub validate_obsolete { - my ($class, $bug, $list) = @_; + my ($class, $bug, $list) = @_; - # Make sure the attachment id is valid and the user has permissions to view - # the bug to which it is attached. Make sure also that the user can view - # the attachment itself. - my @obsolete_attachments; - foreach my $attachid (@$list) { - my $vars = {}; - $vars->{'attach_id'} = $attachid; + # Make sure the attachment id is valid and the user has permissions to view + # the bug to which it is attached. Make sure also that the user can view + # the attachment itself. + my @obsolete_attachments; + foreach my $attachid (@$list) { + my $vars = {}; + $vars->{'attach_id'} = $attachid; - detaint_natural($attachid) - || ThrowUserError('invalid_attach_id', $vars); + detaint_natural($attachid) || ThrowUserError('invalid_attach_id', $vars); - # Make sure the attachment exists in the database. - my $attachment = new Bugzilla::Attachment($attachid) - || ThrowUserError('invalid_attach_id', $vars); + # Make sure the attachment exists in the database. + my $attachment = new Bugzilla::Attachment($attachid) + || ThrowUserError('invalid_attach_id', $vars); - # Check that the user can view and edit this attachment. - $attachment->validate_can_edit - || ThrowUserError('illegal_attachment_edit', { attach_id => $attachment->id }); + # Check that the user can view and edit this attachment. + $attachment->validate_can_edit + || ThrowUserError('illegal_attachment_edit', {attach_id => $attachment->id}); - if ($attachment->bug_id != $bug->bug_id) { - $vars->{'my_bug_id'} = $bug->bug_id; - ThrowUserError('mismatched_bug_ids_on_obsolete', $vars); - } + if ($attachment->bug_id != $bug->bug_id) { + $vars->{'my_bug_id'} = $bug->bug_id; + ThrowUserError('mismatched_bug_ids_on_obsolete', $vars); + } - next if $attachment->isobsolete; + next if $attachment->isobsolete; - push(@obsolete_attachments, $attachment); - } - return @obsolete_attachments; + push(@obsolete_attachments, $attachment); + } + return @obsolete_attachments; } ############################### @@ -806,112 +836,119 @@ Returns: The new attachment object. =cut sub create { - my $class = shift; - my $dbh = Bugzilla->dbh; - - $class->check_required_create_fields(@_); - my $params = $class->run_create_validators(@_); - - # Extract everything which is not a valid column name. - my $bug = delete $params->{bug}; - $params->{bug_id} = $bug->id; - my $data = delete $params->{data}; - my $size = delete $params->{filesize}; - - my $attachment = $class->insert_create_data($params); - my $attachid = $attachment->id; - - # The file is too large to be stored in the DB, so we store it locally. - if ($size > Bugzilla->params->{'maxattachmentsize'} * 1024) { - my $attachdir = bz_locations()->{'attachdir'}; - my $hash = ($attachid % 100) + 100; - $hash =~ s/.*(\d\d)$/group.$1/; - mkdir "$attachdir/$hash", 0770; - chmod 0770, "$attachdir/$hash"; - if (ref $data) { - copy($data, "$attachdir/$hash/attachment.$attachid"); - close $data; - } - else { - open(AH, '>', "$attachdir/$hash/attachment.$attachid"); - binmode AH; - print AH $data; - close AH; - } - $data = ''; # Will be stored in the DB. - } - # If we have a filehandle, we need its content to store it in the DB. - elsif (ref $data) { - local $/; - # Store the content in a temp variable while we close the FH. - my $tmp = <$data>; - close $data; - $data = $tmp; - } + my $class = shift; + my $dbh = Bugzilla->dbh; - my $sth = $dbh->prepare("INSERT INTO attach_data - (id, thedata) VALUES ($attachid, ?)"); + $class->check_required_create_fields(@_); + my $params = $class->run_create_validators(@_); - trick_taint($data); - $sth->bind_param(1, $data, $dbh->BLOB_TYPE); - $sth->execute(); + # Extract everything which is not a valid column name. + my $bug = delete $params->{bug}; + $params->{bug_id} = $bug->id; + my $data = delete $params->{data}; + my $size = delete $params->{filesize}; - $attachment->{bug} = $bug; + my $attachment = $class->insert_create_data($params); + my $attachid = $attachment->id; - # Return the new attachment object. - return $attachment; -} + # The file is too large to be stored in the DB, so we store it locally. + if ($size > Bugzilla->params->{'maxattachmentsize'} * 1024) { + my $attachdir = bz_locations()->{'attachdir'}; + my $hash = ($attachid % 100) + 100; + $hash =~ s/.*(\d\d)$/group.$1/; + mkdir "$attachdir/$hash", 0770; + chmod 0770, "$attachdir/$hash"; + if (ref $data) { + copy($data, "$attachdir/$hash/attachment.$attachid"); + close $data; + } + else { + open(AH, '>', "$attachdir/$hash/attachment.$attachid"); + binmode AH; + print AH $data; + close AH; + } + $data = ''; # Will be stored in the DB. + } -sub run_create_validators { - my ($class, $params) = @_; + # If we have a filehandle, we need its content to store it in the DB. + elsif (ref $data) { + local $/; + + # Store the content in a temp variable while we close the FH. + my $tmp = <$data>; + close $data; + $data = $tmp; + } - $params->{submitter_id} = Bugzilla->user->id || ThrowUserError('invalid_user'); + my $sth = $dbh->prepare( + "INSERT INTO attach_data + (id, thedata) VALUES ($attachid, ?)" + ); - # Let's validate the attachment content first as it may - # alter some other attachment attributes. - $params->{data} = $class->_check_data($params); - $params = $class->SUPER::run_create_validators($params); + trick_taint($data); + $sth->bind_param(1, $data, $dbh->BLOB_TYPE); + $sth->execute(); - $params->{creation_ts} ||= Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); - $params->{modification_time} = $params->{creation_ts}; + $attachment->{bug} = $bug; - return $params; + # Return the new attachment object. + return $attachment; } -sub update { - my $self = shift; - my $dbh = Bugzilla->dbh; - my $user = Bugzilla->user; - my $timestamp = shift || $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); +sub run_create_validators { + my ($class, $params) = @_; - my ($changes, $old_self) = $self->SUPER::update(@_); + $params->{submitter_id} = Bugzilla->user->id || ThrowUserError('invalid_user'); - my ($removed, $added) = Bugzilla::Flag->update_flags($self, $old_self, $timestamp); - if ($removed || $added) { - $changes->{'flagtypes.name'} = [$removed, $added]; - } + # Let's validate the attachment content first as it may + # alter some other attachment attributes. + $params->{data} = $class->_check_data($params); + $params = $class->SUPER::run_create_validators($params); - # Record changes in the activity table. - require Bugzilla::Bug; - foreach my $field (keys %$changes) { - my $change = $changes->{$field}; - $field = "attachments.$field" unless $field eq "flagtypes.name"; - Bugzilla::Bug::LogActivityEntry($self->bug_id, $field, $change->[0], - $change->[1], $user->id, $timestamp, undef, $self->id); - } + $params->{creation_ts} + ||= Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); + $params->{modification_time} = $params->{creation_ts}; - if (scalar(keys %$changes)) { - $dbh->do('UPDATE attachments SET modification_time = ? WHERE attach_id = ?', - undef, ($timestamp, $self->id)); - $dbh->do('UPDATE bugs SET delta_ts = ? WHERE bug_id = ?', - undef, ($timestamp, $self->bug_id)); - $self->{modification_time} = $timestamp; - # because we updated the attachments table after SUPER::update(), we - # need to ensure the cache is flushed. - Bugzilla->memcached->clear({ table => 'attachments', id => $self->id }); - } + return $params; +} - return $changes; +sub update { + my $self = shift; + my $dbh = Bugzilla->dbh; + my $user = Bugzilla->user; + my $timestamp = shift || $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); + + my ($changes, $old_self) = $self->SUPER::update(@_); + + my ($removed, $added) + = Bugzilla::Flag->update_flags($self, $old_self, $timestamp); + if ($removed || $added) { + $changes->{'flagtypes.name'} = [$removed, $added]; + } + + # Record changes in the activity table. + require Bugzilla::Bug; + foreach my $field (keys %$changes) { + my $change = $changes->{$field}; + $field = "attachments.$field" unless $field eq "flagtypes.name"; + Bugzilla::Bug::LogActivityEntry($self->bug_id, $field, $change->[0], + $change->[1], $user->id, $timestamp, undef, $self->id); + } + + if (scalar(keys %$changes)) { + $dbh->do('UPDATE attachments SET modification_time = ? WHERE attach_id = ?', + undef, ($timestamp, $self->id)); + $dbh->do('UPDATE bugs SET delta_ts = ? WHERE bug_id = ?', + undef, ($timestamp, $self->bug_id)); + $self->{modification_time} = $timestamp; + + # because we updated the attachments table after SUPER::update(), we + # need to ensure the cache is flushed. + Bugzilla->memcached->clear({table => 'attachments', id => $self->id}); + } + + return $changes; } =pod @@ -929,30 +966,33 @@ Returns: nothing =cut sub remove_from_db { - my $self = shift; - my $dbh = Bugzilla->dbh; - - $dbh->bz_start_transaction(); - my $flag_ids = $dbh->selectcol_arrayref( - 'SELECT id FROM flags WHERE attach_id = ?', undef, $self->id); - $dbh->do('DELETE FROM flags WHERE ' . $dbh->sql_in('id', $flag_ids)) - if @$flag_ids; - $dbh->do('DELETE FROM attach_data WHERE id = ?', undef, $self->id); - $dbh->do('UPDATE attachments SET mimetype = ?, ispatch = ?, isobsolete = ? - WHERE attach_id = ?', undef, ('text/plain', 0, 1, $self->id)); - $dbh->bz_commit_transaction(); - - my $filename = $self->_get_local_filename; - if (-e $filename) { - unlink $filename or warn "Couldn't unlink $filename: $!"; - } - - # As we don't call SUPER->remove_from_db we need to manually clear - # memcached here. - Bugzilla->memcached->clear({ table => 'attachments', id => $self->id }); - foreach my $flag_id (@$flag_ids) { - Bugzilla->memcached->clear({ table => 'flags', id => $flag_id }); - } + my $self = shift; + my $dbh = Bugzilla->dbh; + + $dbh->bz_start_transaction(); + my $flag_ids + = $dbh->selectcol_arrayref('SELECT id FROM flags WHERE attach_id = ?', + undef, $self->id); + $dbh->do('DELETE FROM flags WHERE ' . $dbh->sql_in('id', $flag_ids)) + if @$flag_ids; + $dbh->do('DELETE FROM attach_data WHERE id = ?', undef, $self->id); + $dbh->do( + 'UPDATE attachments SET mimetype = ?, ispatch = ?, isobsolete = ? + WHERE attach_id = ?', undef, ('text/plain', 0, 1, $self->id) + ); + $dbh->bz_commit_transaction(); + + my $filename = $self->_get_local_filename; + if (-e $filename) { + unlink $filename or warn "Couldn't unlink $filename: $!"; + } + + # As we don't call SUPER->remove_from_db we need to manually clear + # memcached here. + Bugzilla->memcached->clear({table => 'attachments', id => $self->id}); + foreach my $flag_id (@$flag_ids) { + Bugzilla->memcached->clear({table => 'flags', id => $flag_id}); + } } ############################### @@ -961,37 +1001,39 @@ sub remove_from_db { # Extract the content type from the attachment form. sub get_content_type { - my $cgi = Bugzilla->cgi; + my $cgi = Bugzilla->cgi; - return 'text/plain' if ($cgi->param('ispatch') || $cgi->param('attach_text')); + return 'text/plain' if ($cgi->param('ispatch') || $cgi->param('attach_text')); - my $content_type; - my $method = $cgi->param('contenttypemethod') || ''; + my $content_type; + my $method = $cgi->param('contenttypemethod') || ''; - if ($method eq 'list') { - # The user selected a content type from the list, so use their - # selection. - $content_type = $cgi->param('contenttypeselection'); - } - elsif ($method eq 'manual') { - # The user entered a content type manually, so use their entry. - $content_type = $cgi->param('contenttypeentry'); - } - else { - defined $cgi->upload('data') || ThrowUserError('file_not_specified'); - # The user asked us to auto-detect the content type, so use the type - # specified in the HTTP request headers. - $content_type = - $cgi->uploadInfo($cgi->param('data'))->{'Content-Type'}; - $content_type || ThrowUserError("missing_content_type"); - - # Internet Explorer sends image/x-png for PNG images, - # so convert that to image/png to match other browsers. - if ($content_type eq 'image/x-png') { - $content_type = 'image/png'; - } + if ($method eq 'list') { + + # The user selected a content type from the list, so use their + # selection. + $content_type = $cgi->param('contenttypeselection'); + } + elsif ($method eq 'manual') { + + # The user entered a content type manually, so use their entry. + $content_type = $cgi->param('contenttypeentry'); + } + else { + defined $cgi->upload('data') || ThrowUserError('file_not_specified'); + + # The user asked us to auto-detect the content type, so use the type + # specified in the HTTP request headers. + $content_type = $cgi->uploadInfo($cgi->param('data'))->{'Content-Type'}; + $content_type || ThrowUserError("missing_content_type"); + + # Internet Explorer sends image/x-png for PNG images, + # so convert that to image/png to match other browsers. + if ($content_type eq 'image/x-png') { + $content_type = 'image/png'; } - return $content_type; + } + return $content_type; } diff --git a/Bugzilla/Attachment/PatchReader.pm b/Bugzilla/Attachment/PatchReader.pm index d0e221220..5b21b2f3a 100644 --- a/Bugzilla/Attachment/PatchReader.pm +++ b/Bugzilla/Attachment/PatchReader.pm @@ -23,184 +23,212 @@ use Bugzilla::Util; use constant PERLIO_IS_ENABLED => $Config{useperlio}; sub process_diff { - my ($attachment, $format) = @_; - my $dbh = Bugzilla->dbh; - my $cgi = Bugzilla->cgi; - my $lc = Bugzilla->localconfig; - my $vars = {}; + my ($attachment, $format) = @_; + my $dbh = Bugzilla->dbh; + my $cgi = Bugzilla->cgi; + my $lc = Bugzilla->localconfig; + my $vars = {}; - require PatchReader::Raw; - my $reader = new PatchReader::Raw; + require PatchReader::Raw; + my $reader = new PatchReader::Raw; - if ($format eq 'raw') { - require PatchReader::DiffPrinter::raw; - $reader->sends_data_to(new PatchReader::DiffPrinter::raw()); - # Actually print out the patch. - print $cgi->header(-type => 'text/plain'); - disable_utf8(); - $reader->iterate_string('Attachment ' . $attachment->id, $attachment->data); - } - else { - my @other_patches = (); - if ($lc->{interdiffbin} && $lc->{diffpath}) { - # Get the list of attachments that the user can view in this bug. - my @attachments = - @{Bugzilla::Attachment->get_attachments_by_bug($attachment->bug)}; - # Extract patches only. - @attachments = grep {$_->ispatch == 1} @attachments; - # We want them sorted from newer to older. - @attachments = sort { $b->id <=> $a->id } @attachments; - - # Ignore the current patch, but select the one right before it - # chronologically. - my $select_next_patch = 0; - foreach my $attach (@attachments) { - if ($attach->id == $attachment->id) { - $select_next_patch = 1; - } - else { - push(@other_patches, { 'id' => $attach->id, - 'desc' => $attach->description, - 'selected' => $select_next_patch }); - $select_next_patch = 0; - } - } + if ($format eq 'raw') { + require PatchReader::DiffPrinter::raw; + $reader->sends_data_to(new PatchReader::DiffPrinter::raw()); + + # Actually print out the patch. + print $cgi->header(-type => 'text/plain'); + disable_utf8(); + $reader->iterate_string('Attachment ' . $attachment->id, $attachment->data); + } + else { + my @other_patches = (); + if ($lc->{interdiffbin} && $lc->{diffpath}) { + + # Get the list of attachments that the user can view in this bug. + my @attachments + = @{Bugzilla::Attachment->get_attachments_by_bug($attachment->bug)}; + + # Extract patches only. + @attachments = grep { $_->ispatch == 1 } @attachments; + + # We want them sorted from newer to older. + @attachments = sort { $b->id <=> $a->id } @attachments; + + # Ignore the current patch, but select the one right before it + # chronologically. + my $select_next_patch = 0; + foreach my $attach (@attachments) { + if ($attach->id == $attachment->id) { + $select_next_patch = 1; } - - $vars->{'bugid'} = $attachment->bug_id; - $vars->{'attachid'} = $attachment->id; - $vars->{'description'} = $attachment->description; - $vars->{'other_patches'} = \@other_patches; - - setup_template_patch_reader($reader, $vars); - # 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; # Populate ->{data} - utf8::decode($attachment->{data}); + else { + push( + @other_patches, + { + 'id' => $attach->id, + 'desc' => $attach->description, + 'selected' => $select_next_patch + } + ); + $select_next_patch = 0; } - $reader->iterate_string('Attachment ' . $attachment->id, $attachment->data); + } } -} -sub process_interdiff { - my ($old_attachment, $new_attachment, $format) = @_; - my $cgi = Bugzilla->cgi; - my $lc = Bugzilla->localconfig; - my $vars = {}; - - require PatchReader::Raw; - - # Encode attachment data as utf8 if it's going to be displayed in a HTML - # page using the UTF-8 encoding. - if ($format ne 'raw' && Bugzilla->params->{'utf8'}) { - $old_attachment->data; # Populate ->{data} - utf8::decode($old_attachment->{data}); - $new_attachment->data; # Populate ->{data} - utf8::decode($new_attachment->{data}); - } + $vars->{'bugid'} = $attachment->bug_id; + $vars->{'attachid'} = $attachment->id; + $vars->{'description'} = $attachment->description; + $vars->{'other_patches'} = \@other_patches; - # Get old patch data. - my ($old_filename, $old_file_list) = get_unified_diff($old_attachment, $format); - # Get new patch data. - my ($new_filename, $new_file_list) = get_unified_diff($new_attachment, $format); - - my $warning = warn_if_interdiff_might_fail($old_file_list, $new_file_list); - - # Send through interdiff, send output directly to template. - # Must hack path so that interdiff will work. - local $ENV{'PATH'} = $lc->{diffpath}; - - # Open the interdiff pipe, reading from both STDOUT and STDERR - # To avoid deadlocks, we have to read the entire output from all handles - my ($stdout, $stderr) = ('', ''); - my ($pid, $interdiff_stdout, $interdiff_stderr, $use_select); - if ($ENV{MOD_PERL}) { - require Apache2::RequestUtil; - require Apache2::SubProcess; - my $request = Apache2::RequestUtil->request; - (undef, $interdiff_stdout, $interdiff_stderr) = $request->spawn_proc_prog( - $lc->{interdiffbin}, [$old_filename, $new_filename] - ); - $use_select = !PERLIO_IS_ENABLED; - } else { - $interdiff_stderr = gensym; - $pid = open3(gensym, $interdiff_stdout, $interdiff_stderr, - $lc->{interdiffbin}, $old_filename, $new_filename); - $use_select = 1; - } + setup_template_patch_reader($reader, $vars); - if ($format ne 'raw' && Bugzilla->params->{'utf8'}) { - binmode $interdiff_stdout, ':utf8'; - binmode $interdiff_stderr, ':utf8'; - } else { - binmode $interdiff_stdout; - binmode $interdiff_stderr; + # 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; # Populate ->{data} + utf8::decode($attachment->{data}); } + $reader->iterate_string('Attachment ' . $attachment->id, $attachment->data); + } +} - if ($use_select) { - my $select = IO::Select->new(); - $select->add($interdiff_stdout, $interdiff_stderr); - while (my @handles = $select->can_read) { - foreach my $handle (@handles) { - my $line = <$handle>; - if (!defined $line) { - $select->remove($handle); - next; - } - if ($handle == $interdiff_stdout) { - $stdout .= $line; - } else { - $stderr .= $line; - } - } +sub process_interdiff { + my ($old_attachment, $new_attachment, $format, $vars) = @_; + my $cgi = Bugzilla->cgi; + my $lc = Bugzilla->localconfig; + +# my $vars = {}; + + require PatchReader::Raw; + + # Encode attachment data as utf8 if it's going to be displayed in a HTML + # page using the UTF-8 encoding. + if ($format ne 'raw' && Bugzilla->params->{'utf8'}) { + $old_attachment->data; # Populate ->{data} + utf8::decode($old_attachment->{data}); + $new_attachment->data; # Populate ->{data} + utf8::decode($new_attachment->{data}); + } + + # Get old patch data. + my ($old_filename, $old_file_list) = get_unified_diff($old_attachment, $format); + + # Get new patch data. + my ($new_filename, $new_file_list) = get_unified_diff($new_attachment, $format); + + my $warning = warn_if_interdiff_might_fail($old_file_list, $new_file_list); + + # Send through interdiff, send output directly to template. + # Must hack path so that interdiff will work. + local $ENV{'PATH'} = $lc->{diffpath}; + + # Open the interdiff pipe, reading from both STDOUT and STDERR + # To avoid deadlocks, we have to read the entire output from all handles + my ($stdout, $stderr) = ('', ''); + my ($pid, $interdiff_stdout, $interdiff_stderr, $use_select); + if ($ENV{MOD_PERL}) { + require Apache2::RequestUtil; + require Apache2::SubProcess; + my $request = Apache2::RequestUtil->request; + (undef, $interdiff_stdout, $interdiff_stderr) + = $request->spawn_proc_prog($lc->{interdiffbin}, + [$old_filename, $new_filename]); + $use_select = !PERLIO_IS_ENABLED; + } + else { + $interdiff_stderr = gensym; + $pid = open3(gensym, $interdiff_stdout, $interdiff_stderr, $lc->{interdiffbin}, + $old_filename, $new_filename); + $use_select = 1; + } + + if ($format ne 'raw' && Bugzilla->params->{'utf8'}) { + binmode $interdiff_stdout, ':utf8'; + binmode $interdiff_stderr, ':utf8'; + } + else { + binmode $interdiff_stdout; + binmode $interdiff_stderr; + } + + if ($use_select) { + my $select = IO::Select->new(); + $select->add($interdiff_stdout, $interdiff_stderr); + while (my @handles = $select->can_read) { + foreach my $handle (@handles) { + my $line = <$handle>; + if (!defined $line) { + $select->remove($handle); + next; } - waitpid($pid, 0) if $pid; - - } else { - local $/ = undef; - $stdout = <$interdiff_stdout>; - $stdout //= ''; - $stderr = <$interdiff_stderr>; - $stderr //= ''; - } - - close($interdiff_stdout), - close($interdiff_stderr); - - # Tidy up - unlink($old_filename) or warn "Could not unlink $old_filename: $!"; - unlink($new_filename) or warn "Could not unlink $new_filename: $!"; - - # Any output on STDERR means interdiff failed to full process the patches. - # Interdiff's error messages are generic and not useful to end users, so we - # show a generic failure message. - if ($stderr) { - warn($stderr); - $warning = 'interdiff3'; + if ($handle == $interdiff_stdout) { + $stdout .= $line; + } + else { + $stderr .= $line; + } + } } - + waitpid($pid, 0) if $pid; + + } + else { + local $/ = undef; + $stdout = <$interdiff_stdout>; + $stdout //= ''; + $stderr = <$interdiff_stderr>; + $stderr //= ''; + } + + close($interdiff_stdout), close($interdiff_stderr); + + # Tidy up + unlink($old_filename) or warn "Could not unlink $old_filename: $!"; + unlink($new_filename) or warn "Could not unlink $new_filename: $!"; + + # Any output on STDERR means interdiff failed to full process the patches. + # Interdiff's error messages are generic and not useful to end users, so we + # show a generic failure message. + if ($stderr) { + warn($stderr); + $warning = 'interdiff3'; + } + + # BUGBUG Hacked this out for diff2html use... + if (0) { my $reader = new PatchReader::Raw; if ($format eq 'raw') { - require PatchReader::DiffPrinter::raw; - $reader->sends_data_to(new PatchReader::DiffPrinter::raw()); - # Actually print out the patch. - print $cgi->header(-type => 'text/plain'); - disable_utf8(); + require PatchReader::DiffPrinter::raw; + $reader->sends_data_to(new PatchReader::DiffPrinter::raw()); + + # Actually print out the patch. + print $cgi->header(-type => 'text/plain'); + disable_utf8(); } else { - $vars->{'warning'} = $warning if $warning; - $vars->{'bugid'} = $new_attachment->bug_id; - $vars->{'oldid'} = $old_attachment->id; - $vars->{'old_desc'} = $old_attachment->description; - $vars->{'newid'} = $new_attachment->id; - $vars->{'new_desc'} = $new_attachment->description; - - setup_template_patch_reader($reader, $vars); + $vars->{'warning'} = $warning if $warning; + $vars->{'bugid'} = $new_attachment->bug_id; + $vars->{'oldid'} = $old_attachment->id; + $vars->{'old_desc'} = $old_attachment->description; + $vars->{'newid'} = $new_attachment->id; + $vars->{'new_desc'} = $new_attachment->description; + + setup_template_patch_reader($reader, $vars); } - $reader->iterate_string('interdiff #' . $old_attachment->id . - ' #' . $new_attachment->id, $stdout); + $reader->iterate_string( + 'interdiff #' . $old_attachment->id . ' #' . $new_attachment->id, $stdout); + } + + $vars->{'warning'} = $warning if $warning; + $vars->{'bugid'} = $new_attachment->bug_id; + $vars->{'oldid'} = $old_attachment->id; + $vars->{'old_desc'} = $old_attachment->description; + $vars->{'newid'} = $new_attachment->id; + $vars->{'new_desc'} = $new_attachment->description; + $vars->{'difftext'} = $stdout; + return; } ###################### @@ -208,92 +236,92 @@ sub process_interdiff { ###################### sub get_unified_diff { - my ($attachment, $format) = @_; + my ($attachment, $format) = @_; - # Bring in the modules we need. - require PatchReader::Raw; - require PatchReader::DiffPrinter::raw; - require PatchReader::PatchInfoGrabber; - require File::Temp; + # Bring in the modules we need. + require PatchReader::Raw; + require PatchReader::DiffPrinter::raw; + require PatchReader::PatchInfoGrabber; + require File::Temp; - $attachment->ispatch - || ThrowCodeError('must_be_patch', { 'attach_id' => $attachment->id }); + $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 $last_reader = $reader; - - # Grabs the patch file info. - my $patch_info_grabber = new PatchReader::PatchInfoGrabber(); - $last_reader->sends_data_to($patch_info_grabber); - $last_reader = $patch_info_grabber; - - # Prints out to temporary file. - my ($fh, $filename) = File::Temp::tempfile(); - if ($format ne 'raw' && Bugzilla->params->{'utf8'}) { - # The HTML page will be displayed with the UTF-8 encoding. - binmode $fh, ':utf8'; - } - my $raw_printer = new PatchReader::DiffPrinter::raw($fh); - $last_reader->sends_data_to($raw_printer); - $last_reader = $raw_printer; + # Reads in the patch, converting to unified diff in a temp file. + my $reader = new PatchReader::Raw; + my $last_reader = $reader; - # Iterate! - $reader->iterate_string($attachment->id, $attachment->data); + # Grabs the patch file info. + my $patch_info_grabber = new PatchReader::PatchInfoGrabber(); + $last_reader->sends_data_to($patch_info_grabber); + $last_reader = $patch_info_grabber; - return ($filename, $patch_info_grabber->patch_info()->{files}); + # Prints out to temporary file. + my ($fh, $filename) = File::Temp::tempfile(); + if ($format ne 'raw' && Bugzilla->params->{'utf8'}) { + + # The HTML page will be displayed with the UTF-8 encoding. + binmode $fh, ':utf8'; + } + my $raw_printer = new PatchReader::DiffPrinter::raw($fh); + $last_reader->sends_data_to($raw_printer); + $last_reader = $raw_printer; + + # Iterate! + $reader->iterate_string($attachment->id, $attachment->data); + + return ($filename, $patch_info_grabber->patch_info()->{files}); } sub warn_if_interdiff_might_fail { - my ($old_file_list, $new_file_list) = @_; - - # Verify that the list of files diffed is the same. - my @old_files = sort keys %{$old_file_list}; - my @new_files = sort keys %{$new_file_list}; - if (@old_files != @new_files - || join(' ', @old_files) ne join(' ', @new_files)) + my ($old_file_list, $new_file_list) = @_; + + # Verify that the list of files diffed is the same. + my @old_files = sort keys %{$old_file_list}; + my @new_files = sort keys %{$new_file_list}; + if (@old_files != @new_files || join(' ', @old_files) ne join(' ', @new_files)) + { + return 'interdiff1'; + } + + # Verify that the revisions in the files are the same. + foreach my $file (keys %{$old_file_list}) { + if ( exists $old_file_list->{$file}{old_revision} + && exists $new_file_list->{$file}{old_revision} + && $old_file_list->{$file}{old_revision} ne + $new_file_list->{$file}{old_revision}) { - return 'interdiff1'; - } - - # Verify that the revisions in the files are the same. - foreach my $file (keys %{$old_file_list}) { - if (exists $old_file_list->{$file}{old_revision} - && exists $new_file_list->{$file}{old_revision} - && $old_file_list->{$file}{old_revision} ne - $new_file_list->{$file}{old_revision}) - { - return 'interdiff2'; - } + return 'interdiff2'; } - return undef; + } + return undef; } sub setup_template_patch_reader { - my ($last_reader, $vars) = @_; - my $cgi = Bugzilla->cgi; - my $template = Bugzilla->template; - - require PatchReader::DiffPrinter::template; - - # Define the vars for templates. - if (defined $cgi->param('headers')) { - $vars->{'headers'} = $cgi->param('headers'); - } - else { - $vars->{'headers'} = 1; - } - - $vars->{'collapsed'} = $cgi->param('collapsed'); - - # Print everything out. - print $cgi->header(-type => 'text/html'); - - $last_reader->sends_data_to(new PatchReader::DiffPrinter::template($template, - 'attachment/diff-header.html.tmpl', - 'attachment/diff-file.html.tmpl', - 'attachment/diff-footer.html.tmpl', - $vars)); + my ($last_reader, $vars) = @_; + my $cgi = Bugzilla->cgi; + my $template = Bugzilla->template; + + require PatchReader::DiffPrinter::template; + + # Define the vars for templates. + if (defined $cgi->param('headers')) { + $vars->{'headers'} = $cgi->param('headers'); + } + else { + $vars->{'headers'} = 1; + } + + $vars->{'collapsed'} = $cgi->param('collapsed'); + + # Print everything out. + print $cgi->header(-type => 'text/html'); + + $last_reader->sends_data_to(new PatchReader::DiffPrinter::template( + $template, 'attachment/diff-header.html.tmpl', + 'attachment/diff-file.html.tmpl', 'attachment/diff-footer.html.tmpl', + $vars + )); } 1; diff --git a/Bugzilla/Auth.pm b/Bugzilla/Auth.pm index c830f0506..237d43e60 100644 --- a/Bugzilla/Auth.pm +++ b/Bugzilla/Auth.pm @@ -12,9 +12,9 @@ use strict; use warnings; use fields qw( - _info_getter - _verifier - _persister + _info_getter + _verifier + _persister ); use Bugzilla::Constants; @@ -28,218 +28,245 @@ use Bugzilla::Auth::Persist::Cookie; use Socket; sub new { - my ($class, $params) = @_; - my $self = fields::new($class); + my ($class, $params) = @_; + my $self = fields::new($class); - $params ||= {}; - $params->{Login} ||= Bugzilla->params->{'user_info_class'} . ',Cookie,APIKey'; - $params->{Verify} ||= Bugzilla->params->{'user_verify_class'}; + $params ||= {}; + $params->{Login} ||= Bugzilla->params->{'user_info_class'} . ',Cookie,APIKey'; + $params->{Verify} ||= Bugzilla->params->{'user_verify_class'}; - $self->{_info_getter} = new Bugzilla::Auth::Login::Stack($params->{Login}); - $self->{_verifier} = new Bugzilla::Auth::Verify::Stack($params->{Verify}); - # If we ever have any other login persistence methods besides cookies, - # this could become more configurable. - $self->{_persister} = new Bugzilla::Auth::Persist::Cookie(); + $self->{_info_getter} = new Bugzilla::Auth::Login::Stack($params->{Login}); + $self->{_verifier} = new Bugzilla::Auth::Verify::Stack($params->{Verify}); - return $self; + # If we ever have any other login persistence methods besides cookies, + # this could become more configurable. + $self->{_persister} = new Bugzilla::Auth::Persist::Cookie(); + + return $self; } sub login { - my ($self, $type) = @_; - - # Get login info from the cookie, form, environment variables, etc. - my $login_info = $self->{_info_getter}->get_login_info(); + my ($self, $type) = @_; - if ($login_info->{failure}) { - return $self->_handle_login_result($login_info, $type); - } + # Get login info from the cookie, form, environment variables, etc. + my $login_info = $self->{_info_getter}->get_login_info(); - # Now verify their username and password against the DB, LDAP, etc. - if ($self->{_info_getter}->{successful}->requires_verification) { - $login_info = $self->{_verifier}->check_credentials($login_info); - if ($login_info->{failure}) { - return $self->_handle_login_result($login_info, $type); - } - $login_info = - $self->{_verifier}->{successful}->create_or_update_user($login_info); - } - else { - $login_info = $self->{_verifier}->create_or_update_user($login_info); - } + if ($login_info->{failure}) { + return $self->_handle_login_result($login_info, $type); + } + # Now verify their username and password against the DB, LDAP, etc. + if ($self->{_info_getter}->{successful}->requires_verification) { + $login_info = $self->{_verifier}->check_credentials($login_info); if ($login_info->{failure}) { - return $self->_handle_login_result($login_info, $type); + return $self->_handle_login_result($login_info, $type); } + $login_info + = $self->{_verifier}->{successful}->create_or_update_user($login_info); + } + else { + $login_info = $self->{_verifier}->create_or_update_user($login_info); + } + + if ($login_info->{failure}) { + return $self->_handle_login_result($login_info, $type); + } - # Make sure the user isn't disabled. - my $user = $login_info->{user}; - if (!$user->is_enabled) { - return $self->_handle_login_result({ failure => AUTH_DISABLED, - user => $user }, $type); - } - $user->set_authorizer($self); + # Make sure the user isn't disabled. + my $user = $login_info->{user}; + if (!$user->is_enabled) { + return $self->_handle_login_result({failure => AUTH_DISABLED, user => $user}, + $type); + } + $user->set_authorizer($self); - return $self->_handle_login_result($login_info, $type); + return $self->_handle_login_result($login_info, $type); } sub can_change_password { - my ($self) = @_; - my $verifier = $self->{_verifier}->{successful}; - $verifier ||= $self->{_verifier}; - my $getter = $self->{_info_getter}->{successful}; - $getter = $self->{_info_getter} - if (!$getter || $getter->isa('Bugzilla::Auth::Login::Cookie')); - return $verifier->can_change_password && - $getter->user_can_create_account; + my ($self) = @_; + my $verifier = $self->{_verifier}->{successful}; + $verifier ||= $self->{_verifier}; + my $getter = $self->{_info_getter}->{successful}; + $getter = $self->{_info_getter} + if (!$getter || $getter->isa('Bugzilla::Auth::Login::Cookie')); + return $verifier->can_change_password && $getter->user_can_create_account; } sub can_login { - my ($self) = @_; - my $getter = $self->{_info_getter}->{successful}; - $getter = $self->{_info_getter} - if (!$getter || $getter->isa('Bugzilla::Auth::Login::Cookie')); - return $getter->can_login; + my ($self) = @_; + my $getter = $self->{_info_getter}->{successful}; + $getter = $self->{_info_getter} + if (!$getter || $getter->isa('Bugzilla::Auth::Login::Cookie')); + return $getter->can_login; } sub can_logout { - my ($self) = @_; - my $getter = $self->{_info_getter}->{successful}; - # If there's no successful getter, we're not logged in, so of - # course we can't log out! - return 0 unless $getter; - return $getter->can_logout; + my ($self) = @_; + my $getter = $self->{_info_getter}->{successful}; + + # If there's no successful getter, we're not logged in, so of + # course we can't log out! + return 0 unless $getter; + return $getter->can_logout; } sub login_token { - my ($self) = @_; - my $getter = $self->{_info_getter}->{successful}; - if ($getter && $getter->isa('Bugzilla::Auth::Login::Cookie')) { - return $getter->login_token; - } - return undef; + my ($self) = @_; + my $getter = $self->{_info_getter}->{successful}; + if ($getter && $getter->isa('Bugzilla::Auth::Login::Cookie')) { + return $getter->login_token; + } + return undef; } sub user_can_create_account { - my ($self) = @_; - my $verifier = $self->{_verifier}->{successful}; - $verifier ||= $self->{_verifier}; - my $getter = $self->{_info_getter}->{successful}; - $getter = $self->{_info_getter} - if (!$getter || $getter->isa('Bugzilla::Auth::Login::Cookie')); - return $verifier->user_can_create_account - && $getter->user_can_create_account; + my ($self) = @_; + my $verifier = $self->{_verifier}->{successful}; + $verifier ||= $self->{_verifier}; + my $getter = $self->{_info_getter}->{successful}; + $getter = $self->{_info_getter} + if (!$getter || $getter->isa('Bugzilla::Auth::Login::Cookie')); + return $verifier->user_can_create_account && $getter->user_can_create_account; } sub extern_id_used { - my ($self) = @_; - return $self->{_info_getter}->extern_id_used - || $self->{_verifier}->extern_id_used; + my ($self) = @_; + return $self->{_info_getter}->extern_id_used + || $self->{_verifier}->extern_id_used; } sub can_change_email { - return $_[0]->user_can_create_account; + my $can = 1; + + Bugzilla::Hook::process('can_change_email', {can => \$can}); + + return $can ? $_[0]->user_can_create_account : 0; } sub _handle_login_result { - my ($self, $result, $login_type) = @_; - my $dbh = Bugzilla->dbh; - - my $user = $result->{user}; - my $fail_code = $result->{failure}; - - if (!$fail_code) { - # We don't persist logins over GET requests in the WebService, - # because the persistance information can't be re-used again. - # (See Bugzilla::WebService::Server::JSONRPC for more info.) - if ($self->{_info_getter}->{successful}->requires_persistence - and !Bugzilla->request_cache->{auth_no_automatic_login}) - { - $user->{_login_token} = $self->{_persister}->persist_login($user); - } - } - elsif ($fail_code == AUTH_ERROR) { - if ($result->{user_error}) { - ThrowUserError($result->{user_error}, $result->{details}); - } - else { - ThrowCodeError($result->{error}, $result->{details}); - } + my ($self, $result, $login_type) = @_; + my $dbh = Bugzilla->dbh; + + my $user = $result->{user}; + my $fail_code = $result->{failure}; + + if (!$fail_code) { + ## REDHAT EXTENSION BEGIN 1434224 + my $log + = "User " + . $user->login + . " logged in with " + . ref $self->{_info_getter}->{successful}; + + if (Bugzilla->usage_mode == USAGE_MODE_BROWSER) { + $log .= " on agent " . ($ENV{HTTP_USER_AGENT} || 'NONE'); + $log .= " from referer " . ($ENV{HTTP_REFERER} || 'NONE'); } - elsif ($fail_code == AUTH_NODATA) { - $self->{_info_getter}->fail_nodata($self) - if $login_type == LOGIN_REQUIRED; - - # If we're not LOGIN_REQUIRED, we just return the default user. - $user = Bugzilla->user; + Bugzilla->logger->info($log); + ## REDHAT EXTENSION END 1434224 + + # We don't persist logins over GET requests in the WebService, + # because the persistance information can't be re-used again. + # (See Bugzilla::WebService::Server::JSONRPC for more info.) + if ($self->{_info_getter}->{successful}->requires_persistence + and !Bugzilla->request_cache->{auth_no_automatic_login}) + { + $user->{_login_token} = $self->{_persister}->persist_login($user); } - # The username/password may be wrong - # Don't let the user know whether the username exists or whether - # the password was just wrong. (This makes it harder for a cracker - # to find account names by brute force) - elsif ($fail_code == AUTH_LOGINFAILED or $fail_code == AUTH_NO_SUCH_USER) { - my $remaining_attempts = MAX_LOGIN_ATTEMPTS - - ($result->{failure_count} || 0); - ThrowUserError("invalid_login_or_password", - { remaining => $remaining_attempts }); + } + elsif ($fail_code == AUTH_ERROR) { + if ($result->{user_error}) { + ThrowUserError($result->{user_error}, $result->{details}); } - # The account may be disabled - elsif ($fail_code == AUTH_DISABLED) { - $self->{_persister}->logout(); - # XXX This is NOT a good way to do this, architecturally. - $self->{_persister}->clear_browser_cookies(); - # and throw a user error - ThrowUserError("account_disabled", - {'disabled_reason' => $result->{user}->disabledtext}); - } - elsif ($fail_code == AUTH_LOCKOUT) { - my $attempts = $user->account_ip_login_failures; - - # We want to know when the account will be unlocked. This is - # determined by the 5th-from-last login failure (or more/less than - # 5th, if MAX_LOGIN_ATTEMPTS is not 5). - my $determiner = $attempts->[scalar(@$attempts) - MAX_LOGIN_ATTEMPTS]; - my $unlock_at = datetime_from($determiner->{login_time}, - Bugzilla->local_timezone); - $unlock_at->add(minutes => LOGIN_LOCKOUT_INTERVAL); - - # If we were *just* locked out, notify the maintainer about the - # lockout. - if ($result->{just_locked_out}) { - # We're sending to the maintainer, who may be not a Bugzilla - # account, but just an email address. So we use the - # installation's default language for sending the email. - 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) - || ThrowTemplateError($template->error); - MessageToMTA($message); - } - - $unlock_at->set_time_zone($user->timezone); - ThrowUserError('account_locked', - { ip_addr => $determiner->{ip_addr}, unlock_at => $unlock_at }); - } - # If we get here, then we've run out of options, which shouldn't happen. else { - ThrowCodeError("authres_unhandled", { value => $fail_code }); + ThrowCodeError($result->{error}, $result->{details}); + } + } + elsif ($fail_code == AUTH_NODATA) { + $self->{_info_getter}->fail_nodata($self) if $login_type == LOGIN_REQUIRED; + + # If we're not LOGIN_REQUIRED, we just return the default user. + $user = Bugzilla->user; + } + + # The username/password may be wrong + # Don't let the user know whether the username exists or whether + # the password was just wrong. (This makes it harder for a cracker + # to find account names by brute force) + elsif ($fail_code == AUTH_LOGINFAILED or $fail_code == AUTH_NO_SUCH_USER) { + my $remaining_attempts = MAX_LOGIN_ATTEMPTS - ($result->{failure_count} || 0); + ThrowUserError("invalid_login_or_password", {remaining => $remaining_attempts}); + } + ## REDHAT EXTENSION 1262651 BEGIN + elsif ($fail_code == AUTH_RH_RADIUS_LOGINFAILED) { + my $remaining_attempts = MAX_LOGIN_ATTEMPTS - ($result->{failure_count} || 0); + ThrowUserError("rh_auth_radius_failed", {remaining => $remaining_attempts}); + } + ## REDHAT EXTENSION 1262651 BEGIN + # The account may be disabled + elsif ($fail_code == AUTH_DISABLED) { + $self->{_persister}->logout(); + + # XXX This is NOT a good way to do this, architecturally. + $self->{_persister}->clear_browser_cookies(); + + # and throw a user error + ThrowUserError("account_disabled", + {'disabled_reason' => $result->{user}->disabledtext}); + } + elsif ($fail_code == AUTH_LOCKOUT) { + my $attempts = $user->account_ip_login_failures; + + # We want to know when the account will be unlocked. This is + # determined by the 5th-from-last login failure (or more/less than + # 5th, if MAX_LOGIN_ATTEMPTS is not 5). + my $determiner = $attempts->[scalar(@$attempts) - MAX_LOGIN_ATTEMPTS]; + my $unlock_at + = datetime_from($determiner->{login_time}, Bugzilla->local_timezone); + $unlock_at->add(minutes => LOGIN_LOCKOUT_INTERVAL); + + # If we were *just* locked out, notify the maintainer about the + # lockout. + if ($result->{just_locked_out}) { + + # We're sending to the maintainer, who may be not a Bugzilla + # account, but just an email address. So we use the + # installation's default language for sending the email. + 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) + || ThrowTemplateError($template->error); + MessageToMTA($message); } - return $user; + $unlock_at->set_time_zone($user->timezone); + ThrowUserError('account_locked', + {ip_addr => $determiner->{ip_addr}, unlock_at => $unlock_at}); + } + + # If we get here, then we've run out of options, which shouldn't happen. + else { + ThrowCodeError("authres_unhandled", {value => $fail_code}); + } + + return $user; } 1; diff --git a/Bugzilla/Auth/Login.pm b/Bugzilla/Auth/Login.pm index a5f089777..e6196c680 100644 --- a/Bugzilla/Auth/Login.pm +++ b/Bugzilla/Auth/Login.pm @@ -16,18 +16,17 @@ use fields qw(); # Determines whether or not a user can logout. It's really a subroutine, # but we implement it here as a constant. Override it in subclasses if # that particular type of login method cannot log out. -use constant can_logout => 1; -use constant can_login => 1; -use constant requires_persistence => 1; -use constant requires_verification => 1; +use constant can_logout => 1; +use constant can_login => 1; +use constant requires_persistence => 1; +use constant requires_verification => 1; use constant user_can_create_account => 0; -use constant is_automatic => 0; -use constant extern_id_used => 0; +use constant extern_id_used => 0; sub new { - my ($class) = @_; - my $self = fields::new($class); - return $self; + my ($class) = @_; + my $self = fields::new($class); + return $self; } 1; @@ -118,14 +117,6 @@ got from this login method. Defaults to C. Whether or not users can create accounts, if this login method is currently being used by the system. Defaults to C. -=item C - -True if this login method requires no interaction from the user within -Bugzilla. (For example, C auth is "automatic" because the webserver -just passes us an environment variable on most page requests, and does not -ask the user for authentication information directly in Bugzilla.) Defaults -to C. - =item C Whether or not this login method uses the extern_id field. If diff --git a/Bugzilla/Auth/Login/APIKey.pm b/Bugzilla/Auth/Login/APIKey.pm index 63e35578a..a66db13f1 100644 --- a/Bugzilla/Auth/Login/APIKey.pm +++ b/Bugzilla/Auth/Login/APIKey.pm @@ -26,28 +26,36 @@ use constant can_logout => 0; # This method is only available to web services. An API key can never # be used to authenticate a Web request. sub get_login_info { - my ($self) = @_; - my $params = Bugzilla->input_params; - my ($user_id, $login_cookie); + my ($self) = @_; + my $params = Bugzilla->input_params; + my ($user_id, $login_cookie); - my $api_key_text = trim(delete $params->{'Bugzilla_api_key'}); - if (!i_am_webservice() || !$api_key_text) { - return { failure => AUTH_NODATA }; - } + my $api_key_text = trim(delete $params->{'Bugzilla_api_key'}); + if (!i_am_webservice() || !$api_key_text) { + return {failure => AUTH_NODATA}; + } - my $api_key = Bugzilla::User::APIKey->new({ name => $api_key_text }); + my $crypted_key + = bz_crypt($api_key_text, Bugzilla->localconfig->{'site_wide_secret'}, + PASSWORD_DIGEST_ALGORITHM); - if (!$api_key or $api_key->api_key ne $api_key_text) { - # The second part checks the correct capitalisation. Silly MySQL - ThrowUserError("api_key_not_valid"); - } - elsif ($api_key->revoked) { - ThrowUserError('api_key_revoked'); - } + my $api_key = Bugzilla::User::APIKey->new({name => $crypted_key}); - $api_key->update_last_used(); + if (!$api_key or $api_key->api_key ne $crypted_key) { - return { user_id => $api_key->user_id }; + # The second part checks the correct capitalisation. Silly MySQL + ThrowUserError("api_key_not_valid"); + } + elsif ($api_key->banned) { + ThrowUserError('api_key_banned'); + } + elsif ($api_key->revoked) { + ThrowUserError('api_key_revoked'); + } + + $api_key->update_last_used(); + + return {user_id => $api_key->user_id}; } 1; diff --git a/Bugzilla/Auth/Login/CGI.pm b/Bugzilla/Auth/Login/CGI.pm index 6003d62a5..bdde9d8e2 100644 --- a/Bugzilla/Auth/Login/CGI.pm +++ b/Bugzilla/Auth/Login/CGI.pm @@ -21,65 +21,74 @@ use Bugzilla::Error; use Bugzilla::Token; sub get_login_info { - my ($self) = @_; - my $params = Bugzilla->input_params; - my $cgi = Bugzilla->cgi; - - my $login = trim(delete $params->{'Bugzilla_login'}); - my $password = delete $params->{'Bugzilla_password'}; - # The token must match the cookie to authenticate the request. - my $login_token = delete $params->{'Bugzilla_login_token'}; - my $login_cookie = $cgi->cookie('Bugzilla_login_request_cookie'); - - my $valid = 0; - # If the web browser accepts cookies, use them. - if ($login_token && $login_cookie) { - my ($time, undef) = split(/-/, $login_token); - # Regenerate the token based on the information we have. - my $expected_token = issue_hash_token(['login_request', $login_cookie], $time); - $valid = 1 if $expected_token eq $login_token; - $cgi->remove_cookie('Bugzilla_login_request_cookie'); - } - # WebServices and other local scripts can bypass this check. - # This is safe because we won't store a login cookie in this case. - elsif (Bugzilla->usage_mode != USAGE_MODE_BROWSER) { - $valid = 1; - } - # Else falls back to the Referer header and accept local URLs. - # Attachments are served from a separate host (ideally), and so - # an evil attachment cannot abuse this check with a redirect. - elsif (my $referer = $cgi->referer) { - my $urlbase = correct_urlbase(); - $valid = 1 if $referer =~ /^\Q$urlbase\E/; - } - # If the web browser doesn't accept cookies and the Referer header - # is missing, we have no way to make sure that the authentication - # request comes from the user. - elsif ($login && $password) { - ThrowUserError('auth_untrusted_request', { login => $login }); - } - - if (!defined($login) || !defined($password) || !$valid) { - return { failure => AUTH_NODATA }; - } - - return { username => $login, password => $password }; + my ($self) = @_; + my $params = Bugzilla->input_params; + my $cgi = Bugzilla->cgi; + + my $login = trim(delete $params->{'Bugzilla_login'}); + my $password = delete $params->{'Bugzilla_password'}; + + # The token must match the cookie to authenticate the request. + my $login_token = delete $params->{'Bugzilla_login_token'}; + my $login_cookie = $cgi->cookie('Bugzilla_login_request_cookie'); + + my $valid = 0; + + # If the web browser accepts cookies, use them. + if ($login_token && $login_cookie) { + my ($time, undef) = split(/-/, $login_token); + + # Regenerate the token based on the information we have. + my $expected_token = issue_hash_token(['login_request', $login_cookie], $time); + $valid = 1 if $expected_token eq $login_token; + $cgi->remove_cookie('Bugzilla_login_request_cookie'); + } + + # WebServices and other local scripts can bypass this check. + # This is safe because we won't store a login cookie in this case. + elsif (Bugzilla->usage_mode != USAGE_MODE_BROWSER) { + $valid = 1; + } + + # Else falls back to the Referer header and accept local URLs. + # Attachments are served from a separate host (ideally), and so + # an evil attachment cannot abuse this check with a redirect. + elsif (my $referer = $cgi->referer) { + my $urlbase = correct_urlbase(); + ## RED HAT EXTENSION chop off trailing dirs as they are never in referrer + $urlbase =~ s{([^\/])/[^\/].*$}{$1}; + $urlbase =~ s{/$}{}; + $valid = 1 if $referer =~ /^\Q$urlbase\E/; + } + + # If the web browser doesn't accept cookies and the Referer header + # is missing, we have no way to make sure that the authentication + # request comes from the user. + elsif ($login && $password) { + ThrowUserError('auth_untrusted_request', {login => $login}); + } + + if (!defined($login) || !defined($password) || !$valid) { + return {failure => AUTH_NODATA}; + } + + return {username => $login, password => $password}; } 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; + 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/Bugzilla/Auth/Login/Cookie.pm b/Bugzilla/Auth/Login/Cookie.pm index c09f08d24..5526a8c28 100644 --- a/Bugzilla/Auth/Login/Cookie.pm +++ b/Bugzilla/Auth/Login/Cookie.pm @@ -12,7 +12,7 @@ use strict; use warnings; use base qw(Bugzilla::Auth::Login); -use fields qw(_login_token); +use fields qw(_login_token _cookie); use Bugzilla::Constants; use Bugzilla::Error; @@ -23,121 +23,140 @@ use List::Util qw(first); use constant requires_persistence => 0; use constant requires_verification => 0; -use constant can_login => 0; - -sub is_automatic { return $_[0]->login_token ? 0 : 1; } +use constant can_login => 0; # Note that Cookie never consults the Verifier, it always assumes # it has a valid DB account or it fails. sub get_login_info { - my ($self) = @_; - my $cgi = Bugzilla->cgi; - my $dbh = Bugzilla->dbh; - my ($user_id, $login_cookie); - - if (!Bugzilla->request_cache->{auth_no_automatic_login}) { - $login_cookie = $cgi->cookie("Bugzilla_logincookie"); - $user_id = $cgi->cookie("Bugzilla_login"); - - # If cookies cannot be found, this could mean that they haven't - # been made available yet. In this case, look at Bugzilla_cookie_list. - unless ($login_cookie) { - my $cookie = first {$_->name eq 'Bugzilla_logincookie'} - @{$cgi->{'Bugzilla_cookie_list'}}; - $login_cookie = $cookie->value if $cookie; - } - unless ($user_id) { - my $cookie = first {$_->name eq 'Bugzilla_login'} - @{$cgi->{'Bugzilla_cookie_list'}}; - $user_id = $cookie->value if $cookie; - } - - # If the call is for a web service, and an api token is provided, check - # it is valid. - if (i_am_webservice() && Bugzilla->input_params->{Bugzilla_api_token}) { - my $api_token = Bugzilla->input_params->{Bugzilla_api_token}; - my ($token_user_id, undef, undef, $token_type) - = Bugzilla::Token::GetTokenData($api_token); - if (!defined $token_type - || $token_type ne 'api_token' - || $user_id != $token_user_id) - { - ThrowUserError('auth_invalid_token', { token => $api_token }); - } - } + my ($self) = @_; + my $cgi = Bugzilla->cgi; + my $dbh = Bugzilla->dbh; + my ($user_id, $login_cookie); + + my $is_webservice = i_am_webservice(); + my $is_internal = $cgi->http('X-Bugzilla-Internal'); + my $origin = $cgi->http('Origin'); + my $urlbase = correct_urlbase(); + $urlbase =~ s{([^\/])/[^\/].*$}{$1}; + $urlbase =~ s{/$}{}; + + my $can_use_cookies + = !$is_webservice + || defined($is_internal) + && (!defined($origin) || $origin eq $urlbase); + + if ($can_use_cookies) { + $login_cookie = $cgi->cookie("Bugzilla_logincookie"); + $user_id = $cgi->cookie("Bugzilla_login"); + + # If cookies cannot be found, this could mean that they haven't + # been made available yet. In this case, look at Bugzilla_cookie_list. + unless ($login_cookie) { + my $cookie = first { $_->name eq 'Bugzilla_logincookie' } + @{$cgi->{'Bugzilla_cookie_list'}}; + $login_cookie = $cookie->value if $cookie; + } + unless ($user_id) { + my $cookie = first { $_->name eq 'Bugzilla_login' } + @{$cgi->{'Bugzilla_cookie_list'}}; + $user_id = $cookie->value if $cookie; } + trick_taint($login_cookie) if $login_cookie; + $self->cookie($login_cookie); + Bugzilla->request_cache->{auth_did_use_cookie} = 1; + } + elsif ($is_webservice) { # If no cookies were provided, we also look for a login token # passed in the parameters of a webservice my $token = $self->login_token; if ($token && (!$login_cookie || !$user_id)) { - ($user_id, $login_cookie) = ($token->{'user_id'}, $token->{'login_token'}); + ($user_id, $login_cookie) = ($token->{'user_id'}, $token->{'login_token'}); } + } + + my $ip_addr = remote_ip(); + + if ($login_cookie && $user_id) { - my $ip_addr = remote_ip(); + # Anything goes for these params - they're just strings which + # we're going to verify against the db + trick_taint($ip_addr); + trick_taint($login_cookie); + detaint_natural($user_id); - if ($login_cookie && $user_id) { - # Anything goes for these params - they're just strings which - # we're going to verify against the db - trick_taint($ip_addr); - trick_taint($login_cookie); - detaint_natural($user_id); + ## REDHAT EXTENSION START 1182815 1235135 + $dbh->bz_start_transaction(1); + ## REDHAT EXTENSION END 1182815 1235135 - my $db_cookie = - $dbh->selectrow_array('SELECT cookie + my $db_cookie = $dbh->selectrow_array( + 'SELECT cookie FROM logincookies WHERE cookie = ? AND userid = ? - AND (ipaddr = ? OR ipaddr IS NULL)', - undef, ($login_cookie, $user_id, $ip_addr)); - - # If the cookie or token is valid, return a valid username. - # If they were not valid and we are using a webservice, then - # throw an error notifying the client. - if (defined $db_cookie && $login_cookie eq $db_cookie) { - # If we logged in successfully, then update the lastused - # time on the login cookie - $dbh->do("UPDATE logincookies SET lastused = NOW() - WHERE cookie = ?", undef, $login_cookie); - return { user_id => $user_id }; - } - elsif (i_am_webservice()) { - ThrowUserError('invalid_cookies_or_token'); - } + AND (ipaddr = ? OR ipaddr IS NULL)', undef, + ($login_cookie, $user_id, $ip_addr) + ); + + # If the cookie is valid, return a valid username. + if (defined $db_cookie && $login_cookie eq $db_cookie) { + + # If we logged in successfully, then update the lastused + # time on the login cookie + $dbh->do( + "UPDATE logincookies SET lastused = NOW() + WHERE cookie = ?", undef, $login_cookie + ); + + ## REDHAT EXTENSION START 1182815 + $dbh->bz_commit_transaction(); + ## REDHAT EXTENSION END 1182815 + + return {user_id => $user_id}; } - # Either the cookie or token is invalid and we are not authenticating - # via a webservice, or we did not receive a cookie or token. We don't - # want to ever return AUTH_LOGINFAILED, because we don't want Bugzilla to - # actually throw an error when it gets a bad cookie or token. It should just - # look like there was no cookie or token to begin with. - return { failure => AUTH_NODATA }; + ## REDHAT EXTENSION START 1182815 + $dbh->bz_rollback_transaction(); + ## REDHAT EXTENSION END 1182815 + } + + # Either the cookie or token is invalid and we are not authenticating + # via a webservice, or we did not receive a cookie or token. We don't + # want to ever return AUTH_LOGINFAILED, because we don't want Bugzilla to + # actually throw an error when it gets a bad cookie or token. It should just + # look like there was no cookie or token to begin with. + return {failure => AUTH_NODATA}; } sub login_token { - my ($self) = @_; - my $input = Bugzilla->input_params; - my $usage_mode = Bugzilla->usage_mode; + my ($self) = @_; + my $input = Bugzilla->input_params; + my $usage_mode = Bugzilla->usage_mode; - return $self->{'_login_token'} if exists $self->{'_login_token'}; + return $self->{'_login_token'} if exists $self->{'_login_token'}; - if (!i_am_webservice()) { - return $self->{'_login_token'} = undef; - } + if (!i_am_webservice()) { + return $self->{'_login_token'} = undef; + } - # Check if a token was passed in via requests for WebServices - my $token = trim(delete $input->{'Bugzilla_token'}); - return $self->{'_login_token'} = undef if !$token; + # Check if a token was passed in via requests for WebServices + my $token = trim(delete $input->{'Bugzilla_token'}); + return $self->{'_login_token'} = undef if !$token; - my ($user_id, $login_token) = split('-', $token, 2); - if (!detaint_natural($user_id) || !$login_token) { - return $self->{'_login_token'} = undef; - } + my ($user_id, $login_token) = split('-', $token, 2); + if (!detaint_natural($user_id) || !$login_token) { + return $self->{'_login_token'} = undef; + } + + return $self->{'_login_token'} + = {user_id => $user_id, login_token => $login_token}; +} + +sub cookie { + my ($self, $val) = @_; + $self->{_cookie} = $val if @_ > 1; - return $self->{'_login_token'} = { - user_id => $user_id, - login_token => $login_token - }; + return $self->{_cookie}; } 1; diff --git a/Bugzilla/Auth/Login/Env.pm b/Bugzilla/Auth/Login/Env.pm index 653df2bb3..c904d5a21 100644 --- a/Bugzilla/Auth/Login/Env.pm +++ b/Bugzilla/Auth/Login/Env.pm @@ -16,28 +16,30 @@ use parent qw(Bugzilla::Auth::Login); use Bugzilla::Constants; use Bugzilla::Error; -use constant can_logout => 0; -use constant can_login => 0; +use constant can_logout => 0; +use constant can_login => 0; use constant requires_persistence => 0; use constant requires_verification => 0; -use constant is_automatic => 1; -use constant extern_id_used => 1; +use constant extern_id_used => 1; sub get_login_info { - my ($self) = @_; + my ($self) = @_; - my $env_id = $ENV{Bugzilla->params->{"auth_env_id"}} || ''; - my $env_email = $ENV{Bugzilla->params->{"auth_env_email"}} || ''; - my $env_realname = $ENV{Bugzilla->params->{"auth_env_realname"}} || ''; + my $env_id = $ENV{Bugzilla->params->{"auth_env_id"}} || ''; + my $env_email = $ENV{Bugzilla->params->{"auth_env_email"}} || ''; + my $env_realname = $ENV{Bugzilla->params->{"auth_env_realname"}} || ''; - return { failure => AUTH_NODATA } if !$env_email; + return {failure => AUTH_NODATA} if !$env_email; - return { username => $env_email, extern_id => $env_id, - realname => $env_realname }; + return { + username => $env_email, + extern_id => $env_id, + realname => $env_realname + }; } sub fail_nodata { - ThrowCodeError('env_no_email'); + ThrowCodeError('env_no_email'); } 1; diff --git a/Bugzilla/Auth/Login/Stack.pm b/Bugzilla/Auth/Login/Stack.pm index dc35998e4..69b57dec8 100644 --- a/Bugzilla/Auth/Login/Stack.pm +++ b/Bugzilla/Auth/Login/Stack.pm @@ -13,8 +13,8 @@ use warnings; use base qw(Bugzilla::Auth::Login); use fields qw( - _stack - successful + _stack + successful ); use Hash::Util qw(lock_keys); use Bugzilla::Hook; @@ -22,81 +22,81 @@ use Bugzilla::Constants; use List::MoreUtils qw(any); sub new { - my $class = shift; - my $self = $class->SUPER::new(@_); - my $list = shift; - my %methods = map { $_ => "Bugzilla/Auth/Login/$_.pm" } split(',', $list); - lock_keys(%methods); - Bugzilla::Hook::process('auth_login_methods', { modules => \%methods }); - - $self->{_stack} = []; - foreach my $login_method (split(',', $list)) { - my $module = $methods{$login_method}; - require $module; - $module =~ s|/|::|g; - $module =~ s/.pm$//; - push(@{$self->{_stack}}, $module->new(@_)); - } - return $self; + my $class = shift; + my $self = $class->SUPER::new(@_); + my $list = shift; + my %methods = map { $_ => "Bugzilla/Auth/Login/$_.pm" } split(',', $list); + lock_keys(%methods); + Bugzilla::Hook::process('auth_login_methods', {modules => \%methods}); + + $self->{_stack} = []; + foreach my $login_method (split(',', $list)) { + my $module = $methods{$login_method}; + require $module; + $module =~ s|/|::|g; + $module =~ s/.pm$//; + push(@{$self->{_stack}}, $module->new(@_)); + } + return $self; } sub get_login_info { - my $self = shift; - my $result; - foreach my $object (@{$self->{_stack}}) { - # See Bugzilla::WebService::Server::JSONRPC for where and why - # auth_no_automatic_login is used. - if (Bugzilla->request_cache->{auth_no_automatic_login}) { - next if $object->is_automatic; - } - $result = $object->get_login_info(@_); - $self->{successful} = $object; - - # We only carry on down the stack if this method denied all knowledge. - last unless ($result->{failure} - && ($result->{failure} eq AUTH_NODATA - || $result->{failure} eq AUTH_NO_SUCH_USER)); - - # If none of the methods succeed, it's undef. - $self->{successful} = undef; - } - return $result; + my $self = shift; + my $result; + foreach my $object (@{$self->{_stack}}) { + $result = $object->get_login_info(@_); + $self->{successful} = $object; + + # We only carry on down the stack if this method denied all knowledge. + last + unless ($result->{failure} + && ( $result->{failure} eq AUTH_NODATA + || $result->{failure} eq AUTH_NO_SUCH_USER)); + + # If none of the methods succeed, it's undef. + $self->{successful} = undef; + } + return $result; } sub fail_nodata { - my $self = shift; - # We fail from the bottom of the stack. - my @reverse_stack = reverse @{$self->{_stack}}; - foreach my $object (@reverse_stack) { - # We pick the first object that actually has the method - # implemented. - if ($object->can('fail_nodata')) { - $object->fail_nodata(@_); - } + my $self = shift; + + # We fail from the bottom of the stack. + my @reverse_stack = reverse @{$self->{_stack}}; + foreach my $object (@reverse_stack) { + + # We pick the first object that actually has the method + # implemented. + if ($object->can('fail_nodata')) { + $object->fail_nodata(@_); } + } } sub can_login { - my ($self) = @_; - # We return true if any method can log in. - foreach my $object (@{$self->{_stack}}) { - return 1 if $object->can_login; - } - return 0; + my ($self) = @_; + + # We return true if any method can log in. + foreach my $object (@{$self->{_stack}}) { + return 1 if $object->can_login; + } + return 0; } sub user_can_create_account { - my ($self) = @_; - # We return true if any method allows users to create accounts. - foreach my $object (@{$self->{_stack}}) { - return 1 if $object->user_can_create_account; - } - return 0; + my ($self) = @_; + + # We return true if any method allows users to create accounts. + foreach my $object (@{$self->{_stack}}) { + return 1 if $object->user_can_create_account; + } + return 0; } sub extern_id_used { - my ($self) = @_; - return any { $_->extern_id_used } @{ $self->{_stack} }; + my ($self) = @_; + return any { $_->extern_id_used } @{$self->{_stack}}; } 1; diff --git a/Bugzilla/Auth/Persist/Cookie.pm b/Bugzilla/Auth/Persist/Cookie.pm index 2d1291f3b..63cbbda4b 100644 --- a/Bugzilla/Auth/Persist/Cookie.pm +++ b/Bugzilla/Auth/Persist/Cookie.pm @@ -16,149 +16,166 @@ use fields qw(); use Bugzilla::Constants; use Bugzilla::Util; use Bugzilla::Token; +use Bugzilla::Hook; use List::Util qw(first); sub new { - my ($class) = @_; - my $self = fields::new($class); - return $self; + my ($class) = @_; + my $self = fields::new($class); + return $self; } sub persist_login { - my ($self, $user) = @_; - my $dbh = Bugzilla->dbh; - my $cgi = Bugzilla->cgi; - my $input_params = Bugzilla->input_params; - - my $ip_addr; - if ($input_params->{'Bugzilla_restrictlogin'}) { - $ip_addr = remote_ip(); - # The IP address is valid, at least for comparing with itself in a - # subsequent login - trick_taint($ip_addr); - } - - $dbh->bz_start_transaction(); - - my $login_cookie = - Bugzilla::Token::GenerateUniqueToken('logincookies', 'cookie'); - - $dbh->do("INSERT INTO logincookies (cookie, userid, ipaddr, lastused) - VALUES (?, ?, ?, NOW())", - undef, $login_cookie, $user->id, $ip_addr); - - # Issuing a new cookie is a good time to clean up the old - # cookies. - $dbh->do("DELETE FROM logincookies WHERE lastused < " - . $dbh->sql_date_math('LOCALTIMESTAMP(0)', '-', - MAX_LOGINCOOKIE_AGE, 'DAY')); - - $dbh->bz_commit_transaction(); - - # We do not want WebServices to generate login cookies. - # All we need is the login token for User.login. - return $login_cookie if i_am_webservice(); - - # Prevent JavaScript from accessing login cookies. - my %cookieargs = ('-httponly' => 1); - - # Remember cookie only if admin has told so - # or admin didn't forbid it and user told to remember. - if ( Bugzilla->params->{'rememberlogin'} eq 'on' || - (Bugzilla->params->{'rememberlogin'} ne 'off' && - $input_params->{'Bugzilla_remember'} && - $input_params->{'Bugzilla_remember'} eq 'on') ) - { - # Not a session cookie, so set an infinite expiry - $cookieargs{'-expires'} = 'Fri, 01-Jan-2038 00:00:00 GMT'; - } - if (Bugzilla->params->{'ssl_redirect'}) { - # Make these cookies only be sent to us by the browser during - # HTTPS sessions, if we're using SSL. - $cookieargs{'-secure'} = 1; - } - - $cgi->send_cookie(-name => 'Bugzilla_login', - -value => $user->id, - %cookieargs); - $cgi->send_cookie(-name => 'Bugzilla_logincookie', - -value => $login_cookie, - %cookieargs); + my ($self, $user) = @_; + my $dbh = Bugzilla->dbh; + my $cgi = Bugzilla->cgi; + my $input_params = Bugzilla->input_params; + + my $ip_addr; + if ($input_params->{'Bugzilla_restrictlogin'}) { + $ip_addr = remote_ip(); + + # The IP address is valid, at least for comparing with itself in a + # subsequent login + trick_taint($ip_addr); + } + + $dbh->bz_start_transaction(1); + + my $login_cookie + = Bugzilla::Token::GenerateUniqueToken('logincookies', 'cookie'); + + $dbh->do( + "INSERT INTO logincookies (cookie, userid, ipaddr, lastused) + VALUES (?, ?, ?, NOW())", undef, $login_cookie, $user->id, $ip_addr + ); + + # Issuing a new cookie is a good time to clean up the old + # cookies. + $dbh->do("DELETE FROM logincookies WHERE lastused < " + . $dbh->sql_date_math('LOCALTIMESTAMP(0)', '-', MAX_LOGINCOOKIE_AGE, 'DAY')); + + $dbh->bz_commit_transaction(); + + # We do not want WebServices to generate login cookies. + # All we need is the login token for User.login. + return $login_cookie if i_am_webservice(); + + # Prevent JavaScript from accessing login cookies. + my %cookieargs = ('-httponly' => 1); + + # Remember cookie only if admin has told so + # or admin didn't forbid it and user told to remember. + if ( + Bugzilla->params->{'rememberlogin'} eq 'on' + || ( Bugzilla->params->{'rememberlogin'} ne 'off' + && $input_params->{'Bugzilla_remember'} + && $input_params->{'Bugzilla_remember'} eq 'on') + ) + { + # Not a session cookie, so set an infinite expiry + $cookieargs{'-expires'} = 'Fri, 01-Jan-2038 00:00:00 GMT'; + } + + ## REDHAT EXTENSION START 582503 + if ( Bugzilla->params->{'ssl_redirect'} + || Bugzilla->params->{'force_secure_cookies'}) + { + # Make these cookies only be sent to us by the browser during + # HTTPS sessions, if we're using SSL. + $cookieargs{'-secure'} = 1; + } + ## REDHAT EXTENSION END 582503 + + $cgi->send_cookie(-name => 'Bugzilla_login', -value => $user->id, %cookieargs); + $cgi->send_cookie( + -name => 'Bugzilla_logincookie', + -value => $login_cookie, + %cookieargs + ); + + Bugzilla::Hook::process('persist_login', + {cgi => $cgi, cookieargs => \%cookieargs}); } sub logout { - my ($self, $param) = @_; - - my $dbh = Bugzilla->dbh; - my $cgi = Bugzilla->cgi; - my $input = Bugzilla->input_params; - $param = {} unless $param; - my $user = $param->{user} || Bugzilla->user; - my $type = $param->{type} || LOGOUT_ALL; - - if ($type == LOGOUT_ALL) { - $dbh->do("DELETE FROM logincookies WHERE userid = ?", - undef, $user->id); - return; - } - - # The LOGOUT_*_CURRENT options require the current login cookie. - # If a new cookie has been issued during this run, that's the current one. - # If not, it's the one we've received. - my @login_cookies; - my $cookie = first {$_->name eq 'Bugzilla_logincookie'} - @{$cgi->{'Bugzilla_cookie_list'}}; - if ($cookie) { - push(@login_cookies, $cookie->value); - } - elsif ($cookie = $cgi->cookie('Bugzilla_logincookie')) { - push(@login_cookies, $cookie); - } - - # If we are a webservice using a token instead of cookie - # then add that as well to the login cookies to delete - if (my $login_token = $user->authorizer->login_token) { - push(@login_cookies, $login_token->{'login_token'}); - } - - # Make sure that @login_cookies is not empty to not break SQL statements. - push(@login_cookies, '') unless @login_cookies; - - # These queries use both the cookie ID and the user ID as keys. Even - # though we know the userid must match, we still check it in the SQL - # as a sanity check, since there is no locking here, and if the user - # logged out from two machines simultaneously, while someone else - # logged in and got the same cookie, we could be logging the other - # user out here. Yes, this is very very very unlikely, but why take - # chances? - bbaetz - map { trick_taint($_) } @login_cookies; - @login_cookies = map { $dbh->quote($_) } @login_cookies; - if ($type == LOGOUT_KEEP_CURRENT) { - $dbh->do("DELETE FROM logincookies WHERE " . - $dbh->sql_in('cookie', \@login_cookies, 1) . - " AND userid = ?", - undef, $user->id); - } elsif ($type == LOGOUT_CURRENT) { - $dbh->do("DELETE FROM logincookies WHERE " . - $dbh->sql_in('cookie', \@login_cookies) . - " AND userid = ?", - undef, $user->id); - } else { - die("Invalid type $type supplied to logout()"); - } - - if ($type != LOGOUT_KEEP_CURRENT) { - clear_browser_cookies(); - } + my ($self, $param) = @_; + + my $dbh = Bugzilla->dbh; + my $cgi = Bugzilla->cgi; + my $input = Bugzilla->input_params; + $param = {} unless $param; + my $user = $param->{user} || Bugzilla->user; + my $type = $param->{type} || LOGOUT_ALL; + + if ($type == LOGOUT_ALL) { + $dbh->do("DELETE FROM logincookies WHERE userid = ?", undef, $user->id); + return; + } + + # The LOGOUT_*_CURRENT options require the current login cookie. + # If a new cookie has been issued during this run, that's the current one. + # If not, it's the one we've received. + my @login_cookies; + my $cookie = first { $_->name eq 'Bugzilla_logincookie' } + @{$cgi->{'Bugzilla_cookie_list'}}; + if ($cookie) { + push(@login_cookies, $cookie->value); + } + elsif ($cookie = $cgi->cookie('Bugzilla_logincookie')) { + push(@login_cookies, $cookie); + } + + # If we are a webservice using a token instead of cookie + # then add that as well to the login cookies to delete + if (my $login_token = $user->authorizer->login_token) { + push(@login_cookies, $login_token->{'login_token'}); + } + + # Make sure that @login_cookies is not empty to not break SQL statements. + push(@login_cookies, '') unless @login_cookies; + + # These queries use both the cookie ID and the user ID as keys. Even + # though we know the userid must match, we still check it in the SQL + # as a sanity check, since there is no locking here, and if the user + # logged out from two machines simultaneously, while someone else + # logged in and got the same cookie, we could be logging the other + # user out here. Yes, this is very very very unlikely, but why take + # chances? - bbaetz + map { trick_taint($_) } @login_cookies; + @login_cookies = map { $dbh->quote($_) } @login_cookies; + if ($type == LOGOUT_KEEP_CURRENT) { + $dbh->do( + "DELETE FROM logincookies WHERE " + . $dbh->sql_in('cookie', \@login_cookies, 1) + . " AND userid = ?", + undef, $user->id + ); + } + elsif ($type == LOGOUT_CURRENT) { + $dbh->do( + "DELETE FROM logincookies WHERE " + . $dbh->sql_in('cookie', \@login_cookies) + . " AND userid = ?", + undef, $user->id + ); + } + else { + die("Invalid type $type supplied to logout()"); + } + + if ($type != LOGOUT_KEEP_CURRENT) { + clear_browser_cookies(); + } } sub clear_browser_cookies { - my $cgi = Bugzilla->cgi; - $cgi->remove_cookie('Bugzilla_login'); - $cgi->remove_cookie('Bugzilla_logincookie'); - $cgi->remove_cookie('sudo'); + my $cgi = Bugzilla->cgi; + $cgi->remove_cookie('Bugzilla_login'); + $cgi->remove_cookie('Bugzilla_logincookie'); + $cgi->remove_cookie('sudo'); } 1; diff --git a/Bugzilla/Auth/Verify.pm b/Bugzilla/Auth/Verify.pm index 9dc83273b..639760c2b 100644 --- a/Bugzilla/Auth/Verify.pm +++ b/Bugzilla/Auth/Verify.pm @@ -19,113 +19,127 @@ use Bugzilla::User; use Bugzilla::Util; use constant user_can_create_account => 1; -use constant extern_id_used => 0; +use constant extern_id_used => 0; sub new { - my ($class, $login_type) = @_; - my $self = fields::new($class); - return $self; + my ($class, $login_type) = @_; + my $self = fields::new($class); + return $self; } sub can_change_password { - return $_[0]->can('change_password'); + return $_[0]->can('change_password'); } sub create_or_update_user { - my ($self, $params) = @_; - my $dbh = Bugzilla->dbh; - - my $extern_id = $params->{extern_id}; - my $username = $params->{bz_username} || $params->{username}; - my $password = $params->{password} || '*'; - my $real_name = $params->{realname} || ''; - my $user_id = $params->{user_id}; - - # A passed-in user_id always overrides anything else, for determining - # what account we should return. - if (!$user_id) { - my $username_user_id = login_to_id($username || ''); - my $extern_user_id; - if ($extern_id) { - trick_taint($extern_id); - $extern_user_id = $dbh->selectrow_array('SELECT userid - FROM profiles WHERE extern_id = ?', undef, $extern_id); - } - - # If we have both a valid extern_id and a valid username, and they are - # not the same id, then we have a conflict. - if ($username_user_id && $extern_user_id - && $username_user_id ne $extern_user_id) - { - my $extern_name = Bugzilla::User->new($extern_user_id)->login; - return { failure => AUTH_ERROR, error => "extern_id_conflict", - details => {extern_id => $extern_id, - extern_user => $extern_name, - username => $username} }; - } - - # If we have a valid username, but no valid id, - # then we have to create the user. This happens when we're - # passed only a username, and that username doesn't exist already. - if ($username && !$username_user_id && !$extern_user_id) { - validate_email_syntax($username) - || return { failure => AUTH_ERROR, - error => 'auth_invalid_email', - details => {addr => $username} }; - # Usually we'd call validate_password, but external authentication - # systems might follow different standards than ours. So in this - # place here, we call trick_taint without checks. - trick_taint($password); - - # XXX Theoretically this could fail with an error, but the fix for - # that is too involved to be done right now. - my $user = Bugzilla::User->create({ - login_name => $username, - cryptpassword => $password, - realname => $real_name}); - $username_user_id = $user->id; - } - - # If we have a valid username id and an extern_id, but no valid - # extern_user_id, then we have to set the user's extern_id. - if ($extern_id && $username_user_id && !$extern_user_id) { - $dbh->do('UPDATE profiles SET extern_id = ? WHERE userid = ?', - undef, $extern_id, $username_user_id); - Bugzilla->memcached->clear({ table => 'profiles', id => $username_user_id }); - } - - # Finally, at this point, one of these will give us a valid user id. - $user_id = $extern_user_id || $username_user_id; + my ($self, $params) = @_; + my $dbh = Bugzilla->dbh; + + my $extern_id = $params->{extern_id}; + my $username = $params->{bz_username} || $params->{username}; + my $password = $params->{password} || '*'; + my $real_name = $params->{realname} || ''; + my $user_id = $params->{user_id}; + + # A passed-in user_id always overrides anything else, for determining + # what account we should return. + if (!$user_id) { + my $username_user_id = login_to_id($username || ''); + my $extern_user_id; + if ($extern_id) { + trick_taint($extern_id); + $extern_user_id = $dbh->selectrow_array( + 'SELECT userid + FROM profiles WHERE extern_id = ?', undef, $extern_id + ); } - # If we still don't have a valid user_id, then we weren't passed - # enough information in $params, and we should die right here. - ThrowCodeError('bad_arg', {argument => 'params', function => - 'Bugzilla::Auth::Verify::create_or_update_user'}) - unless $user_id; - - my $user = new Bugzilla::User($user_id); - - # Now that we have a valid User, we need to see if any data has to be updated. - my $changed = 0; + # If we have both a valid extern_id and a valid username, and they are + # not the same id, then we have a conflict. + if ( $username_user_id + && $extern_user_id + && $username_user_id ne $extern_user_id) + { + my $extern_name = Bugzilla::User->new($extern_user_id)->login; + return { + failure => AUTH_ERROR, + error => "extern_id_conflict", + details => + {extern_id => $extern_id, extern_user => $extern_name, username => $username} + }; + } - if ($username && lc($user->login) ne lc($username)) { - validate_email_syntax($username) - || return { failure => AUTH_ERROR, error => 'auth_invalid_email', - details => {addr => $username} }; - $user->set_login($username); - $changed = 1; + # If we have a valid username, but no valid id, + # then we have to create the user. This happens when we're + # passed only a username, and that username doesn't exist already. + if ($username && !$username_user_id && !$extern_user_id) { + validate_email_syntax($username) || return { + failure => AUTH_ERROR, + error => 'auth_invalid_email', + details => {addr => $username} + }; + + # Usually we'd call validate_password, but external authentication + # systems might follow different standards than ours. So in this + # place here, we call trick_taint without checks. + trick_taint($password); + + # XXX Theoretically this could fail with an error, but the fix for + # that is too involved to be done right now. + my $user + = Bugzilla::User->create({ + login_name => $username, cryptpassword => $password, realname => $real_name + }); + $username_user_id = $user->id; } - if ($real_name && $user->name ne $real_name) { - # $real_name is more than likely tainted, but we only use it - # in a placeholder and we never use it after this. - trick_taint($real_name); - $user->set_name($real_name); - $changed = 1; + + # If we have a valid username id and an extern_id, but no valid + # extern_user_id, then we have to set the user's extern_id. + if ($extern_id && $username_user_id && !$extern_user_id) { + $dbh->do('UPDATE profiles SET extern_id = ? WHERE userid = ?', + undef, $extern_id, $username_user_id); + Bugzilla->memcached->clear({table => 'profiles', id => $username_user_id}); } - $user->update() if $changed; - return { user => $user }; + # Finally, at this point, one of these will give us a valid user id. + $user_id = $extern_user_id || $username_user_id; + } + + # If we still don't have a valid user_id, then we weren't passed + # enough information in $params, and we should die right here. + ThrowCodeError( + 'bad_arg', + { + argument => 'params', + function => 'Bugzilla::Auth::Verify::create_or_update_user' + } + ) unless $user_id; + + my $user = new Bugzilla::User($user_id); + + # Now that we have a valid User, we need to see if any data has to be updated. + my $changed = 0; + + if ($username && lc($user->login) ne lc($username)) { + validate_email_syntax($username) || return { + failure => AUTH_ERROR, + error => 'auth_invalid_email', + details => {addr => $username} + }; + $user->set_login($username); + $changed = 1; + } + if ($real_name && $user->name ne $real_name) { + + # $real_name is more than likely tainted, but we only use it + # in a placeholder and we never use it after this. + trick_taint($real_name); + $user->set_name($real_name); + $changed = 1; + } + $user->update() if $changed; + + return {user => $user}; } 1; diff --git a/Bugzilla/Auth/Verify/DB.pm b/Bugzilla/Auth/Verify/DB.pm index 28a9310c9..6ed9ba15c 100644 --- a/Bugzilla/Auth/Verify/DB.pm +++ b/Bugzilla/Auth/Verify/DB.pm @@ -19,95 +19,97 @@ use Bugzilla::Util; use Bugzilla::User; sub check_credentials { - my ($self, $login_data) = @_; - my $dbh = Bugzilla->dbh; + my ($self, $login_data) = @_; + my $dbh = Bugzilla->dbh; - my $username = $login_data->{username}; - my $user = new Bugzilla::User({ name => $username }); + my $username = $login_data->{username}; + my $user = new Bugzilla::User({name => $username}); - return { failure => AUTH_NO_SUCH_USER } unless $user; + return {failure => AUTH_NO_SUCH_USER} unless $user; - $login_data->{user} = $user; - $login_data->{bz_username} = $user->login; + $login_data->{user} = $user; + $login_data->{bz_username} = $user->login; + if ($user->account_is_locked_out) { + return {failure => AUTH_LOCKOUT, user => $user}; + } + + my $password = $login_data->{password}; + my $real_password_crypted = $user->cryptpassword; + + # Using the internal crypted password as the salt, + # crypt the password the user entered. + my $entered_password_crypted = bz_crypt($password, $real_password_crypted); + + if ($entered_password_crypted ne $real_password_crypted) { + + # Record the login failure + $user->note_login_failure(); + + # Immediately check if we are locked out if ($user->account_is_locked_out) { - return { failure => AUTH_LOCKOUT, user => $user }; + return {failure => AUTH_LOCKOUT, user => $user, just_locked_out => 1}; } - my $password = $login_data->{password}; - my $real_password_crypted = $user->cryptpassword; - - # Using the internal crypted password as the salt, - # crypt the password the user entered. - my $entered_password_crypted = bz_crypt($password, $real_password_crypted); - - if ($entered_password_crypted ne $real_password_crypted) { - # Record the login failure - $user->note_login_failure(); - - # Immediately check if we are locked out - if ($user->account_is_locked_out) { - return { failure => AUTH_LOCKOUT, user => $user, - just_locked_out => 1 }; - } - - return { failure => AUTH_LOGINFAILED, - failure_count => scalar(@{ $user->account_ip_login_failures }), - }; - } - - # Force the user to change their password if it does not meet the current - # criteria. This should usually only happen if the criteria has changed. - if (Bugzilla->usage_mode == USAGE_MODE_BROWSER && - Bugzilla->params->{password_check_on_login}) - { - my $check = validate_password_check($password); - if ($check) { - return { - failure => AUTH_ERROR, - user_error => $check, - details => { locked_user => $user } - } - } + return { + failure => AUTH_LOGINFAILED, + failure_count => scalar(@{$user->account_ip_login_failures}), + }; + } + + # Force the user to change their password if it does not meet the current + # criteria. This should usually only happen if the criteria has changed. + if ( Bugzilla->usage_mode == USAGE_MODE_BROWSER + && Bugzilla->params->{password_check_on_login}) + { + my $check = validate_password_check($password); + if ($check) { + return { + failure => AUTH_ERROR, + user_error => $check, + details => {locked_user => $user} + }; } + } - # The user's credentials are okay, so delete any outstanding - # password tokens or login failures they may have generated. - Bugzilla::Token::DeletePasswordTokens($user->id, "user_logged_in"); - $user->clear_login_failures(); + # The user's credentials are okay, so delete any outstanding + # password tokens or login failures they may have generated. + Bugzilla::Token::DeletePasswordTokens($user->id, "user_logged_in"); + $user->clear_login_failures(); - my $update_password = 0; + my $update_password = 0; - # If their old password was using crypt() or some different hash - # than we're using now, convert the stored password to using - # whatever hashing system we're using now. - my $current_algorithm = PASSWORD_DIGEST_ALGORITHM; - $update_password = 1 if ($real_password_crypted !~ /{\Q$current_algorithm\E}$/); + # If their old password was using crypt() or some different hash + # than we're using now, convert the stored password to using + # whatever hashing system we're using now. + my $current_algorithm = PASSWORD_DIGEST_ALGORITHM; + $update_password = 1 if ($real_password_crypted !~ /{\Q$current_algorithm\E}$/); - # If their old password was using a different length salt than what - # we're using now, update the password to use the new salt length. - if ($real_password_crypted =~ /^([^,]+),/) { - $update_password = 1 if (length($1) != PASSWORD_SALT_LENGTH); - } + # If their old password was using a different length salt than what + # we're using now, update the password to use the new salt length. + if ($real_password_crypted =~ /^([^,]+),/) { + $update_password = 1 if (length($1) != PASSWORD_SALT_LENGTH); + } - # If needed, update the user's password. - if ($update_password) { - # We can't call $user->set_password because we don't want the password - # complexity rules to apply here. - $user->{cryptpassword} = bz_crypt($password); - $user->update(); - } + # If needed, update the user's password. + if ($update_password) { + + # We can't call $user->set_password because we don't want the password + # complexity rules to apply here. + $user->{cryptpassword} = bz_crypt($password); + $user->update(); + } - return $login_data; + return $login_data; } sub change_password { - my ($self, $user, $password) = @_; - my $dbh = Bugzilla->dbh; - my $cryptpassword = bz_crypt($password); - $dbh->do("UPDATE profiles SET cryptpassword = ? WHERE userid = ?", - undef, $cryptpassword, $user->id); - Bugzilla->memcached->clear({ table => 'profiles', id => $user->id }); + my ($self, $user, $password) = @_; + my $dbh = Bugzilla->dbh; + my $cryptpassword = bz_crypt($password); + $dbh->do("UPDATE profiles SET cryptpassword = ? WHERE userid = ?", + undef, $cryptpassword, $user->id); + Bugzilla->memcached->clear({table => 'profiles', id => $user->id}); } 1; diff --git a/Bugzilla/Auth/Verify/LDAP.pm b/Bugzilla/Auth/Verify/LDAP.pm index e37f55793..c92a38909 100644 --- a/Bugzilla/Auth/Verify/LDAP.pm +++ b/Bugzilla/Auth/Verify/LDAP.pm @@ -13,7 +13,7 @@ use warnings; use base qw(Bugzilla::Auth::Verify); use fields qw( - ldap + ldap ); use Bugzilla::Constants; @@ -28,126 +28,139 @@ use constant admin_can_create_account => 0; use constant user_can_create_account => 0; sub check_credentials { - my ($self, $params) = @_; - my $dbh = Bugzilla->dbh; - - # We need to bind anonymously to the LDAP server. This is - # because we need to get the Distinguished Name of the user trying - # to log in. Some servers (such as iPlanet) allow you to have unique - # uids spread out over a subtree of an area (such as "People"), so - # just appending the Base DN to the uid isn't sufficient to get the - # user's DN. For servers which don't work this way, there will still - # be no harm done. + my ($self, $params) = @_; + my $dbh = Bugzilla->dbh; + + # We need to bind anonymously to the LDAP server. This is + # because we need to get the Distinguished Name of the user trying + # to log in. Some servers (such as iPlanet) allow you to have unique + # uids spread out over a subtree of an area (such as "People"), so + # just appending the Base DN to the uid isn't sufficient to get the + # user's DN. For servers which don't work this way, there will still + # be no harm done. + $self->_bind_ldap_for_search(); + + # Now, we verify that the user exists, and get a LDAP Distinguished + # Name for the user. + my $username = $params->{username}; + my $dn_result + = $self->ldap->search(_bz_search_params($username), attrs => ['dn']); + return { + failure => AUTH_ERROR, + error => "ldap_search_error", + details => {errstr => $dn_result->error, username => $username} + } + if $dn_result->code; + + return {failure => AUTH_NO_SUCH_USER} if !$dn_result->count; + + my $dn = $dn_result->shift_entry->dn; + + # Check the password. + my $pw_result = $self->ldap->bind($dn, password => $params->{password}); + return {failure => AUTH_LOGINFAILED} if $pw_result->code; + + # And now we fill in the user's details. + + # First try the search as the (already bound) user in question. + my $user_entry; + my $error_string; + my $detail_result = $self->ldap->search(_bz_search_params($username)); + if ($detail_result->code) { + + # Stash away the original error, just in case + $error_string = $detail_result->error; + } + else { + $user_entry = $detail_result->shift_entry; + } + + # If that failed (either because the search failed, or returned no + # results) then try re-binding as the initial search user, but only + # if the LDAPbinddn parameter is set. + if (!$user_entry && Bugzilla->params->{"LDAPbinddn"}) { $self->_bind_ldap_for_search(); - # Now, we verify that the user exists, and get a LDAP Distinguished - # Name for the user. - my $username = $params->{username}; - my $dn_result = $self->ldap->search(_bz_search_params($username), - attrs => ['dn']); - return { failure => AUTH_ERROR, error => "ldap_search_error", - details => {errstr => $dn_result->error, username => $username} - } if $dn_result->code; - - return { failure => AUTH_NO_SUCH_USER } if !$dn_result->count; - - my $dn = $dn_result->shift_entry->dn; - - # Check the password. - my $pw_result = $self->ldap->bind($dn, password => $params->{password}); - return { failure => AUTH_LOGINFAILED } if $pw_result->code; - - # And now we fill in the user's details. - - # First try the search as the (already bound) user in question. - my $user_entry; - my $error_string; - my $detail_result = $self->ldap->search(_bz_search_params($username)); - if ($detail_result->code) { - # Stash away the original error, just in case - $error_string = $detail_result->error; - } else { - $user_entry = $detail_result->shift_entry; + $detail_result = $self->ldap->search(_bz_search_params($username)); + if (!$detail_result->code) { + $user_entry = $detail_result->shift_entry; } + } - # If that failed (either because the search failed, or returned no - # results) then try re-binding as the initial search user, but only - # if the LDAPbinddn parameter is set. - if (!$user_entry && Bugzilla->params->{"LDAPbinddn"}) { - $self->_bind_ldap_for_search(); - - $detail_result = $self->ldap->search(_bz_search_params($username)); - if (!$detail_result->code) { - $user_entry = $detail_result->shift_entry; - } + # If we *still* don't have anything in $user_entry then give up. + return { + failure => AUTH_ERROR, + error => "ldap_search_error", + details => {errstr => $error_string, username => $username} } + if !$user_entry; - # If we *still* don't have anything in $user_entry then give up. - return { failure => AUTH_ERROR, error => "ldap_search_error", - details => {errstr => $error_string, username => $username} - } if !$user_entry; + my $mail_attr = Bugzilla->params->{"LDAPmailattribute"}; + if ($mail_attr) { + if (!$user_entry->exists($mail_attr)) { + return { + failure => AUTH_ERROR, + error => "ldap_cannot_retreive_attr", + details => {attr => $mail_attr} + }; + } - my $mail_attr = Bugzilla->params->{"LDAPmailattribute"}; - if ($mail_attr) { - if (!$user_entry->exists($mail_attr)) { - return { failure => AUTH_ERROR, - error => "ldap_cannot_retreive_attr", - details => {attr => $mail_attr} }; - } + my @emails = $user_entry->get_value($mail_attr); - my @emails = $user_entry->get_value($mail_attr); + # Default to the first email address returned. + $params->{bz_username} = $emails[0]; - # Default to the first email address returned. - $params->{bz_username} = $emails[0]; + if (@emails > 1) { - if (@emails > 1) { - # Cycle through the adresses and check if they're Bugzilla logins. - # Use the first one that returns a valid id. - foreach my $email (@emails) { - if ( login_to_id($email) ) { - $params->{bz_username} = $email; - last; - } - } + # Cycle through the adresses and check if they're Bugzilla logins. + # Use the first one that returns a valid id. + foreach my $email (@emails) { + if (login_to_id($email)) { + $params->{bz_username} = $email; + last; } - - } else { - $params->{bz_username} = $username; + } } - $params->{realname} ||= $user_entry->get_value("displayName"); - $params->{realname} ||= $user_entry->get_value("cn"); + } + else { + $params->{bz_username} = $username; + } + + $params->{realname} ||= $user_entry->get_value("displayName"); + $params->{realname} ||= $user_entry->get_value("cn"); - $params->{extern_id} = $username; + $params->{extern_id} = $username; - return $params; + return $params; } sub _bz_search_params { - my ($username) = @_; - $username = escape_filter_value($username); - return (base => Bugzilla->params->{"LDAPBaseDN"}, - scope => "sub", - filter => '(&(' . Bugzilla->params->{"LDAPuidattribute"} - . "=$username)" - . Bugzilla->params->{"LDAPfilter"} . ')'); + my ($username) = @_; + $username = escape_filter_value($username); + return ( + base => Bugzilla->params->{"LDAPBaseDN"}, + scope => "sub", + filter => '(&(' + . Bugzilla->params->{"LDAPuidattribute"} + . "=$username)" + . Bugzilla->params->{"LDAPfilter"} . ')' + ); } sub _bind_ldap_for_search { - my ($self) = @_; - my $bind_result; - if (Bugzilla->params->{"LDAPbinddn"}) { - my ($LDAPbinddn,$LDAPbindpass) = - split(":",Bugzilla->params->{"LDAPbinddn"}); - $bind_result = - $self->ldap->bind($LDAPbinddn, password => $LDAPbindpass); - } - else { - $bind_result = $self->ldap->bind(); - } - ThrowCodeError("ldap_bind_failed", {errstr => $bind_result->error}) - if $bind_result->code; + my ($self) = @_; + my $bind_result; + if (Bugzilla->params->{"LDAPbinddn"}) { + my ($LDAPbinddn, $LDAPbindpass) = split(":", Bugzilla->params->{"LDAPbinddn"}); + $bind_result = $self->ldap->bind($LDAPbinddn, password => $LDAPbindpass); + } + else { + $bind_result = $self->ldap->bind(); + } + ThrowCodeError("ldap_bind_failed", {errstr => $bind_result->error}) + if $bind_result->code; } # We can't just do this in new(), because we're not allowed to throw any @@ -156,27 +169,27 @@ sub _bind_ldap_for_search { # to fix their mistake. (Because Bugzilla->login always calls # Bugzilla::Auth->new, and almost every page calls Bugzilla->login.) sub ldap { - my ($self) = @_; - return $self->{ldap} if $self->{ldap}; - - my @servers = split(/[\s,]+/, Bugzilla->params->{"LDAPserver"}); - ThrowCodeError("ldap_server_not_defined") unless @servers; - - foreach (@servers) { - $self->{ldap} = new Net::LDAP(trim($_)); - last if $self->{ldap}; - } - ThrowCodeError("ldap_connect_failed", { server => join(", ", @servers) }) - unless $self->{ldap}; - - # try to start TLS if needed - if (Bugzilla->params->{"LDAPstarttls"}) { - my $mesg = $self->{ldap}->start_tls(); - ThrowCodeError("ldap_start_tls_failed", { error => $mesg->error() }) - if $mesg->code(); - } - - return $self->{ldap}; + my ($self) = @_; + return $self->{ldap} if $self->{ldap}; + + my @servers = split(/[\s,]+/, Bugzilla->params->{"LDAPserver"}); + ThrowCodeError("ldap_server_not_defined") unless @servers; + + foreach (@servers) { + $self->{ldap} = new Net::LDAP(trim($_)); + last if $self->{ldap}; + } + ThrowCodeError("ldap_connect_failed", {server => join(", ", @servers)}) + unless $self->{ldap}; + + # try to start TLS if needed + if (Bugzilla->params->{"LDAPstarttls"}) { + my $mesg = $self->{ldap}->start_tls(); + ThrowCodeError("ldap_start_tls_failed", {error => $mesg->error()}) + if $mesg->code(); + } + + return $self->{ldap}; } 1; diff --git a/Bugzilla/Auth/Verify/RADIUS.pm b/Bugzilla/Auth/Verify/RADIUS.pm index 283d9b466..2cbde0404 100644 --- a/Bugzilla/Auth/Verify/RADIUS.pm +++ b/Bugzilla/Auth/Verify/RADIUS.pm @@ -23,33 +23,37 @@ use constant admin_can_create_account => 0; use constant user_can_create_account => 0; sub check_credentials { - my ($self, $params) = @_; - my $dbh = Bugzilla->dbh; - my $address_suffix = Bugzilla->params->{'RADIUS_email_suffix'}; - my $username = $params->{username}; - - # If we're using RADIUS_email_suffix, we may need to cut it off from - # the login name. - if ($address_suffix) { - $username =~ s/\Q$address_suffix\E$//i; - } - - # Create RADIUS object. - my $radius = - new Authen::Radius(Host => Bugzilla->params->{'RADIUS_server'}, - Secret => Bugzilla->params->{'RADIUS_secret'}) - || return { failure => AUTH_ERROR, error => 'radius_preparation_error', - details => {errstr => Authen::Radius::strerror() } }; - - # Check the password. - $radius->check_pwd($username, $params->{password}, - Bugzilla->params->{'RADIUS_NAS_IP'} || undef) - || return { failure => AUTH_LOGINFAILED }; - - # Build the user account's e-mail address. - $params->{bz_username} = $username . $address_suffix; - - return $params; + my ($self, $params) = @_; + my $dbh = Bugzilla->dbh; + my $address_suffix = Bugzilla->params->{'RADIUS_email_suffix'}; + my $username = $params->{username}; + + # If we're using RADIUS_email_suffix, we may need to cut it off from + # the login name. + if ($address_suffix) { + $username =~ s/\Q$address_suffix\E$//i; + } + + # Create RADIUS object. + my $radius = new Authen::Radius( + Host => Bugzilla->params->{'RADIUS_server'}, + Secret => Bugzilla->params->{'RADIUS_secret'} + ) + || return { + failure => AUTH_ERROR, + error => 'radius_preparation_error', + details => {errstr => Authen::Radius::strerror()} + }; + + # Check the password. + $radius->check_pwd($username, $params->{password}, + Bugzilla->params->{'RADIUS_NAS_IP'} || undef) + || return {failure => AUTH_LOGINFAILED}; + + # Build the user account's e-mail address. + $params->{bz_username} = $username . $address_suffix; + + return $params; } 1; diff --git a/Bugzilla/Auth/Verify/RedHat.pm b/Bugzilla/Auth/Verify/RedHat.pm new file mode 100644 index 000000000..fd0e84313 --- /dev/null +++ b/Bugzilla/Auth/Verify/RedHat.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::Auth::Verify::RedHat; + +use 5.10.1; +use strict; +use warnings; + +use parent qw(Bugzilla::Auth::Verify); + +use Bugzilla::Constants; +use Bugzilla::Error; +use Bugzilla::Util; +use Bugzilla::Constants; +use Bugzilla::Token; +use Bugzilla::Util; +use Bugzilla::User; +use Authen::Radius; + +use List::Util qw(any); +use Data::Dumper; + +# Bug 1262651 - Radius authentication for admin users. +# +# The purpose of this is to force users with administrative permissions to log +# in using RADIUS (pin+otp) authentication. +# +# This isn't enforced on RPC logins due to practical concerns. This should be +# reviewed when bugzilla auth tokens are introduced. + +sub check_credentials { + my ($self, $params) = @_; + + my $username = $params->{username}; + my $user = Bugzilla::User->new({name => $username}); + + return {failure => AUTH_NO_SUCH_USER} unless $user; + + my $group_str = Bugzilla->params->{'rh_radius_groups'} // ''; # List of groups that require RADIUS auth. + my @groups = split(/,/, $group_str); + my $in_sensitive_group = any { $user->in_group($_) } @groups; + + if ( + (Bugzilla->params->{'RADIUS_secret'} ne '') # If we are using radius + && (Bugzilla->usage_mode == USAGE_MODE_BROWSER) # and is a web interface login attempt + && $in_sensitive_group + ) # and they are a group that requires RADIUS auth + { + ## RED HAT EXTENSION 1657869 + # Then we need to enforce RADIUS authentication + my $res = $self->check_credentials_radius($params); + + # If raidus login failed to validate then check local passwd + if ($res->{failure} && $res->{failure} == AUTH_RH_RADIUS_LOGINFAILED) { + my $res2 = $self->check_credentials_password($params); + + # If local login failed bail with local error + return ($res2) if ($res2->{failure}); + } + + return ($res); + } + + # Otherwise, use password auth + return $self->check_credentials_password($params); +} + +sub check_credentials_radius { + my ($self, $params) = @_; + my $dbh = Bugzilla->dbh; + my $address_suffix = Bugzilla->params->{'RADIUS_email_suffix'}; + my $username = $params->{username}; + + + # If we're using RADIUS_email_suffix, we may need to cut it off from + # the login name. + if ($address_suffix) { + $username =~ s/\Q$address_suffix\E$//i; + } + + # Create RADIUS object. + my $radius = new Authen::Radius( + Host => Bugzilla->params->{'RADIUS_server'}, + Secret => Bugzilla->params->{'RADIUS_secret'} + ) + || return { + failure => AUTH_ERROR, + error => 'radius_preparation_error', + details => {errstr => Authen::Radius::strerror()} + }; + + # Check the password. + unless ($radius->check_pwd( + $username, $params->{password}, Bugzilla->params->{'RADIUS_NAS_IP'} || undef + )) + { + Bugzilla->logger->info( + "Radius Auth failure: " . $radius->get_error . " - " . $radius->strerror); + return {failure => AUTH_RH_RADIUS_LOGINFAILED}; + } + + # Build the user account's e-mail address. + $params->{bz_username} = $username . $address_suffix; + + return $params; +} + +sub check_credentials_password { + my ($self, $login_data) = @_; + my $dbh = Bugzilla->dbh; + + my $username = $login_data->{username}; + my $user = new Bugzilla::User({name => $username}); + + return {failure => AUTH_NO_SUCH_USER} unless $user; + + $login_data->{user} = $user; + $login_data->{bz_username} = $user->login; + + if ($user->account_is_locked_out) { + return {failure => AUTH_LOCKOUT, user => $user}; + } + + my $password = $login_data->{password}; + my $real_password_crypted = $user->cryptpassword; + + # Using the internal crypted password as the salt, + # crypt the password the user entered. + my $entered_password_crypted = bz_crypt($password, $real_password_crypted); + + if ($entered_password_crypted ne $real_password_crypted) { + + # Record the login failure + $user->note_login_failure(); + + # Immediately check if we are locked out + if ($user->account_is_locked_out) { + return {failure => AUTH_LOCKOUT, user => $user, just_locked_out => 1}; + } + + return { + failure => AUTH_LOGINFAILED, + failure_count => scalar(@{$user->account_ip_login_failures}), + }; + } + + # Force the user to change their password if it does not meet the current + # criteria. This should usually only happen if the criteria has changed. + if ( Bugzilla->usage_mode == USAGE_MODE_BROWSER + && Bugzilla->params->{password_check_on_login}) + { + my $check = validate_password_check($password); + if ($check) { + return { + failure => AUTH_ERROR, + user_error => $check, + details => {locked_user => $user} + }; + } + } + + # The user's credentials are okay, so delete any outstanding + # password tokens or login failures they may have generated. + Bugzilla::Token::DeletePasswordTokens($user->id, "user_logged_in"); + $user->clear_login_failures(); + + my $update_password = 0; + + # If their old password was using crypt() or some different hash + # than we're using now, convert the stored password to using + # whatever hashing system we're using now. + my $current_algorithm = PASSWORD_DIGEST_ALGORITHM; + $update_password = 1 if ($real_password_crypted !~ /{\Q$current_algorithm\E}$/); + + # If their old password was using a different length salt than what + # we're using now, update the password to use the new salt length. + if ($real_password_crypted =~ /^([^,]+),/) { + $update_password = 1 if (length($1) != PASSWORD_SALT_LENGTH); + } + + # If needed, update the user's password. + if ($update_password) { + + # We can't call $user->set_password because we don't want the password + # complexity rules to apply here. + $user->{cryptpassword} = bz_crypt($password); + $user->update(); + } + + return $login_data; +} + +sub change_password { + my ($self, $user, $password) = @_; + my $dbh = Bugzilla->dbh; + my $cryptpassword = bz_crypt($password); + $dbh->do("UPDATE profiles SET cryptpassword = ? WHERE userid = ?", + undef, $cryptpassword, $user->id); + Bugzilla->memcached->clear({table => 'profiles', id => $user->id}); +} + +1; diff --git a/Bugzilla/Auth/Verify/Stack.pm b/Bugzilla/Auth/Verify/Stack.pm index 3e5db3cec..9a9412915 100644 --- a/Bugzilla/Auth/Verify/Stack.pm +++ b/Bugzilla/Auth/Verify/Stack.pm @@ -13,8 +13,8 @@ use warnings; use base qw(Bugzilla::Auth::Verify); use fields qw( - _stack - successful + _stack + successful ); use Bugzilla::Hook; @@ -23,70 +23,75 @@ use Hash::Util qw(lock_keys); use List::MoreUtils qw(any); sub new { - my $class = shift; - my $list = shift; - my $self = $class->SUPER::new(@_); - my %methods = map { $_ => "Bugzilla/Auth/Verify/$_.pm" } split(',', $list); - lock_keys(%methods); - Bugzilla::Hook::process('auth_verify_methods', { modules => \%methods }); - - $self->{_stack} = []; - foreach my $verify_method (split(',', $list)) { - my $module = $methods{$verify_method}; - require $module; - $module =~ s|/|::|g; - $module =~ s/.pm$//; - push(@{$self->{_stack}}, $module->new(@_)); - } - return $self; + my $class = shift; + my $list = shift; + my $self = $class->SUPER::new(@_); + my %methods = map { $_ => "Bugzilla/Auth/Verify/$_.pm" } split(',', $list); + lock_keys(%methods); + Bugzilla::Hook::process('auth_verify_methods', {modules => \%methods}); + + $self->{_stack} = []; + foreach my $verify_method (split(',', $list)) { + my $module = $methods{$verify_method}; + require $module; + $module =~ s|/|::|g; + $module =~ s/.pm$//; + push(@{$self->{_stack}}, $module->new(@_)); + } + return $self; } sub can_change_password { - my ($self) = @_; - # We return true if any method can change passwords. - foreach my $object (@{$self->{_stack}}) { - return 1 if $object->can_change_password; - } - return 0; + my ($self) = @_; + + # We return true if any method can change passwords. + foreach my $object (@{$self->{_stack}}) { + return 1 if $object->can_change_password; + } + return 0; } sub check_credentials { - my $self = shift; - my $result; - foreach my $object (@{$self->{_stack}}) { - $result = $object->check_credentials(@_); - $self->{successful} = $object; - last if !$result->{failure}; - # So that if none of them succeed, it's undef. - $self->{successful} = undef; - } - # Returns the result at the bottom of the stack if they all fail. - return $result; + my $self = shift; + my $result; + foreach my $object (@{$self->{_stack}}) { + $result = $object->check_credentials(@_); + $self->{successful} = $object; + last if !$result->{failure}; + + # So that if none of them succeed, it's undef. + $self->{successful} = undef; + } + + # Returns the result at the bottom of the stack if they all fail. + return $result; } sub create_or_update_user { - my $self = shift; - my $result; - foreach my $object (@{$self->{_stack}}) { - $result = $object->create_or_update_user(@_); - last if !$result->{failure}; - } - # Returns the result at the bottom of the stack if they all fail. - return $result; + my $self = shift; + my $result; + foreach my $object (@{$self->{_stack}}) { + $result = $object->create_or_update_user(@_); + last if !$result->{failure}; + } + + # Returns the result at the bottom of the stack if they all fail. + return $result; } sub user_can_create_account { - my ($self) = @_; - # We return true if any method allows the user to create an account. - foreach my $object (@{$self->{_stack}}) { - return 1 if $object->user_can_create_account; - } - return 0; + my ($self) = @_; + + # We return true if any method allows the user to create an account. + foreach my $object (@{$self->{_stack}}) { + return 1 if $object->user_can_create_account; + } + return 0; } sub extern_id_used { - my ($self) = @_; - return any { $_->extern_id_used } @{ $self->{_stack} }; + my ($self) = @_; + return any { $_->extern_id_used } @{$self->{_stack}}; } 1; diff --git a/Bugzilla/Bug.pm b/Bugzilla/Bug.pm index 8b4493f85..1725868bc 100644 --- a/Bugzilla/Bug.pm +++ b/Bugzilla/Bug.pm @@ -30,19 +30,26 @@ use Bugzilla::Status; use Bugzilla::Comment; use Bugzilla::BugUrl; use Bugzilla::BugUserLastVisit; +use Bugzilla::WebService::Util qw(filter); -use List::MoreUtils qw(firstidx uniq part); +use List::MoreUtils qw(apply firstidx uniq part); use List::Util qw(min max first); +use Regexp::Common; use Storable qw(dclone); -use Scalar::Util qw(blessed); +use URI; +use URI::QueryParam; +use Scalar::Util qw(blessed weaken); use parent qw(Bugzilla::Object Exporter); @Bugzilla::Bug::EXPORT = qw( - bug_alias_to_id - LogActivityEntry - editable_bug_fields + bug_alias_to_id + LogActivityEntry + editable_bug_fields ); +# This hash keeps a weak copy of every bug created. +my %CLEANUP; + ##################################################################### # Constants ##################################################################### @@ -51,198 +58,220 @@ use constant DB_TABLE => 'bugs'; use constant ID_FIELD => 'bug_id'; use constant NAME_FIELD => 'bug_id'; use constant LIST_ORDER => ID_FIELD; + # Bugs have their own auditing table, bugs_activity. use constant AUDIT_CREATES => 0; use constant AUDIT_UPDATES => 0; + # This will be enabled later use constant USE_MEMCACHED => 0; # This is a sub because it needs to call other subroutines. sub DB_COLUMNS { - my $dbh = Bugzilla->dbh; - my @custom = grep {$_->type != FIELD_TYPE_MULTI_SELECT} - Bugzilla->active_custom_fields; - my @custom_names = map {$_->name} @custom; - - my @columns = (qw( - assigned_to - bug_file_loc - bug_id - bug_severity - bug_status - cclist_accessible - component_id - creation_ts - delta_ts - estimated_time - everconfirmed - lastdiffed - op_sys - priority - product_id - qa_contact - remaining_time - rep_platform - reporter_accessible - resolution - short_desc - status_whiteboard - target_milestone - version - ), - 'reporter AS reporter_id', - $dbh->sql_date_format('deadline', '%Y-%m-%d') . ' AS deadline', - @custom_names); - - Bugzilla::Hook::process("bug_columns", { columns => \@columns }); - - return @columns; + my $dbh = Bugzilla->dbh; + my @custom = grep { + $_->type != FIELD_TYPE_MULTI_SELECT && $_->type != FIELD_TYPE_ONE_SELECT + } Bugzilla->active_custom_fields; + my @custom_names = map { $_->name } @custom; + + my @columns = ( + qw( + assigned_to + bug_file_loc + bug_id + bug_severity + bug_status + cclist_accessible + component_id + creation_ts + delta_ts + estimated_time + everconfirmed + lastdiffed + op_sys + priority + product_id + qa_contact + remaining_time + rep_platform + reporter_accessible + resolution + short_desc + status_whiteboard + target_milestone + version + ), 'reporter AS reporter_id', + $dbh->sql_date_format('deadline', '%Y-%m-%d') . ' AS deadline', @custom_names + ); + + Bugzilla::Hook::process("bug_columns", {columns => \@columns}); + + return @columns; } sub VALIDATORS { - my $validators = { - alias => \&_check_alias, - assigned_to => \&_check_assigned_to, - blocked => \&_check_dependencies, - bug_file_loc => \&_check_bug_file_loc, - bug_severity => \&_check_select_field, - bug_status => \&_check_bug_status, - cc => \&_check_cc, - comment => \&_check_comment, - component => \&_check_component, - creation_ts => \&_check_creation_ts, - deadline => \&_check_deadline, - dependson => \&_check_dependencies, - dup_id => \&_check_dup_id, - estimated_time => \&_check_time_field, - everconfirmed => \&Bugzilla::Object::check_boolean, - groups => \&_check_groups, - keywords => \&_check_keywords, - op_sys => \&_check_select_field, - priority => \&_check_priority, - product => \&_check_product, - qa_contact => \&_check_qa_contact, - remaining_time => \&_check_time_field, - rep_platform => \&_check_select_field, - resolution => \&_check_resolution, - short_desc => \&_check_short_desc, - status_whiteboard => \&_check_status_whiteboard, - target_milestone => \&_check_target_milestone, - version => \&_check_version, - - cclist_accessible => \&Bugzilla::Object::check_boolean, - reporter_accessible => \&Bugzilla::Object::check_boolean, - }; - - # Set up validators for custom fields. - foreach my $field (Bugzilla->active_custom_fields) { - my $validator; - if ($field->type == FIELD_TYPE_SINGLE_SELECT) { - $validator = \&_check_select_field; - } - elsif ($field->type == FIELD_TYPE_MULTI_SELECT) { - $validator = \&_check_multi_select_field; - } - elsif ($field->type == FIELD_TYPE_DATETIME) { - $validator = \&_check_datetime_field; - } - elsif ($field->type == FIELD_TYPE_DATE) { - $validator = \&_check_date_field; - } - elsif ($field->type == FIELD_TYPE_FREETEXT) { - $validator = \&_check_freetext_field; - } - elsif ($field->type == FIELD_TYPE_BUG_ID) { - $validator = \&_check_bugid_field; - } - elsif ($field->type == FIELD_TYPE_TEXTAREA) { - $validator = \&_check_textarea_field; - } - elsif ($field->type == FIELD_TYPE_INTEGER) { - $validator = \&_check_integer_field; - } - else { - $validator = \&_check_default_field; - } - $validators->{$field->name} = $validator; + my $validators = { + alias => \&_check_alias, + assigned_to => \&_check_assigned_to, + blocked => \&_check_dependencies, + bug_file_loc => \&_check_bug_file_loc, + bug_severity => \&_check_select_field, + bug_status => \&_check_bug_status, + cc => \&_check_cc, + comment => \&_check_comment, + component => \&_check_component, + creation_ts => \&_check_creation_ts, + deadline => \&_check_deadline, + dependson => \&_check_dependencies, + dup_id => \&_check_dup_id, + estimated_time => \&_check_time_field, + everconfirmed => \&Bugzilla::Object::check_boolean, + groups => \&_check_groups, + keywords => \&_check_keywords, + op_sys => \&_check_select_field, + priority => \&_check_priority, + product => \&_check_product, + qa_contact => \&_check_qa_contact, + ## REDHAT EXTENSION START 876015 + docs_contact => \&_check_docs_contact, + ## REDHAT EXTENSION END 876015 + remaining_time => \&_check_time_field, + rep_platform => \&_check_select_field, + resolution => \&_check_resolution, + short_desc => \&_check_short_desc, + status_whiteboard => \&_check_status_whiteboard, + target_milestone => \&_check_target_milestone, + target_release => \&_check_target_release, + version => \&_check_version, + + cclist_accessible => \&Bugzilla::Object::check_boolean, + reporter_accessible => \&Bugzilla::Object::check_boolean, + }; + + # Set up validators for custom fields. + foreach my $field (Bugzilla->active_custom_fields) { + my $validator; + if ($field->type == FIELD_TYPE_SINGLE_SELECT) { + $validator = \&_check_select_field; + } + elsif ($field->type == FIELD_TYPE_MULTI_SELECT) { + $validator = \&_check_multi_select_field; + } + elsif ($field->type == FIELD_TYPE_ONE_SELECT) { + $validator = \&_check_single_select_field; + } + elsif ($field->type == FIELD_TYPE_DATETIME) { + $validator = \&_check_datetime_field; + } + elsif ($field->type == FIELD_TYPE_DATE) { + $validator = \&_check_date_field; + } + elsif ($field->type == FIELD_TYPE_FREETEXT) { + $validator = \&_check_freetext_field; + } + elsif ($field->type == FIELD_TYPE_BUG_ID) { + $validator = \&_check_bugid_field; + } + elsif ($field->type == FIELD_TYPE_TEXTAREA) { + $validator = \&_check_textarea_field; + } + elsif ($field->type == FIELD_TYPE_INTEGER) { + $validator = \&_check_integer_field; } + else { + $validator = \&_check_default_field; + } + $validators->{$field->name} = $validator; + } - return $validators; -}; + return $validators; +} sub VALIDATOR_DEPENDENCIES { - my $cache = Bugzilla->request_cache; - return $cache->{bug_validator_dependencies} - if $cache->{bug_validator_dependencies}; - - my %deps = ( - assigned_to => ['component'], - blocked => ['product'], - bug_status => ['product', 'comment', 'target_milestone'], - cc => ['component'], - comment => ['creation_ts'], - component => ['product'], - dependson => ['product'], - dup_id => ['bug_status', 'resolution'], - groups => ['product'], - keywords => ['product'], - resolution => ['bug_status', 'dependson'], - qa_contact => ['component'], - target_milestone => ['product'], - version => ['product'], - ); - - foreach my $field (@{ Bugzilla->fields }) { - $deps{$field->name} = [ $field->visibility_field->name ] - if $field->{visibility_field_id}; - } - - $cache->{bug_validator_dependencies} = \%deps; - return \%deps; -}; + my $cache = Bugzilla->request_cache; + return $cache->{bug_validator_dependencies} + if $cache->{bug_validator_dependencies}; + + my %deps = ( + assigned_to => ['component'], + blocked => ['product'], + bug_status => ['product', 'comment', 'target_milestone'], + cc => ['component'], + comment => ['creation_ts'], + component => ['product'], + dependson => ['product'], + dup_id => ['bug_status', 'resolution'], + groups => ['product'], + keywords => ['product'], + resolution => ['bug_status', 'dependson'], + qa_contact => ['component'], + ## REDHAT EXTENSION START 876015 + docs_contact => ['component'], + ## REDHAT EXTENSION END 876015 + target_milestone => ['product'], + target_release => ['product'], + version => ['product'], + ); + + foreach my $field (@{Bugzilla->fields}) { + $deps{$field->name} = [$field->visibility_field->name] + if $field->{visibility_field_id}; + } + + ## REDHAT EXTENSION BEGIN 815549 + # We need the groups and product sorted before we process the CC:s + push @{$deps{cc}}, 'groups', 'product'; + ## REDHAT EXTENSION END 815549 + + $cache->{bug_validator_dependencies} = \%deps; + return \%deps; +} sub UPDATE_COLUMNS { - my @custom = grep {$_->type != FIELD_TYPE_MULTI_SELECT} - Bugzilla->active_custom_fields; - my @custom_names = map {$_->name} @custom; - my @columns = qw( - assigned_to - bug_file_loc - bug_severity - bug_status - cclist_accessible - component_id - deadline - estimated_time - everconfirmed - op_sys - priority - product_id - qa_contact - remaining_time - rep_platform - reporter_accessible - resolution - short_desc - status_whiteboard - target_milestone - version - ); - push(@columns, @custom_names); - return @columns; -}; - -use constant NUMERIC_COLUMNS => qw( + my @custom = grep { + $_->type != FIELD_TYPE_MULTI_SELECT && $_->type != FIELD_TYPE_ONE_SELECT + } Bugzilla->active_custom_fields; + + my @custom_names = map { $_->name } @custom; + ## REDHAT EXTENSION 876015: Add docs_contact + my @columns = qw( + assigned_to + bug_file_loc + bug_severity + bug_status + cclist_accessible + component_id + deadline estimated_time + everconfirmed + op_sys + priority + product_id + qa_contact + docs_contact remaining_time + rep_platform + reporter_accessible + resolution + short_desc + status_whiteboard + target_milestone + version + ); + push(@columns, @custom_names); + return @columns; +} + +use constant NUMERIC_COLUMNS => qw( + estimated_time + remaining_time ); sub DATE_COLUMNS { - my @fields = (@{ Bugzilla->fields({ type => [FIELD_TYPE_DATETIME, - FIELD_TYPE_DATE] }) - }); - return map { $_->name } @fields; + my @fields + = (@{Bugzilla->fields({type => [FIELD_TYPE_DATETIME, FIELD_TYPE_DATE]})}); + return map { $_->name } @fields; } # Used in LogActivityEntry(). Gives the max length of lines in the @@ -254,30 +283,30 @@ use constant MAX_LINE_LENGTH => 254; # of Bugzilla. (These are the field names that the WebService and email_in.pl # use.) use constant FIELD_MAP => { - blocks => 'blocked', - commentprivacy => 'comment_is_private', - creation_time => 'creation_ts', - creator => 'reporter', - description => 'comment', - depends_on => 'dependson', - dupe_of => 'dup_id', - id => 'bug_id', - is_confirmed => 'everconfirmed', - is_cc_accessible => 'cclist_accessible', - is_creator_accessible => 'reporter_accessible', - last_change_time => 'delta_ts', - platform => 'rep_platform', - severity => 'bug_severity', - status => 'bug_status', - summary => 'short_desc', - url => 'bug_file_loc', - whiteboard => 'status_whiteboard', + blocks => 'blocked', + commentprivacy => 'comment_is_private', + creation_time => 'creation_ts', + creator => 'reporter', + description => 'comment', + depends_on => 'dependson', + dupe_of => 'dup_id', + id => 'bug_id', + is_confirmed => 'everconfirmed', + is_cc_accessible => 'cclist_accessible', + is_creator_accessible => 'reporter_accessible', + last_change_time => 'delta_ts', + platform => 'rep_platform', + severity => 'bug_severity', + status => 'bug_status', + summary => 'short_desc', + url => 'bug_file_loc', + whiteboard => 'status_whiteboard', + ## REDHAT EXTENSION 653316 + sub_components => 'rh_sub_components', }; -use constant REQUIRED_FIELD_MAP => { - product_id => 'product', - component_id => 'component', -}; +use constant REQUIRED_FIELD_MAP => + {product_id => 'product', component_id => 'component',}; # Creation timestamp is here because it needs to be validated # but it can be NULL in the database (see comments in create above) @@ -295,360 +324,498 @@ use constant REQUIRED_FIELD_MAP => { # # Groups are in a separate table, but must always be validated so that # mandatory groups get set on bugs. -use constant EXTRA_REQUIRED_FIELDS => qw(creation_ts target_milestone cc qa_contact groups); +## REDHAT EXTENSION 876015: Add docs_contact +## REDHAT EXTENSION 965151: Add target_release +use constant EXTRA_REQUIRED_FIELDS => + qw(creation_ts target_milestone cc qa_contact docs_contact groups target_release); ##################################################################### sub new { - my $invocant = shift; - my $class = ref($invocant) || $invocant; - my $param = shift; - - # Remove leading "#" mark if we've just been passed an id. - if (!ref $param && $param =~ /^#([0-9]+)$/) { - $param = $1; - } - - # If we get something that looks like a word (not a number), - # make it the "name" param. - if (!defined $param - || (!ref($param) && $param !~ /^[0-9]+$/) - || (ref($param) && $param->{id} !~ /^[0-9]+$/)) - { - if ($param) { - my $alias = ref($param) ? $param->{id} : $param; - my $bug_id = bug_alias_to_id($alias); - if (! $bug_id) { - my $error_self = {}; - bless $error_self, $class; - $error_self->{'bug_id'} = $alias; - $error_self->{'error'} = 'InvalidBugId'; - return $error_self; - } - $param = { id => $bug_id, - cache => ref($param) ? $param->{cache} : 0 }; - } - else { - # We got something that's not a number. - my $error_self = {}; - bless $error_self, $class; - $error_self->{'bug_id'} = $param; - $error_self->{'error'} = 'InvalidBugId'; - return $error_self; - } - } - - unshift @_, $param; - my $self = $class->SUPER::new(@_); - - # Bugzilla::Bug->new always returns something, but sets $self->{error} - # if the bug wasn't found in the database. - if (!$self) { + my $invocant = shift; + my $class = ref($invocant) || $invocant; + my $param = shift; + + # Remove leading "#" mark if we've just been passed an id. + if (defined $param && !ref $param && $param =~ /^#([0-9]+)$/) { + $param = $1; + } + + # If we get something that looks like a word (not a number), + # make it the "name" param. + if ( !defined $param + || (!ref($param) && $param !~ /^[0-9]+$/) + || (ref($param) && $param->{id} !~ /^[0-9]+$/)) + { + if ($param) { + my $alias = ref($param) ? $param->{id} : $param; + my $bug_id = bug_alias_to_id($alias); + if (!$bug_id) { my $error_self = {}; - if (ref $param) { - $error_self->{bug_id} = $param->{name}; - $error_self->{error} = 'InvalidBugId'; - } - else { - $error_self->{bug_id} = $param; - $error_self->{error} = 'NotFound'; - } bless $error_self, $class; + $error_self->{'bug_id'} = $alias; + $error_self->{'error'} = 'InvalidBugId'; return $error_self; + } + $param = {id => $bug_id, cache => ref($param) ? $param->{cache} : 0}; } + else { + # We got something that's not a number. + my $error_self = {}; + bless $error_self, $class; + $error_self->{'bug_id'} = $param; + $error_self->{'error'} = 'InvalidBugId'; + return $error_self; + } + } + + unshift @_, $param; + my $self = $class->SUPER::new(@_); + + # Bugzilla::Bug->new always returns something, but sets $self->{error} + # if the bug wasn't found in the database. + if (!$self) { + my $error_self = {}; + if (ref $param) { + $error_self->{bug_id} = $param->{name}; + $error_self->{error} = 'InvalidBugId'; + } + else { + $error_self->{bug_id} = $param; + $error_self->{error} = 'NotFound'; + } + bless $error_self, $class; + return $error_self; + } - return $self; + $CLEANUP{$self->id} = $self; + weaken($CLEANUP{$self->id}); + + return $self; } sub initialize { - $_[0]->_create_cf_accessors(); + $_[0]->_create_cf_accessors(); } sub object_cache_key { - my $class = shift; - my $key = $class->SUPER::object_cache_key(@_) - || return; - return $key . ',' . Bugzilla->user->id; + my $class = shift; + my $key = $class->SUPER::object_cache_key(@_) || return; + return $key . ',' . Bugzilla->user->id; +} + +# This is called by Bugzilla::_cleanup() at the end of requests in a persistent environment +# (such as mod_perl) +sub CLEANUP { + foreach my $bug (values %CLEANUP) { + + # $bug will be undef if there are no other references to it. + next unless $bug; + delete $bug->{depends_on_obj}; + delete $bug->{blocks_obj}; + } + %CLEANUP = (); } sub check { - my $class = shift; - my ($param, $field) = @_; + my $class = shift; + my ($param, $field) = @_; - # Bugzilla::Bug throws lots of special errors, so we don't call - # SUPER::check, we just call our new and do our own checks. - my $id = ref($param) - ? ($param->{id} = trim($param->{id})) - : ($param = trim($param)); - ThrowUserError('improper_bug_id_field_value', { field => $field }) unless defined $id; + # Bugzilla::Bug throws lots of special errors, so we don't call + # SUPER::check, we just call our new and do our own checks. + my $id + = ref($param) ? ($param->{id} = trim($param->{id})) : ($param = trim($param)); + ThrowUserError('improper_bug_id_field_value', {field => $field}) + unless defined $id; - my $self = $class->new($param); + my $self = $class->new($param); - if ($self->{error}) { - # For error messages, use the id that was returned by new(), because - # it's cleaned up. - $id = $self->id; + if ($self->{error}) { - if ($self->{error} eq 'NotFound') { - ThrowUserError("bug_id_does_not_exist", { bug_id => $id }); - } - if ($self->{error} eq 'InvalidBugId') { - ThrowUserError("improper_bug_id_field_value", - { bug_id => $id, - field => $field }); - } - } + # For error messages, use the id that was returned by new(), because + # it's cleaned up. + $id = $self->id; - unless ($field && $field =~ /^(dependson|blocked|dup_id)$/) { - $self->check_is_visible($id); + if ($self->{error} eq 'NotFound') { + ThrowUserError("bug_id_does_not_exist", {bug_id => $id}); } - return $self; + if ($self->{error} eq 'InvalidBugId') { + ThrowUserError("improper_bug_id_field_value", {bug_id => $id, field => $field}); + } + } + + unless ($field && $field =~ /^(dependson|blocked|dup_id)$/) { + $self->check_is_visible($id); + } + return $self; } sub check_for_edit { - my $class = shift; - my $bug = $class->check(@_); + my $class = shift; + my $bug = $class->check(@_); - Bugzilla->user->can_edit_product($bug->product_id) - || ThrowUserError("product_edit_denied", { product => $bug->product }); + Bugzilla->user->can_edit_product($bug->product_id) + || ThrowUserError("product_edit_denied", {product => $bug->product}); - return $bug; + return $bug; } sub check_is_visible { - my ($self, $input_id) = @_; - $input_id ||= $self->id; - my $user = Bugzilla->user; + my ($self, $input_id) = @_; + $input_id ||= $self->id; + my $user = Bugzilla->user; - if (!$user->can_see_bug($self->id)) { - # The error the user sees depends on whether or not they are - # logged in (i.e. $user->id contains the user's positive integer ID). - # If we are validating an alias, then use it in the error message - # instead of its corresponding bug ID, to not disclose it. - if ($user->id) { - ThrowUserError("bug_access_denied", { bug_id => $input_id }); - } else { - ThrowUserError("bug_access_query", { bug_id => $input_id }); - } + if (!$user->can_see_bug($self->id)) { + + # The error the user sees depends on whether or not they are + # logged in (i.e. $user->id contains the user's positive integer ID). + # If we are validating an alias, then use it in the error message + # instead of its corresponding bug ID, to not disclose it. + if ($user->id) { + ThrowUserError("bug_access_denied", {bug_id => $input_id}); } + else { + ThrowUserError("bug_access_query", {bug_id => $input_id}); + } + } } sub match { - my $class = shift; - my ($params) = @_; - - # Allow matching certain fields by name (in addition to matching by ID). - my %translate_fields = ( - assigned_to => 'Bugzilla::User', - qa_contact => 'Bugzilla::User', - reporter => 'Bugzilla::User', - product => 'Bugzilla::Product', - component => 'Bugzilla::Component', - ); - my %translated; - - foreach my $field (keys %translate_fields) { - my @ids; - # Convert names to ids. We use "exists" everywhere since people can - # legally specify "undef" to mean IS NULL (even though most of these - # fields can't be NULL, people can still specify it...). - if (exists $params->{$field}) { - my $names = $params->{$field}; - my $type = $translate_fields{$field}; - my $param = $type eq 'Bugzilla::User' ? 'login_name' : 'name'; - # We call Bugzilla::Object::match directly to avoid the - # Bugzilla::User::match implementation which is different. - my $objects = Bugzilla::Object::match($type, { $param => $names }); - push(@ids, map { $_->id } @$objects); - } - # You can also specify ids directly as arguments to this function, - # so include them in the list if they have been specified. - if (exists $params->{"${field}_id"}) { - my $current_ids = $params->{"${field}_id"}; - my @id_array = ref $current_ids ? @$current_ids : ($current_ids); - push(@ids, @id_array); - } - # We do this "or" instead of a "scalar(@ids)" to handle the case - # when people passed only invalid object names. Otherwise we'd - # end up with a SUPER::match call with zero criteria (which dies). - if (exists $params->{$field} or exists $params->{"${field}_id"}) { - $translated{$field} = scalar(@ids) == 1 ? $ids[0] : \@ids; - } + my $class = shift; + my ($params) = @_; + + # Allow matching certain fields by name (in addition to matching by ID). + my %translate_fields = ( + assigned_to => 'Bugzilla::User', + qa_contact => 'Bugzilla::User', + ## REDHAT EXTENSION START 876015 + docs_contact => 'Bugzilla::User', + ## REDHAT EXTENSION END 876015 + reporter => 'Bugzilla::User', + product => 'Bugzilla::Product', + component => 'Bugzilla::Component', + ); + my %translated; + + foreach my $field (keys %translate_fields) { + my @ids; + + # Convert names to ids. We use "exists" everywhere since people can + # legally specify "undef" to mean IS NULL (even though most of these + # fields can't be NULL, people can still specify it...). + if (exists $params->{$field}) { + my $names = $params->{$field}; + my $type = $translate_fields{$field}; + my $param = $type eq 'Bugzilla::User' ? 'login_name' : 'name'; + + # We call Bugzilla::Object::match directly to avoid the + # Bugzilla::User::match implementation which is different. + my $objects = Bugzilla::Object::match($type, {$param => $names}); + push(@ids, map { $_->id } @$objects); + } + + # You can also specify ids directly as arguments to this function, + # so include them in the list if they have been specified. + if (exists $params->{"${field}_id"}) { + my $current_ids = $params->{"${field}_id"}; + my @id_array = ref $current_ids ? @$current_ids : ($current_ids); + push(@ids, @id_array); + } + + # We do this "or" instead of a "scalar(@ids)" to handle the case + # when people passed only invalid object names. Otherwise we'd + # end up with a SUPER::match call with zero criteria (which dies). + if (exists $params->{$field} or exists $params->{"${field}_id"}) { + $translated{$field} = scalar(@ids) == 1 ? $ids[0] : \@ids; + } + } + + # The user fields don't have an _id on the end of them in the database, + # but the product & component fields do, so we have to have separate + # code to deal with the different sets of fields here. + foreach my $field (qw(assigned_to qa_contact reporter)) { + delete $params->{"${field}_id"}; + $params->{$field} = $translated{$field} if exists $translated{$field}; + } + foreach my $field (qw(product component)) { + delete $params->{$field}; + $params->{"${field}_id"} = $translated{$field} if exists $translated{$field}; + } + + ## REDHAT EXTENSION BEGIN 814476 + if (my $alias = delete $params->{alias}) { + my $q + = (ref $alias eq 'ARRAY' and scalar(@$alias) > 1) + ? 'IN (' . join(',', (('?') x scalar(@$alias))) . ')' + : '= ?'; + $params->{WHERE}->{"bug_id IN (SELECT bug_id FROM bugs_aliases WHERE alias $q)"} + = $alias; + } + ## REDHAT EXTENSION END 814476 + + ## REDHAT EXTENSION BEGIN 584957 + if (my $component_id = delete $params->{component_id}) { + if (ref $component_id ne 'ARRAY') { + $component_id = [$component_id]; + } + my $q + = scalar(@$component_id) > 1 + ? 'IN (' . join(',', (('?') x scalar(@$component_id))) . ')' + : '= ?'; + + my $sql = " +(bug_id IN + (SELECT bug_id + FROM bugs b + JOIN rh_multiple_bug_components mbc USING (bug_id) + JOIN components c ON (c.id = mbc.component_id) + WHERE c.id $q + ) + OR bugs.component_id $q +)"; + if (scalar(@$component_id)) { + $params->{WHERE}->{$sql} = [@$component_id, @$component_id]; } - - # The user fields don't have an _id on the end of them in the database, - # but the product & component fields do, so we have to have separate - # code to deal with the different sets of fields here. - foreach my $field (qw(assigned_to qa_contact reporter)) { - delete $params->{"${field}_id"}; - $params->{$field} = $translated{$field} - if exists $translated{$field}; - } - foreach my $field (qw(product component)) { - delete $params->{$field}; - $params->{"${field}_id"} = $translated{$field} - if exists $translated{$field}; - } - - return $class->SUPER::match(@_); + else { + $params->{WHERE}->{$sql} = ['', '']; + } + + } + ## REDHAT EXTENSION END 584957 + + ## REDHAT EXTENSION BEGIN 584954 + if (my $version = delete $params->{version}) { + if (ref $version ne 'ARRAY') { + $version = [$version]; + } + my $q + = scalar(@$version) > 1 + ? 'IN (' . join(',', (('?') x scalar(@$version))) . ')' + : '= ?'; + + my $sql = " +( + bug_id IN ( + SELECT bug_id + FROM rh_multiple_bug_versions mbv + JOIN versions v ON (mbv.version_id = v.id) + WHERE v.value $q) +OR + bugs.version $q +)"; + + $params->{WHERE}->{$sql} = [@$version, @$version]; + } + ## REDHAT EXTENSION END 584954 + + # Match blocked and dependson + if (my $blocked = delete $params->{blocked}) { + my $q + = (ref $blocked eq 'ARRAY' and scalar(@$blocked) > 1) + ? 'IN (' . join(',', (('?') x scalar(@$blocked))) . ')' + : '= ?'; + $params->{WHERE} + ->{"bug_id IN (SELECT dependson FROM dependencies WHERE blocked $q)"} + = $blocked; + } + if (my $depends_on = delete $params->{dependson}) { + my $q + = (ref $depends_on eq 'ARRAY' and scalar(@$depends_on) > 1) + ? 'IN (' . join(',', (('?') x scalar(@$depends_on))) . ')' + : '= ?'; + $params->{WHERE} + ->{"bug_id IN (SELECT blocked FROM dependencies WHERE dependson $q)"} + = $depends_on; + } + + # Match keywords + if (my $keywords = delete $params->{keywords}) { + my $q + = (ref $keywords eq 'ARRAY' and scalar(@$keywords) > 1) + ? 'IN (' . join(',', (('?') x scalar(@$keywords))) . ')' + : '= ?'; + my $query = " +bug_id IN ( + SELECT bug_id + FROM keywords + JOIN keyworddefs ON keywords.keywordid = keyworddefs.id + WHERE keyworddefs.name $q +)"; + + $params->{WHERE}->{$query} = $keywords; + } + + return $class->SUPER::match(@_); } # Helps load up information for bugs for show_bug.cgi and other situations # that will need to access info on lots of bugs. sub preload { - my ($class, $bugs) = @_; - my $user = Bugzilla->user; - - # It would be faster but MUCH more complicated to select all the - # deps for the entire list in one SQL statement. If we ever have - # a profile that proves that that's necessary, we can switch over - # to the more complex method. - my @all_dep_ids; - foreach my $bug (@$bugs) { - push @all_dep_ids, @{ $bug->blocked }, @{ $bug->dependson }; - push @all_dep_ids, @{ $bug->duplicate_ids }; - push @all_dep_ids, @{ $bug->_preload_referenced_bugs }; - } - @all_dep_ids = uniq @all_dep_ids; - # If we don't do this, can_see_bug will do one call per bug in - # the dependency and duplicate lists, in Bugzilla::Template::get_bug_link. - $user->visible_bugs(\@all_dep_ids); + my ($class, $bugs) = @_; + my $user = Bugzilla->user; + + # It would be faster but MUCH more complicated to select all the + # deps for the entire list in one SQL statement. If we ever have + # a profile that proves that that's necessary, we can switch over + # to the more complex method. + my @all_dep_ids; + foreach my $bug (@$bugs) { + push @all_dep_ids, @{$bug->blocked}, @{$bug->dependson}; + push @all_dep_ids, @{$bug->duplicate_ids}; + push @all_dep_ids, @{$bug->_preload_referenced_bugs}; + } + @all_dep_ids = uniq @all_dep_ids; + + # If we don't do this, can_see_bug will do one call per bug in + # the dependency and duplicate lists, in Bugzilla::Template::get_bug_link. + $user->visible_bugs(\@all_dep_ids); } # Helps load up bugs referenced in comments by retrieving them with a single # query from the database and injecting bug objects into the object-cache. sub _preload_referenced_bugs { - my $self = shift; + my $self = shift; - # inject current duplicates into the object-cache first - foreach my $bug (@{ $self->duplicates }) { - $bug->object_cache_set() unless Bugzilla::Bug->object_cache_get($bug->id); - } + # inject current duplicates into the object-cache first + foreach my $bug (@{$self->duplicates}) { + $bug->object_cache_set() unless Bugzilla::Bug->object_cache_get($bug->id); + } - # preload bugs from comments - my $referenced_bug_ids = _extract_bug_ids($self->comments); - my @ref_bug_ids = grep { !Bugzilla::Bug->object_cache_get($_) } @$referenced_bug_ids; + # preload bugs from comments + my $referenced_bug_ids = _extract_bug_ids($self->comments); + my @ref_bug_ids + = grep { !Bugzilla::Bug->object_cache_get($_) } @$referenced_bug_ids; - # inject into object-cache - my $referenced_bugs = Bugzilla::Bug->new_from_list(\@ref_bug_ids); - $_->object_cache_set() foreach @$referenced_bugs; + # inject into object-cache + my $referenced_bugs = Bugzilla::Bug->new_from_list(\@ref_bug_ids); + $_->object_cache_set() foreach @$referenced_bugs; - return $referenced_bug_ids; + return $referenced_bug_ids; } # Extract bug IDs mentioned in comments. This is much faster than calling quoteUrls(). sub _extract_bug_ids { - my $comments = shift; - my @bug_ids; - - my $params = Bugzilla->params; - my @urlbases = ($params->{'urlbase'}); - push(@urlbases, $params->{'sslbase'}) if $params->{'sslbase'}; - my $urlbase_re = '(?:' . join('|', map { qr/$_/ } @urlbases) . ')'; - my $bug_word = template_var('terms')->{bug}; - my $bugs_word = template_var('terms')->{bugs}; - - foreach my $comment (@$comments) { - if ($comment->type == CMT_HAS_DUPE || $comment->type == CMT_DUPE_OF) { - push @bug_ids, $comment->extra_data; - next; - } - my $s = $comment->already_wrapped ? qr/\s/ : qr/\h/; - my $text = $comment->body; - # Full bug links - push @bug_ids, $text =~ /\b$urlbase_re\Qshow_bug.cgi?id=\E([0-9]+)(?:\#c[0-9]+)?/g; - # bug X - my $bug_re = qr/\Q$bug_word\E$s*\#?$s*([0-9]+)/i; - push @bug_ids, $text =~ /\b$bug_re/g; - # bugs X, Y, Z - my $bugs_re = qr/\Q$bugs_word\E$s*\#?$s*([0-9]+)(?:$s*,$s*\#?$s*([0-9]+))+/i; - push @bug_ids, $text =~ /\b$bugs_re/g; - # Old duplicate markers - push @bug_ids, $text =~ /(?<=^\*\*\*\ This\ bug\ has\ been\ marked\ as\ a\ duplicate\ of\ )([0-9]+)(?=\ \*\*\*\Z)/; - } - # Make sure to filter invalid bug IDs. - @bug_ids = grep { $_ < MAX_INT_32 } @bug_ids; - return [uniq @bug_ids]; -} + my $comments = shift; + my @bug_ids; -sub possible_duplicates { - my ($class, $params) = @_; - my $short_desc = $params->{summary}; - my $products = $params->{products} || []; - my $limit = $params->{limit} || MAX_POSSIBLE_DUPLICATES; - $limit = MAX_POSSIBLE_DUPLICATES if $limit > MAX_POSSIBLE_DUPLICATES; - $products = [$products] if !ref($products) eq 'ARRAY'; - - my $orig_limit = $limit; - detaint_natural($limit) - || ThrowCodeError('param_must_be_numeric', - { function => 'possible_duplicates', - param => $orig_limit }); + my $params = Bugzilla->params; + my @urlbases = ($params->{'urlbase'}); + push(@urlbases, $params->{'sslbase'}) if $params->{'sslbase'}; + my $urlbase_re = '(?:' . join('|', map {qr/$_/} @urlbases) . ')'; + my $bug_word = template_var('terms')->{bug}; + my $bugs_word = template_var('terms')->{bugs}; - my $dbh = Bugzilla->dbh; - my $user = Bugzilla->user; - my @words = split(/[\b\s]+/, $short_desc || ''); - # Remove leading/trailing punctuation from words - foreach my $word (@words) { - $word =~ s/(?:^\W+|\W+$)//g; + foreach my $comment (@$comments) { + if ($comment->type == CMT_HAS_DUPE || $comment->type == CMT_DUPE_OF) { + push @bug_ids, $comment->extra_data; + next; } - # And make sure that each word is longer than 2 characters. - @words = grep { defined $_ and length($_) > 2 } @words; + my $s = $comment->already_wrapped ? qr/\s/ : qr/\h/; + my $text = $comment->body; - return [] if !@words; + # Full bug links + push @bug_ids, + $text =~ /\b$urlbase_re\Qshow_bug.cgi?id=\E([0-9]+)(?:\#c[0-9]+)?/g; - my ($where_sql, $relevance_sql); - if ($dbh->FULLTEXT_OR) { - my $joined_terms = join($dbh->FULLTEXT_OR, @words); - ($where_sql, $relevance_sql) = - $dbh->sql_fulltext_search('bugs_fulltext.short_desc', $joined_terms); - $relevance_sql ||= $where_sql; - } - else { - my (@where, @relevance); - foreach my $word (@words) { - my ($term, $rel_term) = $dbh->sql_fulltext_search( - 'bugs_fulltext.short_desc', $word); - push(@where, $term); - push(@relevance, $rel_term || $term); - } + # bug X + my $bug_re = qr/\Q$bug_word\E$s*\#?$s*([0-9]+)/i; + push @bug_ids, $text =~ /\b$bug_re/g; - $where_sql = join(' OR ', @where); - $relevance_sql = join(' + ', @relevance); + # bugs X, Y, Z + my $bugs_re = qr/\Q$bugs_word\E$s*\#?$s*([0-9]+)(?:$s*,$s*\#?$s*([0-9]+))+/i; + push @bug_ids, $text =~ /\b$bugs_re/g; + + # Old duplicate markers + push @bug_ids, $text + =~ /(?<=^\*\*\*\ This\ bug\ has\ been\ marked\ as\ a\ duplicate\ of\ )([0-9]+)(?=\ \*\*\*\Z)/; + } + + # Make sure to filter invalid bug IDs. + @bug_ids = grep { $_ < MAX_INT_32 } @bug_ids; + return [uniq @bug_ids]; +} + +sub possible_duplicates { + my ($class, $params) = @_; + my $short_desc = $params->{summary}; + my $products = $params->{products} || []; + my $limit = $params->{limit} || MAX_POSSIBLE_DUPLICATES; + $limit = MAX_POSSIBLE_DUPLICATES if $limit > MAX_POSSIBLE_DUPLICATES; + $products = [$products] if !ref($products) eq 'ARRAY'; + + my $orig_limit = $limit; + detaint_natural($limit) + || ThrowCodeError('param_must_be_numeric', + {function => 'possible_duplicates', param => $orig_limit}); + + my $dbh = Bugzilla->dbh; + my $user = Bugzilla->user; + my @words = split(/[\b\s]+/, $short_desc || ''); + + # Remove leading/trailing punctuation from words + foreach my $word (@words) { + $word =~ s/(?:^\W+|\W+$)//g; + } + + # And make sure that each word is longer than 2 characters. + @words = grep { defined $_ and length($_) > 2 } @words; + + return [] if !@words; + + my ($where_sql, $relevance_sql); + if ($dbh->FULLTEXT_OR) { + my $joined_terms = join($dbh->FULLTEXT_OR, @words); + ($where_sql, $relevance_sql) + = $dbh->sql_fulltext_search('bugs_fulltext.short_desc', $joined_terms); + $relevance_sql ||= $where_sql; + } + else { + my (@where, @relevance); + foreach my $word (@words) { + my ($term, $rel_term) + = $dbh->sql_fulltext_search('bugs_fulltext.short_desc', $word); + push(@where, $term); + push(@relevance, $rel_term || $term); } - my $product_ids = join(',', map { $_->id } @$products); - my $product_sql = $product_ids ? "AND product_id IN ($product_ids)" : ""; + $where_sql = join(' OR ', @where); + $relevance_sql = join(' + ', @relevance); + } - # Because we collapse duplicates, we want to get slightly more bugs - # than were actually asked for. - my $sql_limit = $limit + 5; + my $product_ids = join(',', map { $_->id } @$products); + my $product_sql = $product_ids ? "AND product_id IN ($product_ids)" : ""; - my $possible_dupes = $dbh->selectall_arrayref( - "SELECT bugs.bug_id AS bug_id, bugs.resolution AS resolution, + # Because we collapse duplicates, we want to get slightly more bugs + # than were actually asked for. + my $sql_limit = $limit + 5; + + my $possible_dupes = $dbh->selectall_arrayref( + "SELECT bugs.bug_id AS bug_id, bugs.resolution AS resolution, ($relevance_sql) AS relevance FROM bugs INNER JOIN bugs_fulltext ON bugs.bug_id = bugs_fulltext.bug_id WHERE ($where_sql) $product_sql - ORDER BY relevance DESC, bug_id DESC " . - $dbh->sql_limit($sql_limit), {Slice=>{}}); - - my @actual_dupe_ids; - # Resolve duplicates into their ultimate target duplicates. - foreach my $bug (@$possible_dupes) { - my $push_id = $bug->{bug_id}; - if ($bug->{resolution} && $bug->{resolution} eq 'DUPLICATE') { - $push_id = _resolve_ultimate_dup_id($bug->{bug_id}); - } - push(@actual_dupe_ids, $push_id); - } - @actual_dupe_ids = uniq @actual_dupe_ids; - if (scalar @actual_dupe_ids > $limit) { - @actual_dupe_ids = @actual_dupe_ids[0..($limit-1)]; + ORDER BY relevance DESC, bug_id DESC " . $dbh->sql_limit($sql_limit), + {Slice => {}} + ); + + my @actual_dupe_ids; + + # Resolve duplicates into their ultimate target duplicates. + foreach my $bug (@$possible_dupes) { + my $push_id = $bug->{bug_id}; + if ($bug->{resolution} && $bug->{resolution} eq 'DUPLICATE') { + $push_id = _resolve_ultimate_dup_id($bug->{bug_id}); } + push(@actual_dupe_ids, $push_id); + } + @actual_dupe_ids = uniq @actual_dupe_ids; + if (scalar @actual_dupe_ids > $limit) { + @actual_dupe_ids = @actual_dupe_ids[0 .. ($limit - 1)]; + } - my $visible = $user->visible_bugs(\@actual_dupe_ids); - return $class->new_from_list($visible); + my $visible = $user->visible_bugs(\@actual_dupe_ids); + return $class->new_from_list($visible); } # Docs for create() (there's no POD in this file yet, but we very @@ -674,597 +841,852 @@ sub possible_duplicates { # # C - An alias for this bug. # C - When this bug is expected to be fixed. +# C - When this bug is expected to be fixed. # C - A string. # C - The initial status of the bug, a string. # C - The URL field. # # C - The full login name of the user who the bug is # initially assigned to. -# C - The full login name of the QA Contact for this bug. +# C - The full login name of the QA Contact for this bug. # Will be ignored if C is off. +# C - The full login name of the Docs Contact for this bug. +# Will be ignored if C is off. # -# C - For time-tracking. Will be ignored if +# C - For time-tracking. Will be ignored if # C is not set, or if the current # user is not a member of the timetrackinggroup. # C - For time-tracking. Will be ignored for the same # reasons as C. +# C - An array of flags that will be applied to the bug. sub create { - my ($class, $params) = @_; - my $dbh = Bugzilla->dbh; - - $dbh->bz_start_transaction(); - - # These fields have default values which we can use if they are undefined. - $params->{bug_severity} = Bugzilla->params->{defaultseverity} - unless defined $params->{bug_severity}; - $params->{priority} = Bugzilla->params->{defaultpriority} - unless defined $params->{priority}; - $params->{op_sys} = Bugzilla->params->{defaultopsys} - unless defined $params->{op_sys}; - $params->{rep_platform} = Bugzilla->params->{defaultplatform} - unless defined $params->{rep_platform}; - # Make sure a comment is always defined. - $params->{comment} = '' unless defined $params->{comment}; - - $class->check_required_create_fields($params); - $params = $class->run_create_validators($params); - - # These are not a fields in the bugs table, so we don't pass them to - # insert_create_data. - my $bug_aliases = delete $params->{alias}; - my $cc_ids = delete $params->{cc}; - my $groups = delete $params->{groups}; - my $depends_on = delete $params->{dependson}; - my $blocked = delete $params->{blocked}; - my $keywords = delete $params->{keywords}; - my $creation_comment = delete $params->{comment}; - my $see_also = delete $params->{see_also}; - - # We don't want the bug to appear in the system until it's correctly - # protected by groups. - my $timestamp = delete $params->{creation_ts}; - - my $ms_values = $class->_extract_multi_selects($params); - my $bug = $class->insert_create_data($params); - - # Add the group restrictions - my $sth_group = $dbh->prepare( - 'INSERT INTO bug_group_map (bug_id, group_id) VALUES (?, ?)'); - foreach my $group (@$groups) { - $sth_group->execute($bug->bug_id, $group->id); - } - - $dbh->do('UPDATE bugs SET creation_ts = ? WHERE bug_id = ?', undef, - $timestamp, $bug->bug_id); - # Update the bug instance as well - $bug->{creation_ts} = $timestamp; - - # Add the CCs - my $sth_cc = $dbh->prepare('INSERT INTO cc (bug_id, who) VALUES (?,?)'); - foreach my $user_id (@$cc_ids) { - $sth_cc->execute($bug->bug_id, $user_id); - } - - # Add in keywords - my $sth_keyword = $dbh->prepare( - 'INSERT INTO keywords (bug_id, keywordid) VALUES (?, ?)'); - foreach my $keyword_id (map($_->id, @$keywords)) { - $sth_keyword->execute($bug->bug_id, $keyword_id); - } - - # Set up dependencies (blocked/dependson) - my $sth_deps = $dbh->prepare( - 'INSERT INTO dependencies (blocked, dependson) VALUES (?, ?)'); - my $sth_bug_time = $dbh->prepare('UPDATE bugs SET delta_ts = ? WHERE bug_id = ?'); - - foreach my $depends_on_id (@$depends_on) { - $sth_deps->execute($bug->bug_id, $depends_on_id); - # Log the reverse action on the other bug. - LogActivityEntry($depends_on_id, 'blocked', '', $bug->bug_id, - $bug->{reporter_id}, $timestamp); - $sth_bug_time->execute($timestamp, $depends_on_id); - } - foreach my $blocked_id (@$blocked) { - $sth_deps->execute($blocked_id, $bug->bug_id); - # Log the reverse action on the other bug. - LogActivityEntry($blocked_id, 'dependson', '', $bug->bug_id, - $bug->{reporter_id}, $timestamp); - $sth_bug_time->execute($timestamp, $blocked_id); - } - - # Insert the values into the multiselect value tables - foreach my $field (keys %$ms_values) { - $dbh->do("DELETE FROM bug_$field where bug_id = ?", - undef, $bug->bug_id); - foreach my $value ( @{$ms_values->{$field}} ) { - $dbh->do("INSERT INTO bug_$field (bug_id, value) VALUES (?,?)", - undef, $bug->bug_id, $value); - } - } - - # Insert any see_also values - if ($see_also) { - my $see_also_array = $see_also; - if (!ref $see_also_array) { - $see_also = trim($see_also); - $see_also_array = [ split(/[\s,]+/, $see_also) ]; - } - foreach my $value (@$see_also_array) { - $bug->add_see_also($value); - } - foreach my $see_also (@{ $bug->see_also }) { - $see_also->insert_create_data($see_also); - } - foreach my $ref_bug (@{ $bug->{_update_ref_bugs} || [] }) { - $ref_bug->update(); - } - delete $bug->{_update_ref_bugs}; - } - - # Comment #0 handling... - - # We now have a bug id so we can fill this out - $creation_comment->{'bug_id'} = $bug->id; - - # Insert the comment. We always insert a comment on bug creation, - # but sometimes it's blank. - Bugzilla::Comment->insert_create_data($creation_comment); - - # Set up aliases - my $sth_aliases = $dbh->prepare('INSERT INTO bugs_aliases (alias, bug_id) VALUES (?, ?)'); - foreach my $alias (@$bug_aliases) { - trick_taint($alias); - $sth_aliases->execute($alias, $bug->bug_id); + my ($class, $params) = @_; + my $dbh = Bugzilla->dbh; + + $dbh->bz_start_transaction(); + + foreach my $fld (@{Bugzilla->fields({obsolete => 0, custom => 1})}) { + next if($fld->name eq 'cf_clone_of'); + if(defined $params->{$fld->name} && !$fld->user_can_edit(Bugzilla->user)){ + ThrowUserError('illegal_change', {field => $fld->name}); + } + } + + my $deleted_params = {}; + Bugzilla::Hook::process('bug_before_create', + {params => $params, deleted_params => $deleted_params}); + + # These fields have default values which we can use if they are undefined. + $params->{bug_severity} = Bugzilla->params->{defaultseverity} + unless defined $params->{bug_severity}; + $params->{priority} = Bugzilla->params->{defaultpriority} + unless defined $params->{priority}; + $params->{op_sys} = Bugzilla->params->{defaultopsys} + unless defined $params->{op_sys}; + $params->{rep_platform} = Bugzilla->params->{defaultplatform} + unless defined $params->{rep_platform}; + + # Make sure a comment is always defined. + $params->{comment} = '' unless defined $params->{comment}; + + $class->check_required_create_fields($params); + $params = $class->run_create_validators($params); + + # These are not a fields in the bugs table, so we don't pass them to + # insert_create_data. + my $bug_aliases = delete $params->{alias}; + my $cc_ids = delete $params->{cc}; + my $groups = delete $params->{groups}; + my $depends_on = delete $params->{dependson}; + my $blocked = delete $params->{blocked}; + my $keywords = delete $params->{keywords}; + my $creation_comment = delete $params->{comment}; + my $see_also = delete $params->{see_also}; + ## REDHAT EXTENSION START 706784 877243 + my $target_release = delete $params->{target_release}; + ## REDHAT EXTENSION END 706784 877243 + + ## REDHAT EXTENSION BEGIN 1175096 + my $extra_components = delete $params->{extra_components}; + my $extra_versions = delete $params->{extra_versions}; + ## REDHAT EXTENSION BEGIN 1175096 + + + ## REDHAT EXTENSION 653316 + my $rh_sub_components = delete $params->{rh_sub_components}; + + # We don't want the bug to appear in the system until it's correctly + # protected by groups. + my $timestamp = delete $params->{creation_ts}; + + my $flags = delete $params->{flags}; + + my $ms_values = $class->_extract_multi_selects($params); + my $ext_values = $class->_extract_single_external($params); + my $bug = $class->insert_create_data($params); + + # Add the group restrictions + my $sth_group + = $dbh->prepare('INSERT INTO bug_group_map (bug_id, group_id) VALUES (?, ?)'); + foreach my $group (@$groups) { + $sth_group->execute($bug->bug_id, $group->id); + } + + $dbh->do('UPDATE bugs SET creation_ts = ? WHERE bug_id = ?', + undef, $timestamp, $bug->bug_id); + + # Update the bug instance as well + $bug->{creation_ts} = $timestamp; + + # Add the CCs + my $sth_cc = $dbh->prepare('INSERT INTO cc (bug_id, who) VALUES (?,?)'); + foreach my $user_id (@$cc_ids) { + $sth_cc->execute($bug->bug_id, $user_id); + } + + # Add in keywords + my $sth_keyword + = $dbh->prepare('INSERT INTO keywords (bug_id, keywordid) VALUES (?, ?)'); + foreach my $keyword_id (map($_->id, @$keywords)) { + $sth_keyword->execute($bug->bug_id, $keyword_id); + } + + # Set up dependencies (blocked/dependson) + my $sth_deps = $dbh->prepare( + 'INSERT INTO dependencies (blocked, dependson) VALUES (?, ?)'); + my $sth_bug_time + = $dbh->prepare('UPDATE bugs SET delta_ts = ? WHERE bug_id = ?'); + + foreach my $depends_on_id (@$depends_on) { + $sth_deps->execute($bug->bug_id, $depends_on_id); + + # Log the reverse action on the other bug. + LogActivityEntry($depends_on_id, 'blocked', '', $bug->bug_id, + $bug->{reporter_id}, $timestamp); + $sth_bug_time->execute($timestamp, $depends_on_id); + } + foreach my $blocked_id (@$blocked) { + $sth_deps->execute($blocked_id, $bug->bug_id); + + # Log the reverse action on the other bug. + LogActivityEntry($blocked_id, 'dependson', '', $bug->bug_id, + $bug->{reporter_id}, $timestamp); + $sth_bug_time->execute($timestamp, $blocked_id); + } + + # Insert the values into the multiselect value tables + foreach my $field (keys %$ms_values) { + $dbh->do("DELETE FROM bug_$field where bug_id = ?", undef, $bug->bug_id); + foreach my $value (@{$ms_values->{$field}}) { + $dbh->do("INSERT INTO bug_$field (bug_id, value) VALUES (?,?)", + undef, $bug->bug_id, $value); + } + } + + # Insert the values into the multiselect value tables + foreach my $field (keys %$ext_values) { + $dbh->do("DELETE FROM bug_$field where bug_id = ?", undef, $bug->bug_id); + $dbh->do("INSERT INTO bug_$field (bug_id, value) VALUES (?,?)", + undef, $bug->bug_id, $ext_values->{$field}) + if ($ext_values->{$field}); + } + + ## REDHAT EXTENSION 1188085 BEGIN + # Apply any flags. + if (defined $flags) { + $bug->set_flags($flags); + foreach my $flag (@{$bug->flags}) { + Bugzilla::Flag->create($flag); + } + + delete $bug->{'flag_types'}; + } + ## REDHAT EXTENSION 1188085 END + + # Insert any see_also values + if ($see_also) { + my $see_also_array = $see_also; + if (!ref $see_also_array) { + $see_also = trim($see_also); + $see_also_array = [split(/[\s,]+/, $see_also)]; + } + foreach my $value (@$see_also_array) { + $bug->add_see_also($value); + } + foreach my $see_also (@{$bug->see_also}) { + $see_also->insert_create_data($see_also); + } + foreach my $ref_bug (@{$bug->{_update_ref_bugs} || []}) { + $ref_bug->update(); + } + delete $bug->{_update_ref_bugs}; + } + + # Comment #0 handling... + + # We now have a bug id so we can fill this out + $creation_comment->{'bug_id'} = $bug->id; + + # Insert the comment. We always insert a comment on bug creation, + # but sometimes it's blank. + Bugzilla::Comment->insert_create_data($creation_comment); + + # Set up aliases + my $sth_aliases + = $dbh->prepare('INSERT INTO bugs_aliases (alias, bug_id) VALUES (?, ?)'); + foreach my $alias (@$bug_aliases) { + trick_taint($alias); + $sth_aliases->execute($alias, $bug->bug_id); + } + + ## REDHAT EXTENSION START 706784 877243 + # Add the target releases values + my $sth_release + = $dbh->prepare('INSERT INTO bugs_release (value, bug_id) VALUES (?, ?)'); + foreach my $release (@$target_release) { + trick_taint($release); + $sth_release->execute($release, $bug->bug_id); + } + ## REDHAT EXTENSION END 706784 877243 + + ## REDHAT EXTENSION START 653316 + # Add the sub component values + if (Bugzilla->params->{usesubcomponents}) { + my $sth_comp + = $dbh->prepare( + 'INSERT INTO bug_rh_sub_components (rh_sub_component_id, bug_id) VALUES (?, ?)' + ); + foreach my $sub_comp (@$rh_sub_components) { + $sth_comp->execute($sub_comp->id, $bug->bug_id); + } + } + ## REDHAT EXTENSION END 653316 + + ## REDHAT EXTENSION BEGIN 1175096 + if (defined($extra_components)) { + my $query = 'INSERT INTO rh_multiple_bug_components (bug_id, component_id)' + . ' VALUES(?, (SELECT id FROM components WHERE name = ? AND product_id = ?))'; + my $sth_mc = $dbh->prepare($query); + foreach my $comp (@$extra_components) { + trick_taint($comp); + $sth_mc->execute($bug->id, $comp, $bug->product_id); + } + } + if (defined($extra_versions)) { + my $query = 'INSERT INTO rh_multiple_bug_versions (bug_id, version_id)' + . ' VALUES(?, (SELECT id FROM versions WHERE value = ? AND product_id = ?))'; + my $sth_mv = $dbh->prepare($query); + foreach my $ver (@$extra_versions) { + trick_taint($ver); + $sth_mv->execute($bug->id, $ver, $bug->product_id); + } + } + ## REDHAT EXTENSION END 1175096 + + Bugzilla::Hook::process( + 'bug_end_of_create', + { + bug => $bug, + timestamp => $timestamp, + params => $params, + deleted_params => $deleted_params } + ); - Bugzilla::Hook::process('bug_end_of_create', { bug => $bug, - timestamp => $timestamp, - }); + ## REDHAT EXTENSION START 1171556 + $bug->_sync_fulltext(new_bug => 1) if (!Bugzilla->is_mysql()); + ## REDHAT EXTENSION END 1171556 - $dbh->bz_commit_transaction(); + $dbh->bz_commit_transaction(); - # Because MySQL doesn't support transactions on the fulltext table, - # we do this after we've committed the transaction. That way we're - # sure we're inserting a good Bug ID. - $bug->_sync_fulltext( new_bug => 1 ); + # Because MySQL doesn't support transactions on the fulltext table, + # we do this after we've committed the transaction. That way we're + # sure we're inserting a good Bug ID. + ## REDHAT EXTENSION START 1171556 + $bug->_sync_fulltext(new_bug => 1) if (Bugzilla->is_mysql()); + ## REDHAT EXTENSION END 1171556 - return $bug; + return $bug; } sub run_create_validators { - my $class = shift; - my $params = $class->SUPER::run_create_validators(@_); + my $class = shift; + my $params = $class->SUPER::run_create_validators(@_); - # Add classification for checking mandatory fields which depend on it - $params->{classification} = $params->{product}->classification->name; + # Add classification for checking mandatory fields which depend on it + $params->{classification} = $params->{product}->classification->name; - my @mandatory_fields = @{ Bugzilla->fields({ is_mandatory => 1, - enter_bug => 1, - obsolete => 0 }) }; - foreach my $field (@mandatory_fields) { - $class->_check_field_is_mandatory($params->{$field->name}, $field, - $params); - } + my @mandatory_fields + = @{Bugzilla->fields({is_mandatory => 1, enter_bug => 1, obsolete => 0})}; + foreach my $field (@mandatory_fields) { + $class->_check_field_is_mandatory($params->{$field->name}, $field, $params); + } - my $product = delete $params->{product}; - $params->{product_id} = $product->id; - my $component = delete $params->{component}; - $params->{component_id} = $component->id; + my $product = delete $params->{product}; + $params->{product_id} = $product->id; + my $component = delete $params->{component}; + $params->{component_id} = $component->id; - # Callers cannot set reporter, creation_ts, or delta_ts. - $params->{reporter} = $class->_check_reporter(); - $params->{delta_ts} = $params->{creation_ts}; + # Callers cannot set reporter, creation_ts, or delta_ts. + $params->{reporter} = $class->_check_reporter(); + $params->{delta_ts} = $params->{creation_ts}; - if ($params->{estimated_time}) { - $params->{remaining_time} = $params->{estimated_time}; - } + if ($params->{estimated_time}) { + $params->{remaining_time} = $params->{estimated_time}; + } - $class->_check_strict_isolation($params->{cc}, $params->{assigned_to}, - $params->{qa_contact}, $product); + ## REDHAT EXTENSION 876015: Add docs_contact + $class->_check_strict_isolation($params->{cc}, $params->{assigned_to}, + $params->{qa_contact}, $params->{docs_contact}, $product); - # You can't set these fields. - delete $params->{lastdiffed}; - delete $params->{bug_id}; - delete $params->{classification}; + # You can't set these fields. + delete $params->{lastdiffed}; + delete $params->{bug_id}; + delete $params->{classification}; - Bugzilla::Hook::process('bug_end_of_create_validators', - { params => $params }); + Bugzilla::Hook::process('bug_end_of_create_validators', {params => $params}); - # And this is not a valid DB field, it's just used as part of - # _check_dependencies to avoid running it twice for both blocked - # and dependson. - delete $params->{_dependencies_validated}; + # And this is not a valid DB field, it's just used as part of + # _check_dependencies to avoid running it twice for both blocked + # and dependson. + delete $params->{_dependencies_validated}; - return $params; + return $params; } sub update { - my $self = shift; - my $dbh = Bugzilla->dbh; - my $user = Bugzilla->user; + my $self = shift; + my $dbh = Bugzilla->dbh; + my $user = Bugzilla->user; - # XXX This is just a temporary hack until all updating happens - # inside this function. - my $delta_ts = shift || $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); + # XXX This is just a temporary hack until all updating happens + # inside this function. + my $delta_ts = shift || $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); - $dbh->bz_start_transaction(); + $dbh->bz_start_transaction(); - my ($changes, $old_bug) = $self->SUPER::update(@_); + my ($changes, $old_bug) = $self->SUPER::update(@_); - Bugzilla::Hook::process('bug_start_of_update', - { timestamp => $delta_ts, bug => $self, - old_bug => $old_bug, changes => $changes }); - - # Certain items in $changes have to be fixed so that they hold - # a name instead of an ID. - foreach my $field (qw(product_id component_id)) { - my $change = delete $changes->{$field}; - if ($change) { - my $new_field = $field; - $new_field =~ s/_id$//; - $changes->{$new_field} = - [$self->{"_old_${new_field}_name"}, $self->$new_field]; - } - } - foreach my $field (qw(qa_contact assigned_to)) { - if ($changes->{$field}) { - my ($from, $to) = @{ $changes->{$field} }; - $from = $old_bug->$field->login if $from; - $to = $self->$field->login if $to; - $changes->{$field} = [$from, $to]; - } - } - - # CC - my @old_cc = map {$_->id} @{$old_bug->cc_users}; - my @new_cc = map {$_->id} @{$self->cc_users}; - my ($removed_cc, $added_cc) = diff_arrays(\@old_cc, \@new_cc); - - if (scalar @$removed_cc) { - $dbh->do('DELETE FROM cc WHERE bug_id = ? AND ' - . $dbh->sql_in('who', $removed_cc), undef, $self->id); - } - foreach my $user_id (@$added_cc) { - $dbh->do('INSERT INTO cc (bug_id, who) VALUES (?,?)', - undef, $self->id, $user_id); - } - # If any changes were found, record it in the activity log - if (scalar @$removed_cc || scalar @$added_cc) { - my $removed_users = Bugzilla::User->new_from_list($removed_cc); - my $added_users = Bugzilla::User->new_from_list($added_cc); - my $removed_names = join(', ', (map {$_->login} @$removed_users)); - my $added_names = join(', ', (map {$_->login} @$added_users)); - $changes->{cc} = [$removed_names, $added_names]; - } - - # Aliases - my $old_aliases = $old_bug->alias; - my $new_aliases = $self->alias; - my ($removed_aliases, $added_aliases) = diff_arrays($old_aliases, $new_aliases); - - foreach my $alias (@$removed_aliases) { - $dbh->do('DELETE FROM bugs_aliases WHERE bug_id = ? AND alias = ?', - undef, $self->id, $alias); - } - foreach my $alias (@$added_aliases) { - trick_taint($alias); - $dbh->do('INSERT INTO bugs_aliases (bug_id, alias) VALUES (?,?)', - undef, $self->id, $alias); - } - # If any changes were found, record it in the activity log - if (scalar @$removed_aliases || scalar @$added_aliases) { - $changes->{alias} = [join(', ', @$removed_aliases), join(', ', @$added_aliases)]; - } - - # Keywords - my @old_kw_ids = map { $_->id } @{$old_bug->keyword_objects}; - my @new_kw_ids = map { $_->id } @{$self->keyword_objects}; - - my ($removed_kw, $added_kw) = diff_arrays(\@old_kw_ids, \@new_kw_ids); - - if (scalar @$removed_kw) { - $dbh->do('DELETE FROM keywords WHERE bug_id = ? AND ' - . $dbh->sql_in('keywordid', $removed_kw), undef, $self->id); - } - foreach my $keyword_id (@$added_kw) { - $dbh->do('INSERT INTO keywords (bug_id, keywordid) VALUES (?,?)', - undef, $self->id, $keyword_id); - } - # If any changes were found, record it in the activity log - if (scalar @$removed_kw || scalar @$added_kw) { - my $removed_keywords = Bugzilla::Keyword->new_from_list($removed_kw); - my $added_keywords = Bugzilla::Keyword->new_from_list($added_kw); - my $removed_names = join(', ', (map {$_->name} @$removed_keywords)); - my $added_names = join(', ', (map {$_->name} @$added_keywords)); - $changes->{keywords} = [$removed_names, $added_names]; - } - - # Dependencies - foreach my $pair ([qw(dependson blocked)], [qw(blocked dependson)]) { - my ($type, $other) = @$pair; - my $old = $old_bug->$type; - my $new = $self->$type; - - my ($removed, $added) = diff_arrays($old, $new); - foreach my $removed_id (@$removed) { - $dbh->do("DELETE FROM dependencies WHERE $type = ? AND $other = ?", - undef, $removed_id, $self->id); - - # Add an activity entry for the other bug. - LogActivityEntry($removed_id, $other, $self->id, '', - $user->id, $delta_ts); - # Update delta_ts on the other bug so that we trigger mid-airs. - $dbh->do('UPDATE bugs SET delta_ts = ? WHERE bug_id = ?', - undef, $delta_ts, $removed_id); - } - foreach my $added_id (@$added) { - $dbh->do("INSERT INTO dependencies ($type, $other) VALUES (?,?)", - undef, $added_id, $self->id); - - # Add an activity entry for the other bug. - LogActivityEntry($added_id, $other, '', $self->id, - $user->id, $delta_ts); - # Update delta_ts on the other bug so that we trigger mid-airs. - $dbh->do('UPDATE bugs SET delta_ts = ? WHERE bug_id = ?', - undef, $delta_ts, $added_id); - } - - if (scalar(@$removed) || scalar(@$added)) { - $changes->{$type} = [join(', ', @$removed), join(', ', @$added)]; - } - } - - # Groups - my %old_groups = map {$_->id => $_} @{$old_bug->groups_in}; - my %new_groups = map {$_->id => $_} @{$self->groups_in}; - my ($removed_gr, $added_gr) = diff_arrays([keys %old_groups], - [keys %new_groups]); - if (scalar @$removed_gr || scalar @$added_gr) { - if (@$removed_gr) { - my $qmarks = join(',', ('?') x @$removed_gr); - $dbh->do("DELETE FROM bug_group_map - WHERE bug_id = ? AND group_id IN ($qmarks)", undef, - $self->id, @$removed_gr); - } - my $sth_insert = $dbh->prepare( - 'INSERT INTO bug_group_map (bug_id, group_id) VALUES (?,?)'); - foreach my $gid (@$added_gr) { - $sth_insert->execute($self->id, $gid); - } - my @removed_names = map { $old_groups{$_}->name } @$removed_gr; - my @added_names = map { $new_groups{$_}->name } @$added_gr; - $changes->{'bug_group'} = [join(', ', @removed_names), - join(', ', @added_names)]; - } - - # Comments - foreach my $comment (@{$self->{added_comments} || []}) { - # Override the Comment's timestamp to be identical to the update - # timestamp. - $comment->{bug_when} = $delta_ts; - $comment = Bugzilla::Comment->insert_create_data($comment); - if ($comment->work_time) { - LogActivityEntry($self->id, "work_time", "", $comment->work_time, - $user->id, $delta_ts); - } - } - - # Comment Privacy - foreach my $comment (@{$self->{comment_isprivate} || []}) { - $comment->update(); - - my ($from, $to) - = $comment->is_private ? (0, 1) : (1, 0); - LogActivityEntry($self->id, "longdescs.isprivate", $from, $to, - $user->id, $delta_ts, $comment->id); - } - - # Clear the cache of comments - delete $self->{comments}; - - # Insert the values into the multiselect value tables - my @multi_selects = grep {$_->type == FIELD_TYPE_MULTI_SELECT} - Bugzilla->active_custom_fields; - foreach my $field (@multi_selects) { - my $name = $field->name; - my ($removed, $added) = diff_arrays($old_bug->$name, $self->$name); - if (scalar @$removed || scalar @$added) { - $changes->{$name} = [join(', ', @$removed), join(', ', @$added)]; - - $dbh->do("DELETE FROM bug_$name where bug_id = ?", - undef, $self->id); - foreach my $value (@{$self->$name}) { - $dbh->do("INSERT INTO bug_$name (bug_id, value) VALUES (?,?)", - undef, $self->id, $value); - } - } - } - - # See Also - - my ($removed_see, $added_see) = - diff_arrays($old_bug->see_also, $self->see_also, 'name'); - - $_->remove_from_db foreach @$removed_see; - $_->insert_create_data($_) foreach @$added_see; - - # If any changes were found, record it in the activity log - if (scalar @$removed_see || scalar @$added_see) { - $changes->{see_also} = [join(', ', map { $_->name } @$removed_see), - join(', ', map { $_->name } @$added_see)]; - } - - # Flags - my ($removed, $added) = Bugzilla::Flag->update_flags($self, $old_bug, $delta_ts); - if ($removed || $added) { - $changes->{'flagtypes.name'} = [$removed, $added]; - } - - $_->update foreach @{ $self->{_update_ref_bugs} || [] }; - delete $self->{_update_ref_bugs}; - - # Log bugs_activity items - # XXX Eventually, when bugs_activity is able to track the dupe_id, - # this code should go below the duplicates-table-updating code below. - foreach my $field (keys %$changes) { - my $change = $changes->{$field}; - my $from = defined $change->[0] ? $change->[0] : ''; - my $to = defined $change->[1] ? $change->[1] : ''; - LogActivityEntry($self->id, $field, $from, $to, - $user->id, $delta_ts); - } - - # Check if we have to update the duplicates table and the other bug. - my ($old_dup, $cur_dup) = ($old_bug->dup_id || 0, $self->dup_id || 0); - if ($old_dup != $cur_dup) { - $dbh->do("DELETE FROM duplicates WHERE dupe = ?", undef, $self->id); - if ($cur_dup) { - $dbh->do('INSERT INTO duplicates (dupe, dupe_of) VALUES (?,?)', - undef, $self->id, $cur_dup); - if (my $update_dup = delete $self->{_dup_for_update}) { - $update_dup->update(); - } - } - - $changes->{'dup_id'} = [$old_dup || undef, $cur_dup || undef]; - } - - Bugzilla::Hook::process('bug_end_of_update', - { bug => $self, timestamp => $delta_ts, changes => $changes, - old_bug => $old_bug }); - - # If any change occurred, refresh the timestamp of the bug. - if (scalar(keys %$changes) || $self->{added_comments} - || $self->{comment_isprivate}) + Bugzilla::Hook::process( + 'bug_start_of_update', { - $dbh->do('UPDATE bugs SET delta_ts = ? WHERE bug_id = ?', - undef, ($delta_ts, $self->id)); - $self->{delta_ts} = $delta_ts; - } - - # Update last-visited - if ($user->is_involved_in_bug($self)) { - $self->update_user_last_visit($user, $delta_ts); - } - - # If a user is no longer involved, remove their last visit entry - my $last_visits = - Bugzilla::BugUserLastVisit->match({ bug_id => $self->id }); - foreach my $lv (@$last_visits) { - $lv->remove_from_db() unless $lv->user->is_involved_in_bug($self); - } - - # Update bug ignore data if user wants to ignore mail for this bug - if (exists $self->{'bug_ignored'}) { - my $bug_ignored_changed; - if ($self->{'bug_ignored'} && !$user->is_bug_ignored($self->id)) { - $dbh->do('INSERT INTO email_bug_ignore - (user_id, bug_id) VALUES (?, ?)', - undef, $user->id, $self->id); - $bug_ignored_changed = 1; - - } - elsif (!$self->{'bug_ignored'} && $user->is_bug_ignored($self->id)) { - $dbh->do('DELETE FROM email_bug_ignore - WHERE user_id = ? AND bug_id = ?', - undef, $user->id, $self->id); - $bug_ignored_changed = 1; - } - delete $user->{bugs_ignored} if $bug_ignored_changed; - } - - $dbh->bz_commit_transaction(); - - # The only problem with this here is that update() is often called - # in the middle of a transaction, and if that transaction is rolled - # back, this change will *not* be rolled back. As we expect rollbacks - # to be extremely rare, that is OK for us. - $self->_sync_fulltext( - update_short_desc => $changes->{short_desc}, - update_comments => $self->{added_comments} || $self->{comment_isprivate} + timestamp => $delta_ts, + bug => $self, + old_bug => $old_bug, + changes => $changes + } + ); + + # Certain items in $changes have to be fixed so that they hold + # a name instead of an ID. + foreach my $field (qw(product_id component_id)) { + my $change = delete $changes->{$field}; + if ($change) { + my $new_field = $field; + $new_field =~ s/_id$//; + $changes->{$new_field} = [$self->{"_old_${new_field}_name"}, $self->$new_field]; + } + } + ## REDHAT EXTENSION 876015: Add docs_contact + foreach my $field (qw(qa_contact docs_contact assigned_to)) { + if ($changes->{$field}) { + my ($from, $to) = @{$changes->{$field}}; + $from = $old_bug->$field->login if $from; + $to = $self->$field->login if $to; + $changes->{$field} = [$from, $to]; + } + } + + # CC + my @old_cc = map { $_->id } @{$old_bug->cc_users}; + my @new_cc = map { $_->id } @{$self->cc_users}; + my ($removed_cc, $added_cc) = diff_arrays(\@old_cc, \@new_cc); + + if (scalar @$removed_cc) { + $dbh->do( + 'DELETE FROM cc WHERE bug_id = ? AND ' . $dbh->sql_in('who', $removed_cc), + undef, $self->id); + } + foreach my $user_id (@$added_cc) { + $dbh->do('INSERT INTO cc (bug_id, who) VALUES (?,?)', + undef, $self->id, $user_id); + } + + # If any changes were found, record it in the activity log + if (scalar @$removed_cc || scalar @$added_cc) { + my $removed_users = Bugzilla::User->new_from_list($removed_cc); + my $added_users = Bugzilla::User->new_from_list($added_cc); + my $removed_names = join(', ', (map { $_->login } @$removed_users)); + my $added_names = join(', ', (map { $_->login } @$added_users)); + $changes->{cc} = [$removed_names, $added_names]; + } + + # Aliases + my $old_aliases = $old_bug->alias; + my $new_aliases = $self->alias; + my ($removed_aliases, $added_aliases) = diff_arrays($old_aliases, $new_aliases); + + foreach my $alias (@$removed_aliases) { + $dbh->do('DELETE FROM bugs_aliases WHERE bug_id = ? AND alias = ?', + undef, $self->id, $alias); + } + foreach my $alias (@$added_aliases) { + trick_taint($alias); + $dbh->do('INSERT INTO bugs_aliases (bug_id, alias) VALUES (?,?)', + undef, $self->id, $alias); + } + + # If any changes were found, record it in the activity log + if (scalar @$removed_aliases || scalar @$added_aliases) { + $changes->{alias} + = [join(', ', @$removed_aliases), join(', ', @$added_aliases)]; + } + + + ## REDHAT EXTENSION START 706784 877243 + my $old_releases = $old_bug->target_release; + my $new_releases = $self->target_release; + my ($removed_releases, $added_releases) + = diff_arrays($old_releases, $new_releases); + + foreach my $release (@$removed_releases) { + $dbh->do('DELETE FROM bugs_release WHERE bug_id = ? AND value = ?', + undef, $self->id, $release); + } + foreach my $release (@$added_releases) { + trick_taint($release); + $dbh->do('INSERT INTO bugs_release (bug_id, value) VALUES (?,?)', + undef, $self->id, $release); + } + + # If any changes were found, record it in the activity log + if (scalar @$removed_releases || scalar @$added_releases) { + $changes->{target_release} + = [join(', ', @$removed_releases), join(', ', @$added_releases)]; + } + ## REDHAT EXTENSION END 706784 877243 + + # Keywords + my @old_kw_ids = map { $_->id } @{$old_bug->keyword_objects}; + my @new_kw_ids = map { $_->id } @{$self->keyword_objects}; + + my ($removed_kw, $added_kw) = diff_arrays(\@old_kw_ids, \@new_kw_ids); + + if (scalar @$removed_kw) { + $dbh->do( + 'DELETE FROM keywords WHERE bug_id = ? AND ' + . $dbh->sql_in('keywordid', $removed_kw), + undef, $self->id ); - - # Remove obsolete internal variables. - delete $self->{'_old_assigned_to'}; - delete $self->{'_old_qa_contact'}; - - # Also flush the visible_bugs cache for this bug as the user's - # relationship with this bug may have changed. - delete $user->{_visible_bugs_cache}->{$self->id}; - - return $changes; + } + foreach my $keyword_id (@$added_kw) { + $dbh->do('INSERT INTO keywords (bug_id, keywordid) VALUES (?,?)', + undef, $self->id, $keyword_id); + } + + # If any changes were found, record it in the activity log + if (scalar @$removed_kw || scalar @$added_kw) { + my $removed_keywords = Bugzilla::Keyword->new_from_list($removed_kw); + my $added_keywords = Bugzilla::Keyword->new_from_list($added_kw); + my $removed_names = join(', ', (map { $_->name } @$removed_keywords)); + my $added_names = join(', ', (map { $_->name } @$added_keywords)); + $changes->{keywords} = [$removed_names, $added_names]; + } + + # Dependencies + foreach my $pair ([qw(dependson blocked)], [qw(blocked dependson)]) { + my ($type, $other) = @$pair; + my $old = $old_bug->$type; + my $new = $self->$type; + + my ($removed, $added) = diff_arrays($old, $new); + foreach my $removed_id (@$removed) { + $dbh->do("DELETE FROM dependencies WHERE $type = ? AND $other = ?", + undef, $removed_id, $self->id); + + # Add an activity entry for the other bug. + LogActivityEntry($removed_id, $other, $self->id, '', $user->id, $delta_ts); + + # Update delta_ts on the other bug so that we trigger mid-airs. + $dbh->do('UPDATE bugs SET delta_ts = ? WHERE bug_id = ?', + undef, $delta_ts, $removed_id); + } + foreach my $added_id (@$added) { + $dbh->do("INSERT INTO dependencies ($type, $other) VALUES (?,?)", + undef, $added_id, $self->id); + + # Add an activity entry for the other bug. + LogActivityEntry($added_id, $other, '', $self->id, $user->id, $delta_ts); + + # Update delta_ts on the other bug so that we trigger mid-airs. + $dbh->do('UPDATE bugs SET delta_ts = ? WHERE bug_id = ?', + undef, $delta_ts, $added_id); + } + + if (scalar(@$removed) || scalar(@$added)) { + $changes->{$type} = [join(', ', @$removed), join(', ', @$added)]; + } + } + + # Groups + my %old_groups = map { $_->id => $_ } @{$old_bug->groups_in}; + my %new_groups = map { $_->id => $_ } @{$self->groups_in}; + my ($removed_gr, $added_gr) + = diff_arrays([keys %old_groups], [keys %new_groups]); + if (scalar @$removed_gr || scalar @$added_gr) { + if (@$removed_gr) { + my $qmarks = join(',', ('?') x @$removed_gr); + $dbh->do( + "DELETE FROM bug_group_map + WHERE bug_id = ? AND group_id IN ($qmarks)", undef, $self->id, + @$removed_gr + ); + } + my $sth_insert + = $dbh->prepare('INSERT INTO bug_group_map (bug_id, group_id) VALUES (?,?)'); + foreach my $gid (@$added_gr) { + $sth_insert->execute($self->id, $gid); + } + my @removed_names = map { $old_groups{$_}->name } @$removed_gr; + my @added_names = map { $new_groups{$_}->name } @$added_gr; + $changes->{'bug_group'} + = [join(', ', @removed_names), join(', ', @added_names)]; + } + + # Comments + foreach my $comment (@{$self->{added_comments} || []}) { + + # Override the Comment's timestamp to be identical to the update + # timestamp. + $comment->{bug_when} = $delta_ts; + $comment = Bugzilla::Comment->insert_create_data($comment); + if ($comment->work_time) { + LogActivityEntry($self->id, "work_time", "", $comment->work_time, $user->id, + $delta_ts); + } + } + + # Comment Privacy + foreach my $comment (@{$self->{comment_isprivate} || []}) { + $comment->update(); + + my ($from, $to) = $comment->is_private ? (0, 1) : (1, 0); + LogActivityEntry($self->id, "longdescs.isprivate", $from, $to, $user->id, + $delta_ts, $comment->id); + } + + # Clear the cache of comments + delete $self->{comments}; + + # Insert the values into the multiselect value tables + my @multi_selects + = grep { $_->type == FIELD_TYPE_MULTI_SELECT } Bugzilla->active_custom_fields; + foreach my $field (@multi_selects) { + my $name = $field->name; + my ($removed, $added) = diff_arrays($old_bug->$name, $self->$name); + if (scalar @$removed || scalar @$added) { + $changes->{$name} = [join(', ', @$removed), join(', ', @$added)]; + + $dbh->do("DELETE FROM bug_$name where bug_id = ?", undef, $self->id); + foreach my $value (@{$self->$name}) { + $dbh->do("INSERT INTO bug_$name (bug_id, value) VALUES (?,?)", + undef, $self->id, $value); + } + } + } + + my @single_selects + = grep { $_->type == FIELD_TYPE_ONE_SELECT } Bugzilla->active_custom_fields; + foreach my $field (@single_selects) { + my $name = $field->name; + my ($removed, $added) = ($old_bug->$name, $self->$name); + if (($old_bug->$name || "") ne ($self->$name || "")) { + $changes->{$name} = [$old_bug->$name, $self->$name]; + + $dbh->do("DELETE FROM bug_$name where bug_id = ?", undef, $self->id); + if ($self->$name && $self->{$name} ne '') { + $dbh->do("INSERT INTO bug_$name (bug_id, value) VALUES (?,?)", + undef, $self->id, $self->{$name}); + } + } + } + + # See Also + + my ($removed_see, $added_see) + = diff_arrays($old_bug->see_also, $self->see_also, 'name'); + + $_->remove_from_db foreach @$removed_see; + $_->insert_create_data($_) foreach @$added_see; + + # If any changes were found, record it in the activity log + if (scalar @$removed_see || scalar @$added_see) { + $changes->{see_also} = [ + join(', ', map { $_->name } @$removed_see), + join(', ', map { $_->name } @$added_see) + ]; + } + + ## REDHAT EXTENSION START 861276 + $_->{_skip_extra_change} = 1 foreach @{$self->{_update_ref_bugs} || []}; + ## REDHAT EXTENSION END 861276 + + # Flags + my ($removed, $added) + = Bugzilla::Flag->update_flags($self, $old_bug, $delta_ts); + if ($removed || $added) { + $changes->{'flagtypes.name'} = [$removed, $added]; + } + + $_->update foreach @{$self->{_update_ref_bugs} || []}; + delete $self->{_update_ref_bugs}; + + ## REDHAT EXTENSION BEGIN 584954 + Bugzilla::Hook::process('bug_modify_changes', {changes => $changes}); + ## REDHAT EXTENSION END 584954 + + # Log bugs_activity items + # XXX Eventually, when bugs_activity is able to track the dupe_id, + # this code should go below the duplicates-table-updating code below. + foreach my $field (keys %$changes) { + my $change = $changes->{$field}; + my $from = defined $change->[0] ? $change->[0] : ''; + my $to = defined $change->[1] ? $change->[1] : ''; + + ## REDHAT EXTENSIONS START 442504 + my $do_log_activity = 1; + Bugzilla::Hook::process( + 'bug_log_activity', + { + bug => $self, + field => $field, + old_value => $from, + new_value => $to, + do_log_activity => \$do_log_activity + } + ); + next if !$do_log_activity; + ## REDHAT EXTENSIONS END 442504 + # REDHAT EXTENSION START 1757211 + LogActivityEntry( + $self->id, $field, $from, $to, $user->id, $delta_ts, + undef, undef, $self, $old_bug + ); + ## REDHAT EXTENSION END 1757211 + } + + # Check if we have to update the duplicates table and the other bug. + my ($old_dup, $cur_dup) = ($old_bug->dup_id || 0, $self->dup_id || 0); + if ($old_dup != $cur_dup) { + $dbh->do("DELETE FROM duplicates WHERE dupe = ?", undef, $self->id); + if ($cur_dup) { + $dbh->do('INSERT INTO duplicates (dupe, dupe_of) VALUES (?,?)', + undef, $self->id, $cur_dup); + if (my $update_dup = delete $self->{_dup_for_update}) { + ## REDHAT EXTENSION START 861276 + $update_dup->{_skip_extra_change} = 1; + ## REDHAT EXTENSION END 861276 + + $update_dup->update(); + } + } + + $changes->{'dup_id'} = [$old_dup || undef, $cur_dup || undef]; + } + + Bugzilla::Hook::process( + 'bug_end_of_update', + { + bug => $self, + timestamp => $delta_ts, + changes => $changes, + old_bug => $old_bug + } + ); + + # If any change occurred, refresh the timestamp of the bug. + if ( scalar(keys %$changes) + || $self->{added_comments} + || $self->{comment_isprivate}) + { + $dbh->do('UPDATE bugs SET delta_ts = ? WHERE bug_id = ?', + undef, ($delta_ts, $self->id)); + $self->{delta_ts} = $delta_ts; + } + + # Update last-visited + if ($user->is_involved_in_bug($self)) { + $self->update_user_last_visit($user, $delta_ts); + } + + # If a user is no longer involved, remove their last visit entry + my $last_visits = Bugzilla::BugUserLastVisit->match({bug_id => $self->id}); + foreach my $lv (@$last_visits) { + $lv->remove_from_db() unless $lv->user->is_involved_in_bug($self); + } + + # Update bug ignore data if user wants to ignore mail for this bug + if (exists $self->{'bug_ignored'}) { + my $bug_ignored_changed; + if ($self->{'bug_ignored'} && !$user->is_bug_ignored($self->id)) { + $dbh->do( + 'INSERT INTO email_bug_ignore + (user_id, bug_id) VALUES (?, ?)', undef, $user->id, $self->id + ); + $bug_ignored_changed = 1; + + } + elsif (!$self->{'bug_ignored'} && $user->is_bug_ignored($self->id)) { + $dbh->do( + 'DELETE FROM email_bug_ignore + WHERE user_id = ? AND bug_id = ?', undef, $user->id, $self->id + ); + $bug_ignored_changed = 1; + } + delete $user->{bugs_ignored} if $bug_ignored_changed; + } + + $dbh->bz_commit_transaction(); + + # The only problem with this here is that update() is often called + # in the middle of a transaction, and if that transaction is rolled + # back, this change will *not* be rolled back. As we expect rollbacks + # to be extremely rare, that is OK for us. + $self->_sync_fulltext( + update_short_desc => $changes->{short_desc}, + update_comments => $self->{added_comments} + || $self->{comment_isprivate} + || exists $changes->{'longdesc'} ## REDHAT EXTENSION 420461 + ); + + # Remove obsolete internal variables. + delete $self->{'_old_assigned_to'}; + delete $self->{'_old_qa_contact'}; + ## REDHAT EXTENSION 876015: Add docs_contact + delete $self->{'_old_docs_contact'}; + + # Also flush the visible_bugs cache for this bug as the user's + # relationship with this bug may have changed. + delete $user->{_visible_bugs_cache}->{$self->id}; + + ## REDHAT EXTENSION START 1287318 + return wantarray ? ($changes, $delta_ts, $old_bug) : $changes; + ## REDHAT EXTENSION END 1287318 } # Used by create(). # We need to handle multi-select fields differently than normal fields, # because they're arrays and don't go into the bugs table. sub _extract_multi_selects { - my ($invocant, $params) = @_; - - my @multi_selects = grep {$_->type == FIELD_TYPE_MULTI_SELECT} - Bugzilla->active_custom_fields; - my %ms_values; - foreach my $field (@multi_selects) { - my $name = $field->name; - if (exists $params->{$name}) { - my $array = delete($params->{$name}) || []; - $ms_values{$name} = $array; - } + my ($invocant, $params) = @_; + + my @multi_selects + = grep { $_->type == FIELD_TYPE_MULTI_SELECT } Bugzilla->active_custom_fields; + my %ms_values; + foreach my $field (@multi_selects) { + my $name = $field->name; + if (exists $params->{$name}) { + my $array = delete($params->{$name}) || []; + $ms_values{$name} = $array; } - return \%ms_values; + } + return \%ms_values; } -# Should be called any time you update short_desc or change a comment. -sub _sync_fulltext { - my ($self, %options) = @_; - my $dbh = Bugzilla->dbh; +# Used by create(). +# We need to handle external fields differently than normal fields, +# because they don't go into the bugs table. +sub _extract_single_external { + my ($invocant, $params) = @_; - my($all_comments, $public_comments); - if ($options{new_bug} || $options{update_comments}) { - my $comments = $dbh->selectall_arrayref( - 'SELECT thetext, isprivate FROM longdescs WHERE bug_id = ?', - undef, $self->id); - $all_comments = join("\n", map { $_->[0] } @$comments); - my @no_private = grep { !$_->[1] } @$comments; - $public_comments = join("\n", map { $_->[0] } @no_private); + my @external + = grep { $_->type == FIELD_TYPE_ONE_SELECT } Bugzilla->active_custom_fields; + my %values; + foreach my $field (@external) { + my $name = $field->name; + if (exists $params->{$name}) { + my $val = delete($params->{$name}); + $values{$name} = $val; } + } + return \%values; +} - if ($options{new_bug}) { - $dbh->do('INSERT INTO bugs_fulltext (bug_id, short_desc, comments, +# Should be called any time you update short_desc or change a comment. +sub _sync_fulltext { + my ($self, %options) = @_; + my $dbh = Bugzilla->dbh; + + my ($all_comments, $public_comments); + if ($options{new_bug} || $options{update_comments}) { + my $comments + = $dbh->selectall_arrayref( + 'SELECT thetext, isprivate FROM longdescs WHERE bug_id = ?', + undef, $self->id); + $all_comments = join("\n", map { $_->[0] } @$comments); + my @no_private = grep { !$_->[1] } @$comments; + $public_comments = join("\n", map { $_->[0] } @no_private); + } + + if ($options{new_bug}) { + $dbh->do( + 'INSERT INTO bugs_fulltext (bug_id, short_desc, comments, comments_noprivate) - VALUES (?, ?, ?, ?)', - undef, - $self->id, $self->short_desc, $all_comments, $public_comments); - } else { - my(@names, @values); - if ($options{update_short_desc}) { - push @names, 'short_desc'; - push @values, $self->short_desc; - } - if ($options{update_comments}) { - push @names, ('comments', 'comments_noprivate'); - push @values, ($all_comments, $public_comments); - } - if (@names) { - $dbh->do('UPDATE bugs_fulltext SET ' . - join(', ', map { "$_ = ?" } @names) . - ' WHERE bug_id = ?', - undef, - @values, $self->id); - } + VALUES (?, ?, ?, ?)', undef, $self->id, $self->short_desc, + $all_comments, $public_comments + ); + } + else { + my (@names, @values); + if ($options{update_short_desc}) { + push @names, 'short_desc'; + push @values, $self->short_desc; + } + if ($options{update_comments}) { + push @names, ('comments', 'comments_noprivate'); + push @values, ($all_comments, $public_comments); } + if (@names) { + $dbh->do( + 'UPDATE bugs_fulltext SET ' + . join(', ', map {"$_ = ?"} @names) + . ' WHERE bug_id = ?', + undef, @values, $self->id + ); + } + } } sub remove_from_db { - my ($self) = @_; - my $dbh = Bugzilla->dbh; + my ($self) = @_; + my $dbh = Bugzilla->dbh; - ThrowCodeError("bug_error", { bug => $self }) if $self->{'error'}; + ThrowCodeError("bug_error", {bug => $self}) if $self->{'error'}; - my $bug_id = $self->{'bug_id'}; - $self->SUPER::remove_from_db(); - # The bugs_fulltext table doesn't support foreign keys. - $dbh->do("DELETE FROM bugs_fulltext WHERE bug_id = ?", undef, $bug_id); + my $bug_id = $self->{'bug_id'}; + + # Delete entries from custom multi-select fields. + my $multi_selects = Bugzilla->fields( + { + custom => 1, + type => [FIELD_TYPE_MULTI_SELECT, FIELD_TYPE_ONE_SELECT], + by_name => 1 + } + ); + + foreach my $field_name (keys %$multi_selects) { + $dbh->do("DELETE FROM bug_" . $field_name . " WHERE bug_id = ?", undef, + $bug_id); + } + + ## REDHAT EXTENSION BEGIN 693936 + Bugzilla::Hook::process('bug_remove_from_db', {bug_id => $bug_id}); + ## REDHAT EXTENSION END 693936 + + $self->SUPER::remove_from_db(); + + # The bugs_fulltext table doesn't support foreign keys. + $dbh->do("DELETE FROM bugs_fulltext WHERE bug_id = ?", undef, $bug_id); } ##################################################################### @@ -1272,96 +1694,137 @@ sub remove_from_db { ##################################################################### sub send_changes { - my ($self, $changes, $vars) = @_; - - my $user = Bugzilla->user; - - my $old_qa = $changes->{'qa_contact'} - ? $changes->{'qa_contact'}->[0] : ''; - my $old_own = $changes->{'assigned_to'} - ? $changes->{'assigned_to'}->[0] : ''; - my $old_cc = $changes->{cc} - ? $changes->{cc}->[0] : ''; - - my %forced = ( - cc => [split(/[,;]+/, $old_cc)], - owner => $old_own, - qacontact => $old_qa, - changer => $user, + my ($self, $changes, $vars, $minor_update) = @_; + + ## REDHAT EXTENSION START 470051 + # If we are adding new bugs to the duplicate, send notifications for it + if (my $dup_changes = $self->{_dup_blocked_changes}) { + my $dup_bug = $self->new($self->dup_id); + $dup_bug->send_changes($dup_changes, $vars); + } + ## REDHAT EXTENSION END 470051 + + my $user = Bugzilla->user; + + my $old_qa = $changes->{'qa_contact'} ? $changes->{'qa_contact'}->[0] : ''; + ## REDHAT EXTENSION 876015: Add docs_contact + my $old_docs + = $changes->{'docs_contact'} ? $changes->{'docs_contact'}->[0] : ''; + my $old_own = $changes->{'assigned_to'} ? $changes->{'assigned_to'}->[0] : ''; + my $old_cc = $changes->{cc} ? $changes->{cc}->[0] : ''; + + my %forced = ( + cc => [split(/[,;]+/, $old_cc)], + owner => $old_own, + qacontact => $old_qa, + docscontact => $old_docs, + changer => $user, + ); + + _send_bugmail( + { + id => $self->id, + type => 'bug', + forced => \%forced, + minor_update => $minor_update, + edited_comment => $vars->{edited_comment}, + }, + $vars + ); + + # If the bug was marked as a duplicate, we need to notify users on the + # other bug of any changes to that bug. + my $new_dup_id = $changes->{'dup_id'} ? $changes->{'dup_id'}->[1] : undef; + if ($new_dup_id) { + _send_bugmail( + { + forced => {changer => $user}, + type => "dupe", + id => $new_dup_id, + minor_update => $minor_update + }, + $vars ); - - _send_bugmail({ id => $self->id, type => 'bug', forced => \%forced }, - $vars); - - # If the bug was marked as a duplicate, we need to notify users on the - # other bug of any changes to that bug. - my $new_dup_id = $changes->{'dup_id'} ? $changes->{'dup_id'}->[1] : undef; - if ($new_dup_id) { - _send_bugmail({ forced => { changer => $user }, type => "dupe", - id => $new_dup_id }, $vars); - } - - # If there were changes in dependencies, we need to notify those - # dependencies. - if ($changes->{'bug_status'}) { - my ($old_status, $new_status) = @{ $changes->{'bug_status'} }; - - # If this bug has changed from opened to closed or vice-versa, - # then all of the bugs we block need to be notified. - if (is_open_state($old_status) ne is_open_state($new_status)) { - my $params = { forced => { changer => $user }, - type => 'dep', - dep_only => 1, - blocker => $self, - changes => $changes }; - - foreach my $id (@{ $self->blocked }) { - $params->{id} = $id; - _send_bugmail($params, $vars); - } - } - } - - # To get a list of all changed dependencies, convert the "changes" arrays - # into a long string, then collapse that string into unique numbers in - # a hash. - my $all_changed_deps = join(', ', @{ $changes->{'dependson'} || [] }); - $all_changed_deps = join(', ', @{ $changes->{'blocked'} || [] }, - $all_changed_deps); - my %changed_deps = map { $_ => 1 } split(', ', $all_changed_deps); - # When clearning one field (say, blocks) and filling in the other - # (say, dependson), an empty string can get into the hash and cause - # an error later. - delete $changed_deps{''}; - - foreach my $id (sort { $a <=> $b } (keys %changed_deps)) { - _send_bugmail({ forced => { changer => $user }, type => "dep", - id => $id }, $vars); - } - - # Sending emails for the referenced bugs. - foreach my $ref_bug_id (uniq @{ $self->{see_also_changes} || [] }) { - _send_bugmail({ forced => { changer => $user }, - id => $ref_bug_id }, $vars); - } + } + + # If there were changes in dependencies, we need to notify those + # dependencies. + if ($changes->{'bug_status'}) { + my ($old_status, $new_status) = @{$changes->{'bug_status'}}; + + # If this bug has changed from opened to closed or vice-versa, + # then all of the bugs we block need to be notified. + if (is_open_state($old_status) ne is_open_state($new_status)) { + my $params = { + forced => {changer => $user}, + type => 'dep', + dep_only => 1, + blocker => $self, + changes => $changes, + minor_update => $minor_update + }; + + foreach my $id (@{$self->blocked}) { + $params->{id} = $id; + _send_bugmail($params, $vars); + } + } + } + + # To get a list of all changed dependencies, convert the "changes" arrays + # into a long string, then collapse that string into unique numbers in + # a hash. + my $all_changed_deps = join(', ', @{$changes->{'dependson'} || []}); + $all_changed_deps + = join(', ', @{$changes->{'blocked'} || []}, $all_changed_deps); + my %changed_deps = map { $_ => 1 } split(', ', $all_changed_deps); + + # When clearning one field (say, blocks) and filling in the other + # (say, dependson), an empty string can get into the hash and cause + # an error later. + delete $changed_deps{''}; + + foreach my $id (sort { $a <=> $b } (keys %changed_deps)) { + _send_bugmail( + { + forced => {changer => $user}, + type => "dep", + id => $id, + minor_update => $minor_update + }, + $vars + ); + } + + # Sending emails for the referenced bugs. + foreach my $ref_bug_id (uniq @{$self->{see_also_changes} || []}) { + _send_bugmail( + { + forced => {changer => $user}, + id => $ref_bug_id, + minor_update => $minor_update + }, + $vars + ); + } } sub _send_bugmail { - my ($params, $vars) = @_; + my ($params, $vars) = @_; - require Bugzilla::BugMail; + require Bugzilla::BugMail; - my $results = - Bugzilla::BugMail::Send($params->{'id'}, $params->{'forced'}, $params); + my $results + = Bugzilla::BugMail::Send($params->{'id'}, $params->{'forced'}, $params); - if (Bugzilla->usage_mode == USAGE_MODE_BROWSER) { - my $template = Bugzilla->template; - $vars->{$_} = $params->{$_} foreach keys %$params; - $vars->{'sent_bugmail'} = $results; - $template->process("bug/process/results.html.tmpl", $vars) - || ThrowTemplateError($template->error()); - $vars->{'header_done'} = 1; - } + if (Bugzilla->usage_mode == USAGE_MODE_BROWSER) { + my $template = Bugzilla->template; + $vars->{$_} = $params->{$_} foreach keys %$params; + $vars->{'sent_bugmail'} = $results; + $template->process("bug/process/results.html.tmpl", $vars) + || ThrowTemplateError($template->error()); + $vars->{'header_done'} = 1; + } } ##################################################################### @@ -1369,928 +1832,1140 @@ sub _send_bugmail { ##################################################################### sub _check_alias { - my ($invocant, $aliases) = @_; - $aliases = ref $aliases ? $aliases : [split(/[\s,]+/, $aliases)]; + my ($invocant, $aliases) = @_; + $aliases = ref $aliases ? $aliases : [split(/[\s,]+/, $aliases)]; - # Remove empty aliases - @$aliases = grep { $_ } @$aliases; + # Remove empty aliases + @$aliases = grep {$_} @$aliases; - foreach my $alias (@$aliases) { - $alias = trim($alias); + foreach my $alias (@$aliases) { + $alias = trim($alias); - # Make sure the alias isn't too long. - if (length($alias) > 40) { - ThrowUserError("alias_too_long"); - } - # Make sure the alias isn't just a number. - if ($alias =~ /^\d+$/) { - ThrowUserError("alias_is_numeric", { alias => $alias }); - } - # Make sure the alias has no commas or spaces. - if ($alias =~ /[, ]/) { - ThrowUserError("alias_has_comma_or_space", { alias => $alias }); - } - # Make sure the alias is unique, or that it's already our alias. - my $other_bug = new Bugzilla::Bug($alias); - if (!$other_bug->{error} - && (!ref $invocant || $other_bug->id != $invocant->id)) - { - ThrowUserError("alias_in_use", { alias => $alias, - bug_id => $other_bug->id }); - } + # Make sure the alias isn't too long. + if (length($alias) > 40) { + ThrowUserError("alias_too_long"); } - return $aliases; + # Make sure the alias isn't just a number. + if ($alias =~ /^\d+$/) { + ThrowUserError("alias_is_numeric", {alias => $alias}); + } + + # Make sure the alias has no commas or spaces. + if ($alias =~ /[, ]/) { + ThrowUserError("alias_has_comma_or_space", {alias => $alias}); + } + + # Make sure the alias is unique, or that it's already our alias. + my $other_bug = new Bugzilla::Bug($alias); + if (!$other_bug->{error} && (!ref $invocant || $other_bug->id != $invocant->id)) + { + ThrowUserError("alias_in_use", {alias => $alias, bug_id => $other_bug->id}); + } + } + + return $aliases; } sub _check_assigned_to { - my ($invocant, $assignee, undef, $params) = @_; - my $user = Bugzilla->user; - my $component = blessed($invocant) ? $invocant->component_obj - : $params->{component}; - - # Default assignee is the component owner. - my $id; - # If this is a new bug, you can only set the assignee if you have editbugs. - # If you didn't specify the assignee, we use the default assignee. - if (!ref $invocant - && (!$user->in_group('editbugs', $component->product_id) || !$assignee)) + my ($invocant, $assignee, undef, $params) = @_; + my $user = Bugzilla->user; + my $component + = blessed($invocant) ? $invocant->component_obj : $params->{component}; + + # Default assignee is the component owner. + my $id; + + # If this is a new bug, you can only set the assignee if you have editbugs. + # If you didn't specify the assignee, we use the default assignee. + if (!ref $invocant + && (!$user->in_group('editbugs', $component->product_id) || !$assignee)) + { + $id = $component->default_assignee->id; + + ## REDHAT EXTENSION START 653316 + # If a sub component is selected, use that for the default value + if (Bugzilla->params->{usesubcomponents} + && scalar @{$params->{rh_sub_components}}) { - $id = $component->default_assignee->id; - } else { - if (!ref $assignee) { - $assignee = trim($assignee); - # When updating a bug, assigned_to can't be empty. - ThrowUserError("reassign_to_empty") if ref $invocant && !$assignee; - $assignee = Bugzilla::User->check($assignee); - } - $id = $assignee->id; - # create() checks this another way, so we don't have to run this - # check during create(). - $invocant->_check_strict_isolation_for_user($assignee) if ref $invocant; + $id = $params->{rh_sub_components}[0]->default_assignee->id; + } + ## REDHAT EXTENSION END 653316 + + } + else { + if (!ref $assignee) { + $assignee = trim($assignee); + + # When updating a bug, assigned_to can't be empty. + ThrowUserError("reassign_to_empty") if ref $invocant && !$assignee; + $assignee = Bugzilla::User->check($assignee); } - return $id; + $id = $assignee->id; + + # create() checks this another way, so we don't have to run this + # check during create(). + $invocant->_check_strict_isolation_for_user($assignee) if ref $invocant; + } + return $id; } sub _check_bug_file_loc { - my ($invocant, $url) = @_; - $url = '' if !defined($url); - $url = trim($url); - # On bug entry, if bug_file_loc is "http://", the default, use an - # empty value instead. However, on bug editing people can set that - # back if they *really* want to. - if (!ref $invocant && $url eq 'http://') { - $url = ''; - } - return $url; + my ($invocant, $url) = @_; + $url = '' if !defined($url); + $url = trim($url); + + # On bug entry, if bug_file_loc is "http://", the default, use an + # empty value instead. However, on bug editing people can set that + # back if they *really* want to. + if (!ref $invocant && $url eq 'http://') { + $url = ''; + } + return $url; } sub _check_bug_status { - my ($invocant, $new_status, undef, $params) = @_; - my $user = Bugzilla->user; - my @valid_statuses; - my $old_status; # Note that this is undef for new bugs. - - my ($product, $comment); - if (ref $invocant) { - @valid_statuses = @{$invocant->statuses_available}; - $product = $invocant->product_obj; - $old_status = $invocant->status; - my $comments = $invocant->{added_comments} || []; - $comment = $comments->[-1]; - } - else { - $product = $params->{product}; - $comment = $params->{comment}; - @valid_statuses = @{ Bugzilla::Bug->statuses_available($product) }; - } - - # Check permissions for users filing new bugs. - if (!ref $invocant) { - if ($user->in_group('editbugs', $product->id) - || $user->in_group('canconfirm', $product->id)) { - # If the user with privs hasn't selected another status, - # select the first one of the list. - unless ($new_status) { - if (scalar(@valid_statuses) == 1) { - $new_status = $valid_statuses[0]; - } - else { - $new_status = ($valid_statuses[0]->name ne 'UNCONFIRMED') ? - $valid_statuses[0] : $valid_statuses[1]; - } - } + my ($invocant, $new_status, undef, $params) = @_; + my $user = Bugzilla->user; + my @valid_statuses; + my $old_status; # Note that this is undef for new bugs. + + my ($product, $comment); + if (ref $invocant) { + @valid_statuses = @{$invocant->statuses_available}; + $product = $invocant->product_obj; + $old_status = $invocant->status; + my $comments = $invocant->{added_comments} || []; + $comment = $comments->[-1]; + } + else { + $product = $params->{product}; + $comment = $params->{comment}; + @valid_statuses = @{Bugzilla::Bug->statuses_available($product)}; + } + + # Check permissions for users filing new bugs. + if (!ref $invocant) { + if ( $user->in_group('editbugs', $product->id) + || $user->in_group('canconfirm', $product->id)) + { + # If the user with privs hasn't selected another status, + # select the first one of the list. + unless ($new_status) { + if (scalar(@valid_statuses) == 1) { + $new_status = $valid_statuses[0]; } else { - # A user with no privs cannot choose the initial status. - # If UNCONFIRMED is valid for this product, use it; else - # use the first bug status available. - if (grep {$_->name eq 'UNCONFIRMED'} @valid_statuses) { - $new_status = 'UNCONFIRMED'; - } - else { - $new_status = $valid_statuses[0]; - } + $new_status + = ($valid_statuses[0]->name ne 'UNCONFIRMED') + ? $valid_statuses[0] + : $valid_statuses[1]; } + } } + else { + # A user with no privs cannot choose the initial status. + # If UNCONFIRMED is valid for this product, use it; else + # use the first bug status available. + if (grep { $_->name eq 'UNCONFIRMED' } @valid_statuses) { + $new_status = 'UNCONFIRMED'; + } + else { + $new_status = $valid_statuses[0]; + } + } + } + + # Time to validate the bug status. + $new_status = Bugzilla::Status->check($new_status) unless ref($new_status); + + # We skip this check if we are changing from a status to itself. + if ((!$old_status || $old_status->id != $new_status->id) + && !grep { $_->name eq $new_status->name } @valid_statuses) + { + ThrowUserError('illegal_bug_status_transition', + {old => $old_status, new => $new_status}); + } + + # Check if a comment is required for this change. + if ($new_status->comment_required_on_change_from($old_status) + && !$comment->{'thetext'}) + { + ThrowUserError( + 'comment_required', + { + old => $old_status ? $old_status->name : undef, + new => $new_status->name, + field => 'bug_status' + } + ); + } - # Time to validate the bug status. - $new_status = Bugzilla::Status->check($new_status) unless ref($new_status); - # We skip this check if we are changing from a status to itself. - if ( (!$old_status || $old_status->id != $new_status->id) - && !grep {$_->name eq $new_status->name} @valid_statuses) - { - ThrowUserError('illegal_bug_status_transition', - { old => $old_status, new => $new_status }); - } + if ( + ref $invocant && ( + $new_status->name eq 'IN_PROGRESS' - # Check if a comment is required for this change. - if ($new_status->comment_required_on_change_from($old_status) - && !$comment->{'thetext'}) - { - ThrowUserError('comment_required', - { old => $old_status ? $old_status->name : undef, - new => $new_status->name, field => 'bug_status' }); - } - - if (ref $invocant - && ($new_status->name eq 'IN_PROGRESS' - # Backwards-compat for the old default workflow. - or $new_status->name eq 'ASSIGNED') - && Bugzilla->params->{"usetargetmilestone"} - && Bugzilla->params->{"musthavemilestoneonaccept"} - # musthavemilestoneonaccept applies only if at least two - # target milestones are defined for the product. - && scalar(@{ $product->milestones }) > 1 - && $invocant->target_milestone eq $product->default_milestone) - { - ThrowUserError("milestone_required", { bug => $invocant }); - } + # Backwards-compat for the old default workflow. + or $new_status->name eq 'ASSIGNED' + ) + && Bugzilla->params->{"usetargetmilestone"} + && Bugzilla->params->{"musthavemilestoneonaccept"} - if (!blessed $invocant) { - $params->{everconfirmed} = $new_status->name eq 'UNCONFIRMED' ? 0 : 1; - } + # musthavemilestoneonaccept applies only if at least two + # target milestones are defined for the product. + && scalar(@{$product->milestones}) > 1 + && $invocant->target_milestone eq $product->default_milestone + ) + { + ThrowUserError("milestone_required", {bug => $invocant}); + } - return $new_status->name; + if (!blessed $invocant) { + $params->{everconfirmed} = $new_status->name eq 'UNCONFIRMED' ? 0 : 1; + } + + return $new_status->name; } sub _check_cc { - my ($invocant, $ccs, undef, $params) = @_; - my $component = blessed($invocant) ? $invocant->component_obj - : $params->{component}; - return [map {$_->id} @{$component->initial_cc}] unless $ccs; - - # Allow comma-separated input as well as arrayrefs. - $ccs = [split(/[,;]+/, $ccs)] if !ref $ccs; - - my %cc_ids; - foreach my $person (@$ccs) { - $person = trim($person); - next unless $person; - my $id = login_to_id($person, THROW_ERROR); - $cc_ids{$id} = 1; + my ($invocant, $ccs, undef, $params) = @_; + my $component + = blessed($invocant) ? $invocant->component_obj : $params->{component}; + ## REDHAT EXTENSION BEGIN 815549 + $ccs = [] unless $ccs; + ## REDHAT EXTENSION END 815549 + + # Allow comma-separated input as well as arrayrefs. + $ccs = [split(/[,;]+/, $ccs)] if !ref $ccs; + + my %cc_ids; + foreach my $person (@$ccs) { + $person = trim($person); + next unless $person; + my $id = login_to_id($person, THROW_ERROR); + $cc_ids{$id} = 1; + } + + ## REDHAT EXTENSION BEGIN 815549 + # Restrict default CC:es to those that can view the bug anyway + my $product_id = $params->{product}->id; + my $bug_groups = $params->{groups}; + + # Enforce Default CC + ## REDHAT EXTENSION START 653316 + # If a sub component is selected, use that for the default CC value + my $default_cc + = (Bugzilla->params->{usesubcomponents} + && scalar(@{$params->{rh_sub_components}})) + ? $params->{rh_sub_components}[0]->initial_cc + : $component->initial_cc; + + foreach my $user (@$default_cc) { + ## REDHAT EXTENSION END 653316 + if (scalar(@$bug_groups) == 0) { + + # There are not group restrictions on the bug, so we add the CC: + $cc_ids{$user->id} = 1; } + else { + foreach my $group (@$bug_groups) { + if ($user->in_group($group->name, $product_id)) { - # Enforce Default CC - $cc_ids{$_->id} = 1 foreach (@{$component->initial_cc}); + # This user can see this bug, so add them to the CC: + $cc_ids{$user->id} = 1; + last; + } + } + } + } + ## REDHAT EXTENSION END 815549 - return [keys %cc_ids]; + return [keys %cc_ids]; } sub _check_comment { - my ($invocant, $comment_txt, undef, $params) = @_; + my ($invocant, $comment_txt, undef, $params) = @_; - # Comment can be empty. We should force it to be empty if the text is undef - if (!defined $comment_txt) { - $comment_txt = ''; - } + # Comment can be empty. We should force it to be empty if the text is undef + if (!defined $comment_txt) { + $comment_txt = ''; + } - # Load up some data - my $isprivate = delete $params->{comment_is_private}; - my $timestamp = $params->{creation_ts}; + # Load up some data + my $isprivate = delete $params->{comment_is_private}; + my $timestamp = $params->{creation_ts}; - # Create the new comment so we can check it - my $comment = { - thetext => $comment_txt, - bug_when => $timestamp, - }; + # Create the new comment so we can check it + my $comment = {thetext => $comment_txt, bug_when => $timestamp,}; - # We don't include the "isprivate" column unless it was specified. - # This allows it to fall back to its database default. - if (defined $isprivate) { - $comment->{isprivate} = $isprivate; - } + # We don't include the "isprivate" column unless it was specified. + # This allows it to fall back to its database default. + if (defined $isprivate) { + $comment->{isprivate} = $isprivate; + } - # Validate comment. We have to do this special as a comment normally - # requires a bug to be already created. For a new bug, the first comment - # obviously can't get the bug if the bug is created after this - # (see bug 590334) - Bugzilla::Comment->check_required_create_fields($comment); - $comment = Bugzilla::Comment->run_create_validators($comment, - { skip => ['bug_id'] } - ); + # Validate comment. We have to do this special as a comment normally + # requires a bug to be already created. For a new bug, the first comment + # obviously can't get the bug if the bug is created after this + # (see bug 590334) + Bugzilla::Comment->check_required_create_fields($comment); + $comment + = Bugzilla::Comment->run_create_validators($comment, {skip => ['bug_id']}); - return $comment; + return $comment; } sub _check_commenton { - my ($invocant, $new_value, $field, $params) = @_; + my ($invocant, $new_value, $field, $params) = @_; - my $has_comment = - ref($invocant) ? $invocant->{added_comments} - : (defined $params->{comment} - and $params->{comment}->{thetext} ne ''); + my $has_comment + = ref($invocant) + ? $invocant->{added_comments} + : (defined $params->{comment} and $params->{comment}->{thetext} ne ''); - my $is_changing = ref($invocant) ? $invocant->$field ne $new_value - : $new_value ne ''; + my $is_changing + = ref($invocant) ? $invocant->$field ne $new_value : $new_value ne ''; - if ($is_changing && !$has_comment) { - my $old_value = ref($invocant) ? $invocant->$field : undef; - ThrowUserError('comment_required', - { field => $field, old => $old_value, new => $new_value }); - } + if ($is_changing && !$has_comment) { + my $old_value = ref($invocant) ? $invocant->$field : undef; + ThrowUserError('comment_required', + {field => $field, old => $old_value, new => $new_value}); + } } sub _check_component { - my ($invocant, $name, undef, $params) = @_; - $name = trim($name); - $name || ThrowUserError("require_component"); - my $product = blessed($invocant) ? $invocant->product_obj - : $params->{product}; - my $old_comp = blessed($invocant) ? $invocant->component : ''; - my $object = Bugzilla::Component->check({ product => $product, name => $name }); - if ($object->name ne $old_comp && !$object->is_active) { - ThrowUserError('value_inactive', { class => ref($object), value => $name }); - } - return $object; + my ($invocant, $name, undef, $params) = @_; + $name = trim($name); + $name || ThrowUserError("require_component"); + my $product = blessed($invocant) ? $invocant->product_obj : $params->{product}; + my $old_comp = blessed($invocant) ? $invocant->component : ''; + my $object = Bugzilla::Component->check({product => $product, name => $name}); + if ($object->name ne $old_comp && !$object->is_active) { + ThrowUserError('value_inactive', {class => ref($object), value => $name}); + } + return $object; } sub _check_creation_ts { - return Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); + return Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); } sub _check_deadline { - my ($invocant, $date) = @_; + my ($invocant, $date) = @_; - # When filing bugs, we're forgiving and just return undef if - # the user isn't a timetracker. When updating bugs, check_can_change_field - # controls permissions, so we don't want to check them here. - if (!ref $invocant and !Bugzilla->user->is_timetracker) { - return undef; - } + # When filing bugs, we're forgiving and just return undef if + # the user isn't a timetracker. When updating bugs, check_can_change_field + # controls permissions, so we don't want to check them here. + if (!ref $invocant and !Bugzilla->user->is_timetracker) { + return undef; + } - # Validate entered deadline - $date = trim($date); - return undef if !$date; - validate_date($date) - || ThrowUserError('illegal_date', { date => $date, - format => 'YYYY-MM-DD' }); - return $date; + # Validate entered deadline + $date = trim($date); + return undef if !$date; + validate_date($date) + || ThrowUserError('illegal_date', {date => $date, format => 'YYYY-MM-DD'}); + return $date; } # Takes two comma/space-separated strings and returns arrayrefs # of valid bug IDs. sub _check_dependencies { - my ($invocant, $value, $field, $params) = @_; - - return $value if $params->{_dependencies_validated}; - - if (!ref $invocant) { - # Only editbugs users can set dependencies on bug entry. - return ([], []) unless Bugzilla->user->in_group( - 'editbugs', $params->{product}->id); - } - - # This is done this way so that dependson and blocked can be in - # VALIDATORS, meaning that they can be in VALIDATOR_DEPENDENCIES, - # which means that they can be checked in the right order during - # bug creation. - my $opposite = $field eq 'dependson' ? 'blocked' : 'dependson'; - my %deps_in = ($field => $value || '', - $opposite => $params->{$opposite} || ''); - - foreach my $type (qw(dependson blocked)) { - my @bug_ids = ref($deps_in{$type}) - ? @{$deps_in{$type}} - : split(/[\s,]+/, $deps_in{$type}); - # Eliminate nulls. - @bug_ids = grep {$_} @bug_ids; - - my @check_access = @bug_ids; - # When we're updating a bug, only added or removed bug_ids are - # checked for whether or not we can see/edit those bugs. - if (ref $invocant) { - my $old = $invocant->$type; - my ($removed, $added) = diff_arrays($old, \@bug_ids); - @check_access = (@$added, @$removed); - - # Check field permissions if we've changed anything. - if (@check_access) { - my $privs; - if (!$invocant->check_can_change_field($type, 0, 1, \$privs)) { - ThrowUserError('illegal_change', { field => $type, - privs => $privs }); - } - } + my ($invocant, $value, $field, $params) = @_; + + return $value if $params->{_dependencies_validated}; + + if (!ref $invocant) { + + # Only editbugs users can set dependencies on bug entry. + return ([], []) + unless Bugzilla->user->in_group('editbugs', $params->{product}->id); + } + + # This is done this way so that dependson and blocked can be in + # VALIDATORS, meaning that they can be in VALIDATOR_DEPENDENCIES, + # which means that they can be checked in the right order during + # bug creation. + my $opposite = $field eq 'dependson' ? 'blocked' : 'dependson'; + my %deps_in = ($field => $value || '', $opposite => $params->{$opposite} || ''); + + foreach my $type (qw(dependson blocked)) { + my @bug_ids + = ref($deps_in{$type}) + ? @{$deps_in{$type}} + : split(/[\s,]+/, $deps_in{$type}); + + # Eliminate nulls. + @bug_ids = grep {$_} @bug_ids; + + my @check_access = @bug_ids; + + # When we're updating a bug, only added or removed bug_ids are + # checked for whether or not we can see/edit those bugs. + if (ref $invocant) { + my $old = $invocant->$type; + my ($removed, $added) = diff_arrays($old, \@bug_ids); + @check_access = (@$added, @$removed); + + # Check field permissions if we've changed anything. + if (@check_access) { + my $privs; + if (!$invocant->check_can_change_field($type, 0, 1, \$privs)) { + ThrowUserError('illegal_change', {field => $type, privs => $privs}); } + } + } - my $user = Bugzilla->user; - foreach my $modified_id (@check_access) { - my $delta_bug = $invocant->check($modified_id); - # Under strict isolation, you can't modify a bug if you can't - # edit it, even if you can see it. - if (Bugzilla->params->{"strict_isolation"}) { - if (!$user->can_edit_product($delta_bug->{'product_id'})) { - ThrowUserError("illegal_change_deps", {field => $type}); - } - } + my $user = Bugzilla->user; + foreach my $modified_id (@check_access) { + my $delta_bug = $invocant->check($modified_id); + + # Under strict isolation, you can't modify a bug if you can't + # edit it, even if you can see it. + if (Bugzilla->params->{"strict_isolation"}) { + if (!$user->can_edit_product($delta_bug->{'product_id'})) { + ThrowUserError("illegal_change_deps", {field => $type}); } - # Replace all aliases by their corresponding bug ID. - @bug_ids = map { $_ =~ /^(\d+)$/ ? $1 : $invocant->check($_, $type)->id } @bug_ids; - $deps_in{$type} = \@bug_ids; + } } - # And finally, check for dependency loops. - my $bug_id = ref($invocant) ? $invocant->id : 0; - my %deps = ValidateDependencies($deps_in{dependson}, $deps_in{blocked}, - $bug_id); + # Replace all aliases by their corresponding bug ID. + @bug_ids + = map { $_ =~ /^(\d+)$/ ? $1 : $invocant->check($_, $type)->id } @bug_ids; + $deps_in{$type} = \@bug_ids; + } + + # And finally, check for dependency loops. + my $bug_id = ref($invocant) ? $invocant->id : 0; + my %deps + = ValidateDependencies($deps_in{dependson}, $deps_in{blocked}, $bug_id); - $params->{$opposite} = $deps{$opposite}; - $params->{_dependencies_validated} = 1; - return $deps{$field}; + $params->{$opposite} = $deps{$opposite}; + $params->{_dependencies_validated} = 1; + return $deps{$field}; } sub _check_dup_id { - my ($self, $dupe_of) = @_; - my $dbh = Bugzilla->dbh; + my ($self, $dupe_of) = @_; + my $dbh = Bugzilla->dbh; + + # Store the bug ID/alias passed by the user for visibility checks. + my $orig_dupe_of = $dupe_of = trim($dupe_of); + $dupe_of || ThrowCodeError('undefined_field', {field => 'dup_id'}); + + # Validate the bug ID. The second argument will force check() to only + # make sure that the bug exists, and convert the alias to the bug ID + # if a string is passed. Group restrictions are checked below. + my $dupe_of_bug = $self->check($dupe_of, 'dup_id'); + $dupe_of = $dupe_of_bug->id; + + # If the dupe is unchanged, we have nothing more to check. + return $dupe_of if ($self->dup_id && $self->dup_id == $dupe_of); + + # If we come here, then the duplicate is new. We have to make sure + # that we can view/change it (issue A on bug 96085). + $dupe_of_bug->check_is_visible($orig_dupe_of); + + # Make sure a loop isn't created when marking this bug + # as duplicate. + _resolve_ultimate_dup_id($self->id, $dupe_of, 1); + + my $cur_dup = $self->dup_id || 0; + if ( $cur_dup != $dupe_of + && Bugzilla->params->{'commentonduplicate'} + && !$self->{added_comments}) + { + ThrowUserError('comment_required'); + } + + # Should we add the reporter to the CC list of the new bug? + # If they can see the bug... + if ($self->reporter->can_see_bug($dupe_of)) { + + # We only add them if they're not the reporter of the other bug. + $self->{_add_dup_cc} = 1 if $dupe_of_bug->reporter->id != $self->reporter->id; + } + + # What if the reporter currently can't see the new bug? In the browser + # interface, we prompt the user. In other interfaces, we default to + # not adding the user, as the safest option. + elsif (Bugzilla->usage_mode == USAGE_MODE_BROWSER) { + + # If we've already confirmed whether the user should be added... + my $cgi = Bugzilla->cgi; + my $add_confirmed = $cgi->param('confirm_add_duplicate'); + if (defined $add_confirmed) { + $self->{_add_dup_cc} = $add_confirmed; + } + else { + # Note that here we don't check if the user is already the reporter + # of the dupe_of bug, since we already checked if they can *see* + # the bug, above. People might have reporter_accessible turned + # off, but cclist_accessible turned on, so they might want to + # add the reporter even though they're already the reporter of the + # dup_of bug. + my $vars = {}; + my $template = Bugzilla->template; - # Store the bug ID/alias passed by the user for visibility checks. - my $orig_dupe_of = $dupe_of = trim($dupe_of); - $dupe_of || ThrowCodeError('undefined_field', { field => 'dup_id' }); - # Validate the bug ID. The second argument will force check() to only - # make sure that the bug exists, and convert the alias to the bug ID - # if a string is passed. Group restrictions are checked below. - my $dupe_of_bug = $self->check($dupe_of, 'dup_id'); - $dupe_of = $dupe_of_bug->id; - - # If the dupe is unchanged, we have nothing more to check. - return $dupe_of if ($self->dup_id && $self->dup_id == $dupe_of); - - # If we come here, then the duplicate is new. We have to make sure - # that we can view/change it (issue A on bug 96085). - $dupe_of_bug->check_is_visible($orig_dupe_of); - - # Make sure a loop isn't created when marking this bug - # as duplicate. - _resolve_ultimate_dup_id($self->id, $dupe_of, 1); - - my $cur_dup = $self->dup_id || 0; - if ($cur_dup != $dupe_of && Bugzilla->params->{'commentonduplicate'} - && !$self->{added_comments}) - { - ThrowUserError('comment_required'); - } - - # Should we add the reporter to the CC list of the new bug? - # If they can see the bug... - if ($self->reporter->can_see_bug($dupe_of)) { - # We only add them if they're not the reporter of the other bug. - $self->{_add_dup_cc} = 1 - if $dupe_of_bug->reporter->id != $self->reporter->id; - } - # What if the reporter currently can't see the new bug? In the browser - # interface, we prompt the user. In other interfaces, we default to - # not adding the user, as the safest option. - elsif (Bugzilla->usage_mode == USAGE_MODE_BROWSER) { - # If we've already confirmed whether the user should be added... - my $cgi = Bugzilla->cgi; - my $add_confirmed = $cgi->param('confirm_add_duplicate'); - if (defined $add_confirmed) { - $self->{_add_dup_cc} = $add_confirmed; - } - else { - # Note that here we don't check if the user is already the reporter - # of the dupe_of bug, since we already checked if they can *see* - # the bug, above. People might have reporter_accessible turned - # off, but cclist_accessible turned on, so they might want to - # add the reporter even though they're already the reporter of the - # dup_of bug. - my $vars = {}; - my $template = Bugzilla->template; - # Ask the user what they want to do about the reporter. - $vars->{'cclist_accessible'} = $dupe_of_bug->cclist_accessible; - $vars->{'original_bug_id'} = $dupe_of; - $vars->{'duplicate_bug_id'} = $self->id; - print $cgi->header(); - $template->process("bug/process/confirm-duplicate.html.tmpl", $vars) - || ThrowTemplateError($template->error()); - exit; - } + # Ask the user what they want to do about the reporter. + $vars->{'cclist_accessible'} = $dupe_of_bug->cclist_accessible; + $vars->{'original_bug_id'} = $dupe_of; + $vars->{'duplicate_bug_id'} = $self->id; + print $cgi->header(); + $template->process("bug/process/confirm-duplicate.html.tmpl", $vars) + || ThrowTemplateError($template->error()); + exit; } + } - return $dupe_of; + return $dupe_of; } sub _check_groups { - my ($invocant, $group_names, undef, $params) = @_; - - my $bug_id = blessed($invocant) ? $invocant->id : undef; - my $product = blessed($invocant) ? $invocant->product_obj - : $params->{product}; - my %add_groups; - - # In email or WebServices, when the "groups" item actually - # isn't specified, then just add the default groups. - if (!defined $group_names) { - my $available = $product->groups_available; - foreach my $group (@$available) { - $add_groups{$group->id} = $group if $group->{is_default}; - } + my ($invocant, $group_names, undef, $params) = @_; + + my $bug_id = blessed($invocant) ? $invocant->id : undef; + my $product = blessed($invocant) ? $invocant->product_obj : $params->{product}; + my %add_groups; + + # In email or WebServices, when the "groups" item actually + # isn't specified, then just add the default groups. + if (!defined $group_names) { + my $available = $product->groups_available; + foreach my $group (@$available) { + $add_groups{$group->id} = $group if $group->{is_default}; } - else { - # Allow a comma-separated list, for email_in.pl. - $group_names = [map { trim($_) } split(',', $group_names)] - if !ref $group_names; + } + else { + # Allow a comma-separated list, for email_in.pl. + $group_names = [map { trim($_) } split(',', $group_names)] if !ref $group_names; - # First check all the groups they chose to set. - my %args = ( product => $product->name, bug_id => $bug_id, action => 'add' ); - foreach my $name (@$group_names) { - my $group = Bugzilla::Group->check_no_disclose({ %args, name => $name }); + # First check all the groups they chose to set. + my %args = (product => $product->name, bug_id => $bug_id, action => 'add'); + foreach my $name (@$group_names) { + my $group = Bugzilla::Group->check_no_disclose({%args, name => $name}); - if (!$product->group_is_settable($group)) { - ThrowUserError('group_restriction_not_allowed', { %args, name => $name }); - } - $add_groups{$group->id} = $group; - } + if (!$product->group_is_settable($group)) { + ThrowUserError('group_restriction_not_allowed', {%args, name => $name}); + } + $add_groups{$group->id} = $group; } + } - # Now enforce mandatory groups. - $add_groups{$_->id} = $_ foreach @{ $product->groups_mandatory }; + # Now enforce mandatory groups. + $add_groups{$_->id} = $_ foreach @{$product->groups_mandatory}; - my @add_groups = values %add_groups; - return \@add_groups; + my @add_groups = values %add_groups; + return \@add_groups; } sub _check_keywords { - my ($invocant, $keywords_in, undef, $params) = @_; + my ($invocant, $keywords_in, undef, $params) = @_; - return [] if !defined $keywords_in; + return [] if !defined $keywords_in; - my $keyword_array = $keywords_in; - if (!ref $keyword_array) { - $keywords_in = trim($keywords_in); - $keyword_array = [split(/[\s,]+/, $keywords_in)]; - } - - my %keywords; - foreach my $keyword (@$keyword_array) { - next unless $keyword; - my $obj = Bugzilla::Keyword->check($keyword); - $keywords{$obj->id} = $obj; - } - return [values %keywords]; + my $keyword_array = $keywords_in; + if (!ref $keyword_array) { + $keywords_in = trim($keywords_in); + $keyword_array = [split(/[\s,]+/, $keywords_in)]; + } + + my %keywords; + foreach my $keyword (@$keyword_array) { + next unless $keyword; + my $obj = Bugzilla::Keyword->check($keyword); + $keywords{$obj->id} = $obj; + } + return [values %keywords]; } sub _check_product { - my ($invocant, $name) = @_; - $name = trim($name); - # If we're updating the bug and they haven't changed the product, - # always allow it. - if (ref $invocant && lc($invocant->product_obj->name) eq lc($name)) { - return $invocant->product_obj; - } - # Check that the product exists and that the user - # is allowed to enter bugs into this product. - my $product = Bugzilla->user->can_enter_product($name, THROW_ERROR); - return $product; -} + my ($invocant, $name) = @_; + $name = trim($name); -sub _check_priority { - my ($invocant, $priority) = @_; - if (!ref $invocant && !Bugzilla->params->{'letsubmitterchoosepriority'}) { - $priority = Bugzilla->params->{'defaultpriority'}; - } - return $invocant->_check_select_field($priority, 'priority'); + # If we're updating the bug and they haven't changed the product, + # always allow it. + if (ref $invocant && lc($invocant->product_obj->name) eq lc($name)) { + return $invocant->product_obj; + } + + # Check that the product exists and that the user + # is allowed to enter bugs into this product. + my $product = Bugzilla->user->can_enter_product($name, THROW_ERROR); + return $product; } -sub _check_qa_contact { - my ($invocant, $qa_contact, undef, $params) = @_; - $qa_contact = trim($qa_contact) if !ref $qa_contact; - my $component = blessed($invocant) ? $invocant->component_obj - : $params->{component}; - if (!ref $invocant) { - # Bugs get no QA Contact on creation if useqacontact is off. - return undef if !Bugzilla->params->{useqacontact}; - # Set the default QA Contact if one isn't specified or if the - # user doesn't have editbugs. - if (!Bugzilla->user->in_group('editbugs', $component->product_id) - || !$qa_contact) - { - return $component->default_qa_contact ? $component->default_qa_contact->id : undef; - } - } +sub _check_priority { + my ($invocant, $priority) = @_; - # If a QA Contact was specified or if we're updating, check - # the QA Contact for validity. - my $id; - if ($qa_contact) { - $qa_contact = Bugzilla::User->check($qa_contact) if !ref $qa_contact; - $id = $qa_contact->id; - # create() checks this another way, so we don't have to run this - # check during create(). - # If there is no QA contact, this check is not required. - $invocant->_check_strict_isolation_for_user($qa_contact) - if (ref $invocant && $id); - } + ## REDHAT EXTENSION START 457016 + # adding setpriority check + if ( !ref $invocant + && !Bugzilla->params->{'letsubmitterchoosepriority'} + && !Bugzilla->user->in_group('setpriority')) + { + $priority = Bugzilla->params->{'defaultpriority'}; + } + ## REDHAT EXTENSION END 457016 - # "0" always means "undef", for QA Contact. - return $id || undef; + return $invocant->_check_select_field($priority, 'priority'); } +sub _check_qa_contact { + my ($invocant, $qa_contact, undef, $params) = @_; + $qa_contact = trim($qa_contact) if !ref $qa_contact; + my $component + = blessed($invocant) ? $invocant->component_obj : $params->{component}; + if (!ref $invocant) { + + # Bugs get no QA Contact on creation if useqacontact is off. + return undef if !Bugzilla->params->{useqacontact}; + + # Set the default QA Contact if one isn't specified or if the + # user doesn't have editbugs. + if ( !Bugzilla->user->in_group('editbugs', $component->product_id) + || !$qa_contact) + { + ## REDHAT EXTENSION START 653316 + # If a sub component is selected, use that for the default value + if (Bugzilla->params->{usesubcomponents} + && scalar @{$params->{rh_sub_components}}) + { + return $params->{rh_sub_components}[0]->default_qa_contact + ? $params->{rh_sub_components}[0]->default_qa_contact->id + : undef; + } + ## REDHAT EXTENSION END 653316 + + return $component->default_qa_contact + ? $component->default_qa_contact->id + : undef; + } + } + + # If a QA Contact was specified or if we're updating, check + # the QA Contact for validity. + my $id; + if ($qa_contact) { + $qa_contact = Bugzilla::User->check($qa_contact) if !ref $qa_contact; + $id = $qa_contact->id; + + # create() checks this another way, so we don't have to run this + # check during create(). + # If there is no QA contact, this check is not required. + $invocant->_check_strict_isolation_for_user($qa_contact) + if (ref $invocant && $id); + } + + # "0" always means "undef", for QA Contact. + return $id || undef; +} + +## REDHAT EXTENSION START 876015 +sub _check_docs_contact { + my ($invocant, $docs_contact, undef, $params) = @_; + $docs_contact = trim($docs_contact) if !ref $docs_contact; + my $component + = blessed($invocant) ? $invocant->component_obj : $params->{component}; + if (!ref $invocant) { + + # Bugs get no Docs Contact on creation if usedocscontact is off. + return undef if !Bugzilla->params->{usedocscontact}; + + # Set the default Docs Contact if one isn't specified or if the + # user doesn't have editbugs. + if ( !Bugzilla->user->in_group('editbugs', $component->product_id) + || !$docs_contact) + { + ## REDHAT EXTENSION START 653316 + # If a sub component is selected, use that for the default value + if (Bugzilla->params->{usesubcomponents} + && scalar @{$params->{rh_sub_components}}) + { + return $params->{rh_sub_components}[0]->default_docs_contact + ? $params->{rh_sub_components}[0]->default_docs_contact->id + : undef; + } + ## REDHAT EXTENSION END 653316 + + return $component->default_docs_contact + ? $component->default_docs_contact->id + : undef; + } + } + + # If a Docs Contact was specified or if we're updating, check + # the Docs Contact for validity. + my $id; + if ($docs_contact) { + $docs_contact = Bugzilla::User->check($docs_contact) if !ref $docs_contact; + $id = $docs_contact->id; + + # create() checks this another way, so we don't have to run this + # check during create(). + # If there is no Docs contact, this check is not required. + $invocant->_check_strict_isolation_for_user($docs_contact) + if (ref $invocant && $id); + } + + # "0" always means "undef", for Docs Contact. + return $id || undef; +} +## REDHAT EXTENSION START 876015 + sub _check_reporter { - my $invocant = shift; - my $reporter; - if (ref $invocant) { - # You cannot change the reporter of a bug. - $reporter = $invocant->reporter->id; - } - else { - # On bug creation, the reporter is the logged in user - # (meaning that they must be logged in first!). - Bugzilla->login(LOGIN_REQUIRED); - $reporter = Bugzilla->user->id; - } - return $reporter; + my $invocant = shift; + my $reporter; + if (ref $invocant) { + + # You cannot change the reporter of a bug. + $reporter = $invocant->reporter->id; + } + else { + # On bug creation, the reporter is the logged in user + # (meaning that they must be logged in first!). + Bugzilla->login(LOGIN_REQUIRED); + $reporter = Bugzilla->user->id; + } + return $reporter; } sub _check_resolution { - my ($invocant, $resolution, undef, $params) = @_; - $resolution = trim($resolution); - my $status = ref($invocant) ? $invocant->status->name - : $params->{bug_status}; - my $is_open = ref($invocant) ? $invocant->status->is_open - : is_open_state($status); - - # Throw a special error for resolving bugs without a resolution - # (or trying to change the resolution to '' on a closed bug without - # using clear_resolution). - ThrowUserError('missing_resolution', { status => $status }) - if !$resolution && !$is_open; - - # Make sure this is a valid resolution. - $resolution = $invocant->_check_select_field($resolution, 'resolution'); - - # Don't allow open bugs to have resolutions. - ThrowUserError('resolution_not_allowed') if $is_open; - - # Check noresolveonopenblockers. - my $dependson = ref($invocant) ? $invocant->dependson - : ($params->{dependson} || []); - if (Bugzilla->params->{"noresolveonopenblockers"} - && $resolution eq 'FIXED' - && (!ref $invocant or !$invocant->resolution - or $resolution ne $invocant->resolution) - && scalar @$dependson) - { - my $dep_bugs = Bugzilla::Bug->new_from_list($dependson); - my $count_open = grep { $_->isopened } @$dep_bugs; - if ($count_open) { - my $bug_id = ref($invocant) ? $invocant->id : undef; - ThrowUserError("still_unresolved_bugs", - { bug_id => $bug_id, dep_count => $count_open }); - } - } - - # Check if they're changing the resolution and need to comment. - if (Bugzilla->params->{'commentonchange_resolution'}) { - $invocant->_check_commenton($resolution, 'resolution', $params); - } - - return $resolution; + my ($invocant, $resolution, undef, $params) = @_; + $resolution = trim($resolution); + my $status = ref($invocant) ? $invocant->status->name : $params->{bug_status}; + my $is_open + = ref($invocant) ? $invocant->status->is_open : is_open_state($status); + + # Throw a special error for resolving bugs without a resolution + # (or trying to change the resolution to '' on a closed bug without + # using clear_resolution). + ThrowUserError('missing_resolution', {status => $status}) + if !$resolution && !$is_open; + + # Make sure this is a valid resolution. + $resolution = $invocant->_check_select_field($resolution, 'resolution'); + + # Don't allow open bugs to have resolutions. + ThrowUserError('resolution_not_allowed') if $is_open; + + # Check noresolveonopenblockers. + my $dependson + = ref($invocant) ? $invocant->dependson : ($params->{dependson} || []); + if ( + Bugzilla->params->{"noresolveonopenblockers"} + && $resolution eq 'FIXED' + && ( !ref $invocant + or !$invocant->resolution + or $resolution ne $invocant->resolution) + && scalar @$dependson + ) + { + my $dep_bugs = Bugzilla::Bug->new_from_list($dependson); + my $count_open = grep { $_->isopened } @$dep_bugs; + if ($count_open) { + my $bug_id = ref($invocant) ? $invocant->id : undef; + ThrowUserError("still_unresolved_bugs", + {bug_id => $bug_id, dep_count => $count_open}); + } + } + + # Check if they're changing the resolution and need to comment. + if (Bugzilla->params->{'commentonchange_resolution'}) { + $invocant->_check_commenton($resolution, 'resolution', $params); + } + + return $resolution; } sub _check_short_desc { - my ($invocant, $short_desc) = @_; - # Set the parameter to itself, but cleaned up - $short_desc = clean_text($short_desc) if $short_desc; + my ($invocant, $short_desc) = @_; - if (!defined $short_desc || $short_desc eq '') { - ThrowUserError("require_summary"); - } - if (length($short_desc) > MAX_FREETEXT_LENGTH) { - ThrowUserError('freetext_too_long', - { field => 'short_desc', text => $short_desc }); - } - return $short_desc; + # Set the parameter to itself, but cleaned up + $short_desc = clean_text($short_desc) if $short_desc; + + if (!defined $short_desc || $short_desc eq '') { + ThrowUserError("require_summary"); + } + if (length($short_desc) > MAX_FREETEXT_LENGTH) { + ThrowUserError('freetext_too_long', + {field => 'short_desc', text => $short_desc}); + } + return $short_desc; } sub _check_status_whiteboard { return defined $_[1] ? $_[1] : ''; } # Unlike other checkers, this one doesn't return anything. +## REDHAT EXTENSION 876015: Add docs_contact sub _check_strict_isolation { - my ($invocant, $ccs, $assignee, $qa_contact, $product) = @_; - return unless Bugzilla->params->{'strict_isolation'}; - - if (ref $invocant) { - my $original = $invocant->new($invocant->id); - - # We only check people if they've been added. This way, if - # strict_isolation is turned on when there are invalid users - # on bugs, people can still add comments and so on. - my @old_cc = map { $_->id } @{$original->cc_users}; - my @new_cc = map { $_->id } @{$invocant->cc_users}; - my ($removed, $added) = diff_arrays(\@old_cc, \@new_cc); - $ccs = Bugzilla::User->new_from_list($added); - - $assignee = $invocant->assigned_to - if $invocant->assigned_to->id != $original->assigned_to->id; - if ($invocant->qa_contact - && (!$original->qa_contact - || $invocant->qa_contact->id != $original->qa_contact->id)) - { - $qa_contact = $invocant->qa_contact; - } - $product = $invocant->product_obj; - } - - my @related_users = @$ccs; - push(@related_users, $assignee) if $assignee; - - if (Bugzilla->params->{'useqacontact'} && $qa_contact) { - push(@related_users, $qa_contact); - } - - @related_users = @{Bugzilla::User->new_from_list(\@related_users)} - if !ref $invocant; - - # For each unique user in @related_users...(assignee and qa_contact - # could be duplicates of users in the CC list) - my %unique_users = map {$_->id => $_} @related_users; - my @blocked_users; - foreach my $id (keys %unique_users) { - my $related_user = $unique_users{$id}; - if (!$related_user->can_edit_product($product->id) || - !$related_user->can_see_product($product->name)) { - push (@blocked_users, $related_user->login); - } + ## REDHAT EXTENSION START 876015 + my ($invocant, $ccs, $assignee, $qa_contact, $docs_contact, $product) = @_; + ## REDHAT EXTENSION END 876015 + return unless Bugzilla->params->{'strict_isolation'}; + + if (ref $invocant) { + my $original = $invocant->new($invocant->id); + + # We only check people if they've been added. This way, if + # strict_isolation is turned on when there are invalid users + # on bugs, people can still add comments and so on. + my @old_cc = map { $_->id } @{$original->cc_users}; + my @new_cc = map { $_->id } @{$invocant->cc_users}; + my ($removed, $added) = diff_arrays(\@old_cc, \@new_cc); + $ccs = Bugzilla::User->new_from_list($added); + + $assignee = $invocant->assigned_to + if $invocant->assigned_to->id != $original->assigned_to->id; + if ( + $invocant->qa_contact + && (!$original->qa_contact + || $invocant->qa_contact->id != $original->qa_contact->id) + ) + { + $qa_contact = $invocant->qa_contact; + } + ## REDHAT EXTENSION START 876015 + if ( + $invocant->docs_contact + && (!$original->docs_contact + || $invocant->docs_contact->id != $original->docs_contact->id) + ) + { + $docs_contact = $invocant->docs_contact; + } + ## REDHAT EXTENSION END 876015 + $product = $invocant->product_obj; + } + + my @related_users = @$ccs; + push(@related_users, $assignee) if $assignee; + + if (Bugzilla->params->{'useqacontact'} && $qa_contact) { + push(@related_users, $qa_contact); + } + ## REDHAT EXTENSION START 876015 + if (Bugzilla->params->{'usedocscontact'} && $docs_contact) { + push(@related_users, $docs_contact); + } + ## REDHAT EXTENSION END 876015 + + @related_users = @{Bugzilla::User->new_from_list(\@related_users)} + if !ref $invocant; + + # For each unique user in @related_users...(assignee and qa_contact + # could be duplicates of users in the CC list) + my %unique_users = map { $_->id => $_ } @related_users; + my @blocked_users; + foreach my $id (keys %unique_users) { + my $related_user = $unique_users{$id}; + if ( !$related_user->can_edit_product($product->id) + || !$related_user->can_see_product($product->name)) + { + push(@blocked_users, $related_user->login); } - if (scalar(@blocked_users)) { - my %vars = ( users => \@blocked_users, - product => $product->name ); - if (ref $invocant) { - $vars{'bug_id'} = $invocant->id; - } - else { - $vars{'new'} = 1; - } - ThrowUserError("invalid_user_group", \%vars); + } + if (scalar(@blocked_users)) { + my %vars = (users => \@blocked_users, product => $product->name); + if (ref $invocant) { + $vars{'bug_id'} = $invocant->id; + } + else { + $vars{'new'} = 1; } + ThrowUserError("invalid_user_group", \%vars); + } } # This is used by various set_ checkers, to make their code simpler. sub _check_strict_isolation_for_user { - my ($self, $user) = @_; - return unless Bugzilla->params->{"strict_isolation"}; - if (!$user->can_edit_product($self->{product_id})) { - ThrowUserError('invalid_user_group', - { users => $user->login, - product => $self->product, - bug_id => $self->id }); - } + my ($self, $user) = @_; + return unless Bugzilla->params->{"strict_isolation"}; + if (!$user->can_edit_product($self->{product_id})) { + ThrowUserError('invalid_user_group', + {users => $user->login, product => $self->product, bug_id => $self->id}); + } } sub _check_tag_name { - my ($invocant, $tag) = @_; + my ($invocant, $tag) = @_; - $tag = clean_text($tag); - $tag || ThrowUserError('no_tag_to_edit'); - ThrowUserError('tag_name_too_long') if length($tag) > MAX_LEN_QUERY_NAME; - trick_taint($tag); - # Tags are all lowercase. - return lc($tag); + $tag = clean_text($tag); + $tag || ThrowUserError('no_tag_to_edit'); + ThrowUserError('tag_name_too_long') if length($tag) > MAX_LEN_QUERY_NAME; + trick_taint($tag); + + # Tags are all lowercase. + return lc($tag); } sub _check_target_milestone { - my ($invocant, $target, undef, $params) = @_; - my $product = blessed($invocant) ? $invocant->product_obj - : $params->{product}; - my $old_target = blessed($invocant) ? $invocant->target_milestone : ''; + my ($invocant, $target, undef, $params) = @_; + my $product = blessed($invocant) ? $invocant->product_obj : $params->{product}; + my $old_target = blessed($invocant) ? $invocant->target_milestone : ''; + $target = trim($target); + $target = $product->default_milestone if !defined $target; + my $object = Bugzilla::Milestone->check({product => $product, name => $target}); + if ($old_target && $object->name ne $old_target && !$object->is_active) { + ThrowUserError('value_inactive', {class => ref($object), value => $target}); + } + return $object->name; +} + +sub _check_target_release { + my ($invocant, $targets, undef, $params) = @_; + my @object_names = (); + my $product = blessed($invocant) ? $invocant->product_obj : $params->{product}; + if (not defined $targets) { + $targets = []; + } + elsif (not ref $targets) { + $targets = [$targets]; + } + + # If no target release is specified, use the default for the product + if (not scalar(@$targets)) { + $targets = [$product->default_release]; + } + + my $old_targets = blessed($invocant) ? $invocant->target_release : []; + + foreach my $target (@$targets) { $target = trim($target); - $target = $product->default_milestone if !defined $target; - my $object = Bugzilla::Milestone->check( - { product => $product, name => $target }); - if ($old_target && $object->name ne $old_target && !$object->is_active) { - ThrowUserError('value_inactive', { class => ref($object), value => $target }); + + my $object = Bugzilla::Release->check({product => $product, name => $target}); + + if (!grep ({ $_ eq $object->name } @$old_targets) && !$object->is_active) { + ThrowUserError('value_inactive', {class => ref($object), value => $target}); } - return $object->name; + + push @object_names, $object->name; + } + + ## REDHAT EXTENSION BEGIN 706784 + # Check that this product can do multiple target releases + if (!$product->multiple_target_releases and scalar(@object_names) > 1) { + ThrowUserError('no_multiple_target_releases', {product_name => $product->name}); + } + ## REDHAT EXTENSION END 706784 + + return \@object_names; } sub _check_time_field { - my ($invocant, $value, $field, $params) = @_; + my ($invocant, $value, $field, $params) = @_; - # When filing bugs, we're forgiving and just return 0 if - # the user isn't a timetracker. When updating bugs, check_can_change_field - # controls permissions, so we don't want to check them here. - if (!ref $invocant and !Bugzilla->user->is_timetracker) { - return 0; - } + # When filing bugs, we're forgiving and just return 0 if + # the user isn't a timetracker. When updating bugs, check_can_change_field + # controls permissions, so we don't want to check them here. + if (!ref $invocant and !Bugzilla->user->is_timetracker) { + return 0; + } - # check_time is in Bugzilla::Object. - return $invocant->check_time($value, $field, $params); + # check_time is in Bugzilla::Object. + return $invocant->check_time($value, $field, $params); } sub _check_version { - my ($invocant, $version, undef, $params) = @_; - $version = trim($version); - my $product = blessed($invocant) ? $invocant->product_obj - : $params->{product}; - my $old_vers = blessed($invocant) ? $invocant->version : ''; - my $object = Bugzilla::Version->check({ product => $product, name => $version }); - if ($object->name ne $old_vers && !$object->is_active) { - ThrowUserError('value_inactive', { class => ref($object), value => $version }); - } - return $object->name; + my ($invocant, $version, undef, $params) = @_; + $version = trim($version); + my $product = blessed($invocant) ? $invocant->product_obj : $params->{product}; + my $old_vers = blessed($invocant) ? $invocant->version : ''; + my $object = Bugzilla::Version->check({product => $product, name => $version}); + if ($object->name ne $old_vers && !$object->is_active) { + ThrowUserError('value_inactive', {class => ref($object), value => $version}); + } + return $object->name; } # Custom Field Validators sub _check_field_is_mandatory { - my ($invocant, $value, $field, $params) = @_; + my ($invocant, $value, $field, $params) = @_; - if (!blessed($field)) { - $field = Bugzilla::Field->new({ name => $field }); - return if !$field; - } + if (!blessed($field)) { + $field = Bugzilla::Field->new({name => $field}); + return if !$field; + } - return if !$field->is_mandatory; + return if !$field->is_mandatory; - return if !$field->is_visible_on_bug($params || $invocant); + return if !$field->is_visible_on_bug($params || $invocant); - return if ($field->type == FIELD_TYPE_SINGLE_SELECT - && scalar @{ get_legal_field_values($field->name) } == 1); + return + if ($field->type == FIELD_TYPE_SINGLE_SELECT + && scalar @{get_legal_field_values($field->name)} == 1); - return if ($field->type == FIELD_TYPE_MULTI_SELECT - && !scalar @{ get_legal_field_values($field->name) }); + return + if ($field->type == FIELD_TYPE_MULTI_SELECT + && !scalar @{get_legal_field_values($field->name)}); - if (ref($value) eq 'ARRAY') { - $value = join('', @$value); - } + if (ref($value) eq 'ARRAY') { + $value = join('', @$value); + } - $value = trim($value); - if (!defined($value) - or $value eq "" - or ($value eq '---' and $field->type == FIELD_TYPE_SINGLE_SELECT) - or ($value =~ EMPTY_DATETIME_REGEX - and $field->type == FIELD_TYPE_DATETIME)) - { - ThrowUserError('required_field', { field => $field }); - } + $value = trim($value); + if ( !defined($value) + or $value eq "" + or ($value eq '---' and $field->type == FIELD_TYPE_SINGLE_SELECT) + or ($value =~ EMPTY_DATETIME_REGEX and $field->type == FIELD_TYPE_DATETIME)) + { + ThrowUserError('required_field', {field => $field}); + } } sub _check_date_field { - my ($invocant, $date) = @_; - return $invocant->_check_datetime_field($date, undef, {date_only => 1}); + my ($invocant, $date) = @_; + return $invocant->_check_datetime_field($date, undef, {date_only => 1}); } sub _check_datetime_field { - my ($invocant, $date_time, $field, $params) = @_; - - # Empty datetimes are empty strings or strings only containing - # 0's, whitespace, and punctuation. - if ($date_time =~ /^[\s0[:punct:]]*$/) { - return undef; - } - - $date_time = trim($date_time); - my ($date, $time) = split(' ', $date_time); - if ($date && !validate_date($date)) { - ThrowUserError('illegal_date', { date => $date, - format => 'YYYY-MM-DD' }); - } - if ($time && $params->{date_only}) { - ThrowUserError('illegal_date', { date => $date_time, - format => 'YYYY-MM-DD' }); - } - if ($time && !validate_time($time)) { - ThrowUserError('illegal_time', { 'time' => $time, - format => 'HH:MM:SS' }); - } - return $date_time + my ($invocant, $date_time, $field, $params) = @_; + + # Empty datetimes are empty strings or strings only containing + # 0's, whitespace, and punctuation. + if ($date_time =~ /^[\s0[:punct:]]*$/) { + return undef; + } + + $date_time = trim($date_time); + my ($date, $time) = split(' ', $date_time); + if ($date && !validate_date($date)) { + ThrowUserError('illegal_date', {date => $date, format => 'YYYY-MM-DD'}); + } + if ($time && $params->{date_only}) { + ThrowUserError('illegal_date', {date => $date_time, format => 'YYYY-MM-DD'}); + } + if ($time && !validate_time($time)) { + ThrowUserError('illegal_time', {'time' => $time, format => 'HH:MM:SS'}); + } + return $date_time; } sub _check_default_field { return defined $_[1] ? trim($_[1]) : ''; } sub _check_freetext_field { - my ($invocant, $text, $field) = @_; + my ($invocant, $text, $field) = @_; - $text = (defined $text) ? trim($text) : ''; - if (length($text) > MAX_FREETEXT_LENGTH) { - ThrowUserError('freetext_too_long', - { field => $field, text => $text }); - } - return $text; + $text = (defined $text) ? trim($text) : ''; + if (length($text) > MAX_FREETEXT_LENGTH + && (!($field eq 'cf_fixed_in' && length($text) <= 32767))) + { + ThrowUserError('freetext_too_long', {field => $field, text => $text}); + } + return $text; } sub _check_multi_select_field { - my ($invocant, $values, $field) = @_; + my ($invocant, $values, $field) = @_; - # Allow users (mostly email_in.pl) to specify multi-selects as - # comma-separated values. - if (defined $values and !ref $values) { - # We don't split on spaces because multi-select values can and often - # do have spaces in them. (Theoretically they can have commas in them - # too, but that's much less common and people should be able to work - # around it pretty cleanly, if they want to use email_in.pl.) - $values = [split(',', $values)]; - } + # Allow users (mostly email_in.pl) to specify multi-selects as + # comma-separated values. + if (defined $values and !ref $values) { - return [] if !$values; - my @checked_values; - foreach my $value (@$values) { - push(@checked_values, $invocant->_check_select_field($value, $field)); - } - return \@checked_values; + # We don't split on spaces because multi-select values can and often + # do have spaces in them. (Theoretically they can have commas in them + # too, but that's much less common and people should be able to work + # around it pretty cleanly, if they want to use email_in.pl.) + $values = [split(',', $values)]; + } + + return [] if !$values; + my @checked_values; + foreach my $value (@$values) { + push(@checked_values, $invocant->_check_select_field($value, $field)); + } + return \@checked_values; +} + +sub _check_single_select_field { + my ($invocant, $value, $field) = @_; + if($value) { + my $object = Bugzilla::Field::Choice->type($field)->check($value); + return $object->name; + } + + return; } sub _check_select_field { - my ($invocant, $value, $field) = @_; - my $object = Bugzilla::Field::Choice->type($field)->check($value); - return $object->name; + my ($invocant, $value, $field) = @_; + my $object = Bugzilla::Field::Choice->type($field)->check($value); + return $object->name; } sub _check_bugid_field { - my ($invocant, $value, $field) = @_; - return undef if !$value; - - # check that the value is a valid, visible bug id - my $checked_id = $invocant->check($value, $field)->id; - - # check for loop (can't have a loop if this is a new bug) - if (ref $invocant) { - _check_relationship_loop($field, $invocant->bug_id, $checked_id); - } + my ($invocant, $value, $field) = @_; + return undef if !$value; - return $checked_id; + # check that the value is a valid, visible bug id + my $checked_id = $invocant->check($value, $field)->id; + + # check for loop (can't have a loop if this is a new bug) + if (ref $invocant) { + _check_relationship_loop($field, $invocant->bug_id, $checked_id); + } + + return $checked_id; } sub _check_textarea_field { - my ($invocant, $text, $field) = @_; + my ($invocant, $text, $field) = @_; - $text = (defined $text) ? trim($text) : ''; + $text = (defined $text) ? trim($text) : ''; - # Web browsers submit newlines as \r\n. - # Sanitize all input to match the web standard. - # XMLRPC input could be either \n or \r\n - $text =~ s/\r?\n/\r\n/g; + # Web browsers submit newlines as \r\n. + # Sanitize all input to match the web standard. + # XMLRPC input could be either \n or \r\n + $text =~ s/\r?\n/\r\n/g; - return $text; + return $text; } sub _check_integer_field { - my ($invocant, $value, $field) = @_; - $value = defined($value) ? trim($value) : ''; + my ($invocant, $value, $field) = @_; + $value = defined($value) ? trim($value) : ''; - if ($value eq '') { - return 0; - } + if ($value eq '') { + return 0; + } - my $orig_value = $value; - if (!detaint_signed($value)) { - ThrowUserError("number_not_integer", - {field => $field, num => $orig_value}); - } - elsif (abs($value) > MAX_INT_32) { - ThrowUserError("number_too_large", - {field => $field, num => $orig_value, max_num => MAX_INT_32}); - } + my $orig_value = $value; + if (!detaint_signed($value)) { + ThrowUserError("number_not_integer", {field => $field, num => $orig_value}); + } + elsif (abs($value) > MAX_INT_32) { + ThrowUserError("number_too_large", + {field => $field, num => $orig_value, max_num => MAX_INT_32}); + } - return $value; + return $value; } sub _check_relationship_loop { - # Generates a dependency tree for a given bug. Calls itself recursively - # to generate sub-trees for the bug's dependencies. - my ($field, $bug_id, $dep_id, $ids) = @_; - - # Don't do anything if this bug doesn't have any dependencies. - return unless defined($dep_id); - - # Check whether we have seen this bug yet - $ids = {} unless defined $ids; - $ids->{$bug_id} = 1; - if ($ids->{$dep_id}) { - ThrowUserError("relationship_loop_single", { - 'bug_id' => $bug_id, - 'dep_id' => $dep_id, - 'field_name' => $field}); - } - - # Get this dependency's record from the database - my $dbh = Bugzilla->dbh; - my $next_dep_id = $dbh->selectrow_array( - "SELECT $field FROM bugs WHERE bug_id = ?", undef, $dep_id); - _check_relationship_loop($field, $dep_id, $next_dep_id, $ids); + # Generates a dependency tree for a given bug. Calls itself recursively + # to generate sub-trees for the bug's dependencies. + my ($field, $bug_id, $dep_id, $ids) = @_; + + # Don't do anything if this bug doesn't have any dependencies. + return unless defined($dep_id); + + # Check whether we have seen this bug yet + $ids = {} unless defined $ids; + $ids->{$bug_id} = 1; + if ($ids->{$dep_id}) { + ThrowUserError("relationship_loop_single", + {'bug_id' => $bug_id, 'dep_id' => $dep_id, 'field_name' => $field}); + } + + # Get this dependency's record from the database + my $dbh = Bugzilla->dbh; + my $next_dep_id + = $dbh->selectrow_array("SELECT $field FROM bugs WHERE bug_id = ?", + undef, $dep_id); + + _check_relationship_loop($field, $dep_id, $next_dep_id, $ids); } ##################################################################### @@ -2298,63 +2973,70 @@ sub _check_relationship_loop { ##################################################################### sub fields { - my $class = shift; - - my @fields = - ( - # Standard Fields - # Keep this ordering in sync with bugzilla.dtd. - qw(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), - - # Conditional Fields - Bugzilla->params->{'useqacontact'} ? "qa_contact" : (), - # Custom Fields - map { $_->name } Bugzilla->active_custom_fields - ); - Bugzilla::Hook::process('bug_fields', {'fields' => \@fields} ); - - return @fields; + my $class = shift; + + my @fields = ( + + # Standard Fields + # Keep this ordering in sync with bugzilla.dtd. + qw(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), + + # Conditional Fields + Bugzilla->params->{'useqacontact'} ? "qa_contact" : (), + ## REDHAT EXTENSION START 876015 + Bugzilla->params->{'usedocscontact'} ? "docs_contact" : (), + ## REDHAT EXTENSION END 876015 + # Custom Fields + map { $_->name } Bugzilla->active_custom_fields + ); + Bugzilla::Hook::process('bug_fields', {'fields' => \@fields}); + + return @fields; } ##################################################################### -# Mutators +# Mutators ##################################################################### # To run check_can_change_field. sub _set_global_validator { - my ($self, $value, $field) = @_; - my $current = $self->$field; - my $privs; - - if (ref $current && ref($current) ne 'ARRAY' - && $current->isa('Bugzilla::Object')) { - $current = $current->id ; - } - if (ref $value && ref($value) ne 'ARRAY' - && $value->isa('Bugzilla::Object')) { - $value = $value->id ; - } - my $can = $self->check_can_change_field($field, $current, $value, \$privs); - if (!$can) { - if ($field eq 'assigned_to' || $field eq 'qa_contact') { - $value = Bugzilla::User->new($value)->login; - $current = Bugzilla::User->new($current)->login; - } - ThrowUserError('illegal_change', { field => $field, - oldvalue => $current, - newvalue => $value, - privs => $privs }); + my ($self, $value, $field) = @_; + my $current = $self->$field; + my $privs; + + if ( ref $current + && ref($current) ne 'ARRAY' + && $current->isa('Bugzilla::Object')) + { + $current = $current->id; + } + if (ref $value && ref($value) ne 'ARRAY' && $value->isa('Bugzilla::Object')) { + $value = $value->id; + } + my $can = $self->check_can_change_field($field, $current, $value, \$privs); + if (!$can) { + ## REDHAT EXTENSION START 876015 + if ( $field eq 'assigned_to' + || $field eq 'qa_contact' + || $field eq 'docs_contact') + { + ## REDHAT EXTENSION START 876015 + $value = Bugzilla::User->new($value)->login; + $current = Bugzilla::User->new($current)->login; } - $self->_check_field_is_mandatory($value, $field); + ThrowUserError('illegal_change', + {field => $field, oldvalue => $current, newvalue => $value, privs => $privs}); + } + $self->_check_field_is_mandatory($value, $field); } @@ -2365,359 +3047,500 @@ sub _set_global_validator { # Note that if you are changing multiple bugs at once, you must pass # other_bugs to set_all in order for it to behave properly. sub set_all { - my $self = shift; - my ($input_params) = @_; - - # Clone the data as we are going to alter it, and this would affect - # subsequent bugs when calling set_all() again, as some fields would - # be modified or no longer defined. - my $params = {}; - %$params = %$input_params; - - # You cannot mark bugs as duplicate when changing several bugs at once - # (because currently there is no way to check for duplicate loops in that - # situation). You also cannot set the alias of several bugs at once. - if ($params->{other_bugs} and scalar @{ $params->{other_bugs} } > 1) { - ThrowUserError('dupe_not_allowed') if exists $params->{dup_id}; - ThrowUserError('multiple_alias_not_allowed') - if defined $params->{alias}; - } - - # For security purposes, and because lots of other checks depend on it, - # we set the product first before anything else. - my $product_changed; # Used only for strict_isolation checks. - if (exists $params->{'product'}) { - $product_changed = $self->_set_product($params->{'product'}, $params); - } - - # strict_isolation checks mean that we should set the groups - # immediately after changing the product. - $self->_add_remove($params, 'groups'); - - if (exists $params->{'dependson'} or exists $params->{'blocked'}) { - my %set_deps; - foreach my $name (qw(dependson blocked)) { - my @dep_ids = @{ $self->$name }; - # If only one of the two fields was passed in, then we need to - # retain the current value for the other one. - if (!exists $params->{$name}) { - $set_deps{$name} = \@dep_ids; - next; - } - - # Explicitly setting them to a particular value overrides - # add/remove. - if (exists $params->{$name}->{set}) { - $set_deps{$name} = $params->{$name}->{set}; - next; - } - - foreach my $add (@{ $params->{$name}->{add} || [] }) { - push(@dep_ids, $add) if !grep($_ == $add, @dep_ids); - } - foreach my $remove (@{ $params->{$name}->{remove} || [] }) { - @dep_ids = grep($_ != $remove, @dep_ids); - } - $set_deps{$name} = \@dep_ids; - } - - $self->set_dependencies($set_deps{'dependson'}, $set_deps{'blocked'}); - } - - if (exists $params->{'keywords'}) { - # Sorting makes the order "add, remove, set", just like for other - # fields. - foreach my $action (sort keys %{ $params->{'keywords'} }) { - $self->modify_keywords($params->{'keywords'}->{$action}, $action); - } - } + my $self = shift; + my ($input_params) = @_; + + ## REDHAT EXTENSION BEGIN 584954 + # extension is adding product to the hook + Bugzilla::Hook::process('bug_before_set_all', + {params => $input_params, bug => $self}); + ## REDHAT EXTENSION END 584954 + + # Clone the data as we are going to alter it, and this would affect + # subsequent bugs when calling set_all() again, as some fields would + # be modified or no longer defined. + my $params = {}; + %$params = %$input_params; + + # You cannot mark bugs as duplicate when changing several bugs at once + # (because currently there is no way to check for duplicate loops in that + # situation). You also cannot set the alias of several bugs at once. + if ($params->{other_bugs} and scalar @{$params->{other_bugs}} > 1) { + ThrowUserError('dupe_not_allowed') if exists $params->{dup_id}; + ThrowUserError('multiple_alias_not_allowed') if defined $params->{alias}; + } + + # For security purposes, and because lots of other checks depend on it, + # we set the product first before anything else. + my $product_changed; # Used only for strict_isolation checks. + if (exists $params->{'product'}) { + $product_changed = $self->_set_product($params->{'product'}, $params); + } + + # strict_isolation checks mean that we should set the groups + # immediately after changing the product. + $self->_add_remove($params, 'groups'); + + if (exists $params->{'dependson'} or exists $params->{'blocked'}) { + my %set_deps; + foreach my $name (qw(dependson blocked)) { + my @dep_ids = @{$self->$name}; + + # If only one of the two fields was passed in, then we need to + # retain the current value for the other one. + if (!exists $params->{$name}) { + $set_deps{$name} = \@dep_ids; + next; + } + + # Explicitly setting them to a particular value overrides + # add/remove. + if (exists $params->{$name}->{set}) { + $set_deps{$name} = $params->{$name}->{set}; + next; + } + + foreach my $add (@{$params->{$name}->{add} || []}) { + push(@dep_ids, $add) if !grep($_ == $add, @dep_ids); + } + foreach my $remove (@{$params->{$name}->{remove} || []}) { + @dep_ids = grep($_ != $remove, @dep_ids); + } + $set_deps{$name} = \@dep_ids; + } + + $self->set_dependencies($set_deps{'dependson'}, $set_deps{'blocked'}); + } + + if (exists $params->{'keywords'}) { + + # Sorting makes the order "add, remove, set", just like for other + # fields. + foreach my $action (sort keys %{$params->{'keywords'}}) { + $self->modify_keywords($params->{'keywords'}->{$action}, $action); + } + } + + if (exists $params->{'comment'} or exists $params->{'work_time'}) { + + # Add a comment as needed to each bug. This is done early because + # there are lots of things that want to check if we added a comment. + $self->add_comment( + $params->{'comment'}->{'body'}, + { + isprivate => $params->{'comment'}->{'is_private'}, + work_time => $params->{'work_time'} + } + ); + } - if (exists $params->{'comment'} or exists $params->{'work_time'}) { - # Add a comment as needed to each bug. This is done early because - # there are lots of things that want to check if we added a comment. - $self->add_comment($params->{'comment'}->{'body'}, - { isprivate => $params->{'comment'}->{'is_private'}, - work_time => $params->{'work_time'} }); - } + if (exists $params->{alias} && $params->{alias}{set}) { + my ($removed_aliases, $added_aliases) + = diff_arrays($self->alias, $params->{alias}{set}); + $params->{alias} = {add => $added_aliases, remove => $removed_aliases,}; + } - if (exists $params->{alias} && $params->{alias}{set}) { - my ($removed_aliases, $added_aliases) = diff_arrays( - $self->alias, $params->{alias}{set}); - $params->{alias} = { - add => $added_aliases, - remove => $removed_aliases, - }; - } + my %normal_set_all; + foreach my $name (keys %$params) { - my %normal_set_all; - foreach my $name (keys %$params) { - # These are handled separately below. - if ($self->can("set_$name")) { - $normal_set_all{$name} = $params->{$name}; - } + # These are handled separately below. + if ($self->can("set_$name")) { + $normal_set_all{$name} = $params->{$name}; } - $self->SUPER::set_all(\%normal_set_all); + } + $self->SUPER::set_all(\%normal_set_all); - $self->reset_assigned_to if $params->{'reset_assigned_to'}; - $self->reset_qa_contact if $params->{'reset_qa_contact'}; + $self->reset_assigned_to if $params->{'reset_assigned_to'}; + $self->reset_qa_contact if $params->{'reset_qa_contact'}; + ## REDHAT EXTENSION START 876015 + $self->reset_docs_contact if $params->{'reset_docs_contact'}; + ## REDHAT EXTENSION END 876015 - $self->_add_remove($params, 'see_also'); + $self->_add_remove($params, 'see_also'); - # And set custom fields. - my @custom_fields = Bugzilla->active_custom_fields; - foreach my $field (@custom_fields) { - my $fname = $field->name; - if (exists $params->{$fname}) { - $self->set_custom_field($field, $params->{$fname}); - } + # And set custom fields. + my @custom_fields = Bugzilla->active_custom_fields; + foreach my $field (@custom_fields) { + my $fname = $field->name; + if (exists $params->{$fname}) { + $self->set_custom_field($field, $params->{$fname}); } + } - $self->_add_remove($params, 'cc'); - $self->_add_remove($params, 'alias'); + $self->_add_remove($params, 'cc'); + $self->_add_remove($params, 'alias'); - # Theoretically you could move a product without ever specifying - # a new assignee or qa_contact, or adding/removing any CCs. So, - # we have to check that the current assignee, qa, and CCs are still - # valid if we've switched products, under strict_isolation. We can only - # do that here, because if they *did* change the assignee, qa, or CC, - # then we don't want to check the original ones, only the new ones. - $self->_check_strict_isolation() if $product_changed; + # Theoretically you could move a product without ever specifying + # a new assignee or qa_contact, or adding/removing any CCs. So, + # we have to check that the current assignee, qa, and CCs are still + # valid if we've switched products, under strict_isolation. We can only + # do that here, because if they *did* change the assignee, qa, or CC, + # then we don't want to check the original ones, only the new ones. + $self->_check_strict_isolation() if $product_changed; } # Helper for set_all that helps with fields that have an "add/remove" # pattern instead of a "set_" pattern. sub _add_remove { - my ($self, $params, $name) = @_; - my @add = @{ $params->{$name}->{add} || [] }; - my @remove = @{ $params->{$name}->{remove} || [] }; - $name =~ s/s$// if $name ne 'alias'; - my $add_method = "add_$name"; - my $remove_method = "remove_$name"; - $self->$add_method($_) foreach @add; - $self->$remove_method($_) foreach @remove; + my ($self, $params, $name) = @_; + my @add = @{$params->{$name}->{add} || []}; + my @remove = @{$params->{$name}->{remove} || []}; + $name =~ s/s$// if $name ne 'alias'; + my $add_method = "add_$name"; + my $remove_method = "remove_$name"; + $self->$add_method($_) foreach @add; + $self->$remove_method($_) foreach @remove; } sub set_assigned_to { - my ($self, $value) = @_; - $self->set('assigned_to', $value); - # Store the old assignee. check_can_change_field() needs it. - $self->{'_old_assigned_to'} = $self->{'assigned_to_obj'}->id; - delete $self->{'assigned_to_obj'}; + my ($self, $value) = @_; + $self->set('assigned_to', $value); + + # Store the old assignee. check_can_change_field() needs it. + $self->{'_old_assigned_to'} = $self->{'assigned_to_obj'}->id; + delete $self->{'assigned_to_obj'}; } + sub reset_assigned_to { - my $self = shift; - my $comp = $self->component_obj; - $self->set_assigned_to($comp->default_assignee); + my $self = shift; + my $comp = $self->component_obj; + $self->set_assigned_to($comp->default_assignee); } sub set_bug_ignored { $_[0]->set('bug_ignored', $_[1]); } sub set_cclist_accessible { $_[0]->set('cclist_accessible', $_[1]); } sub set_comment_is_private { - my ($self, $comments, $isprivate) = @_; - $self->{comment_isprivate} ||= []; - my $is_insider = Bugzilla->user->is_insider; - - $comments = { $comments => $isprivate } unless ref $comments; - - foreach my $comment (@{$self->comments}) { - # Skip unmodified comment privacy. - next unless exists $comments->{$comment->id}; - - my $isprivate = delete $comments->{$comment->id} ? 1 : 0; - if ($isprivate != $comment->is_private) { - ThrowUserError('user_not_insider') unless $is_insider; - $comment->set_is_private($isprivate); - push @{$self->{comment_isprivate}}, $comment; - } - } - - # If there are still entries in $comments, then they are illegal. - ThrowUserError('comment_invalid_isprivate', { id => join(', ', keys %$comments) }) - if scalar keys %$comments; - - # If no comment privacy has been modified, remove this key. - delete $self->{comment_isprivate} unless scalar @{$self->{comment_isprivate}}; + my ($self, $comments, $isprivate) = @_; + $self->{comment_isprivate} ||= []; + my $is_insider = Bugzilla->user->is_insider; + + $comments = {$comments => $isprivate} unless ref $comments; + + foreach my $comment (@{$self->comments}) { + + # Skip unmodified comment privacy. + next unless exists $comments->{$comment->id}; + + my $isprivate = delete $comments->{$comment->id} ? 1 : 0; + if ($isprivate != $comment->is_private) { + ThrowUserError('user_not_insider') unless $is_insider; + $comment->set_is_private($isprivate); + push @{$self->{comment_isprivate}}, $comment; + + ## REDHAT EXTENSION START 404771 765712 + # If a comment is being made private and it is linked to an + # attachment, make the attachment private too + if ($isprivate == 1 and $comment->type == CMT_ATTACHMENT_CREATED) { + my $attach_id = $comment->extra_data; + my $attachment = Bugzilla::Attachment->new($attach_id); + $attachment->set_is_private(1); + $attachment->update(); + } + ## REDHAT EXTENSION END 404771 765712 + } + } + + # If there are still entries in $comments, then they are illegal. + ThrowUserError('comment_invalid_isprivate', {id => join(', ', keys %$comments)}) + if scalar keys %$comments; + + # If no comment privacy has been modified, remove this key. + delete $self->{comment_isprivate} unless scalar @{$self->{comment_isprivate}}; +} + +sub set_component { + my ($self, $name) = @_; + my $old_comp = $self->component_obj; + my $component = $self->_check_component($name); + if ($old_comp->id != $component->id) { + $self->{component_id} = $component->id; + $self->{component} = $component->name; + $self->{component_obj} = $component; + + # For update() + $self->{_old_component_name} = $old_comp->name; + + # Add in the Default CC of the new Component; + ## REDHAT EXTENSION START 653316 1117645 + # If a sub component is selected, use that for the default CC value + my $default_cc + = (Bugzilla->params->{usesubcomponents} + && scalar(@{$self->rh_sub_component_objs})) + ? $self->rh_sub_component_objs->[0]->initial_cc + : $component->initial_cc; + foreach my $cc (@$default_cc) { + ## REDHAT EXTENSION END 653316 1117645 + $self->add_cc($cc, 1); + } + } } -sub set_component { - my ($self, $name) = @_; - my $old_comp = $self->component_obj; - my $component = $self->_check_component($name); - if ($old_comp->id != $component->id) { - $self->{component_id} = $component->id; - $self->{component} = $component->name; - $self->{component_obj} = $component; - # For update() - $self->{_old_component_name} = $old_comp->name; - # Add in the Default CC of the new Component; - foreach my $cc (@{$component->initial_cc}) { - $self->add_cc($cc); +sub set_custom_field { + my ($self, $field, $value) = @_; + + $field->user_can_edit(Bugzilla->user) + || ThrowUserError('illegal_change', {field => $field->name}); + + if (ref $value eq 'ARRAY' && $field->type != FIELD_TYPE_MULTI_SELECT) { + $value = $value->[0]; + } + + ## REDHAT EXTENSION BEGIN 824974 1039815 + # If a hash is given for a multi select custom field, assuming the + # user supplied 'add', 'remove' and 'set' keys. Set is has priority + # over add which has priority over remove. + if (ref $value eq 'HASH' && $field->type == FIELD_TYPE_MULTI_SELECT) { + my $dbh = Bugzilla->dbh; + my $field_name = $field->name; + + # Get the current and allowed values. We need to use SQL to get the + # value to get around restrictions + my $query = "SELECT value FROM bug_$field_name WHERE bug_id = ?"; + my $new_values = $dbh->selectcol_arrayref($query, undef, $self->id); + my $legal_values = $field->legal_values; + + # We need to trap this error here, otherwise we would leak + # if a value was set or not. + foreach my $action ('add', 'remove', 'set') { + foreach my $value (@{$value->{$action} || []}) { + if (!grep { $_->name eq $value } @$legal_values) { + ThrowUserError( + "field_cannot_$action", + {field => $field, value => $value} + ); } + } } -} -sub set_custom_field { - my ($self, $field, $value) = @_; - if (ref $value eq 'ARRAY' && $field->type != FIELD_TYPE_MULTI_SELECT) { - $value = $value->[0]; + # Remove and/or add as required + foreach my $rem_value (@{$value->{remove} || []}) { + @$new_values = grep { $_ ne trim($rem_value) } @$new_values; + } + foreach my $add_value (@{$value->{add} || []}) { + if (not grep { $_ eq trim($add_value) } @$new_values) { + push @$new_values, trim($add_value); + } + } + if (defined $value->{set}) { + @$new_values = map { trim($_) } @{$value->{set}}; } - ThrowCodeError('field_not_custom', { field => $field }) if !$field->custom; - $self->set($field->name, $value); + + # Return the array that is expected + $value = $new_values; + } + ## REDHAT EXTENSION END 824974 1039815 + + ThrowCodeError('field_not_custom', {field => $field}) if !$field->custom; + $self->set($field->name, $value); } sub set_deadline { $_[0]->set('deadline', $_[1]); } + sub set_dependencies { - my ($self, $dependson, $blocked) = @_; - my %extra = ( blocked => $blocked ); - $dependson = $self->_check_dependencies($dependson, 'dependson', \%extra); - $blocked = $extra{blocked}; - # These may already be detainted, but all setters are supposed to - # detaint their input if they've run a validator (just as though - # we had used Bugzilla::Object::set), so we do that here. - detaint_natural($_) foreach (@$dependson, @$blocked); - $self->{'dependson'} = $dependson; - $self->{'blocked'} = $blocked; - delete $self->{depends_on_obj}; - delete $self->{blocks_obj}; + my ($self, $dependson, $blocked) = @_; + my %extra = (blocked => $blocked); + $dependson = $self->_check_dependencies($dependson, 'dependson', \%extra); + $blocked = $extra{blocked}; + + # These may already be detainted, but all setters are supposed to + # detaint their input if they've run a validator (just as though + # we had used Bugzilla::Object::set), so we do that here. + detaint_natural($_) foreach (@$dependson, @$blocked); + $self->{'dependson'} = $dependson; + $self->{'blocked'} = $blocked; + delete $self->{depends_on_obj}; + delete $self->{blocks_obj}; } sub _clear_dup_id { $_[0]->{dup_id} = undef; } + sub set_dup_id { - my ($self, $dup_id) = @_; - my $old = $self->dup_id || 0; - $self->set('dup_id', $dup_id); - my $new = $self->dup_id; - return if $old == $new; - - # Make sure that we have the DUPLICATE resolution. This is needed - # if somebody calls set_dup_id without calling set_bug_status or - # set_resolution. - if ($self->resolution ne 'DUPLICATE') { - # Even if the current status is VERIFIED, we change it back to - # RESOLVED (or whatever the duplicate_or_move_bug_status is) here, - # because that's the same thing the UI does when you click on the - # "Mark as Duplicate" link. If people really want to retain their - # current status, they can use set_bug_status and set the DUPLICATE - # resolution before getting here. - $self->set_bug_status( - Bugzilla->params->{'duplicate_or_move_bug_status'}, - { resolution => 'DUPLICATE' }); - } - - # Update the other bug. - my $dupe_of = new Bugzilla::Bug($self->dup_id); - if (delete $self->{_add_dup_cc}) { - $dupe_of->add_cc($self->reporter); - } - $dupe_of->add_comment("", { type => CMT_HAS_DUPE, - extra_data => $self->id }); - $self->{_dup_for_update} = $dupe_of; - - # Now make sure that we add a duplicate comment on *this* bug. - # (Change an existing comment into a dup comment, if there is one, - # or add an empty dup comment.) - if ($self->{added_comments}) { - my @normal = grep { !defined $_->{type} || $_->{type} == CMT_NORMAL } - @{ $self->{added_comments} }; - # Turn the last one into a dup comment. - $normal[-1]->{type} = CMT_DUPE_OF; - $normal[-1]->{extra_data} = $self->dup_id; - } - else { - $self->add_comment('', { type => CMT_DUPE_OF, - extra_data => $self->dup_id }); - } + my ($self, $dup_id) = @_; + my $old = $self->dup_id || 0; + $self->set('dup_id', $dup_id); + my $new = $self->dup_id; + return if $old == $new; + + # Make sure that we have the DUPLICATE resolution. This is needed + # if somebody calls set_dup_id without calling set_bug_status or + # set_resolution. + if ($self->resolution ne 'DUPLICATE') { + + # Even if the current status is VERIFIED, we change it back to + # RESOLVED (or whatever the duplicate_or_move_bug_status is) here, + # because that's the same thing the UI does when you click on the + # "Mark as Duplicate" link. If people really want to retain their + # current status, they can use set_bug_status and set the DUPLICATE + # resolution before getting here. + $self->set_bug_status(Bugzilla->params->{'duplicate_or_move_bug_status'}, + {resolution => 'DUPLICATE'}); + } + + # Update the other bug. + my $dupe_of = new Bugzilla::Bug($self->dup_id); + if (delete $self->{_add_dup_cc}) { + $dupe_of->add_cc($self->reporter); + } + $dupe_of->add_comment("", {type => CMT_HAS_DUPE, extra_data => $self->id}); + $self->{_dup_for_update} = $dupe_of; + + # Now make sure that we add a duplicate comment on *this* bug. + # (Change an existing comment into a dup comment, if there is one, + # or add an empty dup comment.) + if ($self->{added_comments}) { + my @normal = grep { !defined $_->{type} || $_->{type} == CMT_NORMAL } + @{$self->{added_comments}}; + + # Turn the last one into a dup comment. + $normal[-1]->{type} = CMT_DUPE_OF; + $normal[-1]->{extra_data} = $self->dup_id; + } + else { + $self->add_comment('', {type => CMT_DUPE_OF, extra_data => $self->dup_id}); + } } sub set_estimated_time { $_[0]->set('estimated_time', $_[1]); } -sub _set_everconfirmed { $_[0]->set('everconfirmed', $_[1]); } +sub _set_everconfirmed { $_[0]->set('everconfirmed', $_[1]); } + sub set_flags { - my ($self, $flags, $new_flags) = @_; + my ($self, $flags, $new_flags) = @_; - Bugzilla::Flag->set_flag($self, $_) foreach (@$flags, @$new_flags); + Bugzilla::Flag->set_flag($self, $_) foreach (@$flags, @$new_flags); } -sub set_op_sys { $_[0]->set('op_sys', $_[1]); } -sub set_platform { $_[0]->set('rep_platform', $_[1]); } -sub set_priority { $_[0]->set('priority', $_[1]); } +sub set_op_sys { $_[0]->set('op_sys', $_[1]); } +sub set_platform { $_[0]->set('rep_platform', $_[1]); } +sub set_priority { $_[0]->set('priority', $_[1]); } + # For security reasons, you have to use set_all to change the product. # See the strict_isolation check in set_all for an explanation. sub _set_product { - my ($self, $name, $params) = @_; - my $old_product = $self->product_obj; - my $product = $self->_check_product($name); - - my $product_changed = 0; - if ($old_product->id != $product->id) { - $self->{product_id} = $product->id; - $self->{product} = $product->name; - $self->{product_obj} = $product; - # For update() - $self->{_old_product_name} = $old_product->name; - # Delete fields that depend upon the old Product value. - delete $self->{choices}; - $product_changed = 1; - } - - $params ||= {}; - # We delete these so that they're not set again later in set_all. - my $comp_name = delete $params->{component} || $self->component; - my $vers_name = delete $params->{version} || $self->version; - my $tm_name = delete $params->{target_milestone}; - # This way, if usetargetmilestone is off and we've changed products, - # set_target_milestone will reset our target_milestone to - # $product->default_milestone. But if we haven't changed products, - # we don't reset anything. - if (!defined $tm_name - && (Bugzilla->params->{'usetargetmilestone'} || !$product_changed)) - { - $tm_name = $self->target_milestone; - } - - if ($product_changed && Bugzilla->usage_mode == USAGE_MODE_BROWSER) { - # Try to set each value with the new product. - # Have to set error_mode because Throw*Error calls exit() otherwise. - my $old_error_mode = Bugzilla->error_mode; - Bugzilla->error_mode(ERROR_MODE_DIE); - my $component_ok = eval { $self->set_component($comp_name); 1; }; - my $version_ok = eval { $self->set_version($vers_name); 1; }; - my $milestone_ok = 1; - # Reporters can move bugs between products but not set the TM. - if ($self->check_can_change_field('target_milestone', 0, 1)) { - $milestone_ok = eval { $self->set_target_milestone($tm_name); 1; }; - } - else { - # Have to set this directly to bypass the validators. - $self->{target_milestone} = $product->default_milestone; - } - # If there were any errors thrown, make sure we don't mess up any - # other part of Bugzilla that checks $@. - undef $@; - Bugzilla->error_mode($old_error_mode); - - my $verified = $params->{product_change_confirmed}; - my %vars; - if (!$verified || !$component_ok || !$version_ok || !$milestone_ok) { - $vars{defaults} = { - # Note that because of the eval { set } above, these are - # already set correctly if they're valid, otherwise they're - # set to some invalid value which the template will ignore. - component => $self->component, - version => $self->version, - milestone => $milestone_ok ? $self->target_milestone - : $product->default_milestone - }; - $vars{components} = [map { $_->name } grep($_->is_active, @{$product->components})]; - $vars{milestones} = [map { $_->name } grep($_->is_active, @{$product->milestones})]; - $vars{versions} = [map { $_->name } grep($_->is_active, @{$product->versions})]; - } + my ($self, $name, $params) = @_; + my $old_product = $self->product_obj; + my $product = $self->_check_product($name); + + my $product_changed = 0; + if ($old_product->id != $product->id) { + $self->{product_id} = $product->id; + $self->{product} = $product->name; + $self->{product_obj} = $product; + + # For update() + $self->{_old_product_name} = $old_product->name; + + # Delete fields that depend upon the old Product value. + delete $self->{choices}; + $product_changed = 1; + } + + $params ||= {}; + + # We delete these so that they're not set again later in set_all. + my $comp_name = delete $params->{component} || $self->component; + my $vers_name = delete $params->{version} || $self->version; + my $tm_name = delete $params->{target_milestone}; + + # This way, if usetargetmilestone is off and we've changed products, + # set_target_milestone will reset our target_milestone to + # $product->default_milestone. But if we haven't changed products, + # we don't reset anything. + if (!defined $tm_name + && (Bugzilla->params->{'usetargetmilestone'} || !$product_changed)) + { + $tm_name = $self->target_milestone; + } + + ## REDHAT EXTENSION START 706784 877243 + my $tr_names = $params->{target_release}; + + # This way, if usetargetrelease is off and we've changed products, + # set_target_release will reset our target_release to + # $product->default_release. But if we haven't changed products, + # we don't reset anything. + if (not defined $tr_names + && (Bugzilla->params->{'usetargetrelease'} || !$product_changed)) + { + $tr_names = $self->target_release; + } + ## REDHAT EXTENSION END 706784 877243 + + if ($product_changed && Bugzilla->usage_mode == USAGE_MODE_BROWSER) { + + # Try to set each value with the new product. + # Have to set error_mode because Throw*Error calls exit() otherwise. + my $old_error_mode = Bugzilla->error_mode; + Bugzilla->error_mode(ERROR_MODE_DIE); + my $component_ok = eval { $self->set_component($comp_name); 1; }; + my $version_ok = eval { $self->set_version($vers_name); 1; }; + my $milestone_ok = 1; + + # Reporters can move bugs between products but not set the TM. + if ($self->check_can_change_field('target_milestone', 0, 1)) { + $milestone_ok = eval { $self->set_target_milestone($tm_name); 1; }; + } + else { + # Have to set this directly to bypass the validators. + $self->{target_milestone} = $product->default_milestone; + } + + ## REDHAT EXTENSION START 706784 877243 + my $release_ok = 1; - if (!$verified) { - $vars{verify_bug_groups} = 1; - my $dbh = Bugzilla->dbh; - my @idlist = ($self->id); - push(@idlist, map {$_->id} @{ $params->{other_bugs} }) - if $params->{other_bugs}; - @idlist = uniq @idlist; - # Get the ID of groups which are no longer valid in the new product. - my $gids = $dbh->selectcol_arrayref( - 'SELECT bgm.group_id + # Reporters can move bugs between products but not set the TR. + if ($self->check_can_change_field('target_release', 0, 1)) { + $release_ok = eval { $self->set_target_release($tr_names); 1; }; + } + else { + # Have to set this directly to bypass the validators. + $self->{target_release} = [$product->default_release]; + } + ## REDHAT EXTENSION END 706784 877243 + + # If there were any errors thrown, make sure we don't mess up any + # other part of Bugzilla that checks $@. + undef $@; + Bugzilla->error_mode($old_error_mode); + + my $verified = $params->{product_change_confirmed}; + my %vars; + if ( !$verified + || !$component_ok + || !$version_ok + || !$milestone_ok + || !$release_ok) + { + $vars{defaults} = { + + # Note that because of the eval { set } above, these are + # already set correctly if they're valid, otherwise they're + # set to some invalid value which the template will ignore. + component => $self->component, + version => $self->version, + milestone => $milestone_ok + ? $self->target_milestone + : $product->default_milestone, + release => $release_ok ? $self->target_release : $product->default_release + }; +## REDHAT EXTENSION BEGIN 1151025 + $vars{defaults}->{component} + = [$self->component, @{$params->{'extra_components'}}] + if ($params->{'extra_components'}); +## REDHAT EXTENSION END 1151025 + $vars{components} + = [map { $_->name } grep($_->is_active, @{$product->components})]; + $vars{milestones} + = [map { $_->name } grep($_->is_active, @{$product->milestones})]; + $vars{releases} = [map { $_->name } grep($_->is_active, @{$product->releases})]; + $vars{versions} = [map { $_->name } grep($_->is_active, @{$product->versions})]; + } + + if (!$verified) { + $vars{verify_bug_groups} = 1; + my $dbh = Bugzilla->dbh; + my @idlist = ($self->id); + push(@idlist, map { $_->id } @{$params->{other_bugs}}) if $params->{other_bugs}; + @idlist = uniq @idlist; + + # Get the ID of groups which are no longer valid in the new product. + my $gids = $dbh->selectcol_arrayref( + 'SELECT bgm.group_id FROM bug_group_map AS bgm WHERE bgm.bug_id IN (' . join(',', ('?') x @idlist) . ') AND bgm.group_id NOT IN @@ -2726,159 +3549,214 @@ sub _set_product { WHERE gcm.product_id = ? AND ( (gcm.membercontrol != ? AND gcm.group_id IN (' - . Bugzilla->user->groups_as_string . ')) - OR gcm.othercontrol != ?) )', - undef, (@idlist, $product->id, CONTROLMAPNA, CONTROLMAPNA)); - $vars{'old_groups'} = Bugzilla::Group->new_from_list($gids); - - # Did we come here from editing multiple bugs? (affects how we - # show optional group changes) - $vars{multiple_bugs} = (@idlist > 1) ? 1 : 0; - } - - if (%vars) { - $vars{product} = $product; - $vars{bug} = $self; - my $template = Bugzilla->template; - $template->process("bug/process/verify-new-product.html.tmpl", - \%vars) || ThrowTemplateError($template->error()); - exit; - } + . Bugzilla->user->groups_as_string . ')) + OR gcm.othercontrol != ?) )', undef, + (@idlist, $product->id, CONTROLMAPNA, CONTROLMAPNA) + ); + $vars{'old_groups'} = Bugzilla::Group->new_from_list($gids); + + # Did we come here from editing multiple bugs? (affects how we + # show optional group changes) + $vars{multiple_bugs} = (@idlist > 1) ? 1 : 0; + } + + if (%vars) { + $vars{product} = $product; + $vars{bug} = $self; + my $template = Bugzilla->template; + $template->process("bug/process/verify-new-product.html.tmpl", \%vars) + || ThrowTemplateError($template->error()); + exit; + } + } + else { + # When we're not in the browser (or we didn't change the product), we + # just die if any of these are invalid. + $self->set_component($comp_name); + $self->set_version($vers_name); + if ($product_changed + and !$self->check_can_change_field('target_milestone', 0, 1)) + { + # Have to set this directly to bypass the validators. + $self->{target_milestone} = $product->default_milestone; } else { - # When we're not in the browser (or we didn't change the product), we - # just die if any of these are invalid. - $self->set_component($comp_name); - $self->set_version($vers_name); - if ($product_changed - and !$self->check_can_change_field('target_milestone', 0, 1)) - { - # Have to set this directly to bypass the validators. - $self->{target_milestone} = $product->default_milestone; - } - else { - $self->set_target_milestone($tm_name); - } + $self->set_target_milestone($tm_name); + } + if ($product_changed && !$self->check_can_change_field('target_release', 0, 1)) + { + # Have to set this directly to bypass the validators. + $self->{target_release} = [$product->default_release]; + } + else { + $self->set_target_release($tr_names); } + } - if ($product_changed) { - # Remove groups that can't be set in the new product. - # We copy this array because the original array is modified while we're - # working, and that confuses "foreach". - my @current_groups = @{$self->groups_in}; - foreach my $group (@current_groups) { - if (!$product->group_is_valid($group)) { - $self->remove_group($group); - } - } + if ($product_changed) { - # Make sure the bug is in all the mandatory groups for the new product. - foreach my $group (@{$product->groups_mandatory}) { - $self->add_group($group); - } + # Remove groups that can't be set in the new product. + # We copy this array because the original array is modified while we're + # working, and that confuses "foreach". + my @current_groups = @{$self->groups_in}; + foreach my $group (@current_groups) { + if (!$product->group_is_valid($group)) { + $self->remove_group($group); + } } - - return $product_changed; + + # Make sure the bug is in all the mandatory groups for the new product. + foreach my $group (@{$product->groups_mandatory}) { + $self->add_group($group); + } + } + + return $product_changed; } sub set_qa_contact { - my ($self, $value) = @_; - $self->set('qa_contact', $value); - # Store the old QA contact. check_can_change_field() needs it. - if ($self->{'qa_contact_obj'}) { - $self->{'_old_qa_contact'} = $self->{'qa_contact_obj'}->id; - } - delete $self->{'qa_contact_obj'}; + my ($self, $value) = @_; + $self->set('qa_contact', $value); + + # Store the old QA contact. check_can_change_field() needs it. + if ($self->{'qa_contact_obj'}) { + $self->{'_old_qa_contact'} = $self->{'qa_contact_obj'}->id; + } + delete $self->{'qa_contact_obj'}; } + sub reset_qa_contact { - my $self = shift; - my $comp = $self->component_obj; - $self->set_qa_contact($comp->default_qa_contact); + my $self = shift; + my $comp = $self->component_obj; + $self->set_qa_contact($comp->default_qa_contact); +} +## REDHAT EXTENSION START 876015 +sub set_docs_contact { + my ($self, $value) = @_; + $self->set('docs_contact', $value); + + # Store the old Docs contact. check_can_change_field() needs it. + if ($self->{'docs_contact_obj'}) { + $self->{'_old_docs_contact'} = $self->{'docs_contact_obj'}->id; + } + delete $self->{'docs_contact_obj'}; } + +sub reset_docs_contact { + my $self = shift; + my $comp = $self->component_obj; + $self->set_docs_contact($comp->default_docs_contact); +} +## REDHAT EXTENSION END 876015 sub set_remaining_time { $_[0]->set('remaining_time', $_[1]); } + # Used only when closing a bug or moving between closed states. sub _zero_remaining_time { $_[0]->{'remaining_time'} = 0; } sub set_reporter_accessible { $_[0]->set('reporter_accessible', $_[1]); } + sub set_resolution { - my ($self, $value, $params) = @_; - - my $old_res = $self->resolution; - $self->set('resolution', $value); - delete $self->{choices}; - my $new_res = $self->resolution; + my ($self, $value, $params) = @_; - if ($new_res ne $old_res) { - # Clear the dup_id if we're leaving the dup resolution. - if ($old_res eq 'DUPLICATE') { - $self->_clear_dup_id(); - } - # Duplicates should have no remaining time left. - elsif ($new_res eq 'DUPLICATE' && $self->remaining_time != 0) { - $self->_zero_remaining_time(); - } + my $old_res = $self->resolution; + $self->set('resolution', $value); + delete $self->{choices}; + my $new_res = $self->resolution; + + if ($new_res ne $old_res) { + + # Clear the dup_id if we're leaving the dup resolution. + if ($old_res eq 'DUPLICATE') { + $self->_clear_dup_id(); } - - # We don't check if we're entering or leaving the dup resolution here, - # because we could be moving from being a dup of one bug to being a dup - # of another, theoretically. Note that this code block will also run - # when going between different closed states. - if ($self->resolution eq 'DUPLICATE') { - if (my $dup_id = $params->{dup_id}) { - $self->set_dup_id($dup_id); - } - elsif (!$self->dup_id) { - ThrowUserError('dupe_id_required'); - } + + # Duplicates should have no remaining time left. + elsif ($new_res eq 'DUPLICATE' && $self->remaining_time != 0) { + $self->_zero_remaining_time(); } + } - # This method has handled dup_id, so set_all doesn't have to worry - # about it now. - delete $params->{dup_id}; + # We don't check if we're entering or leaving the dup resolution here, + # because we could be moving from being a dup of one bug to being a dup + # of another, theoretically. Note that this code block will also run + # when going between different closed states. + if ($self->resolution eq 'DUPLICATE') { + if (my $dup_id = $params->{dup_id}) { + $self->set_dup_id($dup_id); + } + elsif (!$self->dup_id) { + ThrowUserError('dupe_id_required'); + } + } + + # This method has handled dup_id, so set_all doesn't have to worry + # about it now. + delete $params->{dup_id}; } + sub clear_resolution { - my $self = shift; - if (!$self->status->is_open) { - ThrowUserError('resolution_cant_clear', { bug_id => $self->id }); - } - $self->{'resolution'} = ''; - $self->_clear_dup_id; + my $self = shift; + if (!$self->status->is_open) { + ThrowUserError('resolution_cant_clear', {bug_id => $self->id}); + } + $self->{'resolution'} = ''; + $self->_clear_dup_id; +} +## REDHAT EXTENSION START 1244617 +sub resolutions_available { + my $self = shift; + + return $self->{'resolutions_available'} + if defined $self->{'resolutions_available'}; + + my $resolution_field = new Bugzilla::Field({name => 'resolution'}); + + # Don't include the empty resolutions + my @resolutions = grep($_->name, @{$resolution_field->legal_values}); + $self->{'resolutions_available'} = \@resolutions; + + return \@resolutions; } -sub set_severity { $_[0]->set('bug_severity', $_[1]); } +## REDHAT EXTENSION END 1244617 +sub set_severity { $_[0]->set('bug_severity', $_[1]); } + sub set_bug_status { - my ($self, $status, $params) = @_; - my $old_status = $self->status; - $self->set('bug_status', $status); - delete $self->{'status'}; - delete $self->{'statuses_available'}; - delete $self->{'choices'}; - my $new_status = $self->status; - - if ($new_status->is_open) { - # Check for the everconfirmed transition - $self->_set_everconfirmed($new_status->name eq 'UNCONFIRMED' ? 0 : 1); - $self->clear_resolution(); - # Calling clear_resolution handled the "resolution" and "dup_id" - # setting, so set_all doesn't have to worry about them. - delete $params->{resolution}; - delete $params->{dup_id}; + my ($self, $status, $params) = @_; + my $old_status = $self->status; + $self->set('bug_status', $status); + delete $self->{'status'}; + delete $self->{'statuses_available'}; + delete $self->{'choices'}; + my $new_status = $self->status; + + if ($new_status->is_open) { + + # Check for the everconfirmed transition + $self->_set_everconfirmed($new_status->name eq 'UNCONFIRMED' ? 0 : 1); + $self->clear_resolution(); + + # Calling clear_resolution handled the "resolution" and "dup_id" + # setting, so set_all doesn't have to worry about them. + delete $params->{resolution}; + delete $params->{dup_id}; + } + else { + # We do this here so that we can make sure closed statuses have + # resolutions. + my $resolution = $self->resolution; + + # We need to check "defined" to prevent people from passing + # a blank resolution in the WebService, which would otherwise fail + # silently. + if (defined $params->{resolution}) { + $resolution = delete $params->{resolution}; } - else { - # We do this here so that we can make sure closed statuses have - # resolutions. - my $resolution = $self->resolution; - # We need to check "defined" to prevent people from passing - # a blank resolution in the WebService, which would otherwise fail - # silently. - if (defined $params->{resolution}) { - $resolution = delete $params->{resolution}; - } - $self->set_resolution($resolution, $params); + $self->set_resolution($resolution, $params); - # Changing between closed statuses zeros the remaining time. - if ($new_status->id != $old_status->id && $self->remaining_time != 0) { - $self->_zero_remaining_time(); - } + # Changing between closed statuses zeros the remaining time. + if ($new_status->id != $old_status->id && $self->remaining_time != 0) { + $self->_zero_remaining_time(); } + } } sub set_status_whiteboard { $_[0]->set('status_whiteboard', $_[1]); } sub set_summary { $_[0]->set('short_desc', $_[1]); } @@ -2886,6 +3764,37 @@ sub set_target_milestone { $_[0]->set('target_milestone', $_[1]); } sub set_url { $_[0]->set('bug_file_loc', $_[1]); } sub set_version { $_[0]->set('version', $_[1]); } +## REDHAT EXTENSION START 706784 877243 +sub set_target_release { $_[0]->set('target_release', $_[1]); } +## REDHAT EXTENSION END 706784 877243 + +## REDHAT EXTENSION START 406451 850909 +# Clear the needinfo flag if the user has ticked the box stating +# 'I am providing the requested information for this bug' +sub set_needinfo { + my ($self, $comment, $isprivate, $clear_needinfo, $clear_my_needinfo) = @_; + return unless($clear_needinfo || $clear_my_needinfo); # Nothing to do. + my @flags; + foreach my $type (@{$self->flag_types}) { + foreach my $flag (@{$type->{flags}}) { + if ($flag->id && $flag->name eq 'needinfo' && $flag->status eq '?') { + ## REDHAT EXTENSION 1729004 + next + if ($clear_my_needinfo + && (!$flag->requestee_id || $flag->requestee_id != Bugzilla->user->id)); + + # Don't remove the needinfo flag if the comment is private and the + # flag setter isn't a member of the private_comment group. + if (!$isprivate || $flag->setter->is_insider) { + push(@flags, {id => $flag->id, status => 'X'}); + } + } + } + } + $self->set_flags(\@flags, []); +} +## REDHAT EXTENSION END 406451 850909 + ######################## # "Add/Remove" Methods # ######################## @@ -2894,374 +3803,420 @@ sub set_version { $_[0]->set('version', $_[1]); } # Accepts a User object or a username. Adds the user only if they # don't already exist as a CC on the bug. +## REDHAT EXTENSION 1269925 Don't add unprivileged users for automated changes. sub add_cc { - my ($self, $user_or_name) = @_; - return if !$user_or_name; - my $user = ref $user_or_name ? $user_or_name - : Bugzilla::User->check($user_or_name); - $self->_check_strict_isolation_for_user($user); - my $cc_users = $self->cc_users; - push(@$cc_users, $user) if !grep($_->id == $user->id, @$cc_users); + my ($self, $user_or_name, $exclude_unprivileged) = @_; + return if !$user_or_name; + my $user + = ref $user_or_name ? $user_or_name : Bugzilla::User->check($user_or_name); + $self->_check_strict_isolation_for_user($user); + my $cc_users = $self->cc_users; + ## REDHAT EXTENSION START 1269925 + my @current_groups = @{$self->groups_in}; + if ($exclude_unprivileged && @current_groups) { + my $product_id = $self->{product_id}; + my $allowed = 0; + foreach my $group (@current_groups) { + if ($user->in_group($group->name, $product_id)) { + + # This user can see this bug, so add them to the CC: + $allowed = 1; + last; + } + } + return unless ($allowed); + } + ## REDHAT EXTENSION END 1269925 + push(@$cc_users, $user) if !grep($_->id == $user->id, @$cc_users); } # Accepts a User object or a username. Removes the User if they exist # in the list, but doesn't throw an error if they don't exist. sub remove_cc { - my ($self, $user_or_name) = @_; - my $user = ref $user_or_name ? $user_or_name - : Bugzilla::User->check($user_or_name); - my $currentUser = Bugzilla->user; - if (!$self->user->{'canedit'} && $user->id != $currentUser->id) { - ThrowUserError('cc_remove_denied'); - } - my $cc_users = $self->cc_users; - @$cc_users = grep { $_->id != $user->id } @$cc_users; + my ($self, $user_or_name) = @_; + my $user + = ref $user_or_name ? $user_or_name : Bugzilla::User->check($user_or_name); + my $currentUser = Bugzilla->user; + if (!$self->user->{'canedit'} && $user->id != $currentUser->id) { + ThrowUserError('cc_remove_denied'); + } + my $cc_users = $self->cc_users; + @$cc_users = grep { $_->id != $user->id } @$cc_users; } sub add_alias { - my ($self, $alias) = @_; - return if !$alias; - my $aliases = $self->_check_alias($alias); - $alias = $aliases->[0]; - my @new_aliases; - my $found = 0; - foreach my $old_alias (@{ $self->alias }) { - if (lc($old_alias) eq lc($alias)) { - push(@new_aliases, $alias); - $found = 1; - } - else { - push(@new_aliases, $old_alias); - } + my ($self, $alias) = @_; + return if !$alias; + my $aliases = $self->_check_alias($alias); + $alias = $aliases->[0]; + my @new_aliases; + my $found = 0; + foreach my $old_alias (@{$self->alias}) { + if (lc($old_alias) eq lc($alias)) { + push(@new_aliases, $alias); + $found = 1; + } + else { + push(@new_aliases, $old_alias); } - push(@new_aliases, $alias) if !$found; - $self->{alias} = \@new_aliases; + } + push(@new_aliases, $alias) if !$found; + $self->{alias} = \@new_aliases; } sub remove_alias { - my ($self, $alias) = @_; - my $bug_aliases = $self->alias; - @$bug_aliases = grep { $_ ne $alias } @$bug_aliases; + my ($self, $alias) = @_; + my $bug_aliases = $self->alias; + @$bug_aliases = grep { $_ ne $alias } @$bug_aliases; } # $bug->add_comment("comment", {isprivate => 1, work_time => 10.5, # type => CMT_NORMAL, extra_data => $data}); sub add_comment { - my ($self, $comment, $params) = @_; + my ($self, $comment, $params) = @_; - $params ||= {}; + $params ||= {}; - # Fill out info that doesn't change and callers may not pass in - $params->{'bug_id'} = $self; - $params->{'thetext'} = defined($comment) ? $comment : ''; + # Fill out info that doesn't change and callers may not pass in + $params->{'bug_id'} = $self; + $params->{'thetext'} = defined($comment) ? $comment : ''; - # Validate all the entered data - Bugzilla::Comment->check_required_create_fields($params); - $params = Bugzilla::Comment->run_create_validators($params); + # Validate all the entered data + Bugzilla::Comment->check_required_create_fields($params); + $params = Bugzilla::Comment->run_create_validators($params); - # This makes it so we won't create new comments when there is nothing - # to add - if ($params->{'thetext'} eq '' - && !($params->{type} || abs($params->{work_time} || 0))) - { - return; - } + # This makes it so we won't create new comments when there is nothing + # to add + if ($params->{'thetext'} eq '' + && !($params->{type} || abs($params->{work_time} || 0))) + { + return; + } - # If the user has explicitly set remaining_time, this will be overridden - # later in set_all. But if they haven't, this keeps remaining_time - # up-to-date. - if ($params->{work_time}) { - $self->set_remaining_time(max($self->remaining_time - $params->{work_time}, 0)); - } + # If the user has explicitly set remaining_time, this will be overridden + # later in set_all. But if they haven't, this keeps remaining_time + # up-to-date. + if ($params->{work_time}) { + $self->set_remaining_time(max($self->remaining_time - $params->{work_time}, 0)); + } - $self->{added_comments} ||= []; + $self->{added_comments} ||= []; - push(@{$self->{added_comments}}, $params); + push(@{$self->{added_comments}}, $params); } +# There was a lot of duplicate code when I wrote this as three separate +# functions, so I just combined them all into one. This is also easier for +# process_bug to use. + + sub modify_keywords { - my ($self, $keywords, $action) = @_; + my ($self, $keywords, $action) = @_; - if (!$action || !grep { $action eq $_ } qw(add remove set)) { - $action = 'set'; - } + if (!$action || !grep { $action eq $_ } qw(add remove set)) { + $action = 'set'; + } - $keywords = $self->_check_keywords($keywords); - my @old_keywords = @{ $self->keyword_objects }; - my @result; + $keywords = $self->_check_keywords($keywords); + my @old_keywords = @{$self->keyword_objects}; + my @result; - if ($action eq 'set') { - @result = @$keywords; + if ($action eq 'set') { + @result = @$keywords; + } + else { + # We're adding or deleting specific keywords. + my %keys = map { $_->id => $_ } @old_keywords; + if ($action eq 'add') { + $keys{$_->id} = $_ foreach @$keywords; } else { - # We're adding or deleting specific keywords. - my %keys = map { $_->id => $_ } @old_keywords; - if ($action eq 'add') { - $keys{$_->id} = $_ foreach @$keywords; - } - else { - delete $keys{$_->id} foreach @$keywords; - } - @result = values %keys; + delete $keys{$_->id} foreach @$keywords; } + @result = values %keys; + } - # Check if anything was added or removed. - my @old_ids = map { $_->id } @old_keywords; - my @new_ids = map { $_->id } @result; - my ($removed, $added) = diff_arrays(\@old_ids, \@new_ids); - my $any_changes = scalar @$removed || scalar @$added; + # Check if anything was added or removed. + my @old_ids = map { $_->id } @old_keywords; + my @new_ids = map { $_->id } @result; + my ($removed, $added) = diff_arrays(\@old_ids, \@new_ids); + my $any_changes = scalar @$removed || scalar @$added; - # Make sure we retain the sort order. - @result = sort {lc($a->name) cmp lc($b->name)} @result; + # Make sure we retain the sort order. + @result = sort { lc($a->name) cmp lc($b->name) } @result; - if ($any_changes) { - my $privs; - my $new = join(', ', (map {$_->name} @result)); - my $check = $self->check_can_change_field('keywords', 0, 1, \$privs) - || ThrowUserError('illegal_change', { field => 'keywords', - oldvalue => $self->keywords, - newvalue => $new, - privs => $privs }); - } - - $self->{'keyword_objects'} = \@result; + if ($any_changes) { + my $privs; + my $new = join(', ', (map { $_->name } @result)); + my $check + = $self->check_can_change_field('keywords', 0, 1, \$privs) || ThrowUserError( + 'illegal_change', + { + field => 'keywords', + oldvalue => $self->keywords, + newvalue => $new, + privs => $privs + } + ); + } + + $self->{'keyword_objects'} = \@result; } sub add_group { - my ($self, $group) = @_; - - # If the user enters "FoO" but the DB has "Foo", $group->name would - # return "Foo" and thus revealing the existence of the group name. - # So we have to store and pass the name as entered by the user to - # the error message, if we have it. - my $group_name = blessed($group) ? $group->name : $group; - my $args = { name => $group_name, product => $self->product, - bug_id => $self->id, action => 'add' }; - - $group = Bugzilla::Group->check_no_disclose($args) if !blessed $group; - - # If the bug is already in this group, then there is nothing to do. - return if $self->in_group($group); - - - # Make sure that bugs in this product can actually be restricted - # to this group by the current user. - $self->product_obj->group_is_settable($group) - || ThrowUserError('group_restriction_not_allowed', $args); - - # OtherControl people can add groups only during a product change, - # and only when the group is not NA for them. - if (!Bugzilla->user->in_group($group->name)) { - my $controls = $self->product_obj->group_controls->{$group->id}; - if (!$self->{_old_product_name} - || $controls->{othercontrol} == CONTROLMAPNA) - { - ThrowUserError('group_restriction_not_allowed', $args); - } - } - - my $current_groups = $self->groups_in; - push(@$current_groups, $group); + my ($self, $group) = @_; + + # If the user enters "FoO" but the DB has "Foo", $group->name would + # return "Foo" and thus revealing the existence of the group name. + # So we have to store and pass the name as entered by the user to + # the error message, if we have it. + my $group_name = blessed($group) ? $group->name : $group; + my $args = { + name => $group_name, + product => $self->product, + bug_id => $self->id, + action => 'add' + }; + + $group = Bugzilla::Group->check_no_disclose($args) if !blessed $group; + + # If the bug is already in this group, then there is nothing to do. + return if $self->in_group($group); + + + # Make sure that bugs in this product can actually be restricted + # to this group by the current user. + $self->product_obj->group_is_settable($group) + || ThrowUserError('group_restriction_not_allowed', $args); + + ## REDHAT EXTENSION START 408671 + # We need to allow all 'othercontrol' type groups to be added + # at bug change time, upstream has this only enabled at product + # change time and at bug entry time + # OtherControl people can add groups only during a product change, + # and only when the group is not NA for them. + #if (!Bugzilla->user->in_group($group->name)) { + # my $controls = $self->product_obj->group_controls->{$group->id}; + # if (!$self->{_old_product_name} + # || $controls->{othercontrol} == CONTROLMAPNA) + # { + # ThrowUserError('group_restriction_not_allowed', $args); + # } + #} + #REDHAT EXTENSION END 408671 + + my $current_groups = $self->groups_in; + push(@$current_groups, $group); } sub remove_group { - my ($self, $group) = @_; + my ($self, $group) = @_; - # See add_group() for the reason why we store the user input. - my $group_name = blessed($group) ? $group->name : $group; - my $args = { name => $group_name, product => $self->product, - bug_id => $self->id, action => 'remove' }; + # See add_group() for the reason why we store the user input. + my $group_name = blessed($group) ? $group->name : $group; + my $args = { + name => $group_name, + product => $self->product, + bug_id => $self->id, + action => 'remove' + }; - $group = Bugzilla::Group->check_no_disclose($args) if !blessed $group; + $group = Bugzilla::Group->check_no_disclose($args) if !blessed $group; - # If the bug isn't in this group, then either the name is misspelled, - # or the group really doesn't exist. Let the user know about this problem. - $self->in_group($group) || ThrowUserError('group_invalid_removal', $args); + # If the bug isn't in this group, then either the name is misspelled, + # or the group really doesn't exist. Let the user know about this problem. + $self->in_group($group) || ThrowUserError('group_invalid_removal', $args); - # Check if this is a valid group for this product. You can *always* - # remove a group that is not valid for this product (set_product does this). - # This particularly happens when we're moving a bug to a new product. - # You still have to be a member of an inactive group to remove it. - if ($self->product_obj->group_is_valid($group)) { - my $controls = $self->product_obj->group_controls->{$group->id}; + # Check if this is a valid group for this product. You can *always* + # remove a group that is not valid for this product (set_product does this). + # This particularly happens when we're moving a bug to a new product. + # You still have to be a member of an inactive group to remove it. + if ($self->product_obj->group_is_valid($group)) { + my $controls = $self->product_obj->group_controls->{$group->id}; - # Nobody can ever remove a Mandatory group, unless it became inactive. - if ($controls->{membercontrol} == CONTROLMAPMANDATORY && $group->is_active) { - ThrowUserError('group_invalid_removal', $args); - } + # Nobody can ever remove a Mandatory group, unless it became inactive. + if ($controls->{membercontrol} == CONTROLMAPMANDATORY && $group->is_active) { + ThrowUserError('group_invalid_removal', $args); + } - # OtherControl people can remove groups only during a product change, - # and only when they are non-Mandatory and non-NA. - if (!Bugzilla->user->in_group($group->name)) { - if (!$self->{_old_product_name} - || $controls->{othercontrol} == CONTROLMAPMANDATORY - || $controls->{othercontrol} == CONTROLMAPNA) - { - ThrowUserError('group_invalid_removal', $args); - } - } + # OtherControl people can remove groups only during a product change, + # and only when they are non-Mandatory and non-NA. + if (!Bugzilla->user->in_group($group->name)) { + if (!$self->{_old_product_name} + || $controls->{othercontrol} == CONTROLMAPMANDATORY + || $controls->{othercontrol} == CONTROLMAPNA) + { + ThrowUserError('group_invalid_removal', $args); + } } + } - my $current_groups = $self->groups_in; - @$current_groups = grep { $_->id != $group->id } @$current_groups; + my $current_groups = $self->groups_in; + @$current_groups = grep { $_->id != $group->id } @$current_groups; } sub add_see_also { - my ($self, $input, $skip_recursion) = @_; + my ($self, $input, $skip_recursion) = @_; - # This is needed by xt/search.t. - $input = $input->name if blessed($input); + # This is needed by xt/search.t. + $input = $input->name if blessed($input); - $input = trim($input); - return if !$input; - - my ($class, $uri) = Bugzilla::BugUrl->class_for($input); - - my $params = { value => $uri, bug_id => $self, class => $class }; - $class->check_required_create_fields($params); - - my $field_values = $class->run_create_validators($params); - my $value = $field_values->{value}->as_string; - trick_taint($value); - $field_values->{value} = $value; - - # We only add the new URI if it hasn't been added yet. URIs are - # case-sensitive, but most of our DBs are case-insensitive, so we do - # this check case-insensitively. - if (!grep { lc($_->name) eq lc($value) } @{ $self->see_also }) { - my $privs; - my $can = $self->check_can_change_field('see_also', '', $value, \$privs); - if (!$can) { - ThrowUserError('illegal_change', { field => 'see_also', - newvalue => $value, - privs => $privs }); - } - # If this is a link to a local bug then save the - # ref bug id for sending changes email. - my $ref_bug = delete $field_values->{ref_bug}; - if ($class->isa('Bugzilla::BugUrl::Bugzilla::Local') - and !$skip_recursion - and $ref_bug->check_can_change_field('see_also', '', $self->id, \$privs)) - { - $ref_bug->add_see_also($self->id, 'skip_recursion'); - push @{ $self->{_update_ref_bugs} }, $ref_bug; - push @{ $self->{see_also_changes} }, $ref_bug->id; - } - push @{ $self->{see_also} }, bless ($field_values, $class); - } -} + $input = trim($input); + return if !$input; -sub remove_see_also { - my ($self, $url, $skip_recursion) = @_; - my $see_also = $self->see_also; + my ($class, $uri) = Bugzilla::BugUrl->class_for($input); - # This is needed by xt/search.t. - $url = $url->name if blessed($url); + my $params = {value => $uri, bug_id => $self, class => $class}; + $class->check_required_create_fields($params); - my ($removed_bug_url, $new_see_also) = - part { lc($_->name) ne lc($url) } @$see_also; + my $field_values = $class->run_create_validators($params); + my $value = $field_values->{value}->as_string; + trick_taint($value); + $field_values->{value} = $value; + # We only add the new URI if it hasn't been added yet. URIs are + # case-sensitive, but most of our DBs are case-insensitive, so we do + # this check case-insensitively. + if (!grep { lc($_->name) eq lc($value) } @{$self->see_also}) { my $privs; - my $can = $self->check_can_change_field('see_also', $see_also, $new_see_also, \$privs); + my $can = $self->check_can_change_field('see_also', '', $value, \$privs); if (!$can) { - ThrowUserError('illegal_change', { field => 'see_also', - oldvalue => $url, - privs => $privs }); + ThrowUserError('illegal_change', + {field => 'see_also', newvalue => $value, privs => $privs}); } - # Since we remove also the url from the referenced bug, - # we need to notify changes for that bug too. - $removed_bug_url = $removed_bug_url->[0]; - if (!$skip_recursion and $removed_bug_url - and $removed_bug_url->isa('Bugzilla::BugUrl::Bugzilla::Local') - and $removed_bug_url->ref_bug_url) + # If this is a link to a local bug then save the + # ref bug id for sending changes email. + my $ref_bug = delete $field_values->{ref_bug}; + if ( $class->isa('Bugzilla::BugUrl::Bugzilla::Local') + and !$skip_recursion + and $ref_bug->check_can_change_field('see_also', '', $self->id, \$privs)) { - my $ref_bug - = Bugzilla::Bug->check($removed_bug_url->ref_bug_url->bug_id); + $ref_bug->add_see_also($self->id, 'skip_recursion'); + push @{$self->{_update_ref_bugs}}, $ref_bug; + push @{$self->{see_also_changes}}, $ref_bug->id; + } + push @{$self->{see_also}}, bless($field_values, $class); + } +} - if (Bugzilla->user->can_edit_product($ref_bug->product_id) - and $ref_bug->check_can_change_field('see_also', $self->id, '', \$privs)) - { - my $self_url = $removed_bug_url->local_uri($self->id); - $ref_bug->remove_see_also($self_url, 'skip_recursion'); - push @{ $self->{_update_ref_bugs} }, $ref_bug; - push @{ $self->{see_also_changes} }, $ref_bug->id; - } +sub remove_see_also { + my ($self, $url, $skip_recursion) = @_; + my $see_also = $self->see_also; + + # This is needed by xt/search.t. + $url = $url->name if blessed($url); + + my ($removed_bug_url, $new_see_also) + = part { lc($_->name) ne lc($url) } @$see_also; + + my $privs; + my $can = $self->check_can_change_field('see_also', $see_also, $new_see_also, + \$privs); + if (!$can) { + ThrowUserError('illegal_change', + {field => 'see_also', oldvalue => $url, privs => $privs}); + } + + # Since we remove also the url from the referenced bug, + # we need to notify changes for that bug too. + $removed_bug_url = $removed_bug_url->[0]; + if ( !$skip_recursion + and $removed_bug_url + and $removed_bug_url->isa('Bugzilla::BugUrl::Bugzilla::Local') + and $removed_bug_url->ref_bug_url) + { + my $ref_bug = Bugzilla::Bug->check($removed_bug_url->ref_bug_url->bug_id); + + if (Bugzilla->user->can_edit_product($ref_bug->product_id) + and $ref_bug->check_can_change_field('see_also', $self->id, '', \$privs)) + { + my $self_url = $removed_bug_url->local_uri($self->id); + $ref_bug->remove_see_also($self_url, 'skip_recursion'); + push @{$self->{_update_ref_bugs}}, $ref_bug; + push @{$self->{see_also_changes}}, $ref_bug->id; } + } - $self->{see_also} = $new_see_also || []; + $self->{see_also} = $new_see_also || []; } sub add_tag { - my ($self, $tag) = @_; - my $dbh = Bugzilla->dbh; - my $user = Bugzilla->user; - $tag = $self->_check_tag_name($tag); + my ($self, $tag) = @_; + my $dbh = Bugzilla->dbh; + my $user = Bugzilla->user; + $tag = $self->_check_tag_name($tag); + + my $tag_id = $user->tags->{$tag}->{id}; + + # If this tag doesn't exist for this user yet, create it. + if (!$tag_id) { + $dbh->do('INSERT INTO tag (user_id, name) VALUES (?, ?)', + undef, ($user->id, $tag)); + + $tag_id = $dbh->selectrow_array( + 'SELECT id FROM tag + WHERE name = ? AND user_id = ?', undef, + ($tag, $user->id) + ); - my $tag_id = $user->tags->{$tag}->{id}; - # If this tag doesn't exist for this user yet, create it. - if (!$tag_id) { - $dbh->do('INSERT INTO tag (user_id, name) VALUES (?, ?)', - undef, ($user->id, $tag)); + # The list has changed. + delete $user->{tags}; + } - $tag_id = $dbh->selectrow_array('SELECT id FROM tag - WHERE name = ? AND user_id = ?', - undef, ($tag, $user->id)); - # The list has changed. - delete $user->{tags}; - } - # Do nothing if this tag is already set for this bug. - return if grep { $_ eq $tag } @{$self->tags}; + # Do nothing if this tag is already set for this bug. + return if grep { $_ eq $tag } @{$self->tags}; - # Increment the counter. Do it before the SQL call below, - # to not count the tag twice. - $user->tags->{$tag}->{bug_count}++; + # Increment the counter. Do it before the SQL call below, + # to not count the tag twice. + $user->tags->{$tag}->{bug_count}++; - $dbh->do('INSERT INTO bug_tag (bug_id, tag_id) VALUES (?, ?)', - undef, ($self->id, $tag_id)); + $dbh->do('INSERT INTO bug_tag (bug_id, tag_id) VALUES (?, ?)', + undef, ($self->id, $tag_id)); - push(@{$self->{tags}}, $tag); + push(@{$self->{tags}}, $tag); } sub remove_tag { - my ($self, $tag) = @_; - my $dbh = Bugzilla->dbh; - my $user = Bugzilla->user; - $tag = $self->_check_tag_name($tag); + my ($self, $tag) = @_; + my $dbh = Bugzilla->dbh; + my $user = Bugzilla->user; + $tag = $self->_check_tag_name($tag); - my $tag_id = exists $user->tags->{$tag} ? $user->tags->{$tag}->{id} : undef; - # Do nothing if the user doesn't use this tag, or didn't set it for this bug. - return unless ($tag_id && grep { $_ eq $tag } @{$self->tags}); + my $tag_id = exists $user->tags->{$tag} ? $user->tags->{$tag}->{id} : undef; - $dbh->do('DELETE FROM bug_tag WHERE bug_id = ? AND tag_id = ?', - undef, ($self->id, $tag_id)); + # Do nothing if the user doesn't use this tag, or didn't set it for this bug. + return unless ($tag_id && grep { $_ eq $tag } @{$self->tags}); - $self->{tags} = [grep { $_ ne $tag } @{$self->tags}]; + $dbh->do('DELETE FROM bug_tag WHERE bug_id = ? AND tag_id = ?', + undef, ($self->id, $tag_id)); - # Decrement the counter, and delete the tag if no bugs are using it anymore. - if (!--$user->tags->{$tag}->{bug_count}) { - $dbh->do('DELETE FROM tag WHERE name = ? AND user_id = ?', - undef, ($tag, $user->id)); + $self->{tags} = [grep { $_ ne $tag } @{$self->tags}]; - # The list has changed. - delete $user->{tags}; - } + # Decrement the counter, and delete the tag if no bugs are using it anymore. + if (!--$user->tags->{$tag}->{bug_count}) { + $dbh->do('DELETE FROM tag WHERE name = ? AND user_id = ?', + undef, ($tag, $user->id)); + + # The list has changed. + delete $user->{tags}; + } } sub tags { - my $self = shift; - my $dbh = Bugzilla->dbh; - my $user = Bugzilla->user; - - # This method doesn't support several users using the same bug object. - if (!exists $self->{tags}) { - $self->{tags} = $dbh->selectcol_arrayref( - 'SELECT name FROM bug_tag + my $self = shift; + my $dbh = Bugzilla->dbh; + my $user = Bugzilla->user; + + # This method doesn't support several users using the same bug object. + if (!exists $self->{tags}) { + $self->{tags} = $dbh->selectcol_arrayref( + 'SELECT name FROM bug_tag INNER JOIN tag ON tag.id = bug_tag.tag_id - WHERE bug_id = ? AND user_id = ?', - undef, ($self->id, $user->id)); - } - return $self->{tags}; + WHERE bug_id = ? AND user_id = ?', undef, ($self->id, $user->id) + ); + } + return $self->{tags}; } ##################################################################### @@ -3271,30 +4226,30 @@ sub tags { # These are accessors that don't need to access the database. # Keep them in alphabetical order. -sub bug_file_loc { return $_[0]->{bug_file_loc} } -sub bug_id { return $_[0]->{bug_id} } -sub bug_severity { return $_[0]->{bug_severity} } -sub bug_status { return $_[0]->{bug_status} } -sub cclist_accessible { return $_[0]->{cclist_accessible} } -sub component_id { return $_[0]->{component_id} } -sub creation_ts { return $_[0]->{creation_ts} } -sub estimated_time { return $_[0]->{estimated_time} } -sub deadline { return $_[0]->{deadline} } -sub delta_ts { return $_[0]->{delta_ts} } -sub error { return $_[0]->{error} } -sub everconfirmed { return $_[0]->{everconfirmed} } -sub lastdiffed { return $_[0]->{lastdiffed} } -sub op_sys { return $_[0]->{op_sys} } -sub priority { return $_[0]->{priority} } -sub product_id { return $_[0]->{product_id} } -sub remaining_time { return $_[0]->{remaining_time} } +sub bug_file_loc { return $_[0]->{bug_file_loc} } +sub bug_id { return $_[0]->{bug_id} } +sub bug_severity { return $_[0]->{bug_severity} } +sub bug_status { return $_[0]->{bug_status} } +sub cclist_accessible { return $_[0]->{cclist_accessible} } +sub component_id { return $_[0]->{component_id} } +sub creation_ts { return $_[0]->{creation_ts} } +sub estimated_time { return $_[0]->{estimated_time} } +sub deadline { return $_[0]->{deadline} } +sub delta_ts { return $_[0]->{delta_ts} } +sub error { return $_[0]->{error} } +sub everconfirmed { return $_[0]->{everconfirmed} } +sub lastdiffed { return $_[0]->{lastdiffed} } +sub op_sys { return $_[0]->{op_sys} } +sub priority { return $_[0]->{priority} } +sub product_id { return $_[0]->{product_id} } +sub remaining_time { return $_[0]->{remaining_time} } sub reporter_accessible { return $_[0]->{reporter_accessible} } -sub rep_platform { return $_[0]->{rep_platform} } -sub resolution { return $_[0]->{resolution} } -sub short_desc { return $_[0]->{short_desc} } -sub status_whiteboard { return $_[0]->{status_whiteboard} } -sub target_milestone { return $_[0]->{target_milestone} } -sub version { return $_[0]->{version} } +sub rep_platform { return $_[0]->{rep_platform} } +sub resolution { return $_[0]->{resolution} } +sub short_desc { return $_[0]->{short_desc} } +sub status_whiteboard { return $_[0]->{status_whiteboard} } +sub target_milestone { return $_[0]->{target_milestone} } +sub version { return $_[0]->{version} } ##################################################################### # Complex Accessors @@ -3313,674 +4268,794 @@ sub version { return $_[0]->{version} } # security holes. sub dup_id { - my ($self) = @_; - return $self->{'dup_id'} if exists $self->{'dup_id'}; + my ($self) = @_; + return $self->{'dup_id'} if exists $self->{'dup_id'}; - $self->{'dup_id'} = undef; - return if $self->{'error'}; + $self->{'dup_id'} = undef; + return if $self->{'error'}; - if ($self->{'resolution'} eq 'DUPLICATE') { - my $dbh = Bugzilla->dbh; - $self->{'dup_id'} = - $dbh->selectrow_array(q{SELECT dupe_of + if ($self->{'resolution'} eq 'DUPLICATE') { + my $dbh = Bugzilla->dbh; + $self->{'dup_id'} = $dbh->selectrow_array( + q{SELECT dupe_of FROM duplicates - WHERE dupe = ?}, - undef, - $self->{'bug_id'}); - } - return $self->{'dup_id'}; + WHERE dupe = ?}, undef, $self->{'bug_id'} + ); + } + return $self->{'dup_id'}; } sub _resolve_ultimate_dup_id { - my ($bug_id, $dupe_of, $loops_are_an_error) = @_; - my $dbh = Bugzilla->dbh; - my $sth = $dbh->prepare('SELECT dupe_of FROM duplicates WHERE dupe = ?'); + my ($bug_id, $dupe_of, $loops_are_an_error) = @_; + my $dbh = Bugzilla->dbh; + my $sth = $dbh->prepare('SELECT dupe_of FROM duplicates WHERE dupe = ?'); - my $this_dup = $dupe_of || $dbh->selectrow_array($sth, undef, $bug_id); - my $last_dup = $bug_id; + my $this_dup = $dupe_of || $dbh->selectrow_array($sth, undef, $bug_id); + my $last_dup = $bug_id; - my %dupes; - while ($this_dup) { - if ($this_dup == $bug_id) { - if ($loops_are_an_error) { - ThrowUserError('dupe_loop_detected', { bug_id => $bug_id, - dupe_of => $dupe_of }); - } - else { - return $last_dup; - } - } - # If $dupes{$this_dup} is already set to 1, then a loop - # already exists which does not involve this bug. - # As the user is not responsible for this loop, do not - # prevent them from marking this bug as a duplicate. - return $last_dup if exists $dupes{$this_dup}; - $dupes{$this_dup} = 1; - $last_dup = $this_dup; - $this_dup = $dbh->selectrow_array($sth, undef, $this_dup); + my %dupes; + while ($this_dup) { + if ($this_dup == $bug_id) { + if ($loops_are_an_error) { + ThrowUserError('dupe_loop_detected', {bug_id => $bug_id, dupe_of => $dupe_of}); + } + else { + return $last_dup; + } } - return $last_dup; + # If $dupes{$this_dup} is already set to 1, then a loop + # already exists which does not involve this bug. + # As the user is not responsible for this loop, do not + # prevent them from marking this bug as a duplicate. + return $last_dup if exists $dupes{$this_dup}; + $dupes{$this_dup} = 1; + $last_dup = $this_dup; + $this_dup = $dbh->selectrow_array($sth, undef, $this_dup); + } + + return $last_dup; } sub actual_time { - my ($self) = @_; - return $self->{'actual_time'} if exists $self->{'actual_time'}; + my ($self) = @_; + return $self->{'actual_time'} if exists $self->{'actual_time'}; - if ( $self->{'error'} || !Bugzilla->user->is_timetracker ) { - $self->{'actual_time'} = undef; - return $self->{'actual_time'}; - } + if ($self->{'error'} || !Bugzilla->user->is_timetracker) { + $self->{'actual_time'} = undef; + return $self->{'actual_time'}; + } - my $sth = Bugzilla->dbh->prepare("SELECT SUM(work_time) + my $sth = Bugzilla->dbh->prepare( + "SELECT SUM(work_time) FROM longdescs - WHERE longdescs.bug_id=?"); - $sth->execute($self->{bug_id}); - $self->{'actual_time'} = $sth->fetchrow_array(); - return $self->{'actual_time'}; + WHERE longdescs.bug_id=?" + ); + $sth->execute($self->{bug_id}); + $self->{'actual_time'} = $sth->fetchrow_array(); + return $self->{'actual_time'}; } sub alias { - my ($self) = @_; - return $self->{'alias'} if exists $self->{'alias'}; - return [] if $self->{'error'}; + my ($self) = @_; + return $self->{'alias'} if exists $self->{'alias'}; + return [] if $self->{'error'}; - my $dbh = Bugzilla->dbh; - $self->{'alias'} = $dbh->selectcol_arrayref( - q{SELECT alias FROM bugs_aliases WHERE bug_id = ? ORDER BY alias}, - undef, $self->bug_id); + my $dbh = Bugzilla->dbh; + $self->{'alias'} + = $dbh->selectcol_arrayref( + q{SELECT alias FROM bugs_aliases WHERE bug_id = ? ORDER BY alias}, + undef, $self->bug_id); - return $self->{'alias'}; + return $self->{'alias'}; } sub any_flags_requesteeble { - my ($self) = @_; - return $self->{'any_flags_requesteeble'} - if exists $self->{'any_flags_requesteeble'}; - return 0 if $self->{'error'}; + my ($self) = @_; + return $self->{'any_flags_requesteeble'} + if exists $self->{'any_flags_requesteeble'}; + return 0 if $self->{'error'}; - my $any_flags_requesteeble = - grep { $_->is_requestable && $_->is_requesteeble } @{$self->flag_types}; - # Useful in case a flagtype is no longer requestable but a requestee - # has been set before we turned off that bit. - $any_flags_requesteeble ||= grep { $_->requestee_id } @{$self->flags}; - $self->{'any_flags_requesteeble'} = $any_flags_requesteeble; + my $any_flags_requesteeble + = grep { $_->is_requestable && $_->is_requesteeble } @{$self->flag_types}; - return $self->{'any_flags_requesteeble'}; + # Useful in case a flagtype is no longer requestable but a requestee + # has been set before we turned off that bit. + $any_flags_requesteeble ||= grep { $_->requestee_id } @{$self->flags}; + $self->{'any_flags_requesteeble'} = $any_flags_requesteeble; + + return $self->{'any_flags_requesteeble'}; } sub attachments { - my ($self) = @_; - return $self->{'attachments'} if exists $self->{'attachments'}; - return [] if $self->{'error'}; + my ($self) = @_; + return $self->{'attachments'} if exists $self->{'attachments'}; + return [] if $self->{'error'}; - $self->{'attachments'} = - Bugzilla::Attachment->get_attachments_by_bug($self, {preload => 1}); - $_->object_cache_set() foreach @{ $self->{'attachments'} }; - return $self->{'attachments'}; + $self->{'attachments'} + = Bugzilla::Attachment->get_attachments_by_bug($self, {preload => 1}); + $_->object_cache_set() foreach @{$self->{'attachments'}}; + return $self->{'attachments'}; } sub assigned_to { - my ($self) = @_; - return $self->{'assigned_to_obj'} if exists $self->{'assigned_to_obj'}; - $self->{'assigned_to'} = 0 if $self->{'error'}; - $self->{'assigned_to_obj'} ||= new Bugzilla::User({ id => $self->{'assigned_to'}, cache => 1 }); - return $self->{'assigned_to_obj'}; + my ($self) = @_; + return $self->{'assigned_to_obj'} if exists $self->{'assigned_to_obj'}; + $self->{'assigned_to'} = 0 if $self->{'error'}; + $self->{'assigned_to_obj'} + ||= new Bugzilla::User({id => $self->{'assigned_to'}, cache => 1}); + return $self->{'assigned_to_obj'}; } sub blocked { - my ($self) = @_; - return $self->{'blocked'} if exists $self->{'blocked'}; - return [] if $self->{'error'}; - $self->{'blocked'} = EmitDependList("dependson", "blocked", $self->bug_id); - return $self->{'blocked'}; + my ($self) = @_; + return $self->{'blocked'} if exists $self->{'blocked'}; + return [] if $self->{'error'}; + $self->{'blocked'} = EmitDependList("dependson", "blocked", $self->bug_id); + return $self->{'blocked'}; } sub blocks_obj { - my ($self) = @_; - $self->{blocks_obj} ||= $self->_bugs_in_order($self->blocked); - return $self->{blocks_obj}; + my ($self) = @_; + $self->{blocks_obj} ||= $self->_bugs_in_order($self->blocked); + return $self->{blocks_obj}; } sub bug_group { - my ($self) = @_; - return join(', ', (map { $_->name } @{$self->groups_in})); + my ($self) = @_; + return join(', ', (map { $_->name } @{$self->groups_in})); } sub related_bugs { - my ($self, $relationship) = @_; - return [] if $self->{'error'}; + my ($self, $relationship) = @_; + return [] if $self->{'error'}; - my $field_name = $relationship->name; - $self->{'related_bugs'}->{$field_name} ||= $self->match({$field_name => $self->id}); - return $self->{'related_bugs'}->{$field_name}; + my $field_name = $relationship->name; + $self->{'related_bugs'}->{$field_name} + ||= $self->match({$field_name => $self->id}); + return $self->{'related_bugs'}->{$field_name}; } sub cc { - my ($self) = @_; - return $self->{'cc'} if exists $self->{'cc'}; - return [] if $self->{'error'}; + my ($self) = @_; + return $self->{'cc'} if exists $self->{'cc'}; + return [] if $self->{'error'}; - my $dbh = Bugzilla->dbh; - $self->{'cc'} = $dbh->selectcol_arrayref( - q{SELECT profiles.login_name FROM cc, profiles + my $dbh = Bugzilla->dbh; + $self->{'cc'} = $dbh->selectcol_arrayref( + q{SELECT profiles.login_name FROM cc, profiles WHERE bug_id = ? AND cc.who = profiles.userid - ORDER BY profiles.login_name}, - undef, $self->bug_id); + ORDER BY profiles.login_name}, undef, $self->bug_id + ); - return $self->{'cc'}; + return $self->{'cc'}; } # XXX Eventually this will become the standard "cc" method used everywhere. sub cc_users { - my $self = shift; - return $self->{'cc_users'} if exists $self->{'cc_users'}; - return [] if $self->{'error'}; - - my $dbh = Bugzilla->dbh; - my $cc_ids = $dbh->selectcol_arrayref( - 'SELECT who FROM cc WHERE bug_id = ?', undef, $self->id); - $self->{'cc_users'} = Bugzilla::User->new_from_list($cc_ids); - return $self->{'cc_users'}; + my $self = shift; + return $self->{'cc_users'} if exists $self->{'cc_users'}; + return [] if $self->{'error'}; + + my $dbh = Bugzilla->dbh; + my $cc_ids = $dbh->selectcol_arrayref('SELECT who FROM cc WHERE bug_id = ?', + undef, $self->id); + $self->{'cc_users'} = Bugzilla::User->new_from_list($cc_ids); + return $self->{'cc_users'}; } sub component { - my ($self) = @_; - return '' if $self->{error}; - $self->{component} //= $self->component_obj->name; - return $self->{component}; + my ($self) = @_; + return '' if $self->{error}; + $self->{component} //= $self->component_obj->name; + return $self->{component}; } # XXX Eventually this will replace component() sub component_obj { - my ($self) = @_; - return $self->{component_obj} if defined $self->{component_obj}; - return {} if $self->{error}; - $self->{component_obj} = - new Bugzilla::Component({ id => $self->{component_id}, cache => 1 }); - return $self->{component_obj}; + my ($self) = @_; + return $self->{component_obj} if defined $self->{component_obj}; + return {} if $self->{error}; + $self->{component_obj} + = new Bugzilla::Component({id => $self->{component_id}, cache => 1}); + return $self->{component_obj}; } sub classification_id { - my ($self) = @_; - return 0 if $self->{error}; - $self->{classification_id} //= $self->product_obj->classification_id; - return $self->{classification_id}; + my ($self) = @_; + return 0 if $self->{error}; + $self->{classification_id} //= $self->product_obj->classification_id; + return $self->{classification_id}; } sub classification { - my ($self) = @_; - return '' if $self->{error}; - $self->{classification} //= $self->product_obj->classification->name; - return $self->{classification}; + my ($self) = @_; + return '' if $self->{error}; + $self->{classification} //= $self->product_obj->classification->name; + return $self->{classification}; } sub default_bug_status { - my $class = shift; - # XXX This should just call new_bug_statuses when the UI accepts closed - # bug statuses instead of accepting them as a parameter. - my @statuses = @_; + my $class = shift; - my $status; - if (scalar(@statuses) == 1) { - $status = $statuses[0]->name; - } - else { - $status = ($statuses[0]->name ne 'UNCONFIRMED') - ? $statuses[0]->name : $statuses[1]->name; - } + # XXX This should just call new_bug_statuses when the UI accepts closed + # bug statuses instead of accepting them as a parameter. + my @statuses = @_; + + my $status; + if (scalar(@statuses) == 1) { + $status = $statuses[0]->name; + } + else { + $status + = ($statuses[0]->name ne 'UNCONFIRMED') + ? $statuses[0]->name + : $statuses[1]->name; + } - return $status; + return $status; } sub dependson { - my ($self) = @_; - return $self->{'dependson'} if exists $self->{'dependson'}; - return [] if $self->{'error'}; - $self->{'dependson'} = - EmitDependList("blocked", "dependson", $self->bug_id); - return $self->{'dependson'}; + my ($self) = @_; + return $self->{'dependson'} if exists $self->{'dependson'}; + return [] if $self->{'error'}; + $self->{'dependson'} = EmitDependList("blocked", "dependson", $self->bug_id); + return $self->{'dependson'}; } sub depends_on_obj { - my ($self) = @_; - $self->{depends_on_obj} ||= $self->_bugs_in_order($self->dependson); - return $self->{depends_on_obj}; + my ($self) = @_; + $self->{depends_on_obj} ||= $self->_bugs_in_order($self->dependson); + 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}; + 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 $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}; + my $dbh = Bugzilla->dbh; + $self->{duplicate_ids} + = $dbh->selectcol_arrayref('SELECT dupe FROM duplicates WHERE dupe_of = ?', + undef, $self->id); + return $self->{duplicate_ids}; +} + +## REDHAT EXTENSION START 612792 +sub clones { + my $self = shift; + return $self->{clones} if exists $self->{clones}; + return [] if $self->{error}; + $self->{clones} = Bugzilla::Bug->new_from_list($self->clone_ids); + return $self->{clones}; +} + +sub clone_ids { + my $self = shift; + return $self->{clone_ids} if exists $self->{clone_ids}; + return [] if $self->{error}; + + my $dbh = Bugzilla->dbh; + $self->{clone_ids} + = $dbh->selectcol_arrayref('SELECT bug_id FROM bugs WHERE cf_clone_of = ?', + undef, $self->id); + return $self->{clone_ids}; } +## REDHAT EXTENSION END 612792 sub flag_types { - my ($self) = @_; - return $self->{'flag_types'} if exists $self->{'flag_types'}; - return [] if $self->{'error'}; + my ($self) = @_; + return $self->{'flag_types'} if exists $self->{'flag_types'}; + return [] if $self->{'error'}; - my $vars = { target_type => 'bug', - product_id => $self->{product_id}, - component_id => $self->{component_id}, - bug_id => $self->bug_id }; + my $vars = { + target_type => 'bug', + product_id => $self->{product_id}, + component_id => $self->{component_id}, + bug_id => $self->bug_id + }; - $self->{'flag_types'} = Bugzilla::Flag->_flag_types($vars); - return $self->{'flag_types'}; + $self->{'flag_types'} = Bugzilla::Flag->_flag_types($vars); + return $self->{'flag_types'}; } sub flags { - my $self = shift; + my $self = shift; - # Don't cache it as it must be in sync with ->flag_types. - $self->{flags} = [map { @{$_->{flags}} } @{$self->flag_types}]; - return $self->{flags}; + # Don't cache it as it must be in sync with ->flag_types. + $self->{flags} = [map { @{$_->{flags}} } @{$self->flag_types}]; + return $self->{flags}; } sub isopened { - my $self = shift; - unless (exists $self->{isopened}) { - $self->{isopened} = is_open_state($self->{bug_status}) ? 1 : 0; - } - return $self->{isopened}; + my $self = shift; + unless (exists $self->{isopened}) { + $self->{isopened} = is_open_state($self->{bug_status}) ? 1 : 0; + } + return $self->{isopened}; } sub keywords { - my ($self) = @_; - return join(', ', (map { $_->name } @{$self->keyword_objects})); + my ($self) = @_; + return join(', ', (map { $_->name } @{$self->keyword_objects})); } # XXX At some point, this should probably replace the normal "keywords" sub. sub keyword_objects { - my $self = shift; - return $self->{'keyword_objects'} if defined $self->{'keyword_objects'}; - return [] if $self->{'error'}; + my $self = shift; + return $self->{'keyword_objects'} if defined $self->{'keyword_objects'}; + return [] if $self->{'error'}; - my $dbh = Bugzilla->dbh; - my $ids = $dbh->selectcol_arrayref( - "SELECT keywordid FROM keywords WHERE bug_id = ?", undef, $self->id); - $self->{'keyword_objects'} = Bugzilla::Keyword->new_from_list($ids); - return $self->{'keyword_objects'}; + my $dbh = Bugzilla->dbh; + my $ids + = $dbh->selectcol_arrayref("SELECT keywordid FROM keywords WHERE bug_id = ?", + undef, $self->id); + $self->{'keyword_objects'} = Bugzilla::Keyword->new_from_list($ids); + return $self->{'keyword_objects'}; } -sub comments { - my ($self, $params) = @_; - return [] if $self->{'error'}; - $params ||= {}; - - if (!defined $self->{'comments'}) { - $self->{'comments'} = Bugzilla::Comment->match({ bug_id => $self->id }); - my $count = 0; - state $is_mysql = Bugzilla->dbh->isa('Bugzilla::DB::Mysql') ? 1 : 0; - foreach my $comment (@{ $self->{'comments'} }) { - $comment->{count} = $count++; - $comment->{bug} = $self; - # XXX - hack for MySQL. Convert [U+....] back into its Unicode - # equivalent for characters above U+FFFF as MySQL older than 5.5.3 - # cannot store them, see Bugzilla::Comment::_check_thetext(). - if ($is_mysql) { - # Perl 5.13.8 and older complain about non-characters. - no warnings 'utf8'; - $comment->{thetext} =~ s/\x{FDD0}\[U\+((?:[1-9A-F]|10)[0-9A-F]{4})\]\x{FDD1}/chr(hex $1)/eg - } - } - # Some bugs may have no comments when upgrading old installations. - Bugzilla::Comment->preload($self->{'comments'}) if $count; - } - my @comments = @{ $self->{'comments'} }; +## REDHAT EXTENSION START 706784 877243 +sub target_release { + my $self = shift; + return $self->{'target_release'} if defined $self->{'target_release'}; + return [] if $self->{'error'}; - my $order = $params->{order} - || Bugzilla->user->setting('comment_sort_order'); - if ($order ne 'oldest_to_newest') { - @comments = reverse @comments; - if ($order eq 'newest_to_oldest_desc_first') { - unshift(@comments, pop @comments); - } - } + my $dbh = Bugzilla->dbh; + my $ids + = $dbh->selectcol_arrayref("SELECT value FROM bugs_release WHERE bug_id = ?", + undef, $self->id); - if ($params->{after}) { - my $from = datetime_from($params->{after}); - @comments = grep { datetime_from($_->creation_ts) > $from } @comments; - } - if ($params->{to}) { - my $to = datetime_from($params->{to}); - @comments = grep { datetime_from($_->creation_ts) <= $to } @comments; - } - return \@comments; + return $self->{'target_release'} = $ids; +} +## REDHAT EXTENSION END 706784 877243 + +sub comments { + my ($self, $params) = @_; + return [] if $self->{'error'}; + $params ||= {}; + + if (!defined $self->{'comments'}) { + $self->{'comments'} = Bugzilla::Comment->match({bug_id => $self->id}); + my $count = 0; + state $is_mysql = Bugzilla->dbh->isa('Bugzilla::DB::Mysql') ? 1 : 0; + foreach my $comment (@{$self->{'comments'}}) { + $comment->{count} = $count++; + $comment->{bug} = $self; + weaken($comment->{bug}); + + # XXX - hack for MySQL. Convert [U+....] back into its Unicode + # equivalent for characters above U+FFFF as MySQL older than 5.5.3 + # cannot store them, see Bugzilla::Comment::_check_thetext(). + if ($is_mysql) { + + # Perl 5.13.8 and older complain about non-characters. + no warnings 'utf8'; + $comment->{thetext} + =~ s/\x{FDD0}\[U\+((?:[1-9A-F]|10)[0-9A-F]{4})\]\x{FDD1}/chr(hex $1)/eg; + } + } + + # Some bugs may have no comments when upgrading old installations. + Bugzilla::Comment->preload($self->{'comments'}) if $count; + } + my @comments = @{$self->{'comments'}}; + + my $order = $params->{order} || Bugzilla->user->setting('comment_sort_order'); + if ($order ne 'oldest_to_newest') { + @comments = reverse @comments; + if ($order eq 'newest_to_oldest_desc_first') { + unshift(@comments, pop @comments); + } + } + + if ($params->{after}) { + my $from = datetime_from($params->{after}); + @comments = grep { datetime_from($_->creation_ts) > $from } @comments; + } + if ($params->{to}) { + my $to = datetime_from($params->{to}); + @comments = grep { datetime_from($_->creation_ts) <= $to } @comments; + } + return \@comments; } sub new_bug_statuses { - my ($class, $product) = @_; - my $user = Bugzilla->user; + my ($class, $product) = @_; + my $user = Bugzilla->user; - # Construct the list of allowable statuses. - my @statuses = @{ Bugzilla::Bug->statuses_available($product) }; + # Construct the list of allowable statuses. + my @statuses = @{Bugzilla::Bug->statuses_available($product)}; - # If the user has no privs... - unless ($user->in_group('editbugs', $product->id) - || $user->in_group('canconfirm', $product->id)) - { - # ... use UNCONFIRMED if available, else use the first status of the list. - my ($unconfirmed) = grep { $_->name eq 'UNCONFIRMED' } @statuses; - - # Because of an apparent Perl bug, "$unconfirmed || $statuses[0]" doesn't - # work, so we're using an "?:" operator. See bug 603314 for details. - @statuses = ($unconfirmed ? $unconfirmed : $statuses[0]); - } + # If the user has no privs... + unless ($user->in_group('editbugs', $product->id) + || $user->in_group('canconfirm', $product->id)) + { + # ... use UNCONFIRMED if available, else use the first status of the list. + my ($unconfirmed) = grep { $_->name eq 'UNCONFIRMED' } @statuses; - return \@statuses; + # Because of an apparent Perl bug, "$unconfirmed || $statuses[0]" doesn't + # work, so we're using an "?:" operator. See bug 603314 for details. + @statuses = ($unconfirmed ? $unconfirmed : $statuses[0]); + } + + return \@statuses; } # This is needed by xt/search.t. sub percentage_complete { - my $self = shift; - return undef if $self->{'error'} || !Bugzilla->user->is_timetracker; - my $remaining = $self->remaining_time; - my $actual = $self->actual_time; - my $total = $remaining + $actual; - return undef if $total == 0; - # Search.pm truncates this value to an integer, so we want to as well, - # since this is mostly used in a test where its value needs to be - # identical to what the database will return. - return int(100 * ($actual / $total)); + my $self = shift; + return undef if $self->{'error'} || !Bugzilla->user->is_timetracker; + my $remaining = $self->remaining_time; + my $actual = $self->actual_time; + my $total = $remaining + $actual; + return undef if $total == 0; + + # Search.pm truncates this value to an integer, so we want to as well, + # since this is mostly used in a test where its value needs to be + # identical to what the database will return. + return int(100 * ($actual / $total)); } sub product { - my ($self) = @_; - return '' if $self->{error}; - $self->{product} //= $self->product_obj->name; - return $self->{product}; + my ($self) = @_; + return '' if $self->{error}; + $self->{product} //= $self->product_obj->name; + return $self->{product}; } # XXX This should eventually replace the "product" subroutine. sub product_obj { - my $self = shift; - return {} if $self->{error}; - $self->{product_obj} ||= - new Bugzilla::Product({ id => $self->{product_id}, cache => 1 }); - return $self->{product_obj}; + my $self = shift; + return {} if $self->{error}; + $self->{product_obj} + ||= new Bugzilla::Product({id => $self->{product_id}, cache => 1}); + return $self->{product_obj}; } sub qa_contact { - my ($self) = @_; - return $self->{'qa_contact_obj'} if exists $self->{'qa_contact_obj'}; - return undef if $self->{'error'}; - - if (Bugzilla->params->{'useqacontact'} && $self->{'qa_contact'}) { - $self->{'qa_contact_obj'} = new Bugzilla::User({ id => $self->{'qa_contact'}, cache => 1 }); - } else { - $self->{'qa_contact_obj'} = undef; - } - return $self->{'qa_contact_obj'}; -} + my ($self) = @_; + return $self->{'qa_contact_obj'} if exists $self->{'qa_contact_obj'}; + return undef if $self->{'error'}; + + if (Bugzilla->params->{'useqacontact'} && $self->{'qa_contact'}) { + $self->{'qa_contact_obj'} + = new Bugzilla::User({id => $self->{'qa_contact'}, cache => 1}); + } + else { + $self->{'qa_contact_obj'} = undef; + } + return $self->{'qa_contact_obj'}; +} + +## REDHAT EXTENSION START 876015 +sub docs_contact { + my ($self) = @_; + return $self->{'docs_contact_obj'} if exists $self->{'docs_contact_obj'}; + return undef if $self->{'error'}; + + if (Bugzilla->params->{'usedocscontact'} && $self->{'docs_contact'}) { + $self->{'docs_contact_obj'} = new Bugzilla::User($self->{'docs_contact'}); + } + else { + $self->{'docs_contact_obj'} = undef; + } + return $self->{'docs_contact_obj'}; +} +## REDHAT EXTENSION END 876015 sub reporter { - my ($self) = @_; - return $self->{'reporter'} if exists $self->{'reporter'}; - $self->{'reporter_id'} = 0 if $self->{'error'}; - $self->{'reporter'} = new Bugzilla::User({ id => $self->{'reporter_id'}, cache => 1 }); - return $self->{'reporter'}; + my ($self) = @_; + return $self->{'reporter'} if exists $self->{'reporter'}; + $self->{'reporter_id'} = 0 if $self->{'error'}; + $self->{'reporter'} + = new Bugzilla::User({id => $self->{'reporter_id'}, cache => 1}); + return $self->{'reporter'}; } sub see_also { - my ($self) = @_; - return [] if $self->{'error'}; - if (!exists $self->{see_also}) { - my $ids = Bugzilla->dbh->selectcol_arrayref( - 'SELECT id FROM bug_see_also WHERE bug_id = ?', - undef, $self->id); + my ($self) = @_; + return [] if $self->{'error'}; + if (!exists $self->{see_also}) { + my $ids + = Bugzilla->dbh->selectcol_arrayref( + 'SELECT id FROM bug_see_also WHERE bug_id = ?', + undef, $self->id); - my $bug_urls = Bugzilla::BugUrl->new_from_list($ids); + my $bug_urls = Bugzilla::BugUrl->new_from_list($ids); - $self->{see_also} = $bug_urls; - } - return $self->{see_also}; + $self->{see_also} = $bug_urls; + } + return $self->{see_also}; } sub status { - my $self = shift; - return undef if $self->{'error'}; + my $self = shift; + return undef if $self->{'error'}; - $self->{'status'} ||= new Bugzilla::Status({name => $self->{'bug_status'}}); - return $self->{'status'}; + $self->{'status'} ||= new Bugzilla::Status({name => $self->{'bug_status'}}); + return $self->{'status'}; } sub statuses_available { - my ($invocant, $product) = @_; + my ($invocant, $product) = @_; - my @statuses; + my @statuses; - if (ref $invocant) { - return [] if $invocant->{'error'}; + if (ref $invocant) { + return [] if $invocant->{'error'}; - return $invocant->{'statuses_available'} - if defined $invocant->{'statuses_available'}; + return $invocant->{'statuses_available'} + if defined $invocant->{'statuses_available'}; - @statuses = @{ $invocant->status->can_change_to }; - $product = $invocant->product_obj; - } else { - @statuses = @{ Bugzilla::Status->can_change_to }; - } + @statuses = @{$invocant->status->can_change_to}; + $product = $invocant->product_obj; + } + else { + @statuses = @{Bugzilla::Status->can_change_to}; + } - # UNCONFIRMED is only a valid status if it is enabled in this product. - if (!$product->allows_unconfirmed) { - @statuses = grep { $_->name ne 'UNCONFIRMED' } @statuses; - } + # UNCONFIRMED is only a valid status if it is enabled in this product. + if (!$product->allows_unconfirmed) { + @statuses = grep { $_->name ne 'UNCONFIRMED' } @statuses; + } - if (ref $invocant) { - my $available = $invocant->_refine_available_statuses(@statuses); - $invocant->{'statuses_available'} = $available; - return $available; - } + if (ref $invocant) { + my $available = $invocant->_refine_available_statuses(@statuses); + $invocant->{'statuses_available'} = $available; + return $available; + } - return \@statuses; + return \@statuses; } sub _refine_available_statuses { - my $self = shift; - my @statuses = @_; - - my @available; - foreach my $status (@statuses) { - # Make sure this is a legal status transition - next if !$self->check_can_change_field( - 'bug_status', $self->status->name, $status->name); - push(@available, $status); - } + my $self = shift; + my @statuses = @_; - # If this bug has an inactive status set, it should still be in the list. - if (!grep($_->name eq $self->status->name, @available)) { - unshift(@available, $self->status); - } - - return \@available; -} + my @available; + foreach my $status (@statuses) { -sub show_attachment_flags { - my ($self) = @_; - return $self->{'show_attachment_flags'} - if exists $self->{'show_attachment_flags'}; - return 0 if $self->{'error'}; + # Make sure this is a legal status transition + next + if !$self->check_can_change_field('bug_status', $self->status->name, + $status->name); + push(@available, $status); + } - # The number of types of flags that can be set on attachments to this bug - # and the number of flags on those attachments. One of these counts must be - # greater than zero in order for the "flags" column to appear in the table - # of attachments. - my $num_attachment_flag_types = Bugzilla::FlagType::count( - { 'target_type' => 'attachment', - 'product_id' => $self->{'product_id'}, - 'component_id' => $self->{'component_id'} }); - my $num_attachment_flags = Bugzilla::Flag->count( - { 'target_type' => 'attachment', - 'bug_id' => $self->bug_id }); + # If this bug has an inactive status set, it should still be in the list. + if (!grep($_->name eq $self->status->name, @available)) { + unshift(@available, $self->status); + } - $self->{'show_attachment_flags'} = - ($num_attachment_flag_types || $num_attachment_flags); + return \@available; +} - return $self->{'show_attachment_flags'}; +sub show_attachment_flags { + my ($self) = @_; + return $self->{'show_attachment_flags'} + if exists $self->{'show_attachment_flags'}; + return 0 if $self->{'error'}; + + # The number of types of flags that can be set on attachments to this bug + # and the number of flags on those attachments. One of these counts must be + # greater than zero in order for the "flags" column to appear in the table + # of attachments. + my $num_attachment_flag_types = Bugzilla::FlagType::count({ + 'target_type' => 'attachment', + 'product_id' => $self->{'product_id'}, + 'component_id' => $self->{'component_id'} + }); + my $num_attachment_flags + = Bugzilla::Flag->count({ + 'target_type' => 'attachment', 'bug_id' => $self->bug_id + }); + + $self->{'show_attachment_flags'} + = ($num_attachment_flag_types || $num_attachment_flags); + + return $self->{'show_attachment_flags'}; } sub groups { - my $self = shift; - return $self->{'groups'} if exists $self->{'groups'}; - return [] if $self->{'error'}; + my $self = shift; + return $self->{'groups'} if exists $self->{'groups'}; + return [] if $self->{'error'}; + + my $dbh = Bugzilla->dbh; + my @groups; + + # Some of this stuff needs to go into Bugzilla::User + + # For every group, we need to know if there is ANY bug_group_map + # record putting the current bug in that group and if there is ANY + # user_group_map record putting the user in that group. + # The LEFT JOINs are checking for record existence. + # + my $grouplist = Bugzilla->user->groups_as_string; + my $sql + = "SELECT DISTINCT groups.id, name, description, category," + . " CASE WHEN bug_group_map.group_id IS NOT NULL" + . " THEN 1 ELSE 0 END,"; + + if (Bugzilla->user->in_group('admin')) { + $sql .= " 1=1, "; + } + else { + $sql .= " CASE WHEN groups.id IN($grouplist) THEN 1 ELSE 0 END,"; + } + + $sql + .= " isactive, membercontrol, othercontrol" + . " FROM groups" + . " LEFT JOIN bug_group_map" + . " ON bug_group_map.group_id = groups.id" + . " AND bug_id = ?" + . " LEFT JOIN group_control_map" + . " ON group_control_map.group_id = groups.id" + . " AND group_control_map.product_id = ? " + . " WHERE isbuggroup = 1" + . " ORDER BY description"; + my $sth = $dbh->prepare($sql); + $sth->execute($self->{'bug_id'}, $self->{'product_id'}); + + while ( + my ( + $groupid, $name, $description, $category, $ison, + $ingroup, $isactive, $membercontrol, $othercontrol + ) + = $sth->fetchrow_array() + ) + { + + $membercontrol ||= 0; + + # For product groups, we only want to use the group if either + # (1) The bit is set and not required, or + # (2) The group is Shown or Default for members and + # the user is a member of the group. + # ## REDHAT EXTENSION START 408671 + # (3) The group is othercontrol type. + if ( + $ison + || ( + $isactive + && ($ingroup || ($othercontrol && $othercontrol == CONTROLMAPSHOWN)) + && ($membercontrol + && ($membercontrol == CONTROLMAPDEFAULT || $membercontrol == CONTROLMAPSHOWN)) + ) + ) + { + my $ismandatory = $isactive && ($membercontrol == CONTROLMAPMANDATORY); - my $dbh = Bugzilla->dbh; - my @groups; - - # Some of this stuff needs to go into Bugzilla::User - - # For every group, we need to know if there is ANY bug_group_map - # record putting the current bug in that group and if there is ANY - # user_group_map record putting the user in that group. - # The LEFT JOINs are checking for record existence. - # - my $grouplist = Bugzilla->user->groups_as_string; - my $sth = $dbh->prepare( - "SELECT DISTINCT groups.id, name, description," . - " CASE WHEN bug_group_map.group_id IS NOT NULL" . - " THEN 1 ELSE 0 END," . - " CASE WHEN groups.id IN($grouplist) THEN 1 ELSE 0 END," . - " isactive, membercontrol, othercontrol" . - " FROM groups" . - " LEFT JOIN bug_group_map" . - " ON bug_group_map.group_id = groups.id" . - " AND bug_id = ?" . - " LEFT JOIN group_control_map" . - " ON group_control_map.group_id = groups.id" . - " AND group_control_map.product_id = ? " . - " WHERE isbuggroup = 1" . - " ORDER BY description"); - $sth->execute($self->{'bug_id'}, - $self->{'product_id'}); - - while (my ($groupid, $name, $description, $ison, $ingroup, $isactive, - $membercontrol, $othercontrol) = $sth->fetchrow_array()) { - - $membercontrol ||= 0; - - # For product groups, we only want to use the group if either - # (1) The bit is set and not required, or - # (2) The group is Shown or Default for members and - # the user is a member of the group. - if ($ison || - ($isactive && $ingroup - && (($membercontrol == CONTROLMAPDEFAULT) - || ($membercontrol == CONTROLMAPSHOWN)) - )) + my $isothercontrol = $isactive && ($othercontrol == CONTROLMAPSHOWN); + + push( + @groups, { - my $ismandatory = $isactive - && ($membercontrol == CONTROLMAPMANDATORY); - - push (@groups, { "bit" => $groupid, - "name" => $name, - "ison" => $ison, - "ingroup" => $ingroup, - "mandatory" => $ismandatory, - "description" => $description }); + "bit" => $groupid, + "name" => $name, + "category" => $category, + "ison" => $ison, + "ingroup" => $ingroup, + "mandatory" => $ismandatory, + "othercontrol" => $isothercontrol, + "description" => $description } + ); } + } - $self->{'groups'} = \@groups; + $self->{'groups'} = \@groups; - return $self->{'groups'}; + return $self->{'groups'}; } sub groups_in { - my $self = shift; - return $self->{'groups_in'} if exists $self->{'groups_in'}; - return [] if $self->{'error'}; - my $group_ids = Bugzilla->dbh->selectcol_arrayref( - 'SELECT group_id FROM bug_group_map WHERE bug_id = ?', - undef, $self->id); - $self->{'groups_in'} = Bugzilla::Group->new_from_list($group_ids); - return $self->{'groups_in'}; + my $self = shift; + return $self->{'groups_in'} if exists $self->{'groups_in'}; + return [] if $self->{'error'}; + my $group_ids + = Bugzilla->dbh->selectcol_arrayref( + 'SELECT group_id FROM bug_group_map WHERE bug_id = ?', + undef, $self->id); + $self->{'groups_in'} = Bugzilla::Group->new_from_list($group_ids); + return $self->{'groups_in'}; } sub in_group { - my ($self, $group) = @_; - return grep($_->id == $group->id, @{$self->groups_in}) ? 1 : 0; + my ($self, $group) = @_; + return grep($_->id == $group->id, @{$self->groups_in}) ? 1 : 0; } sub user { - my $self = shift; - return $self->{'user'} if exists $self->{'user'}; - return {} if $self->{'error'}; - - my $user = Bugzilla->user; - my $prod_id = $self->{'product_id'}; - - my $editbugs = $user->in_group('editbugs', $prod_id); - my $is_reporter = $user->id == $self->{reporter_id} ? 1 : 0; - my $is_assignee = $user->id == $self->{'assigned_to'} ? 1 : 0; - my $is_qa_contact = Bugzilla->params->{'useqacontact'} - && $self->{'qa_contact'} - && $user->id == $self->{'qa_contact'} ? 1 : 0; - - my $canedit = $editbugs || $is_assignee || $is_qa_contact; - my $canconfirm = $editbugs || $user->in_group('canconfirm', $prod_id); - my $has_any_role = $is_reporter || $is_assignee || $is_qa_contact; - - $self->{'user'} = {canconfirm => $canconfirm, - canedit => $canedit, - isreporter => $is_reporter, - has_any_role => $has_any_role}; - return $self->{'user'}; + my $self = shift; + return $self->{'user'} if exists $self->{'user'}; + return {} if $self->{'error'}; + + my $user = Bugzilla->user; + my $prod_id = $self->{'product_id'}; + + my $editbugs = $user->in_group('editbugs', $prod_id); + my $is_reporter = $user->id == $self->{reporter_id} ? 1 : 0; + my $is_assignee = $user->id == $self->{'assigned_to'} ? 1 : 0; + my $is_qa_contact + = Bugzilla->params->{'useqacontact'} + && $self->{'qa_contact'} + && $user->id == $self->{'qa_contact'} ? 1 : 0; + ## REDHAT EXTENSION START 876015 + my $is_docs_contact + = Bugzilla->params->{'usedocscontact'} + && $self->{'docs_contact'} + && $user->id == $self->{'docs_contact'} ? 1 : 0; + + my $canedit = $editbugs || $is_assignee || $is_qa_contact || $is_docs_contact; + my $canconfirm = $editbugs || $user->in_group('canconfirm', $prod_id); + my $has_any_role + = $is_reporter || $is_assignee || $is_qa_contact || $is_docs_contact; + ## REDHAT EXTENSION END 876015 + + $self->{'user'} = { + canconfirm => $canconfirm, + canedit => $canedit, + isreporter => $is_reporter, + has_any_role => $has_any_role + }; + return $self->{'user'}; } # This is intended to get values that can be selected by the user in the # UI. It should not be used for security or validation purposes. sub choices { - my $self = shift; - return $self->{'choices'} if exists $self->{'choices'}; - return {} if $self->{'error'}; - my $user = Bugzilla->user; - - my @products = @{ $user->get_enterable_products }; - # The current product is part of the popup, even if new bugs are no longer - # allowed for that product - if (!grep($_->name eq $self->product_obj->name, @products)) { - unshift(@products, $self->product_obj); - } - my %class_ids = map { $_->classification_id => 1 } @products; - my $classifications = - Bugzilla::Classification->new_from_list([keys %class_ids]); - - my %choices = ( - bug_status => $self->statuses_available, - classification => $classifications, - product => \@products, - component => $self->product_obj->components, - version => $self->product_obj->versions, - target_milestone => $self->product_obj->milestones, - ); - - my $resolution_field = new Bugzilla::Field({ name => 'resolution' }); - # Don't include the empty resolution in drop-downs. - my @resolutions = grep($_->name, @{ $resolution_field->legal_values }); - $choices{'resolution'} = \@resolutions; - - foreach my $key (keys %choices) { - my $value = $self->$key; - $choices{$key} = [grep { $_->is_active || $_->name eq $value } @{ $choices{$key} }]; - } - - $self->{'choices'} = \%choices; - return $self->{'choices'}; + my $self = shift; + return $self->{'choices'} if exists $self->{'choices'}; + return {} if $self->{'error'}; + my $user = Bugzilla->user; + + my @products = @{$user->get_enterable_products}; + + # The current product is part of the popup, even if new bugs are no longer + # allowed for that product + if (!grep($_->name eq $self->product_obj->name, @products)) { + unshift(@products, $self->product_obj); + } + my %class_ids = map { $_->classification_id => 1 } @products; + my $classifications + = Bugzilla::Classification->new_from_list([keys %class_ids]); + + my %choices = ( + bug_status => $self->statuses_available, + classification => $classifications, + product => \@products, + component => $self->product_obj->components, + version => $self->product_obj->versions, + target_milestone => $self->product_obj->milestones, + target_release => $self->product_obj->releases, + ); + + my $resolution_field = new Bugzilla::Field({name => 'resolution'}); + + # Don't include the empty resolution in drop-downs. + my @resolutions = grep($_->name, @{$resolution_field->legal_values}); + $choices{'resolution'} = \@resolutions; + + foreach my $key (keys %choices) { + my $value = $self->$key; + $choices{$key} + = [grep { $_->is_active || $_->name eq $value } @{$choices{$key}}]; + } + + $self->{'choices'} = \%choices; + return $self->{'choices'}; } # Convenience Function. If you need speed, use this. If you need @@ -3989,11 +5064,11 @@ sub choices { # Queries the database for the bug with a given alias, and returns # the ID of the bug if it exists or the undefined value if it doesn't. sub bug_alias_to_id { - my ($alias) = @_; - my $dbh = Bugzilla->dbh; - trick_taint($alias); - return $dbh->selectrow_array( - "SELECT bug_id FROM bugs_aliases WHERE alias = ?", undef, $alias); + my ($alias) = @_; + my $dbh = Bugzilla->dbh; + trick_taint($alias); + return $dbh->selectrow_array("SELECT bug_id FROM bugs_aliases WHERE alias = ?", + undef, $alias); } ##################################################################### @@ -4003,21 +5078,51 @@ sub bug_alias_to_id { # Returns a list of currently active and editable bug fields, # including multi-select fields. sub editable_bug_fields { - my @fields = Bugzilla->dbh->bz_table_columns('bugs'); - # Add multi-select fields - push(@fields, map { $_->name } @{Bugzilla->fields({obsolete => 0, - type => FIELD_TYPE_MULTI_SELECT})}); - # Obsolete custom fields are not editable. - my @obsolete_fields = @{ Bugzilla->fields({obsolete => 1, custom => 1}) }; - @obsolete_fields = map { $_->name } @obsolete_fields; - foreach my $remove ("bug_id", "reporter", "creation_ts", "delta_ts", - "lastdiffed", @obsolete_fields) - { - my $location = firstidx { $_ eq $remove } @fields; - # Ensure field exists before attempting to remove it. - splice(@fields, $location, 1) if ($location > -1); - } - return @fields; + my @fields = Bugzilla->dbh->bz_table_columns('bugs'); + + # Add multi-select fields + push( + @fields, + map { $_->name } @{Bugzilla->fields( + {obsolete => 0, type => [FIELD_TYPE_MULTI_SELECT || FIELD_TYPE_ONE_SELECT]} + ) + } + ); + ## REDHAT EXTENSION BEGIN 1142795 + push @fields, 'target_release'; + ## REDHAT EXTENSION END 1142795 + + # Obsolete custom fields are not editable. + my @obsolete_fields = @{Bugzilla->fields({obsolete => 1, custom => 1})}; + @obsolete_fields = map { $_->name } @obsolete_fields; + foreach + my $remove ("bug_id", "reporter", "creation_ts", "delta_ts", "lastdiffed", + @obsolete_fields) + { + my $location = firstidx { $_ eq $remove } @fields; + + # Ensure field exists before attempting to remove it. + splice(@fields, $location, 1) if ($location > -1); + } + + ## REDHAT EXTENSION BEGIN 826962 + # We need to filter this list, but to do so, we need objects not names + # We also need to specially handle the product_id and component_id fields + my @field_obj + = grep { defined $_ } + map { Bugzilla::Field->new({name => $_}) } + apply {s/^(product|component)_id$/$1/} + @fields; + Bugzilla::Hook::process('bug_filter_fields', {fields => \@field_obj}); + + # And turn it back into an array + @fields = map { /^(product|component)$/ ? "${_}_id" : $_ } + map { $_->name } @field_obj; + ## REDHAT EXTENSION END 826962 + + # Sorted because the old @::log_columns variable, which this replaces, + # was sorted. + return sort(@fields); } # XXX - When Bug::update() will be implemented, we should make this routine @@ -4025,104 +5130,124 @@ sub editable_bug_fields { # Join with bug_status and bugs tables to show bugs with open statuses first, # and then the others sub EmitDependList { - my ($my_field, $target_field, $bug_id, $exclude_resolved) = @_; - my $cache = Bugzilla->request_cache->{bug_dependency_list} ||= {}; + my ($my_field, $target_field, $bug_id, $exclude_resolved) = @_; + my $cache = Bugzilla->request_cache->{bug_dependency_list} ||= {}; - my $dbh = Bugzilla->dbh; - $exclude_resolved = $exclude_resolved ? 1 : 0; - my $is_open_clause = $exclude_resolved ? 'AND is_open = 1' : ''; + my $dbh = Bugzilla->dbh; + $exclude_resolved = $exclude_resolved ? 1 : 0; + my $is_open_clause = $exclude_resolved ? 'AND is_open = 1' : ''; - $cache->{"${target_field}_sth_$exclude_resolved"} ||= $dbh->prepare( - "SELECT $target_field + $cache->{"${target_field}_sth_$exclude_resolved"} ||= $dbh->prepare( + "SELECT $target_field FROM dependencies INNER JOIN bugs ON dependencies.$target_field = bugs.bug_id INNER JOIN bug_status ON bugs.bug_status = bug_status.value WHERE $my_field = ? $is_open_clause - ORDER BY is_open DESC, $target_field"); + ORDER BY is_open DESC, $target_field" + ); - return $dbh->selectcol_arrayref( - $cache->{"${target_field}_sth_$exclude_resolved"}, - undef, $bug_id); + return $dbh->selectcol_arrayref( + $cache->{"${target_field}_sth_$exclude_resolved"}, + undef, $bug_id); } # Creates a lot of bug objects in the same order as the input array. sub _bugs_in_order { - my ($self, $bug_ids) = @_; - return [] unless @$bug_ids; + my ($self, $bug_ids) = @_; + return [] unless @$bug_ids; - my %bug_map; - my $dbh = Bugzilla->dbh; + my %bug_map; + my $dbh = Bugzilla->dbh; - # there's no need to load bugs from the database if they are already in the - # object-cache - my @missing_ids; - foreach my $bug_id (@$bug_ids) { - if (my $bug = Bugzilla::Bug->object_cache_get($bug_id)) { - $bug_map{$bug_id} = $bug; - } - else { - push @missing_ids, $bug_id; - } + # there's no need to load bugs from the database if they are already in the + # object-cache + my @missing_ids; + foreach my $bug_id (@$bug_ids) { + if (my $bug = Bugzilla::Bug->object_cache_get($bug_id)) { + $bug_map{$bug_id} = $bug; } - if (@missing_ids) { - my $bugs = Bugzilla::Bug->new_from_list(\@missing_ids); - $bug_map{$_->id} = $_ foreach @$bugs; + else { + push @missing_ids, $bug_id; } + } + if (@missing_ids) { + my $bugs = Bugzilla::Bug->new_from_list(\@missing_ids); + $bug_map{$_->id} = $_ foreach @$bugs; + } - # Dependencies are often displayed using their aliases instead of their - # bug ID. Load them all at once. - my $rows = $dbh->selectall_arrayref( - 'SELECT bug_id, alias FROM bugs_aliases WHERE ' . - $dbh->sql_in('bug_id', $bug_ids) . ' ORDER BY alias'); + # Dependencies are often displayed using their aliases instead of their + # bug ID. Load them all at once. + my $rows + = $dbh->selectall_arrayref('SELECT bug_id, alias FROM bugs_aliases WHERE ' + . $dbh->sql_in('bug_id', $bug_ids) + . ' ORDER BY alias'); - foreach my $row (@$rows) { - my ($bug_id, $alias) = @$row; - $bug_map{$bug_id}->{alias} ||= []; - push @{ $bug_map{$bug_id}->{alias} }, $alias; - } - # Make sure all bugs have their alias attribute set. - $bug_map{$_}->{alias} ||= [] foreach @$bug_ids; + foreach my $row (@$rows) { + my ($bug_id, $alias) = @$row; + $bug_map{$bug_id}->{alias} ||= []; + push @{$bug_map{$bug_id}->{alias}}, $alias; + } + + # Make sure all bugs have their alias attribute set. + $bug_map{$_}->{alias} ||= [] foreach @$bug_ids; - return [ map { $bug_map{$_} } @$bug_ids ]; + return [map { $bug_map{$_} } @$bug_ids]; } # Get the activity of a bug, starting from $starttime (if given). # This routine assumes Bugzilla::Bug->check has been previously called. sub get_activity { - my ($self, $attach_id, $starttime, $include_comment_tags) = @_; - my $dbh = Bugzilla->dbh; - my $user = Bugzilla->user; - - # Arguments passed to the SQL query. - my @args = ($self->id); - - # Only consider changes since $starttime, if given. - my $datepart = ""; - if (defined $starttime) { - trick_taint($starttime); - push (@args, $starttime); - $datepart = "AND bug_when > ?"; - } - - my $attachpart = ""; - if ($attach_id) { - push(@args, $attach_id); - $attachpart = "AND bugs_activity.attach_id = ?"; - } - - # Only includes attachments the user is allowed to see. - my $suppjoins = ""; - my $suppwhere = ""; - if (!$user->is_insider) { - $suppjoins = "LEFT JOIN attachments + my ($self, $attach_id, $starttime, $include_comment_tags) = @_; + my $dbh = Bugzilla->dbh; + my $user = Bugzilla->user; + + # Arguments passed to the SQL query. + my @args = ($self->id); + + # Only consider changes since $starttime, if given. + my $datepart = ""; + if (defined $starttime) { + trick_taint($starttime); + push(@args, $starttime); + $datepart = "AND bug_when > ?"; + } + + my $attachpart = ""; + if ($attach_id) { + push(@args, $attach_id); + $attachpart = "AND bugs_activity.attach_id = ?"; + } + + # Only includes attachments the user is allowed to see. + my $suppjoins = ""; + my $suppwhere = ""; + if (!$user->is_insider) { + $suppjoins = "LEFT JOIN attachments ON attachments.attach_id = bugs_activity.attach_id"; - $suppwhere = "AND COALESCE(attachments.isprivate, 0) = 0"; - } - - my $query = "SELECT fielddefs.name, bugs_activity.attach_id, " . - $dbh->sql_date_format('bugs_activity.bug_when', '%Y.%m.%d %H:%i:%s') . - " AS bug_when, bugs_activity.removed, bugs_activity.added, profiles.login_name, - bugs_activity.comment_id + $suppwhere = "AND COALESCE(attachments.isprivate, 0) = 0"; + } + + my $query + = "SELECT fielddefs.name, fielddefs.description, bugs_activity.attach_id, " + . $dbh->sql_date_format('bugs_activity.bug_when', '%Y.%m.%d %H:%i:%s') + . " AS bug_when, bugs_activity.removed, bugs_activity.added, + "; + + ## REDHAT EXTENSION START 447765 + if (Bugzilla->user->id) { + $query .= "profiles.login_name, "; + } + else { + $query .= "profiles.realname, "; + } + ## REDHAT EXTENSION END 447765 + ## REDHAT EXTENSION START 847166 + $query .= "bugs_activity.object_id, "; + ## REDHAT EXTENSION END 847166 + + $query .= " + bugs_activity.comment_id, + bugs_activity.id AS activity_order FROM bugs_activity $suppjoins INNER JOIN fielddefs @@ -4134,28 +5259,43 @@ sub get_activity { $attachpart $suppwhere "; - if (Bugzilla->params->{'comment_taggers_group'} - && $include_comment_tags - && !$attach_id) - { - # Only includes comment tag activity for comments the user is allowed to see. - $suppjoins = ""; - $suppwhere = ""; - if (!Bugzilla->user->is_insider) { - $suppjoins = "INNER JOIN longdescs + if ( Bugzilla->params->{'comment_taggers_group'} + && $include_comment_tags + && !$attach_id) + { + # Only includes comment tag activity for comments the user is allowed to see. + $suppjoins = ""; + $suppwhere = ""; + if (!Bugzilla->user->is_insider) { + $suppjoins = "INNER JOIN longdescs ON longdescs.comment_id = longdescs_tags_activity.comment_id"; - $suppwhere = "AND longdescs.isprivate = 0"; - } + $suppwhere = "AND longdescs.isprivate = 0"; + } - $query .= " + $query .= " UNION ALL SELECT 'comment_tag' AS name, - NULL AS attach_id," . - $dbh->sql_date_format('longdescs_tags_activity.bug_when', '%Y.%m.%d %H:%i:%s') . " AS bug_when, + NULL AS description, + NULL AS attach_id," + . $dbh->sql_date_format('longdescs_tags_activity.bug_when', + '%Y.%m.%d %H:%i:%s') + . " AS bug_when, longdescs_tags_activity.removed, - longdescs_tags_activity.added, - profiles.login_name, - longdescs_tags_activity.comment_id as comment_id + longdescs_tags_activity.added,"; + + ## REDHAT EXTENSION START 447765 + if (Bugzilla->user->id) { + $query .= "profiles.login_name, "; + } + else { + $query .= "profiles.realname, "; + } + ## REDHAT EXTENSION END 847166 + ## REDHAT EXTENSION START 847166 + $query .= "NULL as object_id, "; + ## REDHAT EXTENSION END 847166 + $query .= "longdescs_tags_activity.comment_id as comment_id, + longdescs_tags_activity.id AS activity_order FROM longdescs_tags_activity INNER JOIN profiles ON profiles.userid = longdescs_tags_activity.who $suppjoins @@ -4163,168 +5303,308 @@ sub get_activity { $datepart $suppwhere "; - push @args, $self->id; - push @args, $starttime if defined $starttime; - } + push @args, $self->id; + push @args, $starttime if defined $starttime; + } - $query .= "ORDER BY bug_when, comment_id"; + $query .= "ORDER BY bug_when, activity_order"; - my $list = $dbh->selectall_arrayref($query, undef, @args); + my $list = $dbh->selectall_arrayref($query, undef, @args); - my @operations; - my $operation = {}; - my $changes = []; - my $incomplete_data = 0; + my @operations; + my $operation = {}; + my $changes = []; + my $incomplete_data = 0; - foreach my $entry (@$list) { - my ($fieldname, $attachid, $when, $removed, $added, $who, $comment_id) = @$entry; - my %change; - my $activity_visible = 1; + foreach my $entry (@$list) { + my ( + $fieldname, $fielddesc, $attachid, $when, $removed, + $added, $who, $object_id, $comment_id + ) = @$entry; + my %change; + my $activity_visible = 1; - # check if the user should see this field's activity - if (grep { $fieldname eq $_ } TIMETRACKING_FIELDS) { - $activity_visible = $user->is_timetracker; - } - elsif ($fieldname eq 'longdescs.isprivate' - && !$user->is_insider && $added) - { - $activity_visible = 0; - } - else { - $activity_visible = 1; + # check if the user should see this field's activity + if (grep { $fieldname eq $_ } TIMETRACKING_FIELDS) { + $activity_visible = $user->is_timetracker; + } + elsif ($fieldname eq 'longdescs.isprivate' && !$user->is_insider && $added) { + $activity_visible = 0; + } + else { + $activity_visible = 1; + } + ## REDHAT EXTENSIONS START 406111 + Bugzilla::Hook::process( + 'bug_filter_change', + { + user => Bugzilla->user, + field => $fieldname, + added => $added, + removed => $removed, + object_id => $object_id, + visible => \$activity_visible, + bug => $self + } + ); + ## REDHAT EXTENSIONS END 406111 + + if ($activity_visible) { + + # Check for the results of an old Bugzilla data corruption bug + if ( (defined($added) && defined($removed) && $added eq '?' && $removed eq '?') + || (defined($added) && $added =~ /^\? /) + || (defined($removed) && $removed =~ /^\? /)) + { + $incomplete_data = 1; + } + + ## REDHAT EXTENSIONS START 1201616 + if ($fieldname eq 'rh_rule') { + + # wrap in eval to allow template compilation + eval { + require Bugzilla::Extension::RuleEngine::Rule; + my $rule = Bugzilla::Extension::RuleEngine::Rule->new($added); + $added = $rule->name(); + }; + } + ## REDHAT EXTENSIONS END 1201616 + ## REDHAT EXTENSIONS START 1348445 + elsif ($fieldname eq 'dependent_products') { + if ($added) { + my @prods = split /, */, $added; + my $aprods = Bugzilla::Product->new_from_list(\@prods); + $added = join(', ', map { $_->name() } @$aprods); } - if ($activity_visible) { - # Check for the results of an old Bugzilla data corruption bug - if (($added eq '?' && $removed eq '?') - || ($added =~ /^\? / || $removed =~ /^\? /)) { - $incomplete_data = 1; - } - - # An operation, done by 'who' at time 'when', has a number of - # 'changes' associated with it. - # If this is the start of a new operation, store the data from the - # previous one, and set up the new one. - if ($operation->{'who'} - && ($who ne $operation->{'who'} - || $when ne $operation->{'when'})) - { - $operation->{'changes'} = $changes; - push (@operations, $operation); - - # Create new empty anonymous data structures. - $operation = {}; - $changes = []; - } - - # If this is the same field as the previous item, then concatenate - # the data into the same change. - if ($operation->{'who'} && $who eq $operation->{'who'} - && $when eq $operation->{'when'} - && $fieldname eq $operation->{'fieldname'} - && ($comment_id || 0) == ($operation->{'comment_id'} || 0) - && ($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{'removed'} = $removed; - $change{'added'} = $added; - - if ($comment_id) { - $operation->{comment_id} = $change{'comment'} = Bugzilla::Comment->new($comment_id); - } - - push (@$changes, \%change); + if ($removed) { + my @prods = split /, */, $removed; + my $rprods = Bugzilla::Product->new_from_list(\@prods); + $removed = join(', ', map { $_->name() } @$rprods); } - } - - if ($operation->{'who'}) { + } + ## REDHAT EXTENSIONS END 1348445 + + # An operation, done by 'who' at time 'when', has a number of + # 'changes' associated with it. + # If this is the start of a new operation, store the data from the + # previous one, and set up the new one. + if ($operation->{'who'} + && ($who ne $operation->{'who'} || $when ne $operation->{'when'})) + { $operation->{'changes'} = $changes; - push (@operations, $operation); - } - - return(\@operations, $incomplete_data); + push(@operations, $operation); + + # Create new empty anonymous data structures. + $operation = {}; + $changes = []; + } + + # If this is the same field as the previous item, then concatenate + # the data into the same change. + if ( $operation->{'who'} + && $who eq $operation->{'who'} + && $when eq $operation->{'when'} + && $fieldname eq $operation->{'fieldname'} + && ($comment_id || 0) == ($operation->{'comment_id'} || 0) + && ($attachid || 0) == ($operation->{'attachid'} || 0) + && ($fieldname ne 'rh_rule')) + { + 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{'removed'} = $removed; + $change{'added'} = $added; + + if ($comment_id) { + $operation->{comment_id} = $change{'comment'} + = Bugzilla::Comment->new($comment_id); + } + + push(@$changes, \%change); + } + } + + if ($operation->{'who'}) { + $operation->{'changes'} = $changes; + push(@operations, $operation); + } + + return (\@operations, $incomplete_data); +} + +sub has_unsent_changes { + my $self = shift; + return 1 if !defined $self->lastdiffed; + return datetime_from($self->lastdiffed) < datetime_from($self->delta_ts) + ? 1 + : 0; } # Update the bugs_activity table to reflect changes made in bugs. sub LogActivityEntry { - my ($bug_id, $field, $removed, $added, $user_id, $timestamp, $comment_id, - $attach_id) = @_; - my $sth = Bugzilla->dbh->prepare_cached( - 'INSERT INTO bugs_activity - (bug_id, who, bug_when, fieldid, removed, added, comment_id, attach_id) - VALUES (?, ?, ?, ?, ?, ?, ?, ?)'); - - # in the case of CCs, deps, and keywords, there's a possibility that someone - # might try to add or remove a lot of them at once, which might take more - # space than the activity table allows. We'll solve this by splitting it - # into multiple entries if it's too long. - while ($removed || $added) { - my ($removestr, $addstr) = ($removed, $added); - if (length($removestr) > MAX_LINE_LENGTH) { - my $commaposition = find_wrap_point($removed, MAX_LINE_LENGTH); - $removestr = substr($removed, 0, $commaposition); - $removed = substr($removed, $commaposition); - } else { - $removed = ""; # no more entries - } - if (length($addstr) > MAX_LINE_LENGTH) { - my $commaposition = find_wrap_point($added, MAX_LINE_LENGTH); - $addstr = substr($added, 0, $commaposition); - $added = substr($added, $commaposition); - } else { - $added = ""; # no more entries - } - trick_taint($addstr); - trick_taint($removestr); - my $fieldid = get_field_id($field); - $sth->execute($bug_id, $user_id, $timestamp, $fieldid, $removestr, - $addstr, $comment_id, $attach_id); + my ($bug_id, $field, $removed, $added, $user_id, $timestamp, $comment_id, + $attach_id, $bug, $old_bug) + = @_; + my $dbh = Bugzilla->dbh; + + # in the case of CCs, deps, and keywords, there's a possibility that someone + # might try to add or remove a lot of them at once, which might take more + # space than the activity table allows. We'll solve this by splitting it + # into multiple entries if it's too long. + while ($removed || $added) { + my ($removestr, $addstr) = ($removed, $added); + ## REDHAT EXTENSION START 831328 915533 + my $object_id = undef; + if ( $field eq 'flagtypes.name' + or $field eq 'cf_partner' + or $field eq 'cf_verified' + or $field eq 'ext_bz_bug_map.ext_bz_bug_id' + or $field eq 'see_also') + { + my $value = ''; + my $is_added = 0; # REDHAT EXTENSION 1757211 + if ($removed) { + $addstr = ''; # Deal with the added values next time + ($removestr, $removed) = split(/\s*,\s*/, $removed, 2); + $value = $removestr; + } + else { + $removestr = ''; # Prevent error with tainting $removestr + ($addstr, $added) = split(/\s*,\s*/, $added, 2); + $value = $addstr; + $is_added = 1; # REDHAT EXTENSION 1757211 + } + if ($field ne 'see_also') { + ## REDHAT EXTENSION 1757211 + $object_id = _get_object_id($field, $value, $is_added, $bug, $old_bug); + } } + else { + ## REDHAT EXTENSION END 831328 915533 + + if (length($removestr) > MAX_LINE_LENGTH) { + my $commaposition = find_wrap_point($removed, MAX_LINE_LENGTH); + $removestr = substr($removed, 0, $commaposition); + $removed = substr($removed, $commaposition); + } + else { + $removed = ""; # no more entries + } + if (length($addstr) > MAX_LINE_LENGTH) { + my $commaposition = find_wrap_point($added, MAX_LINE_LENGTH); + $addstr = substr($added, 0, $commaposition); + $added = substr($added, $commaposition); + } + else { + $added = ""; # no more entries + } + } + trick_taint($addstr); + trick_taint($removestr); + trick_taint($timestamp); + my $fieldid = get_field_id($field); + + ## REDHAT EXTENSION START 831328 + $dbh->do( + "INSERT INTO bugs_activity + (bug_id, who, bug_when, fieldid, removed, added, comment_id, attach_id, object_id) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", + undef, + ( + $bug_id, $user_id, $timestamp, $fieldid, $removestr, + $addstr, $comment_id, $attach_id, $object_id + ) + ); + ## REDHAT EXTENSION END 831328 + } +} + +sub _get_object_id { + my ($col, $value, $is_added, $bug, $old_bug) = @_; + if ($col eq 'flagtypes.name') { + my ($flag_name) = ($value =~ /^(.*)(?:\+|\-|\?(?:\(.*\))?)$/); + + ## REDHAT EXTENSION START 1757211 + my $product_id = undef; + $product_id = $old_bug->product_id if($old_bug); + $product_id = $bug->product_id if($bug && (!$old_bug || $is_added)); + + my $flag + = Bugzilla::FlagType::match({name => $flag_name, product_id => $product_id}); + ## REDHAT EXTENSION END 1757211 + if (@$flag) { + return $flag->[0]->id; + } + } + elsif ($col eq 'ext_bz_bug_map.ext_bz_bug_id') { + my $query + = "SELECT description, id FROM external_bugzilla ORDER BY LENGTH(description) DESC, id"; + my $ext_bzs = Bugzilla->dbh->selectall_arrayref($query); + foreach my $ext_bz (@$ext_bzs) { + my ($desc, $id) = @$ext_bz; + $desc .= ' '; + if (substr($value, 0, length($desc)) eq $desc) { + return $id; + } + } + } + else { + # We don't need to get the object id for this type + return undef; + } + + # We will e-mail out a warning, since we should never get here + Bugzilla->logger->info("Cannot get object id Col: $col, Value: $value"); + + return undef; } # Update bug_user_last_visit table sub update_user_last_visit { - my ($self, $user, $last_visit_ts) = @_; - my $lv = Bugzilla::BugUserLastVisit->match({ bug_id => $self->id, - user_id => $user->id })->[0]; - - if ($lv) { - $lv->set(last_visit_ts => $last_visit_ts); - $lv->update; - } - else { - Bugzilla::BugUserLastVisit->create({ bug_id => $self->id, - user_id => $user->id, - last_visit_ts => $last_visit_ts }); - } + my ($self, $user, $last_visit_ts) = @_; + my $lv + = Bugzilla::BugUserLastVisit->match({bug_id => $self->id, user_id => $user->id + })->[0]; + + if ($lv) { + $lv->set(last_visit_ts => $last_visit_ts); + $lv->update; + } + else { + Bugzilla::BugUserLastVisit->create({ + bug_id => $self->id, user_id => $user->id, last_visit_ts => $last_visit_ts + }); + } } # Convert WebService API and email_in.pl field names to internal DB field # names. sub map_fields { - my ($params, $except) = @_; - - my %field_values; - foreach my $field (keys %$params) { - # Don't allow setting private fields via email_in or the WebService. - next if $field =~ /^_/; - my $field_name; - if ($except->{$field}) { - $field_name = $field; - } - else { - $field_name = FIELD_MAP->{$field} || $field; - } - $field_values{$field_name} = $params->{$field}; + my ($params, $except) = @_; + + my %field_values; + foreach my $field (keys %$params) { + + # Don't allow setting private fields via email_in or the WebService. + next if $field =~ /^_/; + my $field_name; + if ($except->{$field}) { + $field_name = $field; + } + else { + $field_name = FIELD_MAP->{$field} || $field; } - return \%field_values; + $field_values{$field_name} = $params->{$field}; + } + return \%field_values; } ################################################################################ @@ -4344,238 +5624,329 @@ sub map_fields { # $PrivilegesRequired - return the reason of the failure, if any ################################################################################ sub check_can_change_field { - my $self = shift; - my ($field, $oldvalue, $newvalue, $PrivilegesRequired) = (@_); - my $user = Bugzilla->user; + my $self = shift; + my ($field, $oldvalue, $newvalue, $PrivilegesRequired) = (@_); + my $user = Bugzilla->user; + + return (1) if ($user->in_group('admin')); + + $oldvalue = defined($oldvalue) ? $oldvalue : ''; + $newvalue = defined($newvalue) ? $newvalue : ''; + + # Return true if they haven't changed this field at all. + if ($oldvalue eq $newvalue) { + return 1; + } + elsif (ref($newvalue) eq 'ARRAY' && ref($oldvalue) eq 'ARRAY') { + my ($removed, $added) = diff_arrays($oldvalue, $newvalue); + return 1 if !scalar(@$removed) && !scalar(@$added); + } + elsif (trim($oldvalue) eq trim($newvalue)) { + return 1; - $oldvalue = defined($oldvalue) ? $oldvalue : ''; - $newvalue = defined($newvalue) ? $newvalue : ''; - - # Return true if they haven't changed this field at all. - if ($oldvalue eq $newvalue) { - return 1; - } elsif (ref($newvalue) eq 'ARRAY' && ref($oldvalue) eq 'ARRAY') { - my ($removed, $added) = diff_arrays($oldvalue, $newvalue); - return 1 if !scalar(@$removed) && !scalar(@$added); - } elsif (trim($oldvalue) eq trim($newvalue)) { - return 1; # numeric fields need to be compared using == - } elsif (($field eq 'estimated_time' || $field eq 'remaining_time' - || $field eq 'work_time') - && $oldvalue == $newvalue) + } + elsif ( + ( + $field eq 'estimated_time' + || $field eq 'remaining_time' + || $field eq 'work_time' + ) + && $oldvalue == $newvalue + ) + { + return 1; + } + + my @priv_results; + Bugzilla::Hook::process( + 'bug_check_can_change_field', { - return 1; - } - - my @priv_results; - Bugzilla::Hook::process('bug_check_can_change_field', - { bug => $self, field => $field, - new_value => $newvalue, old_value => $oldvalue, - priv_results => \@priv_results }); - if (my $priv_required = first { $_ > 0 } @priv_results) { - $$PrivilegesRequired = $priv_required; - return 0; - } - my $allow_found = first { $_ == 0 } @priv_results; - if (defined $allow_found) { - return 1; - } - - # Allow anyone to change comments, or set flags - if ($field =~ /^longdesc/ || $field eq 'flagtypes.name') { - return 1; - } - - # If the user isn't allowed to change a field, we must tell them who can. - # We store the required permission set into the $PrivilegesRequired - # variable which gets passed to the error template. - # - # $PrivilegesRequired = PRIVILEGES_REQUIRED_NONE : no privileges required; - # $PrivilegesRequired = PRIVILEGES_REQUIRED_REPORTER : the reporter, assignee or an empowered user; - # $PrivilegesRequired = PRIVILEGES_REQUIRED_ASSIGNEE : the assignee or an empowered user; - # $PrivilegesRequired = PRIVILEGES_REQUIRED_EMPOWERED : an empowered user. - - # Only users in the time-tracking group can change time-tracking fields, - # including the deadline. - if (grep { $_ eq $field } (TIMETRACKING_FIELDS, 'deadline')) { - if (!$user->is_timetracker) { - $$PrivilegesRequired = PRIVILEGES_REQUIRED_EMPOWERED; - return 0; - } - } - - # Allow anyone with (product-specific) "editbugs" privs to change anything. - if ($user->in_group('editbugs', $self->{'product_id'})) { - return 1; + bug => $self, + field => $field, + new_value => $newvalue, + old_value => $oldvalue, + priv_results => \@priv_results + } + ); + if (my $priv_required = first { $_ > 0 } @priv_results) { + $$PrivilegesRequired = $priv_required; + return 0; + } + my $allow_found = first { $_ == 0 } @priv_results; + if (defined $allow_found) { + return 1; + } + + # Allow anyone to change comments, or set flags + if ($field =~ /^longdesc/ || $field eq 'flagtypes.name') { + return 1; + } + +# If the user isn't allowed to change a field, we must tell them who can. +# We store the required permission set into the $PrivilegesRequired +# variable which gets passed to the error template. +# +# $PrivilegesRequired = PRIVILEGES_REQUIRED_NONE : no privileges required; +# $PrivilegesRequired = PRIVILEGES_REQUIRED_REPORTER : the reporter, assignee or an empowered user; +# $PrivilegesRequired = PRIVILEGES_REQUIRED_ASSIGNEE : the assignee or an empowered user; +# $PrivilegesRequired = PRIVILEGES_REQUIRED_EMPOWERED : an empowered user. + + # Only users in the time-tracking group can change time-tracking fields, + # including the deadline. + if (grep { $_ eq $field } (TIMETRACKING_FIELDS, 'deadline')) { + if (!$user->is_timetracker) { + $$PrivilegesRequired = PRIVILEGES_REQUIRED_EMPOWERED; + return 0; + } + } + + # Allow anyone with (product-specific) "editbugs" privs to change anything. + if ($user->in_group('editbugs', $self->{'product_id'})) { + return 1; + } + + # *Only* users with (product-specific) "canconfirm" privs can confirm bugs. + if ($self->_changes_everconfirmed($field, $oldvalue, $newvalue)) { + $$PrivilegesRequired = PRIVILEGES_REQUIRED_EMPOWERED; + return $user->in_group('canconfirm', $self->{'product_id'}); + } + + # Make sure that a valid bug ID has been given. + if (!$self->{'error'}) { + + # Allow the assignee to change anything else. + if ( $self->{'assigned_to'} == $user->id + || $self->{'_old_assigned_to'} && $self->{'_old_assigned_to'} == $user->id) + { + return 1; } - # *Only* users with (product-specific) "canconfirm" privs can confirm bugs. - if ($self->_changes_everconfirmed($field, $oldvalue, $newvalue)) { - $$PrivilegesRequired = PRIVILEGES_REQUIRED_EMPOWERED; - return $user->in_group('canconfirm', $self->{'product_id'}); + # Allow the QA contact to change anything else. + if ( + Bugzilla->params->{'useqacontact'} + && ( ($self->{'qa_contact'} && $self->{'qa_contact'} == $user->id) + || ($self->{'_old_qa_contact'} && $self->{'_old_qa_contact'} == $user->id)) + ) + { + return 1; + } + ## REDHAT EXTENSION START 876015 + # Allow the Docs contact to change anything else. + if ( + Bugzilla->params->{'usedocscontact'} + && ( ($self->{'docs_contact'} && $self->{'docs_contact'} == $user->id) + || ($self->{'_old_docs_contact'} && $self->{'_old_docs_contact'} == $user->id)) + ) + { + return 1; } + ## REDHAT EXTENSION END 876015 + } - # Make sure that a valid bug ID has been given. - if (!$self->{'error'}) { - # Allow the assignee to change anything else. - if ($self->{'assigned_to'} == $user->id - || $self->{'_old_assigned_to'} && $self->{'_old_assigned_to'} == $user->id) - { - return 1; - } + # At this point, the user is either the reporter or an + # unprivileged user. We first check for fields the reporter + # is not allowed to change. - # Allow the QA contact to change anything else. - if (Bugzilla->params->{'useqacontact'} - && (($self->{'qa_contact'} && $self->{'qa_contact'} == $user->id) - || ($self->{'_old_qa_contact'} && $self->{'_old_qa_contact'} == $user->id))) - { - return 1; - } - } + # The reporter may not: + # - reassign bugs, unless the bugs are assigned to them; + # in that case we will have already returned 1 above + # when checking for the assignee of the bug. + if ($field eq 'assigned_to') { + $$PrivilegesRequired = PRIVILEGES_REQUIRED_ASSIGNEE; + return 0; + } - # At this point, the user is either the reporter or an - # unprivileged user. We first check for fields the reporter - # is not allowed to change. + # - change the QA contact + if ($field eq 'qa_contact') { + $$PrivilegesRequired = PRIVILEGES_REQUIRED_ASSIGNEE; + return 0; + } + ## REDHAT EXTENSION START 876015 + # - change the Docs contact + if ($field eq 'docs_contact') { + $$PrivilegesRequired = PRIVILEGES_REQUIRED_ASSIGNEE; + return 0; + } + ## REDHAT EXTENSION END 876015 + # - change the target milestone + if ($field eq 'target_milestone') { + $$PrivilegesRequired = PRIVILEGES_REQUIRED_ASSIGNEE; + return 0; + } - # The reporter may not: - # - reassign bugs, unless the bugs are assigned to them; - # in that case we will have already returned 1 above - # when checking for the assignee of the bug. - if ($field eq 'assigned_to') { - $$PrivilegesRequired = PRIVILEGES_REQUIRED_ASSIGNEE; - return 0; - } - # - change the QA contact - if ($field eq 'qa_contact') { - $$PrivilegesRequired = PRIVILEGES_REQUIRED_ASSIGNEE; - return 0; - } - # - change the target milestone - if ($field eq 'target_milestone') { - $$PrivilegesRequired = PRIVILEGES_REQUIRED_ASSIGNEE; - return 0; - } - # - change the priority (unless they could have set it originally) - if ($field eq 'priority' - && !Bugzilla->params->{'letsubmitterchoosepriority'}) - { - $$PrivilegesRequired = PRIVILEGES_REQUIRED_ASSIGNEE; - return 0; - } - # - unconfirm bugs (confirming them is handled above) - if ($field eq 'everconfirmed') { - $$PrivilegesRequired = PRIVILEGES_REQUIRED_ASSIGNEE; - return 0; - } - # - change the status from one open state to another - if ($field eq 'bug_status' - && is_open_state($oldvalue) && is_open_state($newvalue)) - { - $$PrivilegesRequired = PRIVILEGES_REQUIRED_ASSIGNEE; - return 0; - } + # - change the target release + if ($field eq 'target_release') { + $$PrivilegesRequired = PRIVILEGES_REQUIRED_ASSIGNEE; + return 0; + } - # The reporter is allowed to change anything else. - if (!$self->{'error'} && $self->{'reporter_id'} == $user->id) { - return 1; - } + # - change the priority (unless he could have set it originally) + if ($field eq 'priority' && !Bugzilla->params->{'letsubmitterchoosepriority'}) { + $$PrivilegesRequired = PRIVILEGES_REQUIRED_ASSIGNEE; + return 0; + } - # If we haven't returned by this point, then the user doesn't - # have the necessary permissions to change this field. - $$PrivilegesRequired = PRIVILEGES_REQUIRED_REPORTER; + # - unconfirm bugs (confirming them is handled above) + if ($field eq 'everconfirmed') { + $$PrivilegesRequired = PRIVILEGES_REQUIRED_ASSIGNEE; return 0; + } + + # - change the status from one open state to another + if ( $field eq 'bug_status' + && is_open_state($oldvalue) + && is_open_state($newvalue)) + { + $$PrivilegesRequired = PRIVILEGES_REQUIRED_ASSIGNEE; + return 0; + } + + # The reporter is allowed to change anything else. + if (!$self->{'error'} && $self->{'reporter_id'} == $user->id) { + return 1; + } + + # If we haven't returned by this point, then the user doesn't + # have the necessary permissions to change this field. + $$PrivilegesRequired = PRIVILEGES_REQUIRED_REPORTER; + return 0; } # A helper for check_can_change_field sub _changes_everconfirmed { - my ($self, $field, $old, $new) = @_; - return 1 if $field eq 'everconfirmed'; - if ($field eq 'bug_status') { - if ($self->everconfirmed) { - # Moving a confirmed bug to UNCONFIRMED will change everconfirmed. - return 1 if $new eq 'UNCONFIRMED'; - } - else { - # Moving an unconfirmed bug to an open state that isn't - # UNCONFIRMED will confirm the bug. - return 1 if (is_open_state($new) and $new ne 'UNCONFIRMED'); - } + my ($self, $field, $old, $new) = @_; + return 1 if $field eq 'everconfirmed'; + if ($field eq 'bug_status') { + if ($self->everconfirmed) { + + # Moving a confirmed bug to UNCONFIRMED will change everconfirmed. + return 1 if $new eq 'UNCONFIRMED'; } - return 0; + else { + # Moving an unconfirmed bug to an open state that isn't + # UNCONFIRMED will confirm the bug. + return 1 if (is_open_state($new) and $new ne 'UNCONFIRMED'); + } + } + return 0; } +## REDHAT EXTENSION START 536717 +# Generates a list of dependencies for a given bug. Calls itself recursively +# to generate the entire tree. Will list a bug that a user cannot see (as +# is standard for the bug view), but will not list its dependency. +sub dependent_bugs { + my ($self, $relationship, $bugs) = @_; + $bugs = [] if not defined $bugs; + + # Check the relationship is valid + if ($relationship ne 'dependson' && $relationship ne 'blocked') { + ThrowCodeError('dependent_bugs_bad_relationship'); + } + + my @dependencies; + if ($relationship eq 'dependson') { + @dependencies = @{$self->dependson}; + } + else { + @dependencies = @{$self->blocked}; + } + + # Don't do anything if this bug doesn't have any dependencies. + return [] unless scalar(@dependencies); + + foreach my $dep_id (@dependencies) { + if (!grep { $_ == $dep_id } @$bugs) { + if (Bugzilla->user->can_see_bug($dep_id)) { + my $dep = new Bugzilla::Bug($dep_id); + $dep->dependent_bugs($relationship, $bugs); + } + push @$bugs, $dep_id; + } + } + + return [sort { $a <=> $b } @$bugs]; +} +## REDHAT EXTENSION END 536717 + # # Field Validation # # Validate and return a hash of dependencies sub ValidateDependencies { - my $fields = {}; - # These can be arrayrefs or they can be strings. - $fields->{'dependson'} = shift; - $fields->{'blocked'} = shift; - my $id = shift || 0; - - unless (defined($fields->{'dependson'}) - || defined($fields->{'blocked'})) - { - return; - } - - my $dbh = Bugzilla->dbh; - my %deps; - my %deptree; - my %sth; - $sth{dependson} = $dbh->prepare('SELECT dependson FROM dependencies WHERE blocked = ?'); - $sth{blocked} = $dbh->prepare('SELECT blocked FROM dependencies WHERE dependson = ?'); - - foreach my $pair (["blocked", "dependson"], ["dependson", "blocked"]) { - my ($me, $target) = @{$pair}; - $deptree{$target} = []; - $deps{$target} = []; - next unless $fields->{$target}; - - my %seen; - my $target_array = ref($fields->{$target}) ? $fields->{$target} - : [split(/[\s,]+/, $fields->{$target})]; - foreach my $i (@$target_array) { - if ($id == $i) { - ThrowUserError("dependency_loop_single"); - } - if (!exists $seen{$i}) { - push(@{$deptree{$target}}, $i); - $seen{$i} = 1; - } - } - # populate $deps{$target} as first-level deps only. - # and find remainder of dependency tree in $deptree{$target} - @{$deps{$target}} = @{$deptree{$target}}; - my @stack = @{$deps{$target}}; - while (@stack) { - my $i = shift @stack; - my $dep_list = $dbh->selectcol_arrayref($sth{$target}, undef, $i); - foreach my $t (@$dep_list) { - # ignore any _current_ dependencies involving this bug, - # as they will be overwritten with data from the form. - if ($t != $id && !exists $seen{$t}) { - push(@{$deptree{$target}}, $t); - push @stack, $t; - $seen{$t} = 1; - } - } + my $fields = {}; + + # These can be arrayrefs or they can be strings. + $fields->{'dependson'} = shift; + $fields->{'blocked'} = shift; + my $id = shift || 0; + + unless (defined($fields->{'dependson'}) || defined($fields->{'blocked'})) { + return; + } + + my $dbh = Bugzilla->dbh; + my %deps; + my %deptree; + my %sth; + $sth{dependson} + = $dbh->prepare('SELECT dependson FROM dependencies WHERE blocked = ?'); + $sth{blocked} + = $dbh->prepare('SELECT blocked FROM dependencies WHERE dependson = ?'); + + foreach my $pair (["blocked", "dependson"], ["dependson", "blocked"]) { + my ($me, $target) = @{$pair}; + $deptree{$target} = []; + $deps{$target} = []; + next unless $fields->{$target}; + + my %seen; + my $target_array + = ref($fields->{$target}) + ? $fields->{$target} + : [split(/[\s,]+/, $fields->{$target})]; + foreach my $i (@$target_array) { + if ($id == $i) { + ThrowUserError("dependency_loop_single"); + } + if (!exists $seen{$i}) { + push(@{$deptree{$target}}, $i); + $seen{$i} = 1; + } + } + + # populate $deps{$target} as first-level deps only. + # and find remainder of dependency tree in $deptree{$target} + @{$deps{$target}} = @{$deptree{$target}}; + my @stack = @{$deps{$target}}; + while (@stack) { + my $i = shift @stack; + my $dep_list = $dbh->selectcol_arrayref($sth{$target}, undef, $i); + foreach my $t (@$dep_list) { + + # ignore any _current_ dependencies involving this bug, + # as they will be overwritten with data from the form. + if ($t != $id && !exists $seen{$t}) { + push(@{$deptree{$target}}, $t); + push @stack, $t; + $seen{$t} = 1; } + } } + } - my @deps = @{$deptree{'dependson'}}; - my @blocks = @{$deptree{'blocked'}}; - my %union = (); - my %isect = (); - foreach my $b (@deps, @blocks) { $union{$b}++ && $isect{$b}++ } - my @isect = keys %isect; - if (scalar(@isect) > 0) { - ThrowUserError("dependency_loop_multi", {'deps' => \@isect}); - } - return %deps; + my @deps = @{$deptree{'dependson'}}; + my @blocks = @{$deptree{'blocked'}}; + my %union = (); + my %isect = (); + foreach my $b (@deps, @blocks) { $union{$b}++ && $isect{$b}++ } + my @isect = keys %isect; + if (scalar(@isect) > 0) { + ThrowUserError("dependency_loop_multi", {'deps' => \@isect}); + } + return %deps; } @@ -4584,53 +5955,312 @@ sub ValidateDependencies { ##################################################################### sub _create_cf_accessors { - my ($invocant) = @_; - my $class = ref($invocant) || $invocant; - return if Bugzilla->request_cache->{"${class}_cf_accessors_created"}; - - my $fields = Bugzilla->fields({ custom => 1 }); - foreach my $field (@$fields) { - my $accessor = $class->_accessor_for($field); - my $name = "${class}::" . $field->name; - { - no strict 'refs'; - next if defined *{$name}; - *{$name} = $accessor; - } + my ($invocant) = @_; + my $class = ref($invocant) || $invocant; + return if Bugzilla->request_cache->{"${class}_cf_accessors_created"}; + + ## REDHAT EXTENSION START 1101335 + # We can't use the upstream code here, as we want to get all the custom + # fields, not just the ones that the user can see. + my $fields = Bugzilla::Field->match({custom => 1}); + ## REDHAT EXTENSION END 1101335 + + foreach my $field (@$fields) { + my $accessor = $class->_accessor_for($field); + my $name = "${class}::" . $field->name; + { + no strict 'refs'; + next if defined *{$name}; + *{$name} = $accessor; } + } - Bugzilla->request_cache->{"${class}_cf_accessors_created"} = 1; + Bugzilla->request_cache->{"${class}_cf_accessors_created"} = 1; } sub _accessor_for { - my ($class, $field) = @_; - if ($field->type == FIELD_TYPE_MULTI_SELECT) { - return $class->_multi_select_accessor($field->name); - } - return $class->_cf_accessor($field->name); + my ($class, $field) = @_; + if ($field->type == FIELD_TYPE_MULTI_SELECT) { + return $class->_multi_select_accessor($field->name); + } elsif ( $field->type == FIELD_TYPE_ONE_SELECT) + { + return $class->_single_select_accessor($field->name); + } + return $class->_cf_accessor($field->name); } sub _cf_accessor { - my ($class, $field) = @_; - my $accessor = sub { - my ($self) = @_; - return $self->{$field}; - }; - return $accessor; + my ($class, $field) = @_; + my $accessor = sub { + my ($self) = @_; + return $self->{$field}; + }; + return $accessor; } sub _multi_select_accessor { - my ($class, $field) = @_; - my $accessor = sub { - my ($self) = @_; - $self->{$field} ||= Bugzilla->dbh->selectcol_arrayref( - "SELECT value FROM bug_$field WHERE bug_id = ? ORDER BY value", - undef, $self->id); - return $self->{$field}; - }; - return $accessor; + my ($class, $field) = @_; + my $accessor = sub { + my ($self) = @_; + ## REDHAT EXTENSION START 1039815 1101335 + # Filter result based on what user can see. We can't do this in the + # outer block because modperl caches $accessor + if (not exists $self->{$field}) { + my $filter_list = Bugzilla->user->see_field_values($field); + my $extra_sql = ''; + if (defined $filter_list) { + if (scalar(@$filter_list)) { + $extra_sql = ' AND value IN ( ' + . join(', ', map { Bugzilla->dbh->quote($_->name) } @$filter_list) . ')'; + } + else { + # They cannot see anything + $self->{$field} = []; + return []; + } + } + $self->{$field} + = Bugzilla->dbh->selectcol_arrayref( + "SELECT value FROM bug_$field WHERE bug_id = ?$extra_sql ORDER BY value", + undef, $self->id); + } + ## REDHAT EXTENSION END 1039815 1101335 + return $self->{$field}; + }; + return $accessor; +} + +sub _single_select_accessor { + my ($class, $field) = @_; + my $accessor = sub { + my ($self) = @_; + ## REDHAT EXTENSION START 1039815 1101335 + # Filter result based on what user can see. We can't do this in the + # outer block because modperl caches $accessor + if (not exists $self->{$field}) { + my $filter_list = Bugzilla->user->see_field_values($field); + my $extra_sql = ''; + if (defined $filter_list) { + if (scalar(@$filter_list)) { + $extra_sql = ' AND value IN ( ' + . join(', ', map { Bugzilla->dbh->quote($_->name) } @$filter_list) . ')'; + } + else { + # They cannot see anything + $self->{$field} = []; + return []; + } + } + $self->{$field} + = Bugzilla->dbh->selectrow_array( + "SELECT value FROM bug_$field WHERE bug_id = ?$extra_sql ORDER BY value", + undef, $self->id); + } + ## REDHAT EXTENSION END 1039815 1101335 + return $self->{$field}; + }; + return $accessor; } +sub PreFetch { + my $bugs = shift; + my $params = shift; + my $dbh = Bugzilla->dbh; + + # If there are no bugs, return + return if not defined $bugs or scalar(@$bugs) == 0; + + # Shortcut for writting SQL statements, since it used so often + my $in_clause = "IN (" . join(',', map { $_->bug_id } @$bugs) . ')'; + + # Get the user id of the logged in user. + my $user_id = Bugzilla->user->id; + + # What fields do we need to prefetch (key is the name in the RPC, value + # is the key in the Bugzilla::Bug object + + my $fields = { + actual_time => { + value => 'actual_time', + empty => 0, + sql => qq{ + SELECT bug_id, SUM(work_time) + FROM longdescs + WHERE bug_id $in_clause + GROUP BY bug_id + } + }, + blocks => { + value => 'blocked', + sql => qq{ + SELECT dependson, blocked + FROM dependencies + INNER JOIN bugs ON dependencies.blocked = bugs.bug_id + INNER JOIN bug_status ON bugs.bug_status = bug_status.value + WHERE dependson $in_clause + ORDER BY is_open DESC, blocked + } + }, + cc => { + value => 'cc', + sql => qq{ + SELECT bug_id, profiles.login_name FROM cc, profiles + WHERE bug_id $in_clause + AND cc.who = profiles.userid + ORDER BY profiles.login_name + } + }, + classification => { + value => 'classification', + empty => '', + sql => qq{ + SELECT b.bug_id, c.name + FROM bugs b + JOIN products p ON b.product_id = p.id + JOIN classifications c ON p.classification_id = c.id + WHERE b.bug_id $in_clause + } + }, + component => { + value => 'component', + empty => '', + sql => qq{ + SELECT b.bug_id, c.name AS value + FROM bugs b JOIN components c ON b.component_id = c.id + WHERE b.bug_id $in_clause + }, + }, + depends_on => { + value => 'dependson', + sql => qq{ + SELECT blocked, dependson + FROM dependencies + INNER JOIN bugs ON dependencies.dependson = bugs.bug_id + INNER JOIN bug_status ON bugs.bug_status = bug_status.value + WHERE blocked $in_clause + ORDER BY is_open DESC, dependson + } + }, + groups => { + value => 'groups_in', + sql => qq{ + SELECT bug_id, group_id + FROM bug_group_map + WHERE bug_id $in_clause + } + }, + keywords => { + value => 'keyword_objects', + sql => qq{ + SELECT bug_id, keywordid + FROM keywords + WHERE bug_id $in_clause + } + }, + product => { + value => 'product', + empty => '', + sql => qq{ + SELECT b.bug_id, p.name + FROM bugs b JOIN products p ON b.product_id = p.id + WHERE b.bug_id $in_clause + } + }, + see_also => { + value => 'see_also', + sql => qq{ + SELECT bug_id, id + FROM bug_see_also + WHERE bug_id $in_clause + } + }, + tag => { + value => 'tags', + sql => qq{ + SELECT bug_tag.bug_id, tag.name + FROM tag + JOIN bug_tag ON (bug_tag.tag_id = tag.id) + WHERE bug_tag.bug_id $in_clause + AND tag.user_id = $user_id + } + }, + }; + + # Add the custom fields + my $custom_fields = Bugzilla->fields( + {custom => 1, type => [FIELD_TYPE_MULTI_SELECT, FIELD_TYPE_ONE_SELECT]}); + + foreach my $custom_field (@$custom_fields) { + my $name = $custom_field->name; + $fields->{$name} = { + value => $name, + sql => qq{ + SELECT bug_id, value + FROM bug_$name + WHERE bug_id $in_clause + ORDER BY value + } + }; + } + + # Some extensions might want to change this + Bugzilla::Hook::process("bug_prefetch", + {fields => $fields, in_clause => $in_clause}); + + # We only want to prefetch the fields that we are going to return + # if the params value is specified + if (defined $params) { + $fields = filter($params, $fields); + } + + # Get the data. + my %data = (); + foreach my $field (keys %$fields) { + + # Set the default empty value + if (not defined $fields->{$field}{empty}) { + $fields->{$field}{empty} = []; + } + + # Get all the rows from the database for this field + my $sql = $fields->{$field}{sql}; + my $rows = $dbh->selectall_arrayref($sql); + foreach my $row (@$rows) { + push @{$data{$row->[0]}{$field}}, $row->[1]; + } + } + + # Now populate the data in each bug + my @keys = keys %$fields; + for my $bug (@$bugs) { + foreach my $key (@keys) { + my $value = $data{$bug->bug_id}{$key}; + + # If there is no results, we still want to record that + if (not defined $value) { + $value = $fields->{$key}{empty}; + } + + # Some times we need to convert the values into an object + elsif ($key eq 'groups') { + $value = Bugzilla::Group->new_from_list($value); + } + elsif ($key eq 'keywords') { + $value = Bugzilla::Keyword->new_from_list($value); + } + elsif ($key eq 'see_also') { + $value = Bugzilla::BugUrl->new_from_list($value); + } + + # These values are scalars, so we return the first value + elsif (ref($fields->{$key}{empty}) eq '') { + $value = $value->[0]; + } + + $bug->{$fields->{$key}{value}} = $value; + } + } +} 1; __END__ @@ -4657,6 +6287,10 @@ call L to make the changes permanent. Creates or updates a L for this bug and the supplied $user, the timestamp given as $last_visit. +=item C + +Checks if this bug has changes for which bug mail has not been sent. + =back =head1 B @@ -4923,4 +6557,28 @@ $user, the timestamp given as $last_visit. =item target_milestone +=item set_docs_contact + +=item clone_ids + +=item clones + +=item target_release + +=item resolutions_available + +=item set_target_release + +=item PreFetch + +=item docs_contact + +=item dependent_bugs + +=item set_needinfo + +=item reset_docs_contact + +=item CLEANUP + =back diff --git a/Bugzilla/BugMail.pm b/Bugzilla/BugMail.pm index 110a1ffaf..55a8253bb 100644 --- a/Bugzilla/BugMail.pm +++ b/Bugzilla/BugMail.pm @@ -21,22 +21,24 @@ use Bugzilla::Mailer; use Bugzilla::Hook; use Bugzilla::MIME; +use Clone qw(clone); use Date::Parse; use Date::Format; use Scalar::Util qw(blessed); -use List::MoreUtils qw(uniq); +use List::MoreUtils qw(uniq any); use Storable qw(dclone); - -use constant BIT_DIRECT => 1; -use constant BIT_WATCHING => 2; +use Time::HiRes qw(usleep); +use constant BIT_DIRECT => 1; +use constant BIT_WATCHING => 2; sub relationships { - my $ref = RELATIONSHIPS; - # Clone it so that we don't modify the constant; - my %relationships = %$ref; - Bugzilla::Hook::process('bugmail_relationships', - { relationships => \%relationships }); - return %relationships; + my $ref = RELATIONSHIPS; + + # Clone it so that we don't modify the constant; + my %relationships = %$ref; + Bugzilla::Hook::process('bugmail_relationships', + {relationships => \%relationships}); + return %relationships; } # args: bug_id, and an optional hash ref which may have keys for: @@ -46,547 +48,780 @@ sub relationships { # All the names are email addresses, not userids # values are scalars, except for cc, which is a list sub Send { - my ($id, $forced, $params) = @_; - $params ||= {}; - - my $dbh = Bugzilla->dbh; - my $bug = new Bugzilla::Bug($id); - - my $start = $bug->lastdiffed; - my $end = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); - - # Bugzilla::User objects of people in various roles. More than one person - # can 'have' a role, if the person in that role has changed, or people are - # watching. - my @assignees = ($bug->assigned_to); - my @qa_contacts = $bug->qa_contact || (); - - my @ccs = @{ $bug->cc_users }; - # Include the people passed in as being in particular roles. - # This can include people who used to hold those roles. - # At this point, we don't care if there are duplicates in these arrays. - my $changer = $forced->{'changer'}; - if ($forced->{'owner'}) { - push (@assignees, Bugzilla::User->check($forced->{'owner'})); + my ($id, $forced, $params) = @_; + $params ||= {}; + + my $dbh = Bugzilla->dbh; + ## RED HAT EXTENSION START 1601494 + # Sometimes this load happens before the new bug has synced to the slaves + my $bug; + my $count = 0; + while (!$bug && $count < 10) { + eval { $bug = new Bugzilla::Bug($id); }; + if ($@ || defined $bug->{error}) { + Bugzilla->logger->info("Failed to load bug $id count $count"); + usleep(350); + $bug = undef; } - - if ($forced->{'qacontact'}) { - push (@qa_contacts, Bugzilla::User->check($forced->{'qacontact'})); + $count++; + } + unless ($bug) { + ThrowUserError('bug_id_does_not_exist', {bug_id => $id}); + } + ## RED HAT EXTENSION END 1601494 + + my $start = $bug->lastdiffed; + my $end = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); + + # Bugzilla::User objects of people in various roles. More than one person + # can 'have' a role, if the person in that role has changed, or people are + # watching. + my @assignees = ($bug->assigned_to); + my @qa_contacts = $bug->qa_contact || (); + + my @ccs = @{$bug->cc_users}; + + # Include the people passed in as being in particular roles. + # This can include people who used to hold those roles. + # At this point, we don't care if there are duplicates in these arrays. + my $changer = $forced->{'changer'}; + if ($forced->{'owner'}) { + push(@assignees, Bugzilla::User->check($forced->{'owner'})); + } + + if ($forced->{'qacontact'}) { + push(@qa_contacts, Bugzilla::User->check($forced->{'qacontact'})); + } + + if ($forced->{'cc'}) { + foreach my $cc (@{$forced->{'cc'}}) { + push(@ccs, Bugzilla::User->check($cc)); } - - if ($forced->{'cc'}) { - foreach my $cc (@{$forced->{'cc'}}) { - push(@ccs, Bugzilla::User->check($cc)); - } - } - my %user_cache = map { $_->id => $_ } (@assignees, @qa_contacts, @ccs); + } + my %user_cache = map { $_->id => $_ } (@assignees, @qa_contacts, @ccs); + + ## REDHAT EXTENSION START 876015 + my @docs_contacts = $bug->docs_contact || (); + if ($forced->{'docscontact'}) { + push(@docs_contacts, Bugzilla::User->check($forced->{'docscontact'})); + } + foreach my $contact (@docs_contacts) { + $user_cache{$contact->id} = $contact; + } + ## REDHAT EXTENSION END 876015 + + my @diffs; + if (!$start) { + @diffs = _get_new_bugmail_fields($bug); + } + + my $comments = []; + + if ($params->{dep_only}) { + push( + @diffs, + { + field_name => 'bug_status', + old => $params->{changes}->{bug_status}->[0], + new => $params->{changes}->{bug_status}->[1], + login_name => $changer->login, + who => $changer, + blocker => $params->{blocker} + }, + { + field_name => 'resolution', + old => $params->{changes}->{resolution}->[0], + new => $params->{changes}->{resolution}->[1], + login_name => $changer->login, + who => $changer, + blocker => $params->{blocker} + } + ); + } + else { + push(@diffs, _get_diffs($bug, $end, \%user_cache)); - my @diffs; - if (!$start) { - @diffs = _get_new_bugmail_fields($bug); - } + $comments = $bug->comments({after => $start, to => $end}); - my $comments = []; - - if ($params->{dep_only}) { - push(@diffs, { field_name => 'bug_status', - old => $params->{changes}->{bug_status}->[0], - new => $params->{changes}->{bug_status}->[1], - login_name => $changer->login, - who => $changer, - blocker => $params->{blocker} }, - { field_name => 'resolution', - old => $params->{changes}->{resolution}->[0], - new => $params->{changes}->{resolution}->[1], - login_name => $changer->login, - who => $changer, - blocker => $params->{blocker} }); - } - else { - push(@diffs, _get_diffs($bug, $end, \%user_cache)); + # Skip empty comments. + @$comments = grep { $_->type || $_->body =~ /\S/ } @$comments; - $comments = $bug->comments({ after => $start, to => $end }); - # Skip empty comments. - @$comments = grep { $_->type || $_->body =~ /\S/ } @$comments; + # If no changes have been made, there is no need to process further. + return {'sent' => []} unless scalar(@diffs) || scalar(@$comments); + } - # If no changes have been made, there is no need to process further. - return {'sent' => []} unless scalar(@diffs) || scalar(@$comments); - } + ########################################################################### + # Start of email filtering code + ########################################################################### - ########################################################################### - # Start of email filtering code - ########################################################################### - - # A user_id => roles hash to keep track of people. - my %recipients; - my %watching; - - # We also record bugs that are referenced - my @referenced_bug_ids = (); - - # Now we work out all the people involved with this bug, and note all of - # the relationships in a hash. The keys are userids, the values are an - # array of role constants. - - # CCs - $recipients{$_->id}->{+REL_CC} = BIT_DIRECT foreach (@ccs); - - # Reporter (there's only ever one) - $recipients{$bug->reporter->id}->{+REL_REPORTER} = BIT_DIRECT; - - # QA Contact - if (Bugzilla->params->{'useqacontact'}) { - foreach (@qa_contacts) { - # QA Contact can be blank; ignore it if so. - $recipients{$_->id}->{+REL_QA} = BIT_DIRECT if $_; - } - } + # A user_id => roles hash to keep track of people. + my %recipients; + my %watching; - # Assignee - $recipients{$_->id}->{+REL_ASSIGNEE} = BIT_DIRECT foreach (@assignees); - - # The last relevant set of people are those who are being removed from - # their roles in this change. We get their names out of the diffs. - foreach my $change (@diffs) { - if ($change->{old}) { - # You can't stop being the reporter, so we don't check that - # relationship here. - # Ignore people whose user account has been deleted or renamed. - if ($change->{field_name} eq 'cc') { - foreach my $cc_user (split(/[\s,]+/, $change->{old})) { - my $uid = login_to_id($cc_user); - $recipients{$uid}->{+REL_CC} = BIT_DIRECT if $uid; - } - } - elsif ($change->{field_name} eq 'qa_contact') { - my $uid = login_to_id($change->{old}); - $recipients{$uid}->{+REL_QA} = BIT_DIRECT if $uid; - } - elsif ($change->{field_name} eq 'assigned_to') { - my $uid = login_to_id($change->{old}); - $recipients{$uid}->{+REL_ASSIGNEE} = BIT_DIRECT if $uid; - } - } + # We also record bugs that are referenced + my @referenced_bug_ids = (); - if ($change->{field_name} eq 'dependson' || $change->{field_name} eq 'blocked') { - push @referenced_bug_ids, split(/[\s,]+/, $change->{old} // ''); - push @referenced_bug_ids, split(/[\s,]+/, $change->{new} // ''); - } - } + # Now we work out all the people involved with this bug, and note all of + # the relationships in a hash. The keys are userids, the values are an + # array of role constants. - my $referenced_bugs = scalar(@referenced_bug_ids) - ? Bugzilla::Bug->new_from_list([uniq @referenced_bug_ids]) - : []; + # CCs + $recipients{$_->id}->{+REL_CC} = BIT_DIRECT foreach (@ccs); - # Make sure %user_cache has every user in it so far referenced - foreach my $user_id (keys %recipients) { - $user_cache{$user_id} ||= new Bugzilla::User($user_id); - } - - Bugzilla::Hook::process('bugmail_recipients', - { bug => $bug, recipients => \%recipients, - users => \%user_cache, diffs => \@diffs }); - - # We should not assume %recipients to have any entries. - 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)"); - - # 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]); - } - } + # Reporter (there's only ever one) + $recipients{$bug->reporter->id}->{+REL_REPORTER} = BIT_DIRECT; + + # QA Contact + if (Bugzilla->params->{'useqacontact'}) { + foreach (@qa_contacts) { - # Global watcher - my @watchers = split(/[,\s]+/, Bugzilla->params->{'globalwatchers'}); - foreach (@watchers) { - my $watcher_id = login_to_id($_); - next unless $watcher_id; - $recipients{$watcher_id}->{+REL_GLOBAL_WATCHER} = BIT_DIRECT; + # QA Contact can be blank; ignore it if so. + $recipients{$_->id}->{+REL_QA} = BIT_DIRECT if $_; } + } - # We now have a complete set of all the users, and their relationships to - # the bug in question. However, we are not necessarily going to mail them - # all - there are preferences, permissions checks and all sorts to do yet. - my @sent; - - # The email client will display the Date: header in the desired timezone, - # so we can always use UTC here. - my $date = $params->{dep_only} ? $end : $bug->delta_ts; - $date = format_time($date, '%a, %d %b %Y %T %z', 'UTC'); - - foreach my $user_id (keys %recipients) { - my %rels_which_want; - my $user = $user_cache{$user_id} ||= new Bugzilla::User($user_id); - # Deleted users must be excluded. - next unless $user; - - # If email notifications are disabled for this account, or the bug - # is ignored, there is no need to do additional checks. - next if ($user->email_disabled || $user->is_bug_ignored($id)); - - if ($user->can_see_bug($id)) { - # Go through each role the user has and see if they want mail in - # that role. - foreach my $relationship (keys %{$recipients{$user_id}}) { - if ($user->wants_bug_mail($bug, - $relationship, - $start ? \@diffs : [], - $comments, - $params->{dep_only}, - $changer)) - { - $rels_which_want{$relationship} = - $recipients{$user_id}->{$relationship}; - } - } - } + ## REDHAT EXTENSION START 876015 + # Docs Contact + if (Bugzilla->params->{'usedocscontact'}) { + foreach (@docs_contacts) { - if (scalar(%rels_which_want)) { - # So the user exists, can see the bug, and wants mail in at least - # one role. But do we want to send it to them? - - # We shouldn't send mail if this is a dependency mail and the - # depending bug is not visible to the user. - # This is to avoid leaking the summary of a confidential bug. - my $dep_ok = 1; - if ($params->{dep_only}) { - $dep_ok = $user->can_see_bug($params->{blocker}->id) ? 1 : 0; - } - - # Email the user if the dep check passed. - if ($dep_ok) { - my $sent_mail = sendMail( - { to => $user, - bug => $bug, - comments => $comments, - date => $date, - changer => $changer, - watchers => exists $watching{$user_id} ? - $watching{$user_id} : undef, - diffs => \@diffs, - rels_which_want => \%rels_which_want, - dep_only => $params->{dep_only}, - referenced_bugs => $referenced_bugs, - }); - push(@sent, $user->login) if $sent_mail; - } + # Docs Contact can be blank; ignore it if so. + $recipients{$_->id}->{+REL_DOCS} = BIT_DIRECT if $_; + } + } + ## REDHAT EXTENSION END 876015 + + # Assignee + $recipients{$_->id}->{+REL_ASSIGNEE} = BIT_DIRECT foreach (@assignees); + + # The last relevant set of people are those who are being removed from + # their roles in this change. We get their names out of the diffs. + foreach my $change (@diffs) { + if ($change->{old}) { + + # You can't stop being the reporter, so we don't check that + # relationship here. + # Ignore people whose user account has been deleted or renamed. + if ($change->{field_name} eq 'cc') { + foreach my $cc_user (split(/[\s,]+/, $change->{old})) { + my $uid = login_to_id($cc_user); + $recipients{$uid}->{+REL_CC} = BIT_DIRECT if $uid; } + } + elsif ($change->{field_name} eq 'qa_contact') { + my $uid = login_to_id($change->{old}); + $recipients{$uid}->{+REL_QA} = BIT_DIRECT if $uid; + } + ## REDHAT EXTENSION START 876015 + elsif ($change->{field_name} eq 'docs_contact') { + my $uid = login_to_id($change->{old}); + $recipients{$uid}->{+REL_DOCS} = BIT_DIRECT if $uid; + } + ## REDHAT EXTENSION END 876015 + elsif ($change->{field_name} eq 'assigned_to') { + my $uid = login_to_id($change->{old}); + $recipients{$uid}->{+REL_ASSIGNEE} = BIT_DIRECT if $uid; + } } - # When sending bugmail about a blocker being reopened or resolved, - # we say nothing about changes in the bug being blocked, so we must - # not update lastdiffed in this case. - if (!$params->{dep_only}) { - $dbh->do('UPDATE bugs SET lastdiffed = ? WHERE bug_id = ?', - undef, ($end, $id)); - $bug->{lastdiffed} = $end; + if ($change->{field_name} eq 'dependson' || $change->{field_name} eq 'blocked') + { + push @referenced_bug_ids, split(/[\s,]+/, $change->{old} // ''); + push @referenced_bug_ids, split(/[\s,]+/, $change->{new} // ''); } + } + + my $referenced_bugs + = scalar(@referenced_bug_ids) + ? Bugzilla::Bug->new_from_list([uniq @referenced_bug_ids]) + : []; + + # Make sure %user_cache has every user in it so far referenced + foreach my $user_id (keys %recipients) { + $user_cache{$user_id} ||= new Bugzilla::User($user_id); + } + + Bugzilla::Hook::process( + 'bugmail_recipients', + { + bug => $bug, + recipients => \%recipients, + users => \%user_cache, + diffs => \@diffs + } + ); - return {'sent' => \@sent}; -} + # We should not assume %recipients to have any entries. + if (scalar keys %recipients) { -sub sendMail { - my $params = shift; - - my $user = $params->{to}; - my $bug = $params->{bug}; - my @send_comments = @{ $params->{comments} }; - my $date = $params->{date}; - my $changer = $params->{changer}; - my $watchingRef = $params->{watchers}; - my @diffs = @{ $params->{diffs} }; - my $relRef = $params->{rels_which_want}; - my $dep_only = $params->{dep_only}; - my $referenced_bugs = $params->{referenced_bugs}; - - # Only display changes the user is allowed see. - my @display_diffs; - - foreach my $diff (@diffs) { - my $add_diff = 0; - - if (grep { $_ eq $diff->{field_name} } TIMETRACKING_FIELDS) { - $add_diff = 1 if $user->is_timetracker; - } - elsif (!$diff->{isprivate} || $user->is_insider) { - $add_diff = 1; - } - push(@display_diffs, $diff) if $add_diff; - } + # Find all those user-watching anyone on the current list, who is not + # on it already themselves. + my $involved = join(",", keys %recipients); - if (!$user->is_insider) { - @send_comments = grep { !$_->is_private } @send_comments; - } + my $userwatchers = $dbh->selectall_arrayref( + "SELECT watcher, watched FROM watch + WHERE watched IN ($involved)" + ); - if (!scalar(@display_diffs) && !scalar(@send_comments)) { - # Whoops, no differences! - return 0; + # 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]}}) { + ## no critic (ProhibitBitwiseOperators) + # Actually real bit operations + $recipients{$watch->[0]}->{$role} |= BIT_WATCHING if $bits & BIT_DIRECT; + ## use critic + } + push(@{$watching{$watch->[0]}}, $watch->[1]); + } + } + + # Global watcher + my @watchers = split(/[,\s]+/, Bugzilla->params->{'globalwatchers'}); + foreach (@watchers) { + my $watcher_id = login_to_id($_); + next unless $watcher_id; + $recipients{$watcher_id}->{+REL_GLOBAL_WATCHER} = BIT_DIRECT; + } + + # We now have a complete set of all the users, and their relationships to + # the bug in question. However, we are not necessarily going to mail them + # all - there are preferences, permissions checks and all sorts to do yet. + my @sent; + + # The email client will display the Date: header in the desired timezone, + # so we can always use UTC here. + my $date = $params->{dep_only} ? $end : $bug->delta_ts; + $date = format_time($date, '%a, %d %b %Y %T %z', 'UTC'); + + my $minor_update + = $changer ? ($changer->can_minor_update() && $params->{minor_update}) : 0; + + foreach my $user_id (keys %recipients) { + my %rels_which_want; + my $user = $user_cache{$user_id} ||= new Bugzilla::User($user_id); + + # Deleted users must be excluded. + next unless $user; + + # If email notifications are disabled for this account, or the bug + # is ignored, there is no need to do additional checks. + next if ($user->email_disabled || $user->is_bug_ignored($id)); + + if ($user->can_see_bug($id)) { + + # Go through each role the user has and see if they want mail in + # that role. + foreach my $relationship (keys %{$recipients{$user_id}}) { + if ($user->wants_bug_mail( + $bug, $relationship, $start ? \@diffs : [], $comments, + $params->{dep_only}, $changer, $minor_update + )) + { + $rels_which_want{$relationship} = $recipients{$user_id}->{$relationship}; + } + } } - my (@reasons, @reasons_watch); - while (my ($relationship, $bits) = each %{$relRef}) { - push(@reasons, $relationship) if ($bits & BIT_DIRECT); - push(@reasons_watch, $relationship) if ($bits & BIT_WATCHING); + if (scalar(%rels_which_want)) { + + # So the user exists, can see the bug, and wants mail in at least + # one role. But do we want to send it to them? + + # We shouldn't send mail if this is a dependency mail and the + # depending bug is not visible to the user. + # This is to avoid leaking the summary of a confidential bug. + my $dep_ok = 1; + if ($params->{dep_only}) { + $dep_ok = $user->can_see_bug($params->{blocker}->id) ? 1 : 0; + } + + # Email the user if the dep check passed. + if ($dep_ok) { + my $sent_mail = sendMail({ + to => $user, + bug => $bug, + comments => $comments, + date => $date, + changer => $changer, + watchers => exists $watching{$user_id} ? $watching{$user_id} : undef, + diffs => \@diffs, + rels_which_want => \%rels_which_want, + dep_only => $params->{dep_only}, + referenced_bugs => $referenced_bugs, + edited_comment => $params->{edited_comment}, + }); + push(@sent, $user->login) if $sent_mail; + } } + } + + # When sending bugmail about a blocker being reopened or resolved, + # we say nothing about changes in the bug being blocked, so we must + # not update lastdiffed in this case. + if (!$params->{dep_only}) { + + ## REDHAT EXTENSION START 406111 619547 + Bugzilla::Hook::process( + 'bug_send_changes', + { + bug => $id, + diffs => \@diffs, + start => $start, + end => $end, + sent => \@sent, + comments => $comments, + changer => $changer + } + ); + ## REDHAT EXTENSION END 406111 619547 - my %relationships = relationships(); - my @headerrel = map { $relationships{$_} } @reasons; - my @watchingrel = map { $relationships{$_} } @reasons_watch; - push(@headerrel, 'None') unless @headerrel; - push(@watchingrel, 'None') unless @watchingrel; - push @watchingrel, map { Bugzilla::User->new($_)->login } @$watchingRef; + $dbh->do('UPDATE bugs SET lastdiffed = ? WHERE bug_id = ?', undef, ($end, $id)); + $bug->{lastdiffed} = $end; + } - my @changedfields = uniq map { $_->{field_name} } @display_diffs; + return {'sent' => \@sent}; +} - # 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, 'attachments.created'); +sub sendMail { + my $params = shift; + + my $user = $params->{to}; + my $bug = $params->{bug}; + my @send_comments = @{$params->{comments}}; + my $date = $params->{date}; + my $changer = $params->{changer}; + my $watchingRef = $params->{watchers}; + ## REDHAT EXTENSION BEGIN + # We make this a clone, as we may filter @diffs + my @diffs = @{clone($params->{diffs})}; + ## REDHAT EXTENSION END + my $relRef = $params->{rels_which_want}; + my $referenced_bugs = $params->{referenced_bugs}; + + # Only display changes the user is allowed see. + my @display_diffs; + + foreach my $diff (@diffs) { + my $add_diff = 0; + + if (any { $_ eq $diff->{field_name} } TIMETRACKING_FIELDS) { + $add_diff = 1 if $user->is_timetracker; } - - my $bugmailtype = "changed"; - $bugmailtype = "new" if !$bug->lastdiffed; - $bugmailtype = "dep_changed" if $dep_only; - - my $vars = { - date => $date, - to_user => $user, - bug => $bug, - reasons => \@reasons, - reasons_watch => \@reasons_watch, - reasonsheader => join(" ", @headerrel), - reasonswatchheader => join(" ", @watchingrel), - changer => $changer, - diffs => \@display_diffs, - changedfields => \@changedfields, - referenced_bugs => $user->visible_bugs($referenced_bugs), - new_comments => \@send_comments, - threadingmarker => build_thread_marker($bug->id, $user->id, !$bug->lastdiffed), - bugmailtype => $bugmailtype, - }; - if (Bugzilla->params->{'use_mailer_queue'}) { - enqueue($vars); - } else { - MessageToMTA(_generate_bugmail($vars)); + ## REDHAT EXTENSION START 823399 + elsif (exists($diff->{'field_name'}) + && ($diff->{'field_name'} eq 'blocked' or $diff->{'field_name'} eq 'dependson')) + { + $diff->{old} = _get_diff_alias($diff->{old}, $user); + $diff->{new} = _get_diff_alias($diff->{new}, $user); + $add_diff = 1; + } + ## REDHAT EXTENSION END 823399 + elsif (!$diff->{isprivate} || $user->is_insider) { + $add_diff = 1; } - return 1; + Bugzilla::Hook::process( + 'bug_filter_change', + { + user => $user, + field => $diff->{field_name}, + added => $diff->{new}, + removed => $diff->{old}, + object_id => $diff->{object_id}, + visible => \$add_diff + } + ); + ## REDHAT EXTENSIONS END 406111 + + push(@display_diffs, $diff) if $add_diff; + } + + ## REDHAT EXTENSION BEGIN 823262 + # Add a bugzilla comment header + my $comment_change = 'none'; + if ($user->is_insider and any { $_->is_private } @send_comments) { + $comment_change = 'private'; + } + elsif (any { !$_->is_private } @send_comments) { + $comment_change = 'public'; + } + ## REDHAT EXTENSION END 823262 + + if (!$user->is_insider) { + @send_comments = grep { !$_->is_private } @send_comments; + } + + if (!scalar(@display_diffs) && !scalar(@send_comments)) { + + # Whoops, no differences! + return 0; + } + + my (@reasons, @reasons_watch); + while (my ($relationship, $bits) = each %{$relRef}) { + ## no critic (ProhibitBitwiseOperators) + # These are actually bit operations + push(@reasons, $relationship) if ($bits & BIT_DIRECT); + push(@reasons_watch, $relationship) if ($bits & BIT_WATCHING); + ## use critic + } + + my %relationships = relationships(); + my @headerrel = map { $relationships{$_} } @reasons; + my @watchingrel = map { $relationships{$_} } @reasons_watch; + push(@headerrel, 'None') unless @headerrel; + push(@watchingrel, 'None') unless @watchingrel; + push @watchingrel, map { Bugzilla::User->new($_)->login } @$watchingRef; + + my @changedfields = uniq map { $_->{field_name} } @display_diffs; + + # Add attachments.created to changedfields if one or more + # comments contain information about a new attachment + if (any { $_->type == CMT_ATTACHMENT_CREATED } @send_comments) { + push(@changedfields, 'attachments.created'); + } + + my $bugmailtype = "changed"; + $bugmailtype = "new" if !$bug->lastdiffed; +## BUGBUG $bugmailtype = "dep_changed" if $dep_only; + + my $vars = { + date => $date, + to_user => $user, + bug => $bug, + reasons => \@reasons, + reasons_watch => \@reasons_watch, + reasonsheader => join(" ", @headerrel), + reasonswatchheader => join(" ", @watchingrel), + changer => $changer, + diffs => \@display_diffs, + changedfields => \@changedfields, + referenced_bugs => $user->visible_bugs($referenced_bugs), + new_comments => \@send_comments, + threadingmarker => build_thread_marker($bug->id, $user->id, !$bug->lastdiffed), + bugmailtype => $bugmailtype, + ## REDHAT EXTENSION BEGIN 823262 + comment_change => $comment_change, + ## REDHAT EXTENSION END 823262 + edited_comment => $params->{edited_comment}, + }; + + ## REDHAT EXTENSION BEGIN 823548 + # Get the flags that the user can see. It's easier to do it here + # than in the template. + $vars->{heading_flags} = join(', ', + map { $_->name . $_->status } + grep { $user->can_see_flag($_->type) } @{$bug->flags}); + ## REDHAT EXTENSION END 823548 + + if (Bugzilla->params->{'use_mailer_queue'}) { + enqueue($vars); + } + else { + MessageToMTA(_generate_bugmail($vars)); + } + + return 1; } sub enqueue { - my ($vars) = @_; - # we need to flatten all objects to a hash before pushing to the job queue. - # the hashes need to be inflated in the dequeue method. - $vars->{bug} = _flatten_object($vars->{bug}); - $vars->{to_user} = _flatten_object($vars->{to_user}); - $vars->{changer} = _flatten_object($vars->{changer}); - $vars->{new_comments} = [ map { _flatten_object($_) } @{ $vars->{new_comments} } ]; - foreach my $diff (@{ $vars->{diffs} }) { - $diff->{who} = _flatten_object($diff->{who}); - if (exists $diff->{blocker}) { - $diff->{blocker} = _flatten_object($diff->{blocker}); - } + my ($vars) = @_; + + # we need to flatten all objects to a hash before pushing to the job queue. + # the hashes need to be inflated in the dequeue method. + $vars->{bug} = _flatten_object($vars->{bug}); + $vars->{to_user} = _flatten_object($vars->{to_user}); + $vars->{changer} = _flatten_object($vars->{changer}); + $vars->{new_comments} = [map { _flatten_object($_) } @{$vars->{new_comments}}]; + foreach my $diff (@{$vars->{diffs}}) { + $diff->{who} = _flatten_object($diff->{who}); + if (exists $diff->{blocker}) { + $diff->{blocker} = _flatten_object($diff->{blocker}); } - Bugzilla->job_queue->insert('bug_mail', { vars => $vars }); + } + $vars->{edited_comment} = _flatten_object($vars->{edited_comment}); + Bugzilla->job_queue->insert('bug_mail', {vars => $vars}); + return; } sub dequeue { - my ($payload) = @_; - # clone the payload so we can modify it without impacting TheSchwartz's - # ability to process the job when we've finished - my $vars = dclone($payload); - # inflate objects - $vars->{bug} = Bugzilla::Bug->new_from_hash($vars->{bug}); - $vars->{to_user} = Bugzilla::User->new_from_hash($vars->{to_user}); - $vars->{changer} = Bugzilla::User->new_from_hash($vars->{changer}); - $vars->{new_comments} = [ map { Bugzilla::Comment->new_from_hash($_) } @{ $vars->{new_comments} } ]; - foreach my $diff (@{ $vars->{diffs} }) { - $diff->{who} = Bugzilla::User->new_from_hash($diff->{who}); - if (exists $diff->{blocker}) { - $diff->{blocker} = Bugzilla::Bug->new_from_hash($diff->{blocker}); - } + my ($payload) = @_; + + # clone the payload so we can modify it without impacting TheSchwartz's + # ability to process the job when we've finished + my $vars = dclone($payload); + + # inflate objects + + ## REDHAT EXTENSION BEGIN 1585602 + # We need to be the recipient to see restricted fields + $vars->{to_user} = Bugzilla::User->new_from_hash($vars->{to_user}); + Bugzilla->set_user($vars->{to_user}); + ## REDHAT EXTENSION END 1585602 + + $vars->{bug} = Bugzilla::Bug->new_from_hash($vars->{bug}); + $vars->{changer} = Bugzilla::User->new_from_hash($vars->{changer}); + $vars->{new_comments} + = [map { Bugzilla::Comment->new_from_hash($_) } @{$vars->{new_comments}}]; + foreach my $diff (@{$vars->{diffs}}) { + $diff->{who} = Bugzilla::User->new_from_hash($diff->{who}); + if (exists $diff->{blocker}) { + $diff->{blocker} = Bugzilla::Bug->new_from_hash($diff->{blocker}); } - # generate bugmail and send - MessageToMTA(_generate_bugmail($vars), 1); + } + $vars->{edited_comment} + = Bugzilla::Comment->new_from_hash($vars->{edited_comment}); + + # generate bugmail and send + MessageToMTA(_generate_bugmail($vars), 1); + + return; } sub _flatten_object { - my ($object) = @_; - # nothing to do if it's already flattened - return $object unless blessed($object); - # the same objects are used for each recipient, so cache the flattened hash - my $cache = Bugzilla->request_cache->{bugmail_flat_objects} ||= {}; - my $key = blessed($object) . '-' . $object->id; - return $cache->{$key} ||= $object->flatten_to_hash; + my ($object) = @_; + + # nothing to do if it's already flattened + return $object unless blessed($object); + + # the same objects are used for each recipient, so cache the flattened hash + my $cache = Bugzilla->request_cache->{bugmail_flat_objects} ||= {}; + my $key = blessed($object) . '-' . $object->id; + return $cache->{$key} ||= $object->flatten_to_hash; } sub _generate_bugmail { - my ($vars) = @_; - my $user = $vars->{to_user}; - my $template = Bugzilla->template_inner($user->setting('lang')); - my ($msg_text, $msg_html, $msg_header); - state $use_utf8 = Bugzilla->params->{'utf8'}; - - $template->process("email/bugmail-header.txt.tmpl", $vars, \$msg_header) - || ThrowTemplateError($template->error()); - $template->process("email/bugmail.txt.tmpl", $vars, \$msg_text) - || ThrowTemplateError($template->error()); - - my @parts = ( - Bugzilla::MIME->create( - attributes => { - content_type => 'text/plain', - charset => $use_utf8 ? 'UTF-8' : 'iso-8859-1', - encoding => 'quoted-printable', - }, - body_str => $msg_text, - ) - ); - if ($user->setting('email_format') eq 'html') { - $template->process("email/bugmail.html.tmpl", $vars, \$msg_html) - || ThrowTemplateError($template->error()); - push @parts, Bugzilla::MIME->create( - attributes => { - content_type => 'text/html', - charset => $use_utf8 ? 'UTF-8' : 'iso-8859-1', - encoding => 'quoted-printable', - }, - body_str => $msg_html, - ); - } - - my $email = Bugzilla::MIME->new($msg_header); - if (scalar(@parts) == 1) { - $email->content_type_set($parts[0]->content_type); - } else { - $email->content_type_set('multipart/alternative'); - # Some mail clients need same encoding for each part, even empty ones. - $email->charset_set('UTF-8') if $use_utf8; - } - $email->parts_set(\@parts); - return $email; + my ($vars) = @_; + my $user = $vars->{to_user}; + my $template = Bugzilla->template_inner($user->setting('lang')); + my ($msg_text, $msg_html, $msg_header); + state $use_utf8 = Bugzilla->params->{'utf8'}; + + ## REDHAT EXTENSION 1335690 BEGIN + Bugzilla::Hook::process('email_before_template', {vars => $vars}); + ## REDHAT EXTENSION 1335690 END + + $template->process("email/bugmail-header.txt.tmpl", $vars, \$msg_header) + || ThrowTemplateError($template->error()); + $template->process("email/bugmail.txt.tmpl", $vars, \$msg_text) + || ThrowTemplateError($template->error()); + + my @parts = (Bugzilla::MIME->create( + attributes => { + content_type => 'text/plain', + charset => $use_utf8 ? 'UTF-8' : 'iso-8859-1', + encoding => 'quoted-printable', + }, + body_str => $msg_text, + )); + if ($user->setting('email_format') eq 'html') { + $template->process("email/bugmail.html.tmpl", $vars, \$msg_html) + || ThrowTemplateError($template->error()); + push @parts, + Bugzilla::MIME->create( + attributes => { + content_type => 'text/html', + charset => $use_utf8 ? 'UTF-8' : 'iso-8859-1', + encoding => 'quoted-printable', + }, + body_str => $msg_html, + ); + } + + my $email = Bugzilla::MIME->new($msg_header); + if (scalar(@parts) == 1) { + $email->content_type_set($parts[0]->content_type); + } + else { + $email->content_type_set('multipart/alternative'); + + # Some mail clients need same encoding for each part, even empty ones. + $email->charset_set('UTF-8') if $use_utf8; + } + $email->parts_set(\@parts); + return $email; } sub _get_diffs { - my ($bug, $end, $user_cache) = @_; - my $dbh = Bugzilla->dbh; - - my @args = ($bug->id); - # If lastdiffed is NULL, then we don't limit the search on time. - my $when_restriction = ''; - if ($bug->lastdiffed) { - $when_restriction = ' AND bug_when > ? AND bug_when <= ?'; - push @args, ($bug->lastdiffed, $end); - } + my ($bug, $end, $user_cache) = @_; + my $dbh = Bugzilla->dbh; + + my @args = ($bug->id); - my $diffs = $dbh->selectall_arrayref( - "SELECT fielddefs.name AS field_name, + # If lastdiffed is NULL, then we don't limit the search on time. + my $when_restriction = ''; + if ($bug->lastdiffed) { + $when_restriction = ' AND bug_when > ? AND bug_when <= ?'; + push @args, ($bug->lastdiffed, $end); + } + + my $diffs = $dbh->selectall_arrayref( + "SELECT fielddefs.name AS field_name, 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 + bugs_activity.comment_id, bugs_activity.who, + bugs_activity.object_id FROM bugs_activity INNER JOIN fielddefs ON fielddefs.id = bugs_activity.fieldid WHERE bugs_activity.bug_id = ? $when_restriction - ORDER BY bugs_activity.bug_when, bugs_activity.id", - {Slice=>{}}, @args); - - foreach my $diff (@$diffs) { - $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 = ?', - undef, $diff->{attach_id}); - } - if ($diff->{field_name} eq 'longdescs.isprivate') { - my $comment = Bugzilla::Comment->new($diff->{comment_id}); - $diff->{num} = $comment->count; - $diff->{isprivate} = $diff->{new}; - } + ORDER BY bugs_activity.bug_when, bugs_activity.id", {Slice => {}}, @args + ); + + foreach my $diff (@$diffs) { + $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 = ?', + undef, $diff->{attach_id}); } - - my @changes = (); - foreach my $diff (@$diffs) { - # If this is the same field as the previous item, then concatenate - # the data into the same change. - if (scalar(@changes) - && $diff->{field_name} eq $changes[-1]->{field_name} - && $diff->{bug_when} eq $changes[-1]->{bug_when} - && $diff->{who} eq $changes[-1]->{who} - && ($diff->{attach_id} // 0) == ($changes[-1]->{attach_id} // 0) - && ($diff->{comment_id} // 0) == ($changes[-1]->{comment_id} // 0) - ) { - my $old_change = pop @changes; - $diff->{old} = join_activity_entries($diff->{field_name}, $old_change->{old}, $diff->{old}); - $diff->{new} = join_activity_entries($diff->{field_name}, $old_change->{new}, $diff->{new}); - } - push @changes, $diff; + if ($diff->{field_name} eq 'longdescs.isprivate') { + my $comment = Bugzilla::Comment->new($diff->{comment_id}); + $diff->{num} = $comment->count; + $diff->{isprivate} = $diff->{new}; + } + } + + my @changes = (); + foreach my $diff (@$diffs) { + + # If this is the same field as the previous item, then concatenate + # the data into the same change. + if ( scalar(@changes) + && $diff->{field_name} eq $changes[-1]->{field_name} + && $diff->{bug_when} eq $changes[-1]->{bug_when} + && $diff->{who} eq $changes[-1]->{who} + && ($diff->{attach_id} // 0) == ($changes[-1]->{attach_id} // 0) + && ($diff->{comment_id} // 0) == ($changes[-1]->{comment_id} // 0)) + { + my $old_change = pop @changes; + $diff->{old} = join_activity_entries($diff->{field_name}, $old_change->{old}, + $diff->{old}); + $diff->{new} = join_activity_entries($diff->{field_name}, $old_change->{new}, + $diff->{new}); } + push @changes, $diff; + } - return @changes; + return @changes; } sub _get_new_bugmail_fields { - my $bug = shift; - my @fields = @{ Bugzilla->fields({obsolete => 0, in_new_bugmail => 1}) }; - my @diffs; - my $params = Bugzilla->params; - - foreach my $field (@fields) { - my $name = $field->name; - my $value = $bug->$name; - - next if !$field->is_visible_on_bug($bug) - || ($name eq 'classification' && !$params->{'useclassification'}) - || ($name eq 'status_whiteboard' && !$params->{'usestatuswhiteboard'}) - || ($name eq 'qa_contact' && !$params->{'useqacontact'}) - || ($name eq 'target_milestone' && !$params->{'usetargetmilestone'}); - - if (ref $value eq 'ARRAY') { - $value = join(', ', @$value); - } - elsif (blessed($value) && $value->isa('Bugzilla::User')) { - $value = $value->login; - } - elsif (blessed($value) && $value->isa('Bugzilla::Object')) { - $value = $value->name; - } - elsif ($name eq 'estimated_time') { - # "0.00" (which is what we get from the DB) is true, - # so we explicitly do a numerical comparison with 0. - $value = 0 if $value == 0; - } - elsif ($name eq 'deadline') { - $value = time2str("%Y-%m-%d", str2time($value)) if $value; - } + my $bug = shift; + my @fields = @{Bugzilla->fields({obsolete => 0, in_new_bugmail => 1})}; + my @diffs; + my $params = Bugzilla->params; + + foreach my $field (@fields) { + my $name = $field->name; + my $value = undef; + + ## REDHAT EXTENSION START + if ($name eq 'external_bugzilla.url') { + + # This shouldn't happen (since in_new_bugmail is false), but just + # in case it is, we'll skip it. We cannot restrict the visibility + # based on the full URL. + next; + } + elsif ($name eq 'ext_bz_bug_map.ext_bz_bug_id') { - # If there isn't anything to show, don't include this header. - next unless $value; + # Getting the object id (below) will restrict the visibility. + $value = join(',', + map { $_->type->description . " " . $_->ext_bz_bug_id } @{$bug->external_bugs}); + } + else { + $value = $bug->$name; + } + ## REDHAT EXTENSION END + + next + if !$field->is_visible_on_bug($bug) + || ($name eq 'classification' && !$params->{'useclassification'}) + || ($name eq 'status_whiteboard' && !$params->{'usestatuswhiteboard'}) + || ($name eq 'qa_contact' && !$params->{'useqacontact'}) + || ($name eq 'target_milestone' && !$params->{'usetargetmilestone'}); + + if (ref $value eq 'ARRAY') { + $value = join(', ', @$value); + } + elsif (blessed($value) && $value->isa('Bugzilla::User')) { + $value = $value->login; + } + elsif (blessed($value) && $value->isa('Bugzilla::Object')) { + $value = $value->name; + } + elsif ($name eq 'estimated_time') { - push(@diffs, { - field_name => $name, - who => $bug->reporter, - new => $value}); + # "0.00" (which is what we get from the DB) is true, + # so we explicitly do a numerical comparison with 0. + $value = 0 if $value == 0; + } + elsif ($name eq 'deadline') { + $value = time2str("%Y-%m-%d", str2time($value)) if $value; } - return @diffs; + # If there isn't anything to show, don't include this header. + next unless $value; + + ## REDHAT EXTENSION START 908770 + # If the value is 'unspecified' don't include it in the e-mail + next if lc $value eq 'unspecified'; + ## REDHAT EXTENSION END 908770 + + ## REDHAT EXTENSION BEGIN 825563 847166 + my $object_id = Bugzilla::Bug::_get_object_id($name, $value); + push( + @diffs, + { + field_name => $name, + new => $value, + object_id => $object_id, + who => $bug->reporter + } + ); + ## REDHAT EXTENSION END 825563 847166 + } + + return @diffs; } +## REDHAT EXTENSION BEGIN 823399 +sub _get_diff_alias { + + # Adds the alias to each of the bugs in dependson/blocked + # providing the user can see the other bug + my $value = shift; + my $to_user = shift; + + return '' if not defined $value; + + my @bug_ids = split /[\s,]+/, $value; + my @return = (); + + foreach my $bug_id (@bug_ids) { + my $bug_value = $bug_id; + if ($to_user->can_see_bug($bug_id)) { + my $bug = new Bugzilla::Bug($bug_id); + my $aliases = $bug->alias; + if (scalar @$aliases) { + $bug_value = $bug_id . ' (' . join(',', @$aliases) . ')'; + } + } + push @return, $bug_value; + } + + return join ', ', @return; +} +## REDHAT EXTENSION END 823399 + 1; +__END__ + =head1 NAME BugMail - Routines to generate email notifications when a bug is created or @@ -619,3 +854,5 @@ the flattened hashes, generates the bugmail, and sends it. =item Send =back + +=cut diff --git a/Bugzilla/BugUrl.pm b/Bugzilla/BugUrl.pm index 1d75fe8f1..13cfafd58 100644 --- a/Bugzilla/BugUrl.pm +++ b/Bugzilla/BugUrl.pm @@ -28,48 +28,49 @@ use URI::QueryParam; use constant DB_TABLE => 'bug_see_also'; use constant NAME_FIELD => 'value'; use constant LIST_ORDER => 'id'; + # See Also is tracked in bugs_activity. use constant AUDIT_CREATES => 0; use constant AUDIT_UPDATES => 0; use constant AUDIT_REMOVES => 0; use constant DB_COLUMNS => qw( - id - bug_id - value - class + id + bug_id + value + class ); # This must be strings with the names of the validations, # instead of coderefs, because subclasses override these # validators with their own. use constant VALIDATORS => { - value => '_check_value', - bug_id => '_check_bug_id', - class => \&_check_class, + value => '_check_value', + bug_id => '_check_bug_id', + class => \&_check_class, }; # This is the order we go through all of subclasses and # pick the first one that should handle the url. New # subclasses should be added at the end of the list. use constant SUB_CLASSES => qw( - Bugzilla::BugUrl::Bugzilla::Local - Bugzilla::BugUrl::Bugzilla - Bugzilla::BugUrl::Launchpad - Bugzilla::BugUrl::Google - Bugzilla::BugUrl::Debian - Bugzilla::BugUrl::JIRA - Bugzilla::BugUrl::Trac - Bugzilla::BugUrl::MantisBT - Bugzilla::BugUrl::SourceForge - Bugzilla::BugUrl::GitHub + Bugzilla::BugUrl::Bugzilla::Local + Bugzilla::BugUrl::Bugzilla + Bugzilla::BugUrl::Launchpad + Bugzilla::BugUrl::Google + Bugzilla::BugUrl::Debian + Bugzilla::BugUrl::JIRA + Bugzilla::BugUrl::Trac + Bugzilla::BugUrl::MantisBT + Bugzilla::BugUrl::SourceForge + Bugzilla::BugUrl::GitHub ); ############################### #### Accessors ###### ############################### -sub class { return $_[0]->{class} } +sub class { return $_[0]->{class} } sub bug_id { return $_[0]->{bug_id} } ############################### @@ -77,134 +78,133 @@ sub bug_id { return $_[0]->{bug_id} } ############################### sub new { - my $class = shift; - my $param = shift; - - if (ref $param) { - my $bug_id = $param->{bug_id}; - my $name = $param->{name} || $param->{value}; - if (!defined $bug_id) { - ThrowCodeError('bad_arg', - { argument => 'bug_id', - function => "${class}::new" }); - } - if (!defined $name) { - ThrowCodeError('bad_arg', - { argument => 'name', - function => "${class}::new" }); - } - - my $condition = 'bug_id = ? AND value = ?'; - my @values = ($bug_id, $name); - $param = { condition => $condition, values => \@values }; + my $class = shift; + my $param = shift; + + if (ref $param) { + my $bug_id = $param->{bug_id}; + my $name = $param->{name} || $param->{value}; + if (!defined $bug_id) { + ThrowCodeError('bad_arg', {argument => 'bug_id', function => "${class}::new"}); + } + if (!defined $name) { + ThrowCodeError('bad_arg', {argument => 'name', function => "${class}::new"}); } - unshift @_, $param; - return $class->SUPER::new(@_); + my $condition = 'bug_id = ? AND value = ?'; + my @values = ($bug_id, $name); + $param = {condition => $condition, values => \@values}; + } + + unshift @_, $param; + return $class->SUPER::new(@_); } sub _do_list_select { - my $class = shift; - my $objects = $class->SUPER::_do_list_select(@_); + my $class = shift; + my $objects = $class->SUPER::_do_list_select(@_); - foreach my $object (@$objects) { - eval "use " . $object->class; - # If the class cannot be loaded, then we build a generic object. - bless $object, ($@ ? 'Bugzilla::BugUrl' : $object->class); - } + foreach my $object (@$objects) { + eval "use " . $object->class; - return $objects + # If the class cannot be loaded, then we build a generic object. + bless $object, ($@ ? 'Bugzilla::BugUrl' : $object->class); + } + + return $objects; } # This is an abstract method. It must be overridden # in every subclass. sub should_handle { - my ($class, $input) = @_; - ThrowCodeError('unknown_method', - { method => "${class}::should_handle" }); + my ($class, $input) = @_; + ThrowCodeError('unknown_method', {method => "${class}::should_handle"}); } sub class_for { - my ($class, $value) = @_; - - my @sub_classes = $class->SUB_CLASSES; - Bugzilla::Hook::process("bug_url_sub_classes", - { sub_classes => \@sub_classes }); - - my $uri = URI->new($value); - foreach my $subclass (@sub_classes) { - eval "use $subclass"; - die $@ if $@; - return wantarray ? ($subclass, $uri) : $subclass - if $subclass->should_handle($uri); - } + my ($class, $value) = @_; + + my @sub_classes = $class->SUB_CLASSES; + Bugzilla::Hook::process("bug_url_sub_classes", {sub_classes => \@sub_classes}); - ThrowUserError('bug_url_invalid', { url => $value }); + my $uri = URI->new($value); + foreach my $subclass (@sub_classes) { + eval "use $subclass"; + die $@ if $@; + return wantarray ? ($subclass, $uri) : $subclass + if $subclass->should_handle($uri); + } + + ThrowUserError('bug_url_invalid', {url => $value}); } sub _check_class { - my ($class, $subclass) = @_; - eval "use $subclass"; die $@ if $@; - return $subclass; + my ($class, $subclass) = @_; + eval "use $subclass"; + die $@ if $@; + return $subclass; } sub _check_bug_id { - my ($class, $bug_id) = @_; + my ($class, $bug_id) = @_; - my $bug; - if (blessed $bug_id) { - # We got a bug object passed in, use it - $bug = $bug_id; - $bug->check_is_visible; - } - else { - # We got a bug id passed in, check it and get the bug object - $bug = Bugzilla::Bug->check({ id => $bug_id }); - } + my $bug; + if (blessed $bug_id) { - return $bug->id; + # We got a bug object passed in, use it + $bug = $bug_id; + $bug->check_is_visible; + } + else { + # We got a bug id passed in, check it and get the bug object + $bug = Bugzilla::Bug->check({id => $bug_id}); + } + + return $bug->id; } sub _check_value { - my ($class, $uri) = @_; - - my $value = $uri->as_string; - - if (!$value) { - ThrowCodeError('param_required', - { function => 'add_see_also', param => '$value' }); - } - - # We assume that the URL is an HTTP URL if there is no (something):// - # in front. - if (!$uri->scheme) { - # This works better than setting $uri->scheme('http'), because - # that creates URLs like "http:domain.com" and doesn't properly - # differentiate the path from the domain. - $uri = new URI("http://$value"); - } - elsif ($uri->scheme ne 'http' && $uri->scheme ne 'https') { - ThrowUserError('bug_url_invalid', { url => $value, reason => 'http' }); - } - - # This stops the following edge cases from being accepted: - # * show_bug.cgi?id=1 - # * /show_bug.cgi?id=1 - # * http:///show_bug.cgi?id=1 - if (!$uri->authority or $uri->path !~ m{/}) { - ThrowUserError('bug_url_invalid', - { url => $value, reason => 'path_only' }); - } - - if (length($uri->path) > MAX_BUG_URL_LENGTH) { - ThrowUserError('bug_url_too_long', { url => $uri->path }); - } - - return $uri; + my ($class, $uri) = @_; + + my $value = $uri->as_string; + + if (!$value) { + ThrowCodeError('param_required', + {function => 'add_see_also', param => '$value'}); + } + + # We assume that the URL is an HTTP URL if there is no (something):// + # in front. + if (!$uri->scheme) { + + # This works better than setting $uri->scheme('http'), because + # that creates URLs like "http:domain.com" and doesn't properly + # differentiate the path from the domain. + $uri = new URI("http://$value"); + } + elsif ($uri->scheme ne 'http' && $uri->scheme ne 'https') { + ThrowUserError('bug_url_invalid', {url => $value, reason => 'http'}); + } + + # This stops the following edge cases from being accepted: + # * show_bug.cgi?id=1 + # * /show_bug.cgi?id=1 + # * http:///show_bug.cgi?id=1 + if (!$uri->authority or $uri->path !~ m{/}) { + ThrowUserError('bug_url_invalid', {url => $value, reason => 'path_only'}); + } + + if (length($uri->path) > MAX_BUG_URL_LENGTH) { + ThrowUserError('bug_url_too_long', {url => $uri->path}); + } + + return $uri; } 1; +__END__ + =head1 B =over @@ -217,4 +217,7 @@ sub _check_value { =item bug_id +=item is_private + =back + diff --git a/Bugzilla/BugUrl/Bugzilla.pm b/Bugzilla/BugUrl/Bugzilla.pm index 402ff1509..9d036100f 100644 --- a/Bugzilla/BugUrl/Bugzilla.pm +++ b/Bugzilla/BugUrl/Bugzilla.pm @@ -21,37 +21,39 @@ use Bugzilla::Util; ############################### sub should_handle { - my ($class, $uri) = @_; - return ($uri->path =~ /show_bug\.cgi$/) ? 1 : 0; + my ($class, $uri) = @_; + return ($uri->path =~ /show_bug\.cgi$/) ? 1 : 0; } sub _check_value { - my ($class, $uri) = @_; - - $uri = $class->SUPER::_check_value($uri); - - my $bug_id = $uri->query_param('id'); - # We don't currently allow aliases, because we can't check to see - # if somebody's putting both an alias link and a numeric ID link. - # When we start validating the URL by accessing the other Bugzilla, - # we can allow aliases. - detaint_natural($bug_id); - if (!$bug_id) { - my $value = $uri->as_string; - ThrowUserError('bug_url_invalid', { url => $value, reason => 'id' }); - } - - # Make sure that "id" is the only query parameter. - $uri->query("id=$bug_id"); - # And remove any # part if there is one. - $uri->fragment(undef); - - return $uri; + my ($class, $uri) = @_; + + $uri = $class->SUPER::_check_value($uri); + + my $bug_id = $uri->query_param('id'); + + # We don't currently allow aliases, because we can't check to see + # if somebody's putting both an alias link and a numeric ID link. + # When we start validating the URL by accessing the other Bugzilla, + # we can allow aliases. + detaint_natural($bug_id); + if (!$bug_id) { + my $value = $uri->as_string; + ThrowUserError('bug_url_invalid', {url => $value, reason => 'id'}); + } + + # Make sure that "id" is the only query parameter. + $uri->query("id=$bug_id"); + + # And remove any # part if there is one. + $uri->fragment(undef); + + return $uri; } sub target_bug_id { - my ($self) = @_; - return new URI($self->name)->query_param('id'); + my ($self) = @_; + return new URI($self->name)->query_param('id'); } 1; diff --git a/Bugzilla/BugUrl/Bugzilla/Local.pm b/Bugzilla/BugUrl/Bugzilla/Local.pm index 7b9cb6a4f..3fe0fcb5d 100644 --- a/Bugzilla/BugUrl/Bugzilla/Local.pm +++ b/Bugzilla/BugUrl/Bugzilla/Local.pm @@ -20,77 +20,75 @@ use Bugzilla::Util; #### Initialization #### ############################### -use constant VALIDATOR_DEPENDENCIES => { - value => ['bug_id'], -}; +use constant VALIDATOR_DEPENDENCIES => {value => ['bug_id'],}; ############################### #### Methods #### ############################### sub ref_bug_url { - my $self = shift; - - if (!exists $self->{ref_bug_url}) { - my $ref_bug_id = new URI($self->name)->query_param('id'); - my $ref_bug = Bugzilla::Bug->check($ref_bug_id); - my $ref_value = $self->local_uri($self->bug_id); - $self->{ref_bug_url} = - new Bugzilla::BugUrl::Bugzilla::Local({ bug_id => $ref_bug->id, - value => $ref_value }); - } - return $self->{ref_bug_url}; + my $self = shift; + + if (!exists $self->{ref_bug_url}) { + my $ref_bug_id = new URI($self->name)->query_param('id'); + my $ref_bug = Bugzilla::Bug->check($ref_bug_id); + my $ref_value = $self->local_uri($self->bug_id); + $self->{ref_bug_url} = new Bugzilla::BugUrl::Bugzilla::Local( + {bug_id => $ref_bug->id, value => $ref_value}); + } + return $self->{ref_bug_url}; } sub should_handle { - my ($class, $uri) = @_; - - # Check if it is either a bug id number or an alias. - return 1 if $uri->as_string =~ m/^\w+$/; - - # Check if it is a local Bugzilla uri and call - # Bugzilla::BugUrl::Bugzilla to check if it's a valid Bugzilla - # see also url. - my $canonical_local = URI->new($class->local_uri)->canonical; - if ($canonical_local->authority eq $uri->canonical->authority - and $canonical_local->path eq $uri->canonical->path) - { - return $class->SUPER::should_handle($uri); - } - - return 0; + my ($class, $uri) = @_; + + # Check if it is either a bug id number or an alias. + return 1 if $uri->as_string =~ m/^\w+$/; + + # Check if it is a local Bugzilla uri and call + # Bugzilla::BugUrl::Bugzilla to check if it's a valid Bugzilla + # see also url. + my $canonical_local = URI->new($class->local_uri)->canonical; + if ( $canonical_local->authority eq $uri->canonical->authority + and $canonical_local->path eq $uri->canonical->path) + { + return $class->SUPER::should_handle($uri); + } + + return 0; } sub _check_value { - my ($class, $uri, undef, $params) = @_; - - # At this point we are going to treat any word as a - # bug id/alias to the local Bugzilla. - my $value = $uri->as_string; - if ($value =~ m/^\w+$/) { - $uri = new URI($class->local_uri($value)); - } else { - # It's not a word, then we have to check - # if it's a valid Bugzilla url. - $uri = $class->SUPER::_check_value($uri); - } - - my $ref_bug_id = $uri->query_param('id'); - my $ref_bug = Bugzilla::Bug->check($ref_bug_id); - my $self_bug_id = $params->{bug_id}; - $params->{ref_bug} = $ref_bug; - - if ($ref_bug->id == $self_bug_id) { - ThrowUserError('see_also_self_reference'); - } - - return $uri; + my ($class, $uri, undef, $params) = @_; + + # At this point we are going to treat any word as a + # bug id/alias to the local Bugzilla. + my $value = $uri->as_string; + if ($value =~ m/^\w+$/) { + $uri = new URI($class->local_uri($value)); + } + else { + # It's not a word, then we have to check + # if it's a valid Bugzilla url. + $uri = $class->SUPER::_check_value($uri); + } + + my $ref_bug_id = $uri->query_param('id'); + my $ref_bug = Bugzilla::Bug->check($ref_bug_id); + my $self_bug_id = $params->{bug_id}; + $params->{ref_bug} = $ref_bug; + + if ($ref_bug->id == $self_bug_id) { + ThrowUserError('see_also_self_reference'); + } + + return $uri; } sub local_uri { - my ($self, $bug_id) = @_; - $bug_id ||= ''; - return correct_urlbase() . "show_bug.cgi?id=$bug_id"; + my ($self, $bug_id) = @_; + $bug_id ||= ''; + return correct_urlbase() . "show_bug.cgi?id=$bug_id"; } 1; diff --git a/Bugzilla/BugUrl/Debian.pm b/Bugzilla/BugUrl/Debian.pm index 2b611aa57..f49f2b820 100644 --- a/Bugzilla/BugUrl/Debian.pm +++ b/Bugzilla/BugUrl/Debian.pm @@ -18,28 +18,30 @@ use parent qw(Bugzilla::BugUrl); ############################### sub should_handle { - my ($class, $uri) = @_; - - # Debian BTS URLs can look like various things: - # http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1234 - # http://bugs.debian.org/1234 - return (lc($uri->authority) eq 'bugs.debian.org' - and (($uri->path =~ /bugreport\.cgi$/ - and $uri->query_param('bug') =~ m|^\d+$|) - or $uri->path =~ m|^/\d+$|)) ? 1 : 0; + my ($class, $uri) = @_; + + # Debian BTS URLs can look like various things: + # http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1234 + # http://bugs.debian.org/1234 + return ( + lc($uri->authority) eq 'bugs.debian.org' + and + (($uri->path =~ /bugreport\.cgi$/ and $uri->query_param('bug') =~ m|^\d+$|) + or $uri->path =~ m|^/\d+$|) + ) ? 1 : 0; } sub _check_value { - my $class = shift; + my $class = shift; - my $uri = $class->SUPER::_check_value(@_); + my $uri = $class->SUPER::_check_value(@_); - # This is the shortest standard URL form for Debian BTS URLs, - # and so we reduce all URLs to this. - $uri->path =~ m|^/(\d+)$| || $uri->query_param('bug') =~ m|^(\d+)$|; - $uri = new URI("http://bugs.debian.org/$1"); + # This is the shortest standard URL form for Debian BTS URLs, + # and so we reduce all URLs to this. + $uri->path =~ m|^/(\d+)$| || $uri->query_param('bug') =~ m|^(\d+)$|; + $uri = new URI("http://bugs.debian.org/$1"); - return $uri; + return $uri; } 1; diff --git a/Bugzilla/BugUrl/GitHub.pm b/Bugzilla/BugUrl/GitHub.pm index f14f1d6b0..583837a60 100644 --- a/Bugzilla/BugUrl/GitHub.pm +++ b/Bugzilla/BugUrl/GitHub.pm @@ -18,25 +18,25 @@ use parent qw(Bugzilla::BugUrl); ############################### 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 - # GitHub pull request URLs have only one form: - # https://github.com/USER_OR_TEAM_OR_ORGANIZATION_NAME/REPOSITORY_NAME/pull/111 - return (lc($uri->authority) eq 'github.com' - and $uri->path =~ m!^/[^/]+/[^/]+/(?:issues|pull)/\d+$!) ? 1 : 0; + my ($class, $uri) = @_; + +# GitHub issue URLs have only one form: +# https://github.com/USER_OR_TEAM_OR_ORGANIZATION_NAME/REPOSITORY_NAME/issues/111 +# GitHub pull request URLs have only one form: +# https://github.com/USER_OR_TEAM_OR_ORGANIZATION_NAME/REPOSITORY_NAME/pull/111 + return (lc($uri->authority) eq 'github.com' + and $uri->path =~ m!^/[^/]+/[^/]+/(?:issues|pull)/\d+$!) ? 1 : 0; } sub _check_value { - my ($class, $uri) = @_; + my ($class, $uri) = @_; - $uri = $class->SUPER::_check_value($uri); + $uri = $class->SUPER::_check_value($uri); - # GitHub HTTP URLs redirect to HTTPS, so just use the HTTPS scheme. - $uri->scheme('https'); + # GitHub HTTP URLs redirect to HTTPS, so just use the HTTPS scheme. + $uri->scheme('https'); - return $uri; + return $uri; } 1; diff --git a/Bugzilla/BugUrl/Google.pm b/Bugzilla/BugUrl/Google.pm index 71a9c46fb..106425302 100644 --- a/Bugzilla/BugUrl/Google.pm +++ b/Bugzilla/BugUrl/Google.pm @@ -18,27 +18,27 @@ use parent qw(Bugzilla::BugUrl); ############################### sub should_handle { - my ($class, $uri) = @_; + my ($class, $uri) = @_; - # Google Code URLs only have one form: - # http(s)://code.google.com/p/PROJECT_NAME/issues/detail?id=1234 - return (lc($uri->authority) eq 'code.google.com' - and $uri->path =~ m|^/p/[^/]+/issues/detail$| - and $uri->query_param('id') =~ /^\d+$/) ? 1 : 0; + # Google Code URLs only have one form: + # http(s)://code.google.com/p/PROJECT_NAME/issues/detail?id=1234 + return (lc($uri->authority) eq 'code.google.com' + and $uri->path =~ m|^/p/[^/]+/issues/detail$| + and $uri->query_param('id') =~ /^\d+$/) ? 1 : 0; } sub _check_value { - my ($class, $uri) = @_; - - $uri = $class->SUPER::_check_value($uri); + my ($class, $uri) = @_; - # While Google Code URLs can be either HTTP or HTTPS, - # always go with the HTTP scheme, as that's the default. - if ($uri->scheme eq 'https') { - $uri->scheme('http'); - } + $uri = $class->SUPER::_check_value($uri); - return $uri; + # While Google Code URLs can be either HTTP or HTTPS, + # always go with the HTTP scheme, as that's the default. + if ($uri->scheme eq 'https') { + $uri->scheme('http'); + } + + return $uri; } 1; diff --git a/Bugzilla/BugUrl/JIRA.pm b/Bugzilla/BugUrl/JIRA.pm index e9d2a2d2a..20478a3b1 100644 --- a/Bugzilla/BugUrl/JIRA.pm +++ b/Bugzilla/BugUrl/JIRA.pm @@ -18,25 +18,36 @@ use parent qw(Bugzilla::BugUrl); ############################### sub should_handle { - my ($class, $uri) = @_; - - # JIRA URLs have only one basic form (but the jira is optional): - # https://issues.apache.org/jira/browse/KEY-1234 - # http://issues.example.com/browse/KEY-1234 - return ($uri->path =~ m|/browse/[A-Z][A-Z]+-\d+$|) ? 1 : 0; + my ($class, $uri) = @_; + + ## REDHAT EXTENSION BEGIN + # JBoss JIRA URLs have a different regex for project key + # SG confirm with Vlastimil Elias on 2012-01-10 + if ( $uri->path =~ m|/browse/[A-Z]{2,}\d{0,2}-\d+$| + and $uri->host =~ /^(jira|issues)\.jboss\.org$/) + { + return 1; + } + ## REDHAT EXTENSION END + + # JIRA URLs have only one basic form (but the jira is optional): + # https://issues.apache.org/jira/browse/KEY-1234 + # http://issues.example.com/browse/KEY-1234 + return ($uri->path =~ m|/browse/[A-Z][A-Z]+-\d+$|) ? 1 : 0; } sub _check_value { - my $class = shift; + my $class = shift; + + my $uri = $class->SUPER::_check_value(@_); - my $uri = $class->SUPER::_check_value(@_); + # Make sure there are no query parameters. + $uri->query(undef); - # Make sure there are no query parameters. - $uri->query(undef); - # And remove any # part if there is one. - $uri->fragment(undef); + # And remove any # part if there is one. + $uri->fragment(undef); - return $uri; + return $uri; } 1; diff --git a/Bugzilla/BugUrl/Launchpad.pm b/Bugzilla/BugUrl/Launchpad.pm index 0362747a2..5be8088d1 100644 --- a/Bugzilla/BugUrl/Launchpad.pm +++ b/Bugzilla/BugUrl/Launchpad.pm @@ -18,27 +18,28 @@ use parent qw(Bugzilla::BugUrl); ############################### sub should_handle { - my ($class, $uri) = @_; - - # Launchpad bug URLs can look like various things: - # https://bugs.launchpad.net/ubuntu/+bug/1234 - # https://launchpad.net/bugs/1234 - # All variations end with either "/bugs/1234" or "/+bug/1234" - return ($uri->authority =~ /launchpad\.net$/ - and $uri->path =~ m|bugs?/\d+$|) ? 1 : 0; + my ($class, $uri) = @_; + + # Launchpad bug URLs can look like various things: + # https://bugs.launchpad.net/ubuntu/+bug/1234 + # https://launchpad.net/bugs/1234 + # All variations end with either "/bugs/1234" or "/+bug/1234" + return ($uri->authority =~ /launchpad\.net$/ and $uri->path =~ m|bugs?/\d+$|) + ? 1 + : 0; } sub _check_value { - my ($class, $uri) = @_; + my ($class, $uri) = @_; - $uri = $class->SUPER::_check_value($uri); + $uri = $class->SUPER::_check_value($uri); - # This is the shortest standard URL form for Launchpad bugs, - # and so we reduce all URLs to this. - $uri->path =~ m|bugs?/(\d+)$|; - $uri = new URI("https://launchpad.net/bugs/$1"); + # This is the shortest standard URL form for Launchpad bugs, + # and so we reduce all URLs to this. + $uri->path =~ m|bugs?/(\d+)$|; + $uri = new URI("https://launchpad.net/bugs/$1"); - return $uri; + return $uri; } 1; diff --git a/Bugzilla/BugUrl/MantisBT.pm b/Bugzilla/BugUrl/MantisBT.pm index 60d3b578e..742ae1a47 100644 --- a/Bugzilla/BugUrl/MantisBT.pm +++ b/Bugzilla/BugUrl/MantisBT.pm @@ -18,22 +18,22 @@ use parent qw(Bugzilla::BugUrl); ############################### sub should_handle { - my ($class, $uri) = @_; + my ($class, $uri) = @_; - # MantisBT URLs look like the following ('bugs' directory is optional): - # http://www.mantisbt.org/bugs/view.php?id=1234 - return ($uri->path_query =~ m|view\.php\?id=\d+$|) ? 1 : 0; + # MantisBT URLs look like the following ('bugs' directory is optional): + # http://www.mantisbt.org/bugs/view.php?id=1234 + return ($uri->path_query =~ m|view\.php\?id=\d+$|) ? 1 : 0; } sub _check_value { - my $class = shift; + my $class = shift; - my $uri = $class->SUPER::_check_value(@_); + my $uri = $class->SUPER::_check_value(@_); - # Remove any # part if there is one. - $uri->fragment(undef); + # Remove any # part if there is one. + $uri->fragment(undef); - return $uri; + return $uri; } 1; diff --git a/Bugzilla/BugUrl/SourceForge.pm b/Bugzilla/BugUrl/SourceForge.pm index acba0df28..ffdde42f4 100644 --- a/Bugzilla/BugUrl/SourceForge.pm +++ b/Bugzilla/BugUrl/SourceForge.pm @@ -18,27 +18,27 @@ use parent qw(Bugzilla::BugUrl); ############################### sub should_handle { - my ($class, $uri) = @_; - - # SourceForge tracker URLs have only one form: - # http://sourceforge.net/tracker/?func=detail&aid=111&group_id=111&atid=111 - return (lc($uri->authority) eq 'sourceforge.net' - and $uri->path =~ m|/tracker/| - and $uri->query_param('func') eq 'detail' - and $uri->query_param('aid') - and $uri->query_param('group_id') - and $uri->query_param('atid')) ? 1 : 0; + my ($class, $uri) = @_; + + # SourceForge tracker URLs have only one form: + # http://sourceforge.net/tracker/?func=detail&aid=111&group_id=111&atid=111 + return (lc($uri->authority) eq 'sourceforge.net' + and $uri->path =~ m|/tracker/| + and $uri->query_param('func') eq 'detail' + and $uri->query_param('aid') + and $uri->query_param('group_id') + and $uri->query_param('atid')) ? 1 : 0; } sub _check_value { - my $class = shift; + my $class = shift; - my $uri = $class->SUPER::_check_value(@_); + my $uri = $class->SUPER::_check_value(@_); - # Remove any # part if there is one. - $uri->fragment(undef); + # Remove any # part if there is one. + $uri->fragment(undef); - return $uri; + return $uri; } 1; diff --git a/Bugzilla/BugUrl/Trac.pm b/Bugzilla/BugUrl/Trac.pm index fe74abf33..22418a1df 100644 --- a/Bugzilla/BugUrl/Trac.pm +++ b/Bugzilla/BugUrl/Trac.pm @@ -18,25 +18,26 @@ use parent qw(Bugzilla::BugUrl); ############################### sub should_handle { - my ($class, $uri) = @_; + my ($class, $uri) = @_; - # Trac URLs can look like various things: - # http://dev.mutt.org/trac/ticket/1234 - # http://trac.roundcube.net/ticket/1484130 - return ($uri->path =~ m|/ticket/\d+$|) ? 1 : 0; + # Trac URLs can look like various things: + # http://dev.mutt.org/trac/ticket/1234 + # http://trac.roundcube.net/ticket/1484130 + return ($uri->path =~ m|/ticket/\d+$|) ? 1 : 0; } sub _check_value { - my $class = shift; + my $class = shift; - my $uri = $class->SUPER::_check_value(@_); + my $uri = $class->SUPER::_check_value(@_); - # Make sure there are no query parameters. - $uri->query(undef); - # And remove any # part if there is one. - $uri->fragment(undef); + # Make sure there are no query parameters. + $uri->query(undef); - return $uri; + # And remove any # part if there is one. + $uri->fragment(undef); + + return $uri; } 1; diff --git a/Bugzilla/BugUserLastVisit.pm b/Bugzilla/BugUserLastVisit.pm index d043b121a..d1c351959 100644 --- a/Bugzilla/BugUserLastVisit.pm +++ b/Bugzilla/BugUserLastVisit.pm @@ -25,25 +25,27 @@ use constant LIST_ORDER => 'id'; use constant NAME_FIELD => 'id'; # turn off auditing and exclude these objects from memcached -use constant { AUDIT_CREATES => 0, - AUDIT_UPDATES => 0, - AUDIT_REMOVES => 0, - USE_MEMCACHED => 0 }; +use constant { + AUDIT_CREATES => 0, + AUDIT_UPDATES => 0, + AUDIT_REMOVES => 0, + USE_MEMCACHED => 0 +}; ##################################################################### # Provide accessors for our columns ##################################################################### -sub id { return $_[0]->{id} } -sub bug_id { return $_[0]->{bug_id} } -sub user_id { return $_[0]->{user_id} } +sub id { return $_[0]->{id} } +sub bug_id { return $_[0]->{bug_id} } +sub user_id { return $_[0]->{user_id} } sub last_visit_ts { return $_[0]->{last_visit_ts} } sub user { - my $self = shift; + my $self = shift; - $self->{user} //= Bugzilla::User->new({ id => $self->user_id, cache => 1 }); - return $self->{user}; + $self->{user} //= Bugzilla::User->new({id => $self->user_id, cache => 1}); + return $self->{user}; } 1; diff --git a/Bugzilla/CGI.pm b/Bugzilla/CGI.pm index 9b1ff9235..f3393f793 100644 --- a/Bugzilla/CGI.pm +++ b/Bugzilla/CGI.pm @@ -20,282 +20,330 @@ use Bugzilla::Hook; use Bugzilla::Search::Recent; use File::Basename; +use HTTP::BrowserDetect; sub _init_bz_cgi_globals { - my $invocant = shift; - # We need to disable output buffering - see bug 179174 - $| = 1; - - # Ignore SIGTERM and SIGPIPE - this prevents DB corruption. If the user closes - # their browser window while a script is running, the web server sends these - # signals, and we don't want to die half way through a write. - $SIG{TERM} = 'IGNORE'; - $SIG{PIPE} = 'IGNORE'; - - # We don't precompile any functions here, that's done specially in - # mod_perl code. - $invocant->_setup_symbols(qw(:no_xhtml :oldstyle_urls :private_tempfiles - :unique_headers)); -} + my $invocant = shift; -BEGIN { __PACKAGE__->_init_bz_cgi_globals() if i_am_cgi(); } + # We need to disable output buffering - see bug 179174 + $| = 1; -sub new { - my ($invocant, @args) = @_; - my $class = ref($invocant) || $invocant; - - # Under mod_perl, CGI's global variables get reset on each request, - # so we need to set them up again every time. - $class->_init_bz_cgi_globals() if $ENV{MOD_PERL}; - - my $self = $class->SUPER::new(@args); - - # 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 (my $path_info = $self->path_info) { - my @whitelist = ("rest.cgi"); - Bugzilla::Hook::process('path_info_whitelist', { whitelist => \@whitelist }); - if (!grep($_ eq $script, @whitelist)) { - # IIS includes the full path to the script in PATH_INFO, - # so we have to extract the real PATH_INFO from it, - # else we will be redirected outside Bugzilla. - my $script_name = $self->script_name; - $path_info =~ s/^\Q$script_name\E//; - if ($script_name && $path_info) { - print $self->redirect($self->url(-path => 0, -query => 1)); - } - } - } + # Ignore SIGTERM and SIGPIPE - this prevents DB corruption. If the user closes + # their browser window while a script is running, the web server sends these + # signals, and we don't want to die half way through a write. + $SIG{TERM} = 'IGNORE'; + $SIG{PIPE} = 'IGNORE'; - # Send appropriate charset - $self->charset(Bugzilla->params->{'utf8'} ? 'UTF-8' : ''); - - # Redirect to urlbase/sslbase if we are not viewing an attachment. - if ($self->url_is_attachment_base and $script ne 'attachment.cgi') { - $self->redirect_to_urlbase(); - } + # We don't precompile any functions here, that's done specially in + # mod_perl code. + $invocant->_setup_symbols(qw(:no_xhtml :oldstyle_urls :private_tempfiles + :unique_headers)); +} - # Check for errors - # All of the Bugzilla code wants to do this, so do it here instead of - # in each script - - my $err = $self->cgi_error; - - if ($err) { - # Note that this error block is only triggered by CGI.pm for malformed - # multipart requests, and so should never happen unless there is a - # browser bug. - - print $self->header(-status => $err); - - # ThrowCodeError wants to print the header, so it grabs Bugzilla->cgi - # which creates a new Bugzilla::CGI object, which fails again, which - # ends up here, and calls ThrowCodeError, and then recurses forever. - # So don't use it. - # In fact, we can't use templates at all, because we need a CGI object - # to determine the template lang as well as the current url (from the - # template) - # Since this is an internal error which indicates a severe browser bug, - # just die. - die "CGI parsing error: $err"; - } +BEGIN { __PACKAGE__->_init_bz_cgi_globals() if i_am_cgi(); } - return $self; +sub new { + my ($invocant, @args) = @_; + my $class = ref($invocant) || $invocant; + + # Under mod_perl, CGI's global variables get reset on each request, + # so we need to set them up again every time. + $class->_init_bz_cgi_globals() if $ENV{MOD_PERL}; + + my $self = $class->SUPER::new(@args); + + # 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 (my $path_info = $self->path_info) { + my @whitelist = ("rest.cgi"); + Bugzilla::Hook::process('path_info_whitelist', {whitelist => \@whitelist}); + if (!grep($_ eq $script, @whitelist)) { + + # IIS includes the full path to the script in PATH_INFO, + # so we have to extract the real PATH_INFO from it, + # else we will be redirected outside Bugzilla. + my $script_name = $self->script_name; + $path_info =~ s/^\Q$script_name\E//; + if ($script_name && $path_info) { + 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. + if ($self->url_is_attachment_base and $script ne 'attachment.cgi') { + $self->redirect_to_urlbase(); + } + + # Check for errors + # All of the Bugzilla code wants to do this, so do it here instead of + # in each script + + my $err = $self->cgi_error; + + if ($err) { + + # Note that this error block is only triggered by CGI.pm for malformed + # multipart requests, and so should never happen unless there is a + # browser bug. + + print $self->header(-status => $err); + + # ThrowCodeError wants to print the header, so it grabs Bugzilla->cgi + # which creates a new Bugzilla::CGI object, which fails again, which + # ends up here, and calls ThrowCodeError, and then recurses forever. + # So don't use it. + # In fact, we can't use templates at all, because we need a CGI object + # to determine the template lang as well as the current url (from the + # template) + # Since this is an internal error which indicates a severe browser bug, + # just die. + die "CGI parsing error: $err"; + } + + return $self; } # We want this sorted plus the ability to exclude certain params sub canonicalise_query { - my ($self, @exclude) = @_; + my ($self, @exclude) = @_; - # Reconstruct the URL by concatenating the sorted param=value pairs - my @parameters; - foreach my $key (sort($self->param())) { - # Leave this key out if it's in the exclude list - next if grep { $_ eq $key } @exclude; + # Reconstruct the URL by concatenating the sorted param=value pairs + my @parameters; + foreach my $key (sort($self->param())) { - # Remove the Boolean Charts for standard query.cgi fields - # They are listed in the query URL already - next if $key =~ /^(field|type|value)(-\d+){3}$/; + # Leave this key out if it's in the exclude list + next if grep { $_ eq $key } @exclude; - my $esc_key = url_quote($key); + # Remove the Boolean Charts for standard query.cgi fields + # They are listed in the query URL already + next if $key =~ /^(field|type|value)(-\d+){3}$/; - foreach my $value ($self->param($key)) { - # Omit params with an empty value - if (defined($value) && $value ne '') { - my $esc_value = url_quote($value); - - push(@parameters, "$esc_key=$esc_value"); - } - } - } + my $esc_key = url_quote($key); - return join("&", @parameters); -} + foreach my $value ($self->param($key)) { -sub clean_search_url { - my $self = shift; - # Delete any empty URL parameter. - my @cgi_params = $self->param; - - foreach my $param (@cgi_params) { - if (defined $self->param($param) && $self->param($param) eq '') { - $self->delete($param); - $self->delete("${param}_type"); - } + # Omit params with an empty value + if (defined($value) && $value ne '') { + my $esc_value = url_quote($value); - # Custom Search stuff is empty if it's "noop". We also keep around - # the old Boolean Chart syntax for backwards-compatibility. - if (($param =~ /\d-\d-\d/ || $param =~ /^[[:alpha:]]\d+$/) - && defined $self->param($param) && $self->param($param) eq 'noop') - { - $self->delete($param); - } - - # Any "join" for custom search that's an AND can be removed, because - # that's the default. - if (($param =~ /^j\d+$/ || $param eq 'j_top') - && $self->param($param) eq 'AND') - { - $self->delete($param); - } + push(@parameters, "$esc_key=$esc_value"); + } } + } - # Delete leftovers from the login form - $self->delete('Bugzilla_remember', 'GoAheadAndLogIn'); + return join("&", @parameters); +} - # Delete the token if we're not performing an action which needs it - unless ((defined $self->param('remtype') - && ($self->param('remtype') eq 'asdefault' - || $self->param('remtype') eq 'asnamed')) - || (defined $self->param('remaction') - && $self->param('remaction') eq 'forget')) - { - $self->delete("token"); - } +sub clean_search_url { + my $self = shift; - foreach my $num (1,2,3) { - # If there's no value in the email field, delete the related fields. - if (!$self->param("email$num")) { - foreach my $field (qw(type assigned_to reporter qa_contact cc longdesc)) { - $self->delete("email$field$num"); - } - } - } + # Delete any empty URL parameter. + my @cgi_params = $self->param; - # chfieldto is set to "Now" by default in query.cgi. But if none - # of the other chfield parameters are set, it's meaningless. - if (!defined $self->param('chfieldfrom') && !$self->param('chfield') - && !defined $self->param('chfieldvalue') && $self->param('chfieldto') - && lc($self->param('chfieldto')) eq 'now') - { - $self->delete('chfieldto'); + foreach my $param (@cgi_params) { + if (defined $self->param($param) && $self->param($param) eq '') { + $self->delete($param); + $self->delete("${param}_type"); } - # cmdtype "doit" is the default from query.cgi, but it's only meaningful - # if there's a remtype parameter. - if (defined $self->param('cmdtype') && $self->param('cmdtype') eq 'doit' - && !defined $self->param('remtype')) + # Custom Search stuff is empty if it's "noop". We also keep around + # the old Boolean Chart syntax for backwards-compatibility. + if ( ($param =~ /\d-\d-\d/ || $param =~ /^[[:alpha:]]\d+$/) + && defined $self->param($param) + && $self->param($param) eq 'noop') { - $self->delete('cmdtype'); + $self->delete($param); } - # "Reuse same sort as last time" is actually the default, so we don't - # need it in the URL. - if ($self->param('order') - && $self->param('order') eq 'Reuse same sort as last time') + # Any "join" for custom search that's an AND can be removed, because + # that's the default. + if (($param =~ /^j\d+$/ || $param eq 'j_top') && $self->param($param) eq 'AND') { - $self->delete('order'); - } + $self->delete($param); + } + } + + # Delete leftovers from the login form + $self->delete('Bugzilla_remember', 'GoAheadAndLogIn'); + + # Delete the token if we're not performing an action which needs it + unless ( + ( + defined $self->param('remtype') + && ( $self->param('remtype') eq 'asdefault' + || $self->param('remtype') eq 'asnamed') + ) + || (defined $self->param('remaction') && $self->param('remaction') eq 'forget') + ) + { + $self->delete("token"); + } + + foreach my $num (1, 2, 3) { + + # If there's no value in the email field, delete the related fields. + if (!$self->param("email$num")) { + ## REDHAT EXTENSION 876015: Add docs_contact + foreach + my $field (qw(type assigned_to reporter qa_contact docs_contact cc longdesc)) + { + $self->delete("email$field$num"); + } + } + } + + # chfieldto is set to "Now" by default in query.cgi. But if none + # of the other chfield parameters are set, it's meaningless. + if ( !defined $self->param('chfieldfrom') + && !$self->param('chfield') + && !defined $self->param('chfieldvalue') + && $self->param('chfieldto') + && lc($self->param('chfieldto')) eq 'now') + { + $self->delete('chfieldto'); + } + + # cmdtype "doit" is the default from query.cgi, but it's only meaningful + # if there's a remtype parameter. + if ( defined $self->param('cmdtype') + && $self->param('cmdtype') eq 'doit' + && !defined $self->param('remtype')) + { + $self->delete('cmdtype'); + } + + # "Reuse same sort as last time" is actually the default, so we don't + # need it in the URL. + if ( $self->param('order') + && $self->param('order') eq 'Reuse same sort as last time') + { + $self->delete('order'); + } + + # list_id is added in buglist.cgi after calling clean_search_url, + # and doesn't need to be saved in saved searches. + $self->delete('list_id'); + + # no_redirect is used internally by redirect_search_url(). + $self->delete('no_redirect'); + + # And now finally, if query_format is our only parameter, that + # really means we have no parameters, so we should delete query_format. + if ($self->param('query_format') && scalar($self->param()) == 1) { + $self->delete('query_format'); + } +} - # list_id is added in buglist.cgi after calling clean_search_url, - # and doesn't need to be saved in saved searches. - $self->delete('list_id'); +sub check_etag { + my ($self, $valid_etag) = @_; - # no_redirect is used internally by redirect_search_url(). - $self->delete('no_redirect'); + # ETag support. + my $if_none_match = $self->http('If-None-Match'); + return if !$if_none_match; - # And now finally, if query_format is our only parameter, that - # really means we have no parameters, so we should delete query_format. - if ($self->param('query_format') && scalar($self->param()) == 1) { - $self->delete('query_format'); - } -} + my @if_none = split(/[\s,]+/, $if_none_match); + foreach my $possible_etag (@if_none) { -sub check_etag { - my ($self, $valid_etag) = @_; - - # ETag support. - my $if_none_match = $self->http('If-None-Match'); - return if !$if_none_match; - - my @if_none = split(/[\s,]+/, $if_none_match); - foreach my $possible_etag (@if_none) { - # remove quotes from begin and end of the string - $possible_etag =~ s/^\"//g; - $possible_etag =~ s/\"$//g; - if ($possible_etag eq $valid_etag or $possible_etag eq '*') { - return 1; - } + # remove quotes from begin and end of the string + $possible_etag =~ s/^\"//g; + $possible_etag =~ s/\"$//g; + if ($possible_etag eq $valid_etag or $possible_etag eq '*') { + return 1; } + } - return 0; + return 0; } # Have to add the cookies in. sub multipart_start { - my $self = shift; - - my %args = @_; - - # CGI.pm::multipart_start doesn't honour its own charset information, so - # we do it ourselves here - if (defined $self->charset() && defined $args{-type}) { - # Remove any existing charset specifier - $args{-type} =~ s/;.*$//; - # and add the specified one - $args{-type} .= '; charset=' . $self->charset(); - } - - my $headers = $self->SUPER::multipart_start(%args); - # Eliminate the one extra CRLF at the end. - $headers =~ s/$CGI::CRLF$//; - # Add the cookies. We have to do it this way instead of - # passing them to multpart_start, because CGI.pm's multipart_start - # doesn't understand a '-cookie' argument pointing to an arrayref. - foreach my $cookie (@{$self->{Bugzilla_cookie_list}}) { - $headers .= "Set-Cookie: ${cookie}${CGI::CRLF}"; - } - $headers .= $CGI::CRLF; - $self->{_multipart_in_progress} = 1; - return $headers; + my $self = shift; + + my %args = @_; + + # CGI.pm::multipart_start doesn't honour its own charset information, so + # we do it ourselves here + if (defined $self->charset() && defined $args{-type}) { + + # Remove any existing charset specifier + $args{-type} =~ s/;.*$//; + + # and add the specified one + $args{-type} .= '; charset=' . $self->charset(); + } + + my $headers = $self->SUPER::multipart_start(%args); + + # Eliminate the one extra CRLF at the end. + $headers =~ s/$CGI::CRLF$//; + + # Add the cookies. We have to do it this way instead of + # passing them to multpart_start, because CGI.pm's multipart_start + # doesn't understand a '-cookie' argument pointing to an arrayref. + foreach my $cookie (@{$self->{Bugzilla_cookie_list}}) { + $headers .= "Set-Cookie: ${cookie}${CGI::CRLF}"; + } + $headers .= $CGI::CRLF; + $self->{_multipart_in_progress} = 1; + return $headers; } sub close_standby_message { - my ($self, $contenttype, $disp, $disp_prefix, $extension) = @_; - $self->set_dated_content_disp($disp, $disp_prefix, $extension); + my ($self, $contenttype, $disp, $disp_prefix, $extension) = @_; + $self->set_dated_content_disp($disp, $disp_prefix, $extension); + + if ($self->{_multipart_in_progress}) { + print $self->multipart_end(); + print $self->multipart_start(-type => $contenttype); + } + elsif (!$self->{_header_done}) { + print $self->header($contenttype); + } +} - if ($self->{_multipart_in_progress}) { - print $self->multipart_end(); - print $self->multipart_start(-type => $contenttype); - } - elsif (!$self->{_header_done}) { - print $self->header($contenttype); - } +sub is_safe_referer { + my ($self) = @_; + my $safe_referer_re = do { + + # Note that urlbase must end with a /. + # It almost certainly does, but let's be extra careful. + my $urlbase = correct_urlbase(); + ## RED HAT EXTENSION chop off trailing dirs as they are never in referrer + $urlbase =~ s{([^\/])/[^\/].*$}{$1}; + $urlbase =~ s{/$}{}; + qr{ + # Begins with literal urlbase + ^ (*COMMIT) + \Q$urlbase\E + # followed by a slash or end of string + (?: / + | $ ) + }sx + }; + + # Safe referers are ones that begin with the urlbase. + my $referer = $self->referer; + return $referer && $referer =~ $safe_referer_re; } our $ALLOW_UNSAFE_RESPONSE = 0; + # responding to text/plain or text/html is safe # responding to any request with a referer header is safe # some things need to have unsafe responses (attachment.cgi) # everything else should get a 403. sub _prevent_unsafe_response { - my ($self, $headers) = @_; - my $safe_content_type_re = qr{ + my ($self, $headers) = @_; + my $safe_content_type_re = qr{ ^ (*COMMIT) # COMMIT makes the regex faster # by preventing back-tracking. see also perldoc pelre. # application/x-javascript, xml, atom+xml, rdf+xml, xml-dtd, and json @@ -309,388 +357,452 @@ sub _prevent_unsafe_response { # used for HTTP push responses | multipart/x-mixed-replace) }sx; - my $safe_referer_re = do { - # Note that urlbase must end with a /. - # It almost certainly does, but let's be extra careful. - my $urlbase = correct_urlbase(); - $urlbase =~ s{/$}{}; - qr{ - # Begins with literal urlbase - ^ (*COMMIT) - \Q$urlbase\E - # followed by a slash or end of string - (?: / - | $ ) - }sx - }; - - return if $ALLOW_UNSAFE_RESPONSE; - - if (Bugzilla->usage_mode == USAGE_MODE_BROWSER) { - # Safe content types are ones that arn't images. - # For now let's assume plain text and html are not valid images. - my $content_type = $headers->{'-type'} // $headers->{'-content_type'} // 'text/html'; - my $is_safe_content_type = $content_type =~ $safe_content_type_re; - - # Safe referers are ones that begin with the urlbase. - my $referer = $self->referer; - my $is_safe_referer = $referer && $referer =~ $safe_referer_re; - - if (!$is_safe_referer && !$is_safe_content_type) { - print $self->SUPER::header(-type => 'text/html', -status => '403 Forbidden'); - if ($content_type ne 'text/html') { - print "Untrusted Referer Header\n"; - if ($ENV{MOD_PERL}) { - my $r = $self->r; - $r->rflush; - $r->status(200); - } - } - exit; + + return if $ALLOW_UNSAFE_RESPONSE; + + if (Bugzilla->usage_mode == USAGE_MODE_BROWSER) { + + # Safe content types are ones that arn't images. + # For now let's assume plain text and html are not valid images. + my $content_type = $headers->{'-type'} // $headers->{'-content_type'} + // 'text/html'; + my $is_safe_content_type = $content_type =~ $safe_content_type_re; + + if (!$self->is_safe_referer && !$is_safe_content_type) { + print $self->SUPER::header(-type => 'text/html', -status => '403 Forbidden'); + if ($content_type ne 'text/html') { + print "Untrusted Referer Header\n"; + if ($ENV{MOD_PERL}) { + my $r = $self->r; + $r->rflush; + $r->status(200); } + } + exit; } + } } -# Override header so we can add the cookies in -sub header { - my $self = shift; +# If CGI & the browser supports SameSite then we are OK +sub samesite_supported { + my ($self) = @_; + my %safe_browsers = (firefox => 61, chrome => 68,); - my %headers; - my $user = Bugzilla->user; + if (CGI->VERSION >= 4.36) { + my $ua = HTTP::BrowserDetect->new($self->user_agent()); + my $browser = $ua->browser(); + my $version = $ua->browser_major(); - # If there's only one parameter, then it's a Content-Type. - if (scalar(@_) == 1) { - %headers = ('-type' => shift(@_)); - } - else { - %headers = @_; + # Most browsers ship security fetaures to all OSs + if ( $browser + && defined $safe_browsers{$browser} + && $version >= $safe_browsers{$browser}) + { + return 1; } - $self->_prevent_unsafe_response(\%headers); - if ($self->{'_content_disp'}) { - $headers{'-content_disposition'} = $self->{'_content_disp'}; + # Others are less diligent + if ($browser && $browser eq 'edge' && $ua->os_string =~ m/^win10/) { + return 1; } + } - if (!$user->id && $user->authorizer->can_login - && !$self->cookie('Bugzilla_login_request_cookie')) - { - my %args; - $args{'-secure'} = 1 if Bugzilla->params->{ssl_redirect}; + return; +} - $self->send_cookie(-name => 'Bugzilla_login_request_cookie', - -value => generate_random_password(), - -httponly => 1, - %args); - } +# Must restrict searches if the user is logged in, the request is a GET, and the referer isn't safe. +sub should_restrict_searches { + my ($self) = @_; + my $user = Bugzilla->user; + my $method = uc $self->request_method; + + return + $user->id != 0 + && $method eq 'GET' + && !$self->samesite_supported + && !$self->is_safe_referer; +} - # Add the cookies in if we have any - if (scalar(@{$self->{Bugzilla_cookie_list}})) { - $headers{'-cookie'} = $self->{Bugzilla_cookie_list}; - } +# Override header so we can add the cookies in +sub header { + my $self = shift; + + my %headers; + my $user = Bugzilla->user; + + # If there's only one parameter, then it's a Content-Type. + if (scalar(@_) == 1) { + %headers = ('-type' => shift(@_)); + } + else { + %headers = @_; + } + $self->_prevent_unsafe_response(\%headers); + + if ($self->{'_content_disp'}) { + $headers{'-content_disposition'} = $self->{'_content_disp'}; + } + + if (!$user->id + && $user->authorizer->can_login + && !$self->cookie('Bugzilla_login_request_cookie')) + { + my %args; + $args{'-secure'} = 1 if Bugzilla->params->{ssl_redirect}; + + $self->send_cookie( + -name => 'Bugzilla_login_request_cookie', + -value => generate_random_password(), + -httponly => 1, + %args + ); + } - # Add Strict-Transport-Security (STS) header if this response - # is over SSL and the strict_transport_security param is turned on. - if ($self->https && !$self->url_is_attachment_base - && Bugzilla->params->{'strict_transport_security'} ne 'off') - { - my $sts_opts = 'max-age=' . MAX_STS_AGE; - if (Bugzilla->params->{'strict_transport_security'} - eq 'include_subdomains') - { - $sts_opts .= '; includeSubDomains'; - } - - $headers{'-strict_transport_security'} = $sts_opts; - } + # Add the cookies in if we have any + if (scalar(@{$self->{Bugzilla_cookie_list}})) { + $headers{'-cookie'} = $self->{Bugzilla_cookie_list}; + } - # Add X-Frame-Options header to prevent framing and subsequent - # possible clickjacking problems. - unless ($self->url_is_attachment_base) { - $headers{'-x_frame_options'} = 'SAMEORIGIN'; + # Add Strict-Transport-Security (STS) header if this response + # is over SSL and the strict_transport_security param is turned on. + if ( $self->https + && !$self->url_is_attachment_base + && Bugzilla->params->{'strict_transport_security'} ne 'off') + { + my $sts_opts = 'max-age=' . MAX_STS_AGE; + if (Bugzilla->params->{'strict_transport_security'} eq 'include_subdomains') { + $sts_opts .= '; includeSubDomains'; } - # Add X-XSS-Protection header to prevent simple XSS attacks - # and enforce the blocking (rather than the rewriting) mode. - $headers{'-x_xss_protection'} = '1; mode=block'; + $headers{'-strict_transport_security'} = $sts_opts; + } - # Add X-Content-Type-Options header to prevent browsers sniffing - # the MIME type away from the declared Content-Type. - $headers{'-x_content_type_options'} = 'nosniff'; + # Add X-Frame-Options header to prevent framing and subsequent + # possible clickjacking problems. + unless ($self->url_is_attachment_base) { + $headers{'-x_frame_options'} = 'SAMEORIGIN'; + } - Bugzilla::Hook::process('cgi_headers', - { cgi => $self, headers => \%headers } - ); - $self->{_header_done} = 1; + # Add X-XSS-Protection header to prevent simple XSS attacks + # and enforce the blocking (rather than the rewriting) mode. + $headers{'-x_xss_protection'} = '1; mode=block'; - return $self->SUPER::header(%headers) || ""; -} + # Add X-Content-Type-Options header to prevent browsers sniffing + # the MIME type away from the declared Content-Type. + $headers{'-x_content_type_options'} = 'nosniff'; -sub param { - my $self = shift; - local $CGI::LIST_CONTEXT_WARN = 0; - - # When we are just requesting the value of a parameter... - if (scalar(@_) == 1) { - my @result = $self->SUPER::param(@_); - - # Also look at the URL parameters, after we look at the POST - # parameters. This is to allow things like login-form submissions - # with URL parameters in the form's "target" attribute. - if (!scalar(@result) - && $self->request_method && $self->request_method eq 'POST') - { - @result = $self->url_param(@_); - } + Bugzilla::Hook::process('cgi_headers', {cgi => $self, headers => \%headers}); + $self->{_header_done} = 1; - # Fix UTF-8-ness of input parameters. - if (Bugzilla->params->{'utf8'}) { - @result = map { _fix_utf8($_) } @result; - } + return $self->SUPER::header(%headers) || ""; +} - return wantarray ? @result : $result[0]; - } - # And for various other functions in CGI.pm, we need to correctly - # return the URL parameters in addition to the POST parameters when - # asked for the list of parameters. - elsif (!scalar(@_) && $self->request_method - && $self->request_method eq 'POST') +sub param { + my $self = shift; + local $CGI::LIST_CONTEXT_WARN = 0; + + # When we are just requesting the value of a parameter... + if (scalar(@_) == 1) { + my @result = $self->SUPER::param(@_); + + # Also look at the URL parameters, after we look at the POST + # parameters. This is to allow things like login-form submissions + # with URL parameters in the form's "target" attribute. + if ( !scalar(@result) + && $self->request_method + && $self->request_method eq 'POST') { - my @post_params = $self->SUPER::param; - my @url_params = $self->url_param; - my %params = map { $_ => 1 } (@post_params, @url_params); - return keys %params; + @result = $self->url_param(@_); } - return $self->SUPER::param(@_); + # Fix UTF-8-ness of input parameters. + if (Bugzilla->params->{'utf8'}) { + @result = map { _fix_utf8($_) } @result; + } + + return wantarray ? @result : $result[0]; + } + + # And for various other functions in CGI.pm, we need to correctly + # return the URL parameters in addition to the POST parameters when + # asked for the list of parameters. + elsif (!scalar(@_) && $self->request_method && $self->request_method eq 'POST') + { + my @post_params = $self->SUPER::param; + my @url_params = $self->url_param; + my %params = map { $_ => 1 } (@post_params, @url_params); + return keys %params; + } + + return $self->SUPER::param(@_); } sub url_param { - my $self = shift; - # Some servers fail to set the QUERY_STRING parameter, which - # causes undef issues - $ENV{'QUERY_STRING'} //= ''; - return $self->SUPER::url_param(@_); + my $self = shift; + + # Some servers fail to set the QUERY_STRING parameter, which + # causes undef issues + $ENV{'QUERY_STRING'} //= ''; + return $self->SUPER::url_param(@_); } sub _fix_utf8 { - my $input = shift; - # The is_utf8 is here in case CGI gets smart about utf8 someday. - utf8::decode($input) if defined $input && !ref $input && !utf8::is_utf8($input); - return $input; + my $input = shift; + + # The is_utf8 is here in case CGI gets smart about utf8 someday. + utf8::decode($input) if defined $input && !ref $input && !utf8::is_utf8($input); + return $input; } sub should_set { - my ($self, $param) = @_; - my $set = (defined $self->param($param) - or defined $self->param("defined_$param")) - ? 1 : 0; - return $set; + my ($self, $param) = @_; + my $set + = (defined $self->param($param) or defined $self->param("defined_$param")) + ? 1 + : 0; + return $set; } -# The various parts of Bugzilla which create cookies don't want to have to -# pass them around to all of the callers. Instead, store them locally here, -# and then output as required from |header|. -sub send_cookie { - my $self = shift; - - # Move the param list into a hash for easier handling. - my %paramhash; - my @paramlist; - my ($key, $value); - while ($key = shift) { - $value = shift; - $paramhash{$key} = $value; - } - - # Complain if -value is not given or empty (bug 268146). - if (!exists($paramhash{'-value'}) || !$paramhash{'-value'}) { - ThrowCodeError('cookies_need_value'); - } +sub cookie { + my ($self, @args) = @_; - # Add the default path and the domain in. - $paramhash{'-path'} = Bugzilla->params->{'cookiepath'}; - $paramhash{'-domain'} = Bugzilla->params->{'cookiedomain'} - if Bugzilla->params->{'cookiedomain'}; + # Called with one arg to read a cookie + if (@args == 1) { + return undef if Bugzilla->request_cache->{nocookies}; - # Move the param list back into an array for the call to cookie(). - foreach (keys(%paramhash)) { - unshift(@paramlist, $_ => $paramhash{$_}); + my $origin = $self->http('Origin'); + my $urlbase = correct_urlbase(); + ## RED HAT EXTENSION chop off trailing dirs as they are never in referrer + $urlbase =~ s{([^\/])/[^\/].*$}{$1}; + $urlbase =~ s{/$}{}; + if ($origin && $urlbase ne $origin) { + ## RED HAT EXTENSION 1623795 - Don't cache as it breaks SSO + #Bugzilla->request_cache->{nocookies} = 1; + return; } + } - push(@{$self->{'Bugzilla_cookie_list'}}, $self->cookie(@paramlist)); + return $self->SUPER::cookie(@args); +} + +# The various parts of Bugzilla which create cookies don't want to have to +# pass them around to all of the callers. Instead, store them locally here, +# and then output as required from |header|. +sub send_cookie { + my $self = shift; + + # Move the param list into a hash for easier handling. + my %paramhash; + my @paramlist; + my ($key, $value); + while ($key = shift) { + $value = shift; + $paramhash{$key} = $value; + } + + # Complain if -value is not given or empty (bug 268146). + if (!exists($paramhash{'-value'}) || !$paramhash{'-value'}) { + ThrowCodeError('cookies_need_value'); + } + + if (CGI->VERSION >= 4.36) { + $paramhash{'-samesite'} = 'Lax'; + } + + # Add the default path and the domain in. + $paramhash{'-path'} = Bugzilla->params->{'cookiepath'}; + $paramhash{'-domain'} = Bugzilla->params->{'cookiedomain'} + if Bugzilla->params->{'cookiedomain'}; + + # Move the param list back into an array for the call to cookie(). + foreach (keys(%paramhash)) { + unshift(@paramlist, $_ => $paramhash{$_}); + } + + push(@{$self->{'Bugzilla_cookie_list'}}, $self->cookie(@paramlist)); } # Cookies are removed by setting an expiry date in the past. # This method is a send_cookie wrapper doing exactly this. sub remove_cookie { - my $self = shift; - my ($cookiename) = (@_); - - # Expire the cookie, giving a non-empty dummy value (bug 268146). - $self->send_cookie('-name' => $cookiename, - '-expires' => 'Tue, 15-Sep-1998 21:49:00 GMT', - '-value' => 'X'); + my $self = shift; + my ($cookiename) = (@_); + + # Expire the cookie, giving a non-empty dummy value (bug 268146). + $self->send_cookie( + '-name' => $cookiename, + '-expires' => 'Tue, 15-Sep-1998 21:49:00 GMT', + '-value' => 'X' + ); } # This helps implement Bugzilla::Search::Recent, and also shortens search # URLs that get POSTed to buglist.cgi. sub redirect_search_url { - my $self = shift; - - # If there is no parameter, there is nothing to do. - return unless $self->param; - - # If we're retreiving an old list, we never need to redirect or - # do anything related to Bugzilla::Search::Recent. - return if $self->param('regetlastlist'); - - my $user = Bugzilla->user; - - if ($user->id) { - # There are two conditions that could happen here--we could get a URL - # with no list id, and we could get a URL with a list_id that isn't - # ours. - my $list_id = $self->param('list_id'); - if ($list_id) { - # If we have a valid list_id, no need to redirect or clean. - return if Bugzilla::Search::Recent->check_quietly( - { id => $list_id }); - } - } - elsif ($self->request_method ne 'POST') { - # Logged-out users who do a GET don't get a list_id, don't get - # their URLs cleaned, and don't get redirected. - return; - } + my $self = shift; - my $no_redirect = $self->param('no_redirect'); - $self->clean_search_url(); - - # Make sure we still have params still after cleaning otherwise we - # do not want to store a list_id for an empty search. - if ($user->id && $self->param) { - # Insert a placeholder Bugzilla::Search::Recent, so that we know what - # the id of the resulting search will be. This is then pulled out - # of the Referer header when viewing show_bug.cgi to know what - # bug list we came from. - my $recent_search = Bugzilla::Search::Recent->create_placeholder; - $self->param('list_id', $recent_search->id); - } + # If there is no parameter, there is nothing to do. + return unless $self->param; + + # If we're retreiving an old list, we never need to redirect or + # do anything related to Bugzilla::Search::Recent. + return if $self->param('regetlastlist'); + + my $user = Bugzilla->user; - # Browsers which support history.replaceState do not need to be - # redirected. We can fix the URL on the fly. - return if $no_redirect; + if ($user->id) { - # GET requests that lacked a list_id are always redirected. POST requests - # are only redirected if they're under the CGI_URI_LIMIT though. - my $self_url = $self->self_url(); - if ($self->request_method() ne 'POST' or length($self_url) < CGI_URI_LIMIT) { - print $self->redirect(-url => $self_url); - exit; + # There are two conditions that could happen here--we could get a URL + # with no list id, and we could get a URL with a list_id that isn't + # ours. + my $list_id = $self->param('list_id'); + if ($list_id) { + + # If we have a valid list_id, no need to redirect or clean. + return if Bugzilla::Search::Recent->check_quietly({id => $list_id}); } + } + elsif ($self->request_method ne 'POST') { + + # Logged-out users who do a GET don't get a list_id, don't get + # their URLs cleaned, and don't get redirected. + return; + } + + my $no_redirect = 1;#$self->param('no_redirect'); + $self->clean_search_url(); + + # Make sure we still have params still after cleaning otherwise we + # do not want to store a list_id for an empty search. + if ($user->id && $self->param) { + + # Insert a placeholder Bugzilla::Search::Recent, so that we know what + # the id of the resulting search will be. This is then pulled out + # of the Referer header when viewing show_bug.cgi to know what + # bug list we came from. + my $recent_search = Bugzilla::Search::Recent->create_placeholder; + $self->param('list_id', $recent_search->id); + } + + # Browsers which support history.replaceState do not need to be + # redirected. We can fix the URL on the fly. + return if $no_redirect; + + # GET requests that lacked a list_id are always redirected. POST requests + # are only redirected if they're under the CGI_URI_LIMIT though. + my $self_url = $self->url(-relative => 1, -path => 1, -query => 1); + if ($self->request_method() ne 'POST' or length($self_url) < CGI_URI_LIMIT) { + print $self->redirect(-url => $self_url); + exit; + } } sub redirect_to_https { - my $self = shift; - my $sslbase = Bugzilla->params->{'sslbase'}; - # If this is a POST, we don't want ?POSTDATA in the query string. - # We expect the client to re-POST, which may be a violation of - # the HTTP spec, but the only time we're expecting it often is - # in the WebService, and WebService clients usually handle this - # correctly. - $self->delete('POSTDATA'); - my $url = $sslbase . $self->url('-path_info' => 1, '-query' => 1, - '-relative' => 1); - - # XML-RPC clients (SOAP::Lite at least) require a 301 to redirect properly - # and do not work with 302. Our redirect really is permanent anyhow, so - # it doesn't hurt to make it a 301. - print $self->redirect(-location => $url, -status => 301); - - # When using XML-RPC with mod_perl, we need the headers sent immediately. - $self->r->rflush if $ENV{MOD_PERL}; - exit; + my $self = shift; + my $sslbase = Bugzilla->params->{'sslbase'}; + + # If this is a POST, we don't want ?POSTDATA in the query string. + # We expect the client to re-POST, which may be a violation of + # the HTTP spec, but the only time we're expecting it often is + # in the WebService, and WebService clients usually handle this + # correctly. + $self->delete('POSTDATA'); + my $url + = $sslbase . $self->url('-path_info' => 1, '-query' => 1, '-relative' => 1); + + # XML-RPC clients (SOAP::Lite at least) require a 301 to redirect properly + # and do not work with 302. Our redirect really is permanent anyhow, so + # it doesn't hurt to make it a 301. + print $self->redirect(-location => $url, -status => 301); + + # When using XML-RPC with mod_perl, we need the headers sent immediately. + $self->r->rflush if $ENV{MOD_PERL}; + exit; } # Redirect to the urlbase version of the current URL. sub redirect_to_urlbase { - my $self = shift; - my $path = $self->url('-path_info' => 1, '-query' => 1, '-relative' => 1); - print $self->redirect('-location' => correct_urlbase() . $path); - exit; + my $self = shift; + my $path = $self->url('-path_info' => 1, '-query' => 1, '-relative' => 1); + print $self->redirect('-location' => correct_urlbase() . $path); + exit; } sub url_is_attachment_base { - my ($self, $id) = @_; - return 0 if !use_attachbase() or !i_am_cgi(); - my $attach_base = Bugzilla->params->{'attachment_base'}; - # If we're passed an id, we only want one specific attachment base - # for a particular bug. If we're not passed an ID, we just want to - # know if our current URL matches the attachment_base *pattern*. - my $regex; - if ($id) { - $attach_base =~ s/\%bugid\%/$id/; - $regex = quotemeta($attach_base); - } - else { - # In this circumstance we run quotemeta first because we need to - # insert an active regex meta-character afterward. - $regex = quotemeta($attach_base); - $regex =~ s/\\\%bugid\\\%/\\d+/; - } - $regex = "^$regex"; - return ($self->url =~ $regex) ? 1 : 0; + my ($self, $id) = @_; + return 0 if !use_attachbase() or !i_am_cgi(); + my $attach_base = Bugzilla->params->{'attachment_base'}; + + # If we're passed an id, we only want one specific attachment base + # for a particular bug. If we're not passed an ID, we just want to + # know if our current URL matches the attachment_base *pattern*. + my $regex; + if ($id) { + $attach_base =~ s/\%bugid\%/$id/; + $regex = quotemeta($attach_base); + } + else { + # In this circumstance we run quotemeta first because we need to + # insert an active regex meta-character afterward. + $regex = quotemeta($attach_base); + $regex =~ s/\\\%bugid\\\%/\\d+/; + } + $regex = "^$regex"; + ## RED HAT EXTENSION START 1657579 + # If behind a proxy you might be on port 80, even though the public url is SSL + $regex =~ s/^https:/https?:/ if (Bugzilla->params->{'inbound_proxies'} ne ''); + ## RED HAT EXTENSION END 1657579 + return ($self->url =~ $regex) ? 1 : 0; } sub set_dated_content_disp { - my ($self, $type, $prefix, $ext) = @_; + my ($self, $type, $prefix, $ext) = @_; - my @time = localtime(time()); - my $date = sprintf "%04d-%02d-%02d", 1900+$time[5], $time[4]+1, $time[3]; - my $filename = "$prefix-$date.$ext"; + my @time = localtime(time()); + my $date = sprintf "%04d-%02d-%02d", 1900 + $time[5], $time[4] + 1, $time[3]; + my $filename = "$prefix-$date.$ext"; - $filename =~ s/\s/_/g; # Remove whitespace to avoid HTTP header tampering - $filename =~ s/\\/_/g; # Remove backslashes as well - $filename =~ s/"/\\"/g; # escape quotes + $filename =~ s/\s/_/g; # Remove whitespace to avoid HTTP header tampering + $filename =~ s/\\/_/g; # Remove backslashes as well + $filename =~ s/"/\\"/g; # escape quotes - my $disposition = "$type; filename=\"$filename\""; + my $disposition = "$type; filename=\"$filename\""; - $self->{'_content_disp'} = $disposition; + $self->{'_content_disp'} = $disposition; } ########################## # Vars TIEHASH Interface # ########################## -# Fix the TIEHASH interface (scalar $cgi->Vars) to return and accept +# Fix the TIEHASH interface (scalar $cgi->Vars) to return and accept # arrayrefs. sub STORE { - my $self = shift; - my ($param, $value) = @_; - if (defined $value and ref $value eq 'ARRAY') { - return $self->param(-name => $param, -value => $value); - } - return $self->SUPER::STORE(@_); + my $self = shift; + my ($param, $value) = @_; + if (defined $value and ref $value eq 'ARRAY') { + return $self->param(-name => $param, -value => $value); + } + return $self->SUPER::STORE(@_); } sub FETCH { - my ($self, $param) = @_; - return $self if $param eq 'CGI'; # CGI.pm did this, so we do too. - my @result = $self->param($param); - return undef if !scalar(@result); - return $result[0] if scalar(@result) == 1; - return \@result; + my ($self, $param) = @_; + return $self if $param eq 'CGI'; # CGI.pm did this, so we do too. + my @result = $self->param($param); + return undef if !scalar(@result); + return $result[0] if scalar(@result) == 1; + return \@result; } -# For the Vars TIEHASH interface: the normal CGI.pm DELETE doesn't return +# For the Vars TIEHASH interface: the normal CGI.pm DELETE doesn't return # the value deleted, but Perl's "delete" expects that value. sub DELETE { - my ($self, $param) = @_; - my $value = $self->FETCH($param); - $self->delete($param); - return $value; + my ($self, $param) = @_; + my $value = $self->FETCH($param); + $self->delete($param); + return $value; } - 1; __END__ @@ -781,6 +893,11 @@ Ends a part of the multipart document, and starts another part. Sets an appropriate date-dependent value for the Content Disposition header for a downloadable resource. + +=item C + +Check if the CGI module on the server and browser both support SameSite cookies. + =back =head1 SEE ALSO @@ -807,4 +924,10 @@ L, L =item header +=item should_restrict_searches + +=item cookie + +=item is_safe_referer + =back diff --git a/Bugzilla/Chart.pm b/Bugzilla/Chart.pm index 3c69006aa..1abf3dbe4 100644 --- a/Bugzilla/Chart.pm +++ b/Bugzilla/Chart.pm @@ -26,405 +26,424 @@ use Date::Parse; use List::Util qw(max); sub new { - my $invocant = shift; - my $class = ref($invocant) || $invocant; - - # Create a ref to an empty hash and bless it - my $self = {}; - bless($self, $class); - - if ($#_ == 0) { - # Construct from a CGI object. - $self->init($_[0]); - } - else { - die("CGI object not passed in - invalid number of args \($#_\)($_)"); - } + my $invocant = shift; + my $class = ref($invocant) || $invocant; + + # Create a ref to an empty hash and bless it + my $self = {}; + bless($self, $class); + + if ($#_ == 0) { - return $self; + # Construct from a CGI object. + $self->init($_[0]); + } + else { + die("CGI object not passed in - invalid number of args \($#_\)($_)"); + } + + return $self; } sub init { - my $self = shift; - my $cgi = shift; - - # The data structure is a list of lists (lines) of Series objects. - # There is a separate list for the labels. - # - # The URL encoding is: - # line0=67&line0=73&line1=81&line2=67... - # &label0=B+/+R+/+CONFIRMED&label1=... - # &select0=1&select3=1... - # &cumulate=1&datefrom=2002-02-03&dateto=2002-04-04&ctype=html... - # >=1&labelgt=Grand+Total - foreach my $param ($cgi->param()) { - # Store all the lines - if ($param =~ /^line(\d+)$/) { - foreach my $series_id ($cgi->param($param)) { - detaint_natural($series_id) - || ThrowCodeError("invalid_series_id"); - my $series = new Bugzilla::Series($series_id); - push(@{$self->{'lines'}[$1]}, $series) if $series; - } - } - - # Store all the labels - if ($param =~ /^label(\d+)$/) { - $self->{'labels'}[$1] = $cgi->param($param); - } - } - - # Store the miscellaneous metadata - $self->{'cumulate'} = $cgi->param('cumulate') ? 1 : 0; - $self->{'gt'} = $cgi->param('gt') ? 1 : 0; - $self->{'labelgt'} = $cgi->param('labelgt'); - $self->{'datefrom'} = $cgi->param('datefrom'); - $self->{'dateto'} = $cgi->param('dateto'); - - # If we are cumulating, a grand total makes no sense - $self->{'gt'} = 0 if $self->{'cumulate'}; - - # Make sure the dates are ones we are able to interpret - foreach my $date ('datefrom', 'dateto') { - if ($self->{$date}) { - $self->{$date} = str2time($self->{$date}) - || ThrowUserError("illegal_date", { date => $self->{$date}}); - } + my $self = shift; + my $cgi = shift; + + # The data structure is a list of lists (lines) of Series objects. + # There is a separate list for the labels. + # + # The URL encoding is: + # line0=67&line0=73&line1=81&line2=67... + # &label0=B+/+R+/+CONFIRMED&label1=... + # &select0=1&select3=1... + # &cumulate=1&datefrom=2002-02-03&dateto=2002-04-04&ctype=html... + # >=1&labelgt=Grand+Total + foreach my $param ($cgi->param()) { + + # Store all the lines + if ($param =~ /^line(\d+)$/) { + foreach my $series_id ($cgi->param($param)) { + detaint_natural($series_id) || ThrowCodeError("invalid_series_id"); + my $series = new Bugzilla::Series($series_id); + push(@{$self->{'lines'}[$1]}, $series) if $series; + } } - # datefrom can't be after dateto - if ($self->{'datefrom'} && $self->{'dateto'} && - $self->{'datefrom'} > $self->{'dateto'}) - { - ThrowUserError('misarranged_dates', { 'datefrom' => scalar $cgi->param('datefrom'), - 'dateto' => scalar $cgi->param('dateto') }); + # Store all the labels + if ($param =~ /^label(\d+)$/) { + $self->{'labels'}[$1] = $cgi->param($param); } + } + + # Store the miscellaneous metadata + $self->{'cumulate'} = $cgi->param('cumulate') ? 1 : 0; + $self->{'gt'} = $cgi->param('gt') ? 1 : 0; + $self->{'labelgt'} = $cgi->param('labelgt'); + $self->{'datefrom'} = $cgi->param('datefrom'); + $self->{'dateto'} = $cgi->param('dateto'); + + # If we are cumulating, a grand total makes no sense + $self->{'gt'} = 0 if $self->{'cumulate'}; + + # Make sure the dates are ones we are able to interpret + foreach my $date ('datefrom', 'dateto') { + if ($self->{$date}) { + $self->{$date} = str2time($self->{$date}) + || ThrowUserError("illegal_date", {date => $self->{$date}}); + } + } + + # datefrom can't be after dateto + if ( $self->{'datefrom'} + && $self->{'dateto'} + && $self->{'datefrom'} > $self->{'dateto'}) + { + ThrowUserError( + 'misarranged_dates', + { + 'datefrom' => scalar $cgi->param('datefrom'), + 'dateto' => scalar $cgi->param('dateto') + } + ); + } } # Alter Chart so that the selected series are added to it. sub add { - my $self = shift; - my @series_ids = @_; - - # Get the current size of the series; required for adding Grand Total later - my $current_size = scalar($self->getSeriesIDs()); - - # Count the number of added series - my $added = 0; - # Create new Series and push them on to the list of lines. - # Note that new lines have no label; the display template is responsible - # for inventing something sensible. - foreach my $series_id (@series_ids) { - my $series = new Bugzilla::Series($series_id); - if ($series) { - push(@{$self->{'lines'}}, [$series]); - push(@{$self->{'labels'}}, ""); - $added++; - } + my $self = shift; + my @series_ids = @_; + + # Get the current size of the series; required for adding Grand Total later + my $current_size = scalar($self->getSeriesIDs()); + + # Count the number of added series + my $added = 0; + + # Create new Series and push them on to the list of lines. + # Note that new lines have no label; the display template is responsible + # for inventing something sensible. + foreach my $series_id (@series_ids) { + my $series = new Bugzilla::Series($series_id); + if ($series) { + push(@{$self->{'lines'}}, [$series]); + push(@{$self->{'labels'}}, ""); + $added++; } - - # If we are going from < 2 to >= 2 series, add the Grand Total line. - if (!$self->{'gt'}) { - if ($current_size < 2 && - $current_size + $added >= 2) - { - $self->{'gt'} = 1; - } + } + + # If we are going from < 2 to >= 2 series, add the Grand Total line. + if (!$self->{'gt'}) { + if ($current_size < 2 && $current_size + $added >= 2) { + $self->{'gt'} = 1; } + } } # Alter Chart so that the selections are removed from it. sub remove { - my $self = shift; - my @line_ids = @_; - - foreach my $line_id (@line_ids) { - if ($line_id == 65536) { - # Magic value - delete Grand Total. - $self->{'gt'} = 0; - } - else { - delete($self->{'lines'}->[$line_id]); - delete($self->{'labels'}->[$line_id]); - } + my $self = shift; + my @line_ids = @_; + + foreach my $line_id (@line_ids) { + if ($line_id == 65536) { + + # Magic value - delete Grand Total. + $self->{'gt'} = 0; + } + else { + delete($self->{'lines'}->[$line_id]); + delete($self->{'labels'}->[$line_id]); } + } } # Alter Chart so that the selections are summed. sub sum { - my $self = shift; - my @line_ids = @_; - - # We can't add the Grand Total to things. - @line_ids = grep(!/^65536$/, @line_ids); - - # We can't add less than two things. - return if scalar(@line_ids) < 2; - - my @series; - my $label = ""; - my $biggestlength = 0; - - # We rescue the Series objects of all the series involved in the sum. - foreach my $line_id (@line_ids) { - my @line = @{$self->{'lines'}->[$line_id]}; - - foreach my $series (@line) { - push(@series, $series); - } - - # We keep the label that labels the line with the most series. - if (scalar(@line) > $biggestlength) { - $biggestlength = scalar(@line); - $label = $self->{'labels'}->[$line_id]; - } + my $self = shift; + my @line_ids = @_; + + # We can't add the Grand Total to things. + @line_ids = grep(!/^65536$/, @line_ids); + + # We can't add less than two things. + return if scalar(@line_ids) < 2; + + my @series; + my $label = ""; + my $biggestlength = 0; + + # We rescue the Series objects of all the series involved in the sum. + foreach my $line_id (@line_ids) { + my @line = @{$self->{'lines'}->[$line_id]}; + + foreach my $series (@line) { + push(@series, $series); + } + + # We keep the label that labels the line with the most series. + if (scalar(@line) > $biggestlength) { + $biggestlength = scalar(@line); + $label = $self->{'labels'}->[$line_id]; } + } - $self->remove(@line_ids); + $self->remove(@line_ids); - push(@{$self->{'lines'}}, \@series); - push(@{$self->{'labels'}}, $label); + push(@{$self->{'lines'}}, \@series); + push(@{$self->{'labels'}}, $label); } sub data { - my $self = shift; - $self->{'_data'} ||= $self->readData(); - return $self->{'_data'}; + my $self = shift; + $self->{'_data'} ||= $self->readData(); + return $self->{'_data'}; } # Convert the Chart's data into a plottable form in $self->{'_data'}. sub readData { - my $self = shift; - my @data; - my @maxvals; - - # Note: you get a bad image if getSeriesIDs returns nothing - # We need to handle errors better. - my $series_ids = join(",", $self->getSeriesIDs()); - - return [] unless $series_ids; - - # Work out the date boundaries for our data. - my $dbh = Bugzilla->dbh; - - # The date used is the one given if it's in a sensible range; otherwise, - # it's the earliest or latest date in the database as appropriate. - my $datefrom = $dbh->selectrow_array("SELECT MIN(series_date) " . - "FROM series_data " . - "WHERE series_id IN ($series_ids)"); - $datefrom = str2time($datefrom); - - if ($self->{'datefrom'} && $self->{'datefrom'} > $datefrom) { - $datefrom = $self->{'datefrom'}; - } - - my $dateto = $dbh->selectrow_array("SELECT MAX(series_date) " . - "FROM series_data " . - "WHERE series_id IN ($series_ids)"); - $dateto = str2time($dateto); - - if ($self->{'dateto'} && $self->{'dateto'} < $dateto) { - $dateto = $self->{'dateto'}; - } - - # Convert UNIX times back to a date format usable for SQL queries. - my $sql_from = time2str('%Y-%m-%d', $datefrom); - my $sql_to = time2str('%Y-%m-%d', $dateto); - - # Prepare the query which retrieves the data for each series - my $query = "SELECT " . $dbh->sql_to_days('series_date') . " - " . - $dbh->sql_to_days('?') . ", series_value " . - "FROM series_data " . - "WHERE series_id = ? " . - "AND series_date >= ?"; - if ($dateto) { - $query .= " AND series_date <= ?"; - } - - my $sth = $dbh->prepare($query); - - my $gt_index = $self->{'gt'} ? scalar(@{$self->{'lines'}}) : undef; - my $line_index = 0; - - $maxvals[$gt_index] = 0 if $gt_index; - - my @datediff_total; - - foreach my $line (@{$self->{'lines'}}) { - # Even if we end up with no data, we need an empty arrayref to prevent - # errors in the PNG-generating code - $data[$line_index] = []; - $maxvals[$line_index] = 0; - - foreach my $series (@$line) { - - # Get the data for this series and add it on - if ($dateto) { - $sth->execute($sql_from, $series->{'series_id'}, $sql_from, $sql_to); - } - else { - $sth->execute($sql_from, $series->{'series_id'}, $sql_from); - } - my $points = $sth->fetchall_arrayref(); - - foreach my $point (@$points) { - my ($datediff, $value) = @$point; - $data[$line_index][$datediff] ||= 0; - $data[$line_index][$datediff] += $value; - if ($data[$line_index][$datediff] > $maxvals[$line_index]) { - $maxvals[$line_index] = $data[$line_index][$datediff]; - } - - $datediff_total[$datediff] += $value; - - # Add to the grand total, if we are doing that - if ($gt_index) { - $data[$gt_index][$datediff] ||= 0; - $data[$gt_index][$datediff] += $value; - if ($data[$gt_index][$datediff] > $maxvals[$gt_index]) { - $maxvals[$gt_index] = $data[$gt_index][$datediff]; - } - } - } + my $self = shift; + my @data; + my @maxvals; + + # Note: you get a bad image if getSeriesIDs returns nothing + # We need to handle errors better. + my $series_ids = join(",", $self->getSeriesIDs()); + + return [] unless $series_ids; + + # Work out the date boundaries for our data. + my $dbh = Bugzilla->dbh; + + # The date used is the one given if it's in a sensible range; otherwise, + # it's the earliest or latest date in the database as appropriate. + my $datefrom + = $dbh->selectrow_array("SELECT MIN(series_date) " + . "FROM series_data " + . "WHERE series_id IN ($series_ids)"); + $datefrom = str2time($datefrom); + + if ($self->{'datefrom'} && $self->{'datefrom'} > $datefrom) { + $datefrom = $self->{'datefrom'}; + } + + my $dateto + = $dbh->selectrow_array("SELECT MAX(series_date) " + . "FROM series_data " + . "WHERE series_id IN ($series_ids)"); + $dateto = str2time($dateto); + + if ($self->{'dateto'} && $self->{'dateto'} < $dateto) { + $dateto = $self->{'dateto'}; + } + + # Convert UNIX times back to a date format usable for SQL queries. + my $sql_from = time2str('%Y-%m-%d', $datefrom); + my $sql_to = time2str('%Y-%m-%d', $dateto); + + # Prepare the query which retrieves the data for each series + my $query + = "SELECT " + . $dbh->sql_to_days('series_date') . " - " + . $dbh->sql_to_days('?') + . ", series_value " + . "FROM series_data " + . "WHERE series_id = ? " + . "AND series_date >= ?"; + if ($dateto) { + $query .= " AND series_date <= ?"; + } + + my $sth = $dbh->prepare($query); + + my $gt_index = $self->{'gt'} ? scalar(@{$self->{'lines'}}) : undef; + my $line_index = 0; + + $maxvals[$gt_index] = 0 if $gt_index; + + my @datediff_total; + + foreach my $line (@{$self->{'lines'}}) { + + # Even if we end up with no data, we need an empty arrayref to prevent + # errors in the PNG-generating code + $data[$line_index] = []; + $maxvals[$line_index] = 0; + + foreach my $series (@$line) { + + # Get the data for this series and add it on + if ($dateto) { + $sth->execute($sql_from, $series->{'series_id'}, $sql_from, $sql_to); + } + else { + $sth->execute($sql_from, $series->{'series_id'}, $sql_from); + } + my $points = $sth->fetchall_arrayref(); + + foreach my $point (@$points) { + my ($datediff, $value) = @$point; + $data[$line_index][$datediff] ||= 0; + $data[$line_index][$datediff] += $value; + if ($data[$line_index][$datediff] > $maxvals[$line_index]) { + $maxvals[$line_index] = $data[$line_index][$datediff]; } - # We are done with the series making up this line, go to the next one - $line_index++; - } + $datediff_total[$datediff] += $value; - # calculate maximum y value - if ($self->{'cumulate'}) { - # Make sure we do not try to take the max of an array with undef values - my @processed_datediff; - while (@datediff_total) { - my $datediff = shift @datediff_total; - push @processed_datediff, $datediff if defined($datediff); + # Add to the grand total, if we are doing that + if ($gt_index) { + $data[$gt_index][$datediff] ||= 0; + $data[$gt_index][$datediff] += $value; + if ($data[$gt_index][$datediff] > $maxvals[$gt_index]) { + $maxvals[$gt_index] = $data[$gt_index][$datediff]; + } } - $self->{'y_max_value'} = max(@processed_datediff); - } - else { - $self->{'y_max_value'} = max(@maxvals); - } - $self->{'y_max_value'} |= 1; # For log() - - # Align the max y value: - # For one- or two-digit numbers, increase y_max_value until divisible by 8 - # For larger numbers, see the comments below to figure out what's going on - if ($self->{'y_max_value'} < 100) { - do { - ++$self->{'y_max_value'}; - } while ($self->{'y_max_value'} % 8 != 0); - } - else { - # First, get the # of digits in the y_max_value - my $num_digits = 1+int(log($self->{'y_max_value'})/log(10)); - - # We want to zero out all but the top 2 digits - my $mask_length = $num_digits - 2; - $self->{'y_max_value'} /= 10**$mask_length; - $self->{'y_max_value'} = int($self->{'y_max_value'}); - $self->{'y_max_value'} *= 10**$mask_length; - - # Add 10^$mask_length to the max value - # Continue to increase until it's divisible by 8 * 10^($mask_length-1) - # (Throwing in the -1 keeps at least the smallest digit at zero) - do { - $self->{'y_max_value'} += 10**$mask_length; - } while ($self->{'y_max_value'} % (8*(10**($mask_length-1))) != 0); + } } - - # Add the x-axis labels into the data structure - my $date_progression = generateDateProgression($datefrom, $dateto); - unshift(@data, $date_progression); + # We are done with the series making up this line, go to the next one + $line_index++; + } - if ($self->{'gt'}) { - # Add Grand Total to label list - push(@{$self->{'labels'}}, $self->{'labelgt'}); + # calculate maximum y value + if ($self->{'cumulate'}) { - $data[$gt_index] ||= []; + # Make sure we do not try to take the max of an array with undef values + my @processed_datediff; + while (@datediff_total) { + my $datediff = shift @datediff_total; + push @processed_datediff, $datediff if defined($datediff); } - - return \@data; + $self->{'y_max_value'} = max(@processed_datediff); + } + else { + $self->{'y_max_value'} = max(@maxvals); + } + $self->{'y_max_value'} |= 1; # For log() + + # Align the max y value: + # For one- or two-digit numbers, increase y_max_value until divisible by 8 + # For larger numbers, see the comments below to figure out what's going on + if ($self->{'y_max_value'} < 100) { + do { + ++$self->{'y_max_value'}; + } while ($self->{'y_max_value'} % 8 != 0); + } + else { + # First, get the # of digits in the y_max_value + my $num_digits = 1 + int(log($self->{'y_max_value'}) / log(10)); + + # We want to zero out all but the top 2 digits + my $mask_length = $num_digits - 2; + $self->{'y_max_value'} /= 10**$mask_length; + $self->{'y_max_value'} = int($self->{'y_max_value'}); + $self->{'y_max_value'} *= 10**$mask_length; + + # Add 10^$mask_length to the max value + # Continue to increase until it's divisible by 8 * 10^($mask_length-1) + # (Throwing in the -1 keeps at least the smallest digit at zero) + do { + $self->{'y_max_value'} += 10**$mask_length; + } while ($self->{'y_max_value'} % (8 * (10**($mask_length - 1))) != 0); + } + + + # Add the x-axis labels into the data structure + my $date_progression = generateDateProgression($datefrom, $dateto); + unshift(@data, $date_progression); + + if ($self->{'gt'}) { + + # Add Grand Total to label list + push(@{$self->{'labels'}}, $self->{'labelgt'}); + + $data[$gt_index] ||= []; + } + + return \@data; } # Flatten the data structure into a list of series_ids sub getSeriesIDs { - my $self = shift; - my @series_ids; + my $self = shift; + my @series_ids; - foreach my $line (@{$self->{'lines'}}) { - foreach my $series (@$line) { - push(@series_ids, $series->{'series_id'}); - } + foreach my $line (@{$self->{'lines'}}) { + foreach my $series (@$line) { + push(@series_ids, $series->{'series_id'}); } + } - return @series_ids; + return @series_ids; } # Class method to get the data necessary to populate the "select series" # widgets on various pages. sub getVisibleSeries { - my %cats; - - my $grouplist = Bugzilla->user->groups_as_string; - - # Get all visible series - my $dbh = Bugzilla->dbh; - my $serieses = $dbh->selectall_arrayref("SELECT cc1.name, cc2.name, " . - "series.name, series.series_id " . - "FROM series " . - "INNER JOIN series_categories AS cc1 " . - " ON series.category = cc1.id " . - "INNER JOIN series_categories AS cc2 " . - " ON series.subcategory = cc2.id " . - "LEFT JOIN category_group_map AS cgm " . - " ON series.category = cgm.category_id " . - " AND cgm.group_id NOT IN($grouplist) " . - "WHERE creator = ? OR (is_public = 1 AND cgm.category_id IS NULL) " . - $dbh->sql_group_by('series.series_id', 'cc1.name, cc2.name, ' . - 'series.name'), - undef, Bugzilla->user->id); - foreach my $series (@$serieses) { - my ($cat, $subcat, $name, $series_id) = @$series; - $cats{$cat}{$subcat}{$name} = $series_id; - } - - return \%cats; + my %cats; + + my $grouplist = Bugzilla->user->groups_as_string; + + # Get all visible series + my $dbh = Bugzilla->dbh; + my $serieses = $dbh->selectall_arrayref( + "SELECT cc1.name, cc2.name, " + . "series.name, series.series_id " + . "FROM series " + . "INNER JOIN series_categories AS cc1 " + . " ON series.category = cc1.id " + . "INNER JOIN series_categories AS cc2 " + . " ON series.subcategory = cc2.id " + . "LEFT JOIN category_group_map AS cgm " + . " ON series.category = cgm.category_id " + . " AND cgm.group_id NOT IN($grouplist) " + . "WHERE creator = ? OR (is_public = 1 AND cgm.category_id IS NULL) " + . $dbh->sql_group_by( + 'series.series_id', 'cc1.name, cc2.name, ' . 'series.name' + ), + undef, + Bugzilla->user->id + ); + foreach my $series (@$serieses) { + my ($cat, $subcat, $name, $series_id) = @$series; + $cats{$cat}{$subcat}{$name} = $series_id; + } + + return \%cats; } sub generateDateProgression { - my ($datefrom, $dateto) = @_; - my @progression; - - $dateto = $dateto || time(); - my $oneday = 60 * 60 * 24; - - # When the from and to dates are converted by str2time(), you end up with - # a time figure representing midnight at the beginning of that day. We - # adjust the times by 1/3 and 2/3 of a day respectively to prevent - # edge conditions in time2str(). - $datefrom += $oneday / 3; - $dateto += (2 * $oneday) / 3; - - while ($datefrom < $dateto) { - push (@progression, time2str("%Y-%m-%d", $datefrom)); - $datefrom += $oneday; - } + my ($datefrom, $dateto) = @_; + my @progression; + + $dateto = $dateto || time(); + my $oneday = 60 * 60 * 24; + + # When the from and to dates are converted by str2time(), you end up with + # a time figure representing midnight at the beginning of that day. We + # adjust the times by 1/3 and 2/3 of a day respectively to prevent + # edge conditions in time2str(). + $datefrom += $oneday / 3; + $dateto += (2 * $oneday) / 3; - return \@progression; + while ($datefrom < $dateto) { + push(@progression, time2str("%Y-%m-%d", $datefrom)); + $datefrom += $oneday; + } + + return \@progression; } sub dump { - my $self = shift; - - # Make sure we've read in our data - my $data = $self->data; - - require Data::Dumper; - say "
Bugzilla::Chart object:";
-    print html_quote(Data::Dumper::Dumper($self));
-    print "
"; + my $self = shift; + + # Make sure we've read in our data + my $data = $self->data; + + require Data::Dumper; + say "
Bugzilla::Chart object:";
+  print html_quote(Data::Dumper::Dumper($self));
+  print "
"; } 1; diff --git a/Bugzilla/Classification.pm b/Bugzilla/Classification.pm index 09f71baaf..1ea86f592 100644 --- a/Bugzilla/Classification.pm +++ b/Bugzilla/Classification.pm @@ -26,26 +26,26 @@ use parent qw(Bugzilla::Field::ChoiceInterface Bugzilla::Object Exporter); use constant IS_CONFIG => 1; -use constant DB_TABLE => 'classifications'; +use constant DB_TABLE => 'classifications'; use constant LIST_ORDER => 'sortkey, name'; use constant DB_COLUMNS => qw( - id - name - description - sortkey + id + name + description + sortkey ); use constant UPDATE_COLUMNS => qw( - name - description - sortkey + name + description + sortkey ); use constant VALIDATORS => { - name => \&_check_name, - description => \&_check_description, - sortkey => \&_check_sortkey, + name => \&_check_name, + description => \&_check_description, + sortkey => \&_check_sortkey, }; ############################### @@ -53,29 +53,31 @@ use constant VALIDATORS => { ############################### sub remove_from_db { - my $self = shift; - my $dbh = Bugzilla->dbh; + my $self = shift; + my $dbh = Bugzilla->dbh; - ThrowUserError("classification_not_deletable") if ($self->id == 1); + ThrowUserError("classification_not_deletable") if ($self->id == 1); - $dbh->bz_start_transaction(); + $dbh->bz_start_transaction(); - # Reclassify products to the default classification, if needed. - my $product_ids = $dbh->selectcol_arrayref( - 'SELECT id FROM products WHERE classification_id = ?', undef, $self->id); - - if (@$product_ids) { - $dbh->do('UPDATE products SET classification_id = 1 WHERE ' - . $dbh->sql_in('id', $product_ids)); - foreach my $id (@$product_ids) { - Bugzilla->memcached->clear({ table => 'products', id => $id }); - } - Bugzilla->memcached->clear_config(); + # Reclassify products to the default classification, if needed. + my $product_ids + = $dbh->selectcol_arrayref( + 'SELECT id FROM products WHERE classification_id = ?', + undef, $self->id); + + if (@$product_ids) { + $dbh->do('UPDATE products SET classification_id = 1 WHERE ' + . $dbh->sql_in('id', $product_ids)); + foreach my $id (@$product_ids) { + Bugzilla->memcached->clear({table => 'products', id => $id}); } + Bugzilla->memcached->clear_config(); + } - $self->SUPER::remove_from_db(); + $self->SUPER::remove_from_db(); - $dbh->bz_commit_transaction(); + $dbh->bz_commit_transaction(); } @@ -84,38 +86,41 @@ sub remove_from_db { ############################### sub _check_name { - my ($invocant, $name) = @_; - - $name = trim($name); - $name || ThrowUserError('classification_not_specified'); - - if (length($name) > MAX_CLASSIFICATION_SIZE) { - ThrowUserError('classification_name_too_long', {'name' => $name}); - } - - my $classification = new Bugzilla::Classification({name => $name}); - if ($classification && (!ref $invocant || $classification->id != $invocant->id)) { - ThrowUserError("classification_already_exists", { name => $classification->name }); - } - return $name; + my ($invocant, $name) = @_; + + $name = trim($name); + $name || ThrowUserError('classification_not_specified'); + + if (length($name) > MAX_CLASSIFICATION_SIZE) { + ThrowUserError('classification_name_too_long', {'name' => $name}); + } + + my $classification = new Bugzilla::Classification({name => $name}); + if ($classification && (!ref $invocant || $classification->id != $invocant->id)) + { + ThrowUserError("classification_already_exists", + {name => $classification->name}); + } + return $name; } sub _check_description { - my ($invocant, $description) = @_; + my ($invocant, $description) = @_; - $description = trim($description || ''); - return $description; + $description = trim($description || ''); + return $description; } sub _check_sortkey { - my ($invocant, $sortkey) = @_; - - $sortkey ||= 0; - my $stored_sortkey = $sortkey; - if (!detaint_natural($sortkey) || $sortkey > MAX_SMALLINT) { - ThrowUserError('classification_invalid_sortkey', { 'sortkey' => $stored_sortkey }); - } - return $sortkey; + my ($invocant, $sortkey) = @_; + + $sortkey ||= 0; + my $stored_sortkey = $sortkey; + if (!detaint_natural($sortkey) || $sortkey > MAX_SMALLINT) { + ThrowUserError('classification_invalid_sortkey', + {'sortkey' => $stored_sortkey}); + } + return $sortkey; } ##################################### @@ -124,41 +129,45 @@ sub _check_sortkey { use constant FIELD_NAME => 'classification'; use constant is_default => 0; -use constant is_active => 1; +use constant is_active => 1; ############################### #### Methods #### ############################### -sub set_name { $_[0]->set('name', $_[1]); } +sub set_name { $_[0]->set('name', $_[1]); } sub set_description { $_[0]->set('description', $_[1]); } -sub set_sortkey { $_[0]->set('sortkey', $_[1]); } +sub set_sortkey { $_[0]->set('sortkey', $_[1]); } sub product_count { - my $self = shift; - my $dbh = Bugzilla->dbh; + my $self = shift; + my $dbh = Bugzilla->dbh; - if (!defined $self->{'product_count'}) { - $self->{'product_count'} = $dbh->selectrow_array(q{ + if (!defined $self->{'product_count'}) { + $self->{'product_count'} = $dbh->selectrow_array( + q{ SELECT COUNT(*) FROM products - WHERE classification_id = ?}, undef, $self->id) || 0; - } - return $self->{'product_count'}; + WHERE classification_id = ?}, undef, $self->id + ) || 0; + } + return $self->{'product_count'}; } sub products { - my $self = shift; - my $dbh = Bugzilla->dbh; + my $self = shift; + my $dbh = Bugzilla->dbh; - if (!$self->{'products'}) { - my $product_ids = $dbh->selectcol_arrayref(q{ + if (!$self->{'products'}) { + my $product_ids = $dbh->selectcol_arrayref( + q{ SELECT id FROM products WHERE classification_id = ? - ORDER BY name}, undef, $self->id); + ORDER BY name}, undef, $self->id + ); - $self->{'products'} = Bugzilla::Product->new_from_list($product_ids); - } - return $self->{'products'}; + $self->{'products'} = Bugzilla::Product->new_from_list($product_ids); + } + return $self->{'products'}; } ############################### @@ -166,7 +175,7 @@ sub products { ############################### sub description { return $_[0]->{'description'}; } -sub sortkey { return $_[0]->{'sortkey'}; } +sub sortkey { return $_[0]->{'sortkey'}; } ############################### @@ -177,27 +186,32 @@ sub sortkey { return $_[0]->{'sortkey'}; } # in global/choose-product.html.tmpl. sub sort_products_by_classification { - my $products = shift; - my $list; - - if (Bugzilla->params->{'useclassification'}) { - my $class = {}; - # Get all classifications with at least one product. - foreach my $product (@$products) { - $class->{$product->classification_id}->{'object'} ||= - new Bugzilla::Classification($product->classification_id); - # Nice way to group products per classification, without querying - # the DB again. - push(@{$class->{$product->classification_id}->{'products'}}, $product); - } - $list = [sort {$a->{'object'}->sortkey <=> $b->{'object'}->sortkey - || lc($a->{'object'}->name) cmp lc($b->{'object'}->name)} - (values %$class)]; - } - else { - $list = [{object => undef, products => $products}]; + my $products = shift; + my $list; + + if (Bugzilla->params->{'useclassification'}) { + my $class = {}; + + # Get all classifications with at least one product. + foreach my $product (@$products) { + $class->{$product->classification_id}->{'object'} + ||= new Bugzilla::Classification($product->classification_id); + + # Nice way to group products per classification, without querying + # the DB again. + push(@{$class->{$product->classification_id}->{'products'}}, $product); } - return $list; + $list = [ + sort { + $a->{'object'}->sortkey <=> $b->{'object'}->sortkey + || lc($a->{'object'}->name) cmp lc($b->{'object'}->name) + } (values %$class) + ]; + } + else { + $list = [{object => undef, products => $products}]; + } + return $list; } 1; diff --git a/Bugzilla/Comment.pm b/Bugzilla/Comment.pm index b036907d7..4dd2f59aa 100644 --- a/Bugzilla/Comment.pm +++ b/Bugzilla/Comment.pm @@ -17,11 +17,12 @@ use Bugzilla::Attachment; use Bugzilla::Comment::TagWeights; use Bugzilla::Constants; use Bugzilla::Error; +use Bugzilla::Hook; use Bugzilla::User; use Bugzilla::Util; use List::Util qw(first); -use Scalar::Util qw(blessed); +use Scalar::Util qw(blessed weaken isweak); ############################### #### Initialization #### @@ -33,47 +34,48 @@ use constant AUDIT_CREATES => 0; use constant AUDIT_UPDATES => 0; use constant DB_COLUMNS => qw( - comment_id - bug_id - who - bug_when - work_time - thetext - isprivate - already_wrapped - type - extra_data + comment_id + bug_id + who + bug_when + work_time + thetext + isprivate + already_wrapped + type + extra_data ); use constant UPDATE_COLUMNS => qw( - isprivate - type - extra_data + isprivate + type + extra_data ); use constant DB_TABLE => 'longdescs'; use constant ID_FIELD => 'comment_id'; + # In some rare cases, two comments can have identical timestamps. If # this happens, we want to be sure that the comment added later shows up # later in the sequence. use constant LIST_ORDER => 'bug_when, comment_id'; use constant VALIDATORS => { - bug_id => \&_check_bug_id, - who => \&_check_who, - bug_when => \&_check_bug_when, - work_time => \&_check_work_time, - thetext => \&_check_thetext, - isprivate => \&_check_isprivate, - extra_data => \&_check_extra_data, - type => \&_check_type, + bug_id => \&_check_bug_id, + who => \&_check_who, + bug_when => \&_check_bug_when, + work_time => \&_check_work_time, + thetext => \&_check_thetext, + isprivate => \&_check_isprivate, + extra_data => \&_check_extra_data, + type => \&_check_type, }; use constant VALIDATOR_DEPENDENCIES => { - extra_data => ['type'], - bug_id => ['who'], - work_time => ['who', 'bug_id'], - isprivate => ['who'], + extra_data => ['type'], + bug_id => ['who'], + work_time => ['who', 'bug_id'], + isprivate => ['who'], }; ######################### @@ -81,95 +83,100 @@ use constant VALIDATOR_DEPENDENCIES => { ######################### sub update { - my $self = shift; - my ($changes, $old_comment) = $self->SUPER::update(@_); - - if (exists $changes->{'thetext'} || exists $changes->{'isprivate'}) { - $self->bug->_sync_fulltext( update_comments => 1); - } - - my @old_tags = @{ $old_comment->tags }; - my @new_tags = @{ $self->tags }; - my ($removed_tags, $added_tags) = diff_arrays(\@old_tags, \@new_tags); - - if (@$removed_tags || @$added_tags) { - my $dbh = Bugzilla->dbh; - my $when = $dbh->selectrow_array("SELECT LOCALTIMESTAMP(0)"); - my $sth_delete = $dbh->prepare( - "DELETE FROM longdescs_tags WHERE comment_id = ? AND tag = ?" - ); - my $sth_insert = $dbh->prepare( - "INSERT INTO longdescs_tags(comment_id, tag) VALUES (?, ?)" - ); - my $sth_activity = $dbh->prepare( - "INSERT INTO longdescs_tags_activity + my $self = shift; + my ($changes, $old_comment) = $self->SUPER::update(@_); + + if (exists $changes->{'thetext'} || exists $changes->{'isprivate'}) { + $self->bug->_sync_fulltext(update_comments => 1); + } + + my @old_tags = @{$old_comment->tags}; + my @new_tags = @{$self->tags}; + my ($removed_tags, $added_tags) = diff_arrays(\@old_tags, \@new_tags); + + if (@$removed_tags || @$added_tags) { + my $dbh = Bugzilla->dbh; + my $when = $dbh->selectrow_array("SELECT LOCALTIMESTAMP(0)"); + my $sth_delete = $dbh->prepare( + "DELETE FROM longdescs_tags WHERE comment_id = ? AND tag = ?"); + my $sth_insert + = $dbh->prepare("INSERT INTO longdescs_tags(comment_id, tag) VALUES (?, ?)"); + my $sth_activity = $dbh->prepare( + "INSERT INTO longdescs_tags_activity (bug_id, comment_id, who, bug_when, added, removed) VALUES (?, ?, ?, ?, ?, ?)" - ); - - foreach my $tag (@$removed_tags) { - my $weighted = Bugzilla::Comment::TagWeights->new({ name => $tag }); - if ($weighted) { - if ($weighted->weight == 1) { - $weighted->remove_from_db(); - } else { - $weighted->set_weight($weighted->weight - 1); - $weighted->update(); - } - } - trick_taint($tag); - $sth_delete->execute($self->id, $tag); - $sth_activity->execute( - $self->bug_id, $self->id, Bugzilla->user->id, $when, '', $tag); - } + ); - foreach my $tag (@$added_tags) { - my $weighted = Bugzilla::Comment::TagWeights->new({ name => $tag }); - if ($weighted) { - $weighted->set_weight($weighted->weight + 1); - $weighted->update(); - } else { - Bugzilla::Comment::TagWeights->create({ tag => $tag, weight => 1 }); - } - trick_taint($tag); - $sth_insert->execute($self->id, $tag); - $sth_activity->execute( - $self->bug_id, $self->id, Bugzilla->user->id, $when, $tag, ''); + foreach my $tag (@$removed_tags) { + my $weighted = Bugzilla::Comment::TagWeights->new({name => $tag}); + if ($weighted) { + if ($weighted->weight == 1) { + $weighted->remove_from_db(); } + else { + $weighted->set_weight($weighted->weight - 1); + $weighted->update(); + } + } + trick_taint($tag); + $sth_delete->execute($self->id, $tag); + $sth_activity->execute($self->bug_id, $self->id, Bugzilla->user->id, $when, '', + $tag); + } + + foreach my $tag (@$added_tags) { + my $weighted = Bugzilla::Comment::TagWeights->new({name => $tag}); + if ($weighted) { + $weighted->set_weight($weighted->weight + 1); + $weighted->update(); + } + else { + Bugzilla::Comment::TagWeights->create({tag => $tag, weight => 1}); + } + trick_taint($tag); + $sth_insert->execute($self->id, $tag); + $sth_activity->execute($self->bug_id, $self->id, Bugzilla->user->id, $when, + $tag, ''); } + } - return $changes; + return $changes; } # Speeds up displays of comment lists by loading all author objects and tags at # once for a whole list. sub preload { - my ($class, $comments) = @_; - # Author - my %user_ids = map { $_->{who} => 1 } @$comments; - my $users = Bugzilla::User->new_from_list([keys %user_ids]); - my %user_map = map { $_->id => $_ } @$users; - foreach my $comment (@$comments) { - $comment->{author} = $user_map{$comment->{who}}; - } - # Tags - if (Bugzilla->params->{'comment_taggers_group'}) { - my $dbh = Bugzilla->dbh; - my @comment_ids = map { $_->id } @$comments; - my %comment_map = map { $_->id => $_ } @$comments; - my $rows = $dbh->selectall_arrayref( - "SELECT comment_id, " . $dbh->sql_group_concat('tag', "','") . " + my ($class, $comments) = @_; + + # Author + my %user_ids = map { $_->{who} => 1 } @$comments; + my $users = Bugzilla::User->new_from_list([keys %user_ids]); + my %user_map = map { $_->id => $_ } @$users; + foreach my $comment (@$comments) { + $comment->{author} = $user_map{$comment->{who}}; + } + + # Tags + if (Bugzilla->params->{'comment_taggers_group'}) { + my $dbh = Bugzilla->dbh; + my @comment_ids = map { $_->id } @$comments; + my %comment_map = map { $_->id => $_ } @$comments; + my $rows = $dbh->selectall_arrayref( + "SELECT comment_id, " . $dbh->sql_group_concat('tag', "','") . " FROM longdescs_tags - WHERE " . $dbh->sql_in('comment_id', \@comment_ids) . ' ' . - $dbh->sql_group_by('comment_id')); - foreach my $row (@$rows) { - $comment_map{$row->[0]}->{tags} = [ split(/,/, $row->[1]) ]; - } - # Also sets the 'tags' attribute for comments which have no entry - # in the longdescs_tags table, else calling $comment->tags will - # trigger another SQL query again. - $comment_map{$_}->{tags} ||= [] foreach @comment_ids; + WHERE " + . $dbh->sql_in('comment_id', \@comment_ids) . ' ' + . $dbh->sql_group_by('comment_id') + ); + foreach my $row (@$rows) { + $comment_map{$row->[0]}->{tags} = [split(/,/, $row->[1])]; } + + # Also sets the 'tags' attribute for comments which have no entry + # in the longdescs_tags table, else calling $comment->tags will + # trigger another SQL query again. + $comment_map{$_}->{tags} ||= [] foreach @comment_ids; + } } ############################### @@ -177,130 +184,139 @@ sub preload { ############################### sub already_wrapped { return $_[0]->{'already_wrapped'}; } -sub body { return $_[0]->{'thetext'}; } -sub bug_id { return $_[0]->{'bug_id'}; } -sub creation_ts { return $_[0]->{'bug_when'}; } -sub is_private { return $_[0]->{'isprivate'}; } -sub work_time { - # Work time is returned as a string (see bug 607909) - return 0 if $_[0]->{'work_time'} + 0 == 0; - return $_[0]->{'work_time'}; +sub body { return $_[0]->{'thetext'}; } +sub bug_id { return $_[0]->{'bug_id'}; } +sub creation_ts { return $_[0]->{'bug_when'}; } +sub is_private { return $_[0]->{'isprivate'}; } + +sub work_time { + + # Work time is returned as a string (see bug 607909) + return 0 if $_[0]->{'work_time'} + 0 == 0; + return $_[0]->{'work_time'}; } -sub type { return $_[0]->{'type'}; } -sub extra_data { return $_[0]->{'extra_data'} } +sub type { return $_[0]->{'type'}; } +sub extra_data { return $_[0]->{'extra_data'} } sub tags { - my ($self) = @_; - state $comment_taggers_group = Bugzilla->params->{'comment_taggers_group'}; - return [] unless $comment_taggers_group; - $self->{'tags'} ||= Bugzilla->dbh->selectcol_arrayref( - "SELECT tag + my ($self) = @_; + state $comment_taggers_group = Bugzilla->params->{'comment_taggers_group'}; + return [] unless $comment_taggers_group; + $self->{'tags'} ||= Bugzilla->dbh->selectcol_arrayref( + "SELECT tag FROM longdescs_tags WHERE comment_id = ? - ORDER BY tag", - undef, $self->id); - return $self->{'tags'}; + ORDER BY tag", undef, $self->id + ); + return $self->{'tags'}; } sub collapsed { - my ($self) = @_; - state $comment_taggers_group = Bugzilla->params->{'comment_taggers_group'}; - return 0 unless $comment_taggers_group; - return $self->{collapsed} if exists $self->{collapsed}; - - state $collapsed_comment_tags = Bugzilla->params->{'collapsed_comment_tags'}; - $self->{collapsed} = 0; - Bugzilla->request_cache->{comment_tags_collapsed} - ||= [ split(/\s*,\s*/, $collapsed_comment_tags) ]; - my @collapsed_tags = @{ Bugzilla->request_cache->{comment_tags_collapsed} }; - foreach my $my_tag (@{ $self->tags }) { - $my_tag = lc($my_tag); - foreach my $collapsed_tag (@collapsed_tags) { - if ($my_tag eq lc($collapsed_tag)) { - $self->{collapsed} = 1; - last; - } - } - last if $self->{collapsed}; + my ($self) = @_; + state $comment_taggers_group = Bugzilla->params->{'comment_taggers_group'}; + return 0 unless $comment_taggers_group; + return $self->{collapsed} if exists $self->{collapsed}; + + state $collapsed_comment_tags = Bugzilla->params->{'collapsed_comment_tags'}; + $self->{collapsed} = 0; + Bugzilla->request_cache->{comment_tags_collapsed} + ||= [split(/\s*,\s*/, $collapsed_comment_tags)]; + my @collapsed_tags = @{Bugzilla->request_cache->{comment_tags_collapsed}}; + foreach my $my_tag (@{$self->tags}) { + $my_tag = lc($my_tag); + foreach my $collapsed_tag (@collapsed_tags) { + if ($my_tag eq lc($collapsed_tag)) { + $self->{collapsed} = 1; + last; + } } - return $self->{collapsed}; + last if $self->{collapsed}; + } + return $self->{collapsed}; } sub bug { - my $self = shift; - require Bugzilla::Bug; - $self->{bug} ||= new Bugzilla::Bug($self->bug_id); - return $self->{bug}; + my $self = shift; + require Bugzilla::Bug; + +# note $bug exists as a strong reference to keep $self->{bug} defined until the end of this method + my $bug = $self->{bug} ||= new Bugzilla::Bug($self->bug_id); + weaken($self->{bug}) unless isweak($self->{bug}); + return $bug; } sub is_about_attachment { - my ($self) = @_; - return 1 if ($self->type == CMT_ATTACHMENT_CREATED - or $self->type == CMT_ATTACHMENT_UPDATED); - return 0; + my ($self) = @_; + return 1 + if ($self->type == CMT_ATTACHMENT_CREATED + or $self->type == CMT_ATTACHMENT_UPDATED); + return 0; } sub attachment { - my ($self) = @_; - return undef if not $self->is_about_attachment; - $self->{attachment} ||= - new Bugzilla::Attachment({ id => $self->extra_data, cache => 1 }); - return $self->{attachment}; + my ($self) = @_; + return undef if not $self->is_about_attachment; + $self->{attachment} + ||= new Bugzilla::Attachment({id => $self->extra_data, cache => 1}); + return $self->{attachment}; } -sub author { - my $self = shift; - $self->{'author'} - ||= new Bugzilla::User({ id => $self->{'who'}, cache => 1 }); - return $self->{'author'}; +sub author { + my $self = shift; + $self->{'author'} ||= new Bugzilla::User({id => $self->{'who'}, cache => 1}); + return $self->{'author'}; } sub body_full { - my ($self, $params) = @_; - $params ||= {}; - my $template = Bugzilla->template_inner; - my $body; - if ($self->type) { - $template->process("bug/format_comment.txt.tmpl", - { comment => $self, %$params }, \$body) - || ThrowTemplateError($template->error()); - $body =~ s/^X//; - } - else { - $body = $self->body; - } - if ($params->{wrap} and !$self->already_wrapped) { - $body = wrap_comment($body); - } - return $body; + my ($self, $params) = @_; + $params ||= {}; + my $template = Bugzilla->template_inner; + my $body; + if ($self->type) { + $template->process("bug/format_comment.txt.tmpl", {comment => $self, %$params}, + \$body) + || ThrowTemplateError($template->error()); + $body =~ s/^X//; + } + else { + $body = $self->body; + } + if ($params->{wrap} and !$self->already_wrapped) { + $body = wrap_comment($body); + } + return $body; } ############ # Mutators # ############ -sub set_is_private { $_[0]->set('isprivate', $_[1]); } -sub set_type { $_[0]->set('type', $_[1]); } -sub set_extra_data { $_[0]->set('extra_data', $_[1]); } +sub set_is_private { $_[0]->set('isprivate', $_[1]); } +sub set_type { $_[0]->set('type', $_[1]); } +sub set_extra_data { $_[0]->set('extra_data', $_[1]); } sub add_tag { - my ($self, $tag) = @_; - $tag = $self->_check_tag($tag); - - my $tags = $self->tags; - return if grep { lc($tag) eq lc($_) } @$tags; - push @$tags, $tag; - $self->{'tags'} = [ sort @$tags ]; + my ($self, $tag) = @_; + $tag = $self->_check_tag($tag); + + my $tags = $self->tags; + return if grep { lc($tag) eq lc($_) } @$tags; + push @$tags, $tag; + $self->{'tags'} = [sort @$tags]; + Bugzilla::Hook::process("comment_after_add_tag", + {comment => $self, tag => $tag}); } sub remove_tag { - my ($self, $tag) = @_; - $tag = $self->_check_tag($tag); - - my $tags = $self->tags; - my $index = first { lc($tags->[$_]) eq lc($tag) } 0..scalar(@$tags) - 1; - return unless defined $index; - splice(@$tags, $index, 1); + my ($self, $tag) = @_; + $tag = $self->_check_tag($tag); + + my $tags = $self->tags; + my $index = first { lc($tags->[$_]) eq lc($tag) } 0 .. scalar(@$tags) - 1; + return unless defined $index; + splice(@$tags, $index, 1); + Bugzilla::Hook::process("comment_after_remove_tag", + {comment => $self, tag => $tag}); } ############## @@ -308,180 +324,180 @@ sub remove_tag { ############## sub run_create_validators { - my $self = shift; - my $params = $self->SUPER::run_create_validators(@_); - # Sometimes this run_create_validators is called with parameters that - # skip bug_id validation, so it might not exist in the resulting hash. - if (defined $params->{bug_id}) { - $params->{bug_id} = $params->{bug_id}->id; - } - return $params; + my $self = shift; + my $params = $self->SUPER::run_create_validators(@_); + + # Sometimes this run_create_validators is called with parameters that + # skip bug_id validation, so it might not exist in the resulting hash. + if (defined $params->{bug_id}) { + $params->{bug_id} = $params->{bug_id}->id; + } + return $params; } sub _check_extra_data { - my ($invocant, $extra_data, undef, $params) = @_; - my $type = blessed($invocant) ? $invocant->type : $params->{type}; + my ($invocant, $extra_data, undef, $params) = @_; + my $type = blessed($invocant) ? $invocant->type : $params->{type}; - if ($type == CMT_NORMAL) { - if (defined $extra_data) { - ThrowCodeError('comment_extra_data_not_allowed', - { type => $type, extra_data => $extra_data }); - } + if ($type == CMT_NORMAL) { + if (defined $extra_data) { + ThrowCodeError('comment_extra_data_not_allowed', + {type => $type, extra_data => $extra_data}); + } + } + else { + if (!defined $extra_data) { + ThrowCodeError('comment_extra_data_required', {type => $type}); + } + elsif ($type == CMT_ATTACHMENT_CREATED or $type == CMT_ATTACHMENT_UPDATED) { + my $attachment = Bugzilla::Attachment->check({id => $extra_data}); + $extra_data = $attachment->id; } else { - if (!defined $extra_data) { - ThrowCodeError('comment_extra_data_required', { type => $type }); - } - elsif ($type == CMT_ATTACHMENT_CREATED - or $type == CMT_ATTACHMENT_UPDATED) - { - my $attachment = Bugzilla::Attachment->check({ - id => $extra_data }); - $extra_data = $attachment->id; - } - else { - my $original = $extra_data; - detaint_natural($extra_data) - or ThrowCodeError('comment_extra_data_not_numeric', - { type => $type, extra_data => $original }); - } + my $original = $extra_data; + detaint_natural($extra_data) + or ThrowCodeError('comment_extra_data_not_numeric', + {type => $type, extra_data => $original}); } + } - return $extra_data; + return $extra_data; } sub _check_type { - my ($invocant, $type) = @_; - $type ||= CMT_NORMAL; - my $original = $type; - detaint_natural($type) - or ThrowCodeError('comment_type_invalid', { type => $original }); - return $type; + my ($invocant, $type) = @_; + $type ||= CMT_NORMAL; + my $original = $type; + detaint_natural($type) + or ThrowCodeError('comment_type_invalid', {type => $original}); + return $type; } sub _check_bug_id { - my ($invocant, $bug_id) = @_; - - ThrowCodeError('param_required', {function => 'Bugzilla::Comment->create', - param => 'bug_id'}) unless $bug_id; - - my $bug; - if (blessed $bug_id) { - # We got a bug object passed in, use it - $bug = $bug_id; - $bug->check_is_visible; - } - else { - # We got a bug id passed in, check it and get the bug object - $bug = Bugzilla::Bug->check({ id => $bug_id }); - } - - # Make sure the user can edit the product - Bugzilla->user->can_edit_product($bug->{product_id}); - - # Make sure the user can comment - my $privs; - $bug->check_can_change_field('longdesc', 0, 1, \$privs) - || ThrowUserError('illegal_change', - { field => 'longdesc', privs => $privs }); - return $bug; + my ($invocant, $bug_id) = @_; + + ThrowCodeError('param_required', + {function => 'Bugzilla::Comment->create', param => 'bug_id'}) + unless $bug_id; + + my $bug; + if (blessed $bug_id) { + + # We got a bug object passed in, use it + $bug = $bug_id; + $bug->check_is_visible; + } + else { + # We got a bug id passed in, check it and get the bug object + $bug = Bugzilla::Bug->check({id => $bug_id}); + } + + # Make sure the user can edit the product + Bugzilla->user->can_edit_product($bug->{product_id}); + + # Make sure the user can comment + my $privs; + $bug->check_can_change_field('longdesc', 0, 1, \$privs) + || ThrowUserError('illegal_change', {field => 'longdesc', privs => $privs}); + return $bug; } sub _check_who { - my ($invocant, $who) = @_; - Bugzilla->login(LOGIN_REQUIRED); - return Bugzilla->user->id; + my ($invocant, $who) = @_; + Bugzilla->login(LOGIN_REQUIRED); + return Bugzilla->user->id; } sub _check_bug_when { - my ($invocant, $when) = @_; + my ($invocant, $when) = @_; - # Make sure the timestamp is defined, default to a timestamp from the db - if (!defined $when) { - $when = Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); - } + # Make sure the timestamp is defined, default to a timestamp from the db + if (!defined $when) { + $when = Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); + } - # Make sure the timestamp parses - if (!datetime_from($when)) { - ThrowCodeError('invalid_timestamp', { timestamp => $when }); - } + # Make sure the timestamp parses + if (!datetime_from($when)) { + ThrowCodeError('invalid_timestamp', {timestamp => $when}); + } - return $when; + return $when; } sub _check_work_time { - my ($invocant, $value_in, $field, $params) = @_; - - # Call down to Bugzilla::Object, letting it know negative - # values are ok - my $time = $invocant->check_time($value_in, $field, $params, 1); - my $privs; - $params->{bug_id}->check_can_change_field('work_time', 0, $time, \$privs) - || ThrowUserError('illegal_change', - { field => 'work_time', privs => $privs }); - return $time; + my ($invocant, $value_in, $field, $params) = @_; + + # Call down to Bugzilla::Object, letting it know negative + # values are ok + my $time = $invocant->check_time($value_in, $field, $params, 1); + my $privs; + $params->{bug_id}->check_can_change_field('work_time', 0, $time, \$privs) + || ThrowUserError('illegal_change', {field => 'work_time', privs => $privs}); + return $time; } sub _check_thetext { - my ($invocant, $thetext) = @_; - - ThrowCodeError('param_required',{function => 'Bugzilla::Comment->create', - param => 'thetext'}) unless defined $thetext; - - # Remove any trailing whitespace. Leading whitespace could be - # a valid part of the comment. - $thetext =~ s/\s*$//s; - $thetext =~ s/\r\n?/\n/g; # Get rid of \r. - - # Characters above U+FFFF cannot be stored by MySQL older than 5.5.3 as they - # require the new utf8mb4 character set. Other DB servers are handling them - # without any problem. So we need to replace these characters if we use MySQL, - # else the comment is truncated. - # XXX - Once we use utf8mb4 for comments, this hack for MySQL can go away. - state $is_mysql = Bugzilla->dbh->isa('Bugzilla::DB::Mysql') ? 1 : 0; - if ($is_mysql) { - # Perl 5.13.8 and older complain about non-characters. - no warnings 'utf8'; - $thetext =~ s/([\x{10000}-\x{10FFFF}])/"\x{FDD0}[" . uc(sprintf('U+%04x', ord($1))) . "]\x{FDD1}"/eg; - } - - ThrowUserError('comment_too_long') if length($thetext) > MAX_COMMENT_LENGTH; - return $thetext; + my ($invocant, $thetext) = @_; + + ThrowCodeError('param_required', + {function => 'Bugzilla::Comment->create', param => 'thetext'}) + unless defined $thetext; + + # Remove any trailing whitespace. Leading whitespace could be + # a valid part of the comment. + $thetext =~ s/\s*$//s; + $thetext =~ s/\r\n?/\n/g; # Get rid of \r. + + # Characters above U+FFFF cannot be stored by MySQL older than 5.5.3 as they + # require the new utf8mb4 character set. Other DB servers are handling them + # without any problem. So we need to replace these characters if we use MySQL, + # else the comment is truncated. + # XXX - Once we use utf8mb4 for comments, this hack for MySQL can go away. + state $is_mysql = Bugzilla->dbh->isa('Bugzilla::DB::Mysql') ? 1 : 0; + if ($is_mysql) { + + # Perl 5.13.8 and older complain about non-characters. + no warnings 'utf8'; + $thetext + =~ s/([\x{10000}-\x{10FFFF}])/"\x{FDD0}[" . uc(sprintf('U+%04x', ord($1))) . "]\x{FDD1}"/eg; + } + + ThrowUserError('comment_too_long') if length($thetext) > MAX_COMMENT_LENGTH; + return $thetext; } sub _check_isprivate { - my ($invocant, $isprivate) = @_; - if ($isprivate && !Bugzilla->user->is_insider) { - ThrowUserError('user_not_insider'); - } - return $isprivate ? 1 : 0; + my ($invocant, $isprivate) = @_; + if ($isprivate && !Bugzilla->user->is_insider) { + ThrowUserError('user_not_insider'); + } + return $isprivate ? 1 : 0; } sub _check_tag { - my ($invocant, $tag) = @_; - length($tag) < MIN_COMMENT_TAG_LENGTH - and ThrowUserError('comment_tag_too_short', { tag => $tag }); - length($tag) > MAX_COMMENT_TAG_LENGTH - and ThrowUserError('comment_tag_too_long', { tag => $tag }); - $tag =~ /^[\w\d\._-]+$/ - or ThrowUserError('comment_tag_invalid', { tag => $tag }); - return $tag; + my ($invocant, $tag) = @_; + length($tag) < MIN_COMMENT_TAG_LENGTH + and ThrowUserError('comment_tag_too_short', {tag => $tag}); + length($tag) > MAX_COMMENT_TAG_LENGTH + and ThrowUserError('comment_tag_too_long', {tag => $tag}); + $tag =~ /^[\w\d\._-]+$/ or ThrowUserError('comment_tag_invalid', {tag => $tag}); + return $tag; } sub count { - my ($self) = @_; + my ($self) = @_; - return $self->{'count'} if defined $self->{'count'}; + return $self->{'count'} if defined $self->{'count'}; - my $dbh = Bugzilla->dbh; - ($self->{'count'}) = $dbh->selectrow_array( - "SELECT COUNT(*) + my $dbh = Bugzilla->dbh; + ($self->{'count'}) = $dbh->selectrow_array( + "SELECT COUNT(*) FROM longdescs WHERE bug_id = ? - AND bug_when <= ?", - undef, $self->bug_id, $self->creation_ts); + AND bug_when <= ?", undef, $self->bug_id, $self->creation_ts + ); - return --$self->{'count'}; + return --$self->{'count'}; } 1; @@ -649,4 +665,6 @@ A string, the full text of the comment as it would be displayed to an end-user. =item update +=item activity + =back diff --git a/Bugzilla/Comment/TagWeights.pm b/Bugzilla/Comment/TagWeights.pm index 7dba53e34..5355cad7f 100644 --- a/Bugzilla/Comment/TagWeights.pm +++ b/Bugzilla/Comment/TagWeights.pm @@ -21,20 +21,20 @@ use constant AUDIT_UPDATES => 0; use constant AUDIT_REMOVES => 0; use constant DB_COLUMNS => qw( - id - tag - weight + id + tag + weight ); use constant UPDATE_COLUMNS => qw( - weight + weight ); use constant DB_TABLE => 'longdescs_tags_weights'; use constant ID_FIELD => 'id'; use constant NAME_FIELD => 'tag'; use constant LIST_ORDER => 'weight DESC'; -use constant VALIDATORS => { }; +use constant VALIDATORS => {}; # There's no gain to caching these objects use constant USE_MEMCACHED => 0; diff --git a/Bugzilla/Component.pm b/Bugzilla/Component.pm index d5a6ece5d..1d80eb8e7 100644 --- a/Bugzilla/Component.pm +++ b/Bugzilla/Component.pm @@ -17,6 +17,8 @@ use Bugzilla::Constants; use Bugzilla::Util; use Bugzilla::Error; use Bugzilla::User; +use Bugzilla::Field; +use Bugzilla::Field::Choice; use Bugzilla::FlagType; use Bugzilla::Series; @@ -27,150 +29,160 @@ use Scalar::Util qw(blessed); ############################### use constant DB_TABLE => 'components'; + # This is mostly for the editfields.cgi case where ->get_all is called. use constant LIST_ORDER => 'product_id, name'; +## REDHAT EXTENSION 876015: Add initialdocscontact use constant DB_COLUMNS => qw( - id - name - product_id - initialowner - initialqacontact - description - isactive + id + name + product_id + initialowner + initialqacontact + initialdocscontact + description + isactive + always_cc ); +## REDHAT EXTENSION 876015: Add initialdocscontact +## REDHAT EXTENSION 1326981: Add bvp_template_id use constant UPDATE_COLUMNS => qw( - name - initialowner - initialqacontact - description - isactive + name + initialowner + initialqacontact + initialdocscontact + description + isactive + always_cc ); -use constant REQUIRED_FIELD_MAP => { - product_id => 'product', -}; +use constant REQUIRED_FIELD_MAP => {product_id => 'product',}; +## REDHAT EXTENSION 876015: Add initialdocscontact use constant VALIDATORS => { - create_series => \&Bugzilla::Object::check_boolean, - product => \&_check_product, - initialowner => \&_check_initialowner, - initialqacontact => \&_check_initialqacontact, - description => \&_check_description, - initial_cc => \&_check_cc_list, - name => \&_check_name, - isactive => \&Bugzilla::Object::check_boolean, + create_series => \&Bugzilla::Object::check_boolean, + product => \&_check_product, + initialowner => \&_check_initialowner, + initialqacontact => \&_check_initialqacontact, + initialdocscontact => \&_check_initialdocscontact, + description => \&_check_description, + initial_cc => \&_check_cc_list, + name => \&_check_name, + isactive => \&Bugzilla::Object::check_boolean, }; -use constant VALIDATOR_DEPENDENCIES => { - name => ['product'], -}; +## REDHAT EXTENSION START 1326559 +VALIDATORS->{always_cc} = \&_check_always_cc; +## REDHAT EXTENSION END 1326559 + +use constant VALIDATOR_DEPENDENCIES => {name => ['product'],}; ############################### sub new { - my $class = shift; - my $param = shift; - my $dbh = Bugzilla->dbh; - - my $product; - if (ref $param and !defined $param->{id}) { - $product = $param->{product}; - my $name = $param->{name}; - if (!defined $product) { - ThrowCodeError('bad_arg', - {argument => 'product', - function => "${class}::new"}); - } - if (!defined $name) { - ThrowCodeError('bad_arg', - {argument => 'name', - function => "${class}::new"}); - } - - my $condition = 'product_id = ? AND name = ?'; - my @values = ($product->id, $name); - $param = { condition => $condition, values => \@values }; + my $class = shift; + my $param = shift; + my $dbh = Bugzilla->dbh; + + my $product; + if (ref $param and !defined $param->{id}) { + $product = $param->{product}; + my $name = $param->{name}; + if (!defined $product) { + ThrowCodeError('bad_arg', {argument => 'product', function => "${class}::new"}); } + if (!defined $name) { + ThrowCodeError('bad_arg', {argument => 'name', function => "${class}::new"}); + } + + my $condition = 'product_id = ? AND name = ?'; + my @values = ($product->id, $name); + $param = {condition => $condition, values => \@values}; + } - unshift @_, $param; - my $component = $class->SUPER::new(@_); - # Add the product object as attribute only if the component exists. - $component->{product} = $product if ($component && $product); - return $component; + unshift @_, $param; + my $component = $class->SUPER::new(@_); + + # Add the product object as attribute only if the component exists. + $component->{product} = $product if ($component && $product); + return $component; } sub create { - my $class = shift; - my $dbh = Bugzilla->dbh; + my $class = shift; + my $dbh = Bugzilla->dbh; - $dbh->bz_start_transaction(); + $dbh->bz_start_transaction(); - $class->check_required_create_fields(@_); - my $params = $class->run_create_validators(@_); - my $cc_list = delete $params->{initial_cc}; - my $create_series = delete $params->{create_series}; - my $product = delete $params->{product}; - $params->{product_id} = $product->id; + $class->check_required_create_fields(@_); + my $params = $class->run_create_validators(@_); + my $cc_list = delete $params->{initial_cc}; + my $create_series = delete $params->{create_series}; + my $product = delete $params->{product}; + $params->{product_id} = $product->id; - my $component = $class->insert_create_data($params); - $component->{product} = $product; + my $component = $class->insert_create_data($params); + $component->{product} = $product; - # We still have to fill the component_cc table. - $component->_update_cc_list($cc_list) if $cc_list; + # We still have to fill the component_cc table. + $component->_update_cc_list($cc_list) if $cc_list; - # Create series for the new component. - $component->_create_series() if $create_series; + # Create series for the new component. + $component->_create_series() if $create_series; - $dbh->bz_commit_transaction(); - return $component; + $dbh->bz_commit_transaction(); + return $component; } sub update { - my $self = shift; - my $changes = $self->SUPER::update(@_); - - # Update the component_cc table if necessary. - if (defined $self->{cc_ids}) { - my $diff = $self->_update_cc_list($self->{cc_ids}); - $changes->{cc_list} = $diff if defined $diff; - } - return $changes; + my $self = shift; + my $changes = $self->SUPER::update(@_); + + # Update the component_cc table if necessary. + if (defined $self->{cc_ids}) { + my $diff = $self->_update_cc_list($self->{cc_ids}); + $changes->{cc_list} = $diff if defined $diff; + } + return $changes; } sub remove_from_db { - my $self = shift; - my $dbh = Bugzilla->dbh; + my $self = shift; + my $dbh = Bugzilla->dbh; - $self->_check_if_controller(); # From ChoiceInterface + $self->_check_if_controller(); # From ChoiceInterface - $dbh->bz_start_transaction(); + $dbh->bz_start_transaction(); - # Products must have at least one component. - my @components = @{ $self->product->components }; - if (scalar(@components) == 1) { - ThrowUserError('component_is_last', { comp => $self }); - } + # Products must have at least one component. + my @components = @{$self->product->components}; + if (scalar(@components) == 1) { + ThrowUserError('component_is_last', {comp => $self}); + } - if ($self->bug_count) { - if (Bugzilla->params->{'allowbugdeletion'}) { - require Bugzilla::Bug; - foreach my $bug_id (@{$self->bug_ids}) { - # Note: We allow admins to delete bugs even if they can't - # see them, as long as they can see the product. - my $bug = new Bugzilla::Bug($bug_id); - $bug->remove_from_db(); - } - } else { - ThrowUserError('component_has_bugs', {nb => $self->bug_count}); - } + if ($self->bug_count) { + if (Bugzilla->params->{'allowbugdeletion'}) { + require Bugzilla::Bug; + foreach my $bug_id (@{$self->bug_ids}) { + + # Note: We allow admins to delete bugs even if they can't + # see them, as long as they can see the product. + my $bug = new Bugzilla::Bug($bug_id); + $bug->remove_from_db(); + } + } + else { + ThrowUserError('component_has_bugs', {nb => $self->bug_count}); } - # Update the list of components in the product object. - $self->product->{components} = [grep { $_->id != $self->id } @components]; - $self->SUPER::remove_from_db(); + } - $dbh->bz_commit_transaction(); + # Update the list of components in the product object. + $self->product->{components} = [grep { $_->id != $self->id } @components]; + $self->SUPER::remove_from_db(); + + $dbh->bz_commit_transaction(); } ################################ @@ -178,226 +190,309 @@ sub remove_from_db { ################################ sub _check_name { - my ($invocant, $name, undef, $params) = @_; - my $product = blessed($invocant) ? $invocant->product : $params->{product}; - - $name = trim($name); - $name || ThrowUserError('component_blank_name'); - - if (length($name) > MAX_COMPONENT_SIZE) { - ThrowUserError('component_name_too_long', {'name' => $name}); - } - - my $component = new Bugzilla::Component({product => $product, name => $name}); - if ($component && (!ref $invocant || $component->id != $invocant->id)) { - ThrowUserError('component_already_exists', { name => $component->name, - product => $product }); - } - return $name; + my ($invocant, $name, undef, $params) = @_; + my $product = blessed($invocant) ? $invocant->product : $params->{product}; + + $name = trim($name); + $name || ThrowUserError('component_blank_name'); + + if (length($name) > MAX_COMPONENT_SIZE) { + ThrowUserError('component_name_too_long', {'name' => $name}); + } + + my $component = new Bugzilla::Component({product => $product, name => $name}); + if ($component && (!ref $invocant || $component->id != $invocant->id)) { + ThrowUserError('component_already_exists', + {name => $component->name, product => $product}); + } + return $name; } sub _check_description { - my ($invocant, $description) = @_; + my ($invocant, $description) = @_; - $description = trim($description); - $description || ThrowUserError('component_blank_description'); - return $description; + $description = trim($description); + $description || ThrowUserError('component_blank_description'); + return $description; } sub _check_initialowner { - my ($invocant, $owner) = @_; + my ($invocant, $owner) = @_; - $owner || ThrowUserError('component_need_initialowner'); - my $owner_id = Bugzilla::User->check($owner)->id; - return $owner_id; + $owner || ThrowUserError('component_need_initialowner'); + my $owner_id = Bugzilla::User->check($owner)->id; + return $owner_id; } sub _check_initialqacontact { - my ($invocant, $qa_contact) = @_; + my ($invocant, $qa_contact) = @_; + + my $qa_contact_id; + if (Bugzilla->params->{'useqacontact'}) { + $qa_contact_id = Bugzilla::User->check($qa_contact)->id if $qa_contact; + } + elsif (ref $invocant) { + $qa_contact_id = $invocant->{initialqacontact}; + } + return $qa_contact_id; +} - my $qa_contact_id; - if (Bugzilla->params->{'useqacontact'}) { - $qa_contact_id = Bugzilla::User->check($qa_contact)->id if $qa_contact; - } - elsif (ref $invocant) { - $qa_contact_id = $invocant->{initialqacontact}; - } - return $qa_contact_id; +## REDHAT EXTENSION START 876015 +sub _check_initialdocscontact { + my ($invocant, $docs_contact) = @_; + + my $docs_contact_id; + if (Bugzilla->params->{'usedocscontact'}) { + $docs_contact_id = Bugzilla::User->check($docs_contact)->id if $docs_contact; + } + elsif (ref $invocant) { + $docs_contact_id = $invocant->{initialdocscontact}; + } + return $docs_contact_id; } +## REDHAT EXTENSION END 876015 sub _check_product { - my ($invocant, $product) = @_; - $product || ThrowCodeError('param_required', - { function => "$invocant->create", param => 'product' }); - return Bugzilla->user->check_can_admin_product($product->name); + my ($invocant, $product) = @_; + $product + || ThrowCodeError('param_required', + {function => "$invocant->create", param => 'product'}); + return Bugzilla->user->check_can_admin_product($product->name); } sub _check_cc_list { - my ($invocant, $cc_list) = @_; + my ($invocant, $cc_list) = @_; + + my %cc_ids; + foreach my $cc (@$cc_list) { + my $id = login_to_id($cc, THROW_ERROR); + $cc_ids{$id} = 1; + } + return [keys %cc_ids]; +} - my %cc_ids; - foreach my $cc (@$cc_list) { - my $id = login_to_id($cc, THROW_ERROR); - $cc_ids{$id} = 1; - } - return [keys %cc_ids]; +## REDHAT EXTENSION START 1326559 +sub _check_always_cc { + my ($invocant, $str) = @_; + + $str = trim($str || ''); + return $str; } +## REDHAT EXTENSION END 1326559 ############################### #### Methods #### ############################### sub _update_cc_list { - my ($self, $cc_list) = @_; - my $dbh = Bugzilla->dbh; - - my $old_cc_list = - $dbh->selectcol_arrayref('SELECT user_id FROM component_cc - WHERE component_id = ?', undef, $self->id); - - my ($removed, $added) = diff_arrays($old_cc_list, $cc_list); - my $diff; - if (scalar @$removed || scalar @$added) { - $diff = [join(', ', @$removed), join(', ', @$added)]; - } - - $dbh->do('DELETE FROM component_cc WHERE component_id = ?', undef, $self->id); - - my $sth = $dbh->prepare('INSERT INTO component_cc - (user_id, component_id) VALUES (?, ?)'); - $sth->execute($_, $self->id) foreach (@$cc_list); - - return $diff; + my ($self, $cc_list) = @_; + my $dbh = Bugzilla->dbh; + + my $old_cc_list = $dbh->selectcol_arrayref( + 'SELECT user_id FROM component_cc + WHERE component_id = ?', undef, $self->id + ); + + my ($removed, $added) = diff_arrays($old_cc_list, $cc_list); + my $diff; + if (scalar @$removed || scalar @$added) { + $diff = [join(', ', @$removed), join(', ', @$added)]; + } + + $dbh->do('DELETE FROM component_cc WHERE component_id = ?', undef, $self->id); + + my $sth = $dbh->prepare( + 'INSERT INTO component_cc + (user_id, component_id) VALUES (?, ?)' + ); + $sth->execute($_, $self->id) foreach (@$cc_list); + + ## REDHAT EXTENSION START 1571412 + $self->audit_log({initialcc => $diff}) + if (defined($diff) && $self->AUDIT_UPDATES); + ## REDHAT EXTENSION END 1571412 + + return $diff; } sub _create_series { - my $self = shift; - - # Insert default charting queries for this product. - # If they aren't using charting, this won't do any harm. - my $prodcomp = "&product=" . url_quote($self->product->name) . - "&component=" . url_quote($self->name); - - my $open_query = 'field0-0-0=resolution&type0-0-0=notregexp&value0-0-0=.' . - $prodcomp; - my $nonopen_query = 'field0-0-0=resolution&type0-0-0=regexp&value0-0-0=.' . - $prodcomp; - - my @series = ([get_text('series_all_open'), $open_query], - [get_text('series_all_closed'), $nonopen_query]); - - foreach my $sdata (@series) { - my $series = new Bugzilla::Series(undef, $self->product->name, - $self->name, $sdata->[0], - Bugzilla->user->id, 1, $sdata->[1], 1); - $series->writeToDatabase(); - } + my $self = shift; + + # Insert default charting queries for this product. + # If they aren't using charting, this won't do any harm. + my $prodcomp + = "&product=" + . url_quote($self->product->name) + . "&component=" + . url_quote($self->name); + + my $open_query + = 'field0-0-0=resolution&type0-0-0=notregexp&value0-0-0=.' . $prodcomp; + my $nonopen_query + = 'field0-0-0=resolution&type0-0-0=regexp&value0-0-0=.' . $prodcomp; + + my @series = ( + [get_text('series_all_open'), $open_query], + [get_text('series_all_closed'), $nonopen_query] + ); + + foreach my $sdata (@series) { + my $series + = new Bugzilla::Series(undef, $self->product->name, $self->name, $sdata->[0], + Bugzilla->user->id, 1, $sdata->[1], 1); + $series->writeToDatabase(); + } } -sub set_name { $_[0]->set('name', $_[1]); } +sub set_name { $_[0]->set('name', $_[1]); } sub set_description { $_[0]->set('description', $_[1]); } -sub set_is_active { $_[0]->set('isactive', $_[1]); } +sub set_is_active { $_[0]->set('isactive', $_[1]); } + sub set_default_assignee { - my ($self, $owner) = @_; + my ($self, $owner) = @_; - $self->set('initialowner', $owner); - # Reset the default owner object. - delete $self->{default_assignee}; + $self->set('initialowner', $owner); + + # Reset the default owner object. + delete $self->{default_assignee}; } + sub set_default_qa_contact { - my ($self, $qa_contact) = @_; + my ($self, $qa_contact) = @_; + + $self->set('initialqacontact', $qa_contact); + + # Reset the default QA contact object. + delete $self->{default_qa_contact}; +} +## REDHAT EXTENSION START 876015 +sub set_default_docs_contact { + my ($self, $docs_contact) = @_; + + $self->set('initialdocscontact', $docs_contact); - $self->set('initialqacontact', $qa_contact); - # Reset the default QA contact object. - delete $self->{default_qa_contact}; + # Reset the default QA contact object. + delete $self->{default_docs_contact}; } +## REDHAT EXTENSION END 876015 + sub set_cc_list { - my ($self, $cc_list) = @_; + my ($self, $cc_list) = @_; + + $self->{cc_ids} = $self->_check_cc_list($cc_list); - $self->{cc_ids} = $self->_check_cc_list($cc_list); - # Reset the list of CC user objects. - delete $self->{initial_cc}; + # Reset the list of CC user objects. + delete $self->{initial_cc}; } sub bug_count { - my $self = shift; - my $dbh = Bugzilla->dbh; - - if (!defined $self->{'bug_count'}) { - $self->{'bug_count'} = $dbh->selectrow_array(q{ - SELECT COUNT(*) FROM bugs - WHERE component_id = ?}, undef, $self->id) || 0; - } - return $self->{'bug_count'}; + my $self = shift; + my $dbh = Bugzilla->dbh; + + if (!defined $self->{'bug_count'}) { + ## REDHAT EXTENSION BEGIN 584957 + $self->{'bug_count'} = $dbh->selectrow_array( + q{ + SELECT COUNT(*) + FROM ( + SELECT bug_id + FROM bugs + WHERE component_id = ? + UNION ALL + SELECT bug_id + FROM rh_multiple_bug_components WHERE component_id = ? + ) AS x}, undef, $self->id, $self->id + ) || 0; + ## REDHAT EXTENSION END 584957 + } + return $self->{'bug_count'}; } sub bug_ids { - my $self = shift; - my $dbh = Bugzilla->dbh; + my $self = shift; + my $dbh = Bugzilla->dbh; - if (!defined $self->{'bugs_ids'}) { - $self->{'bugs_ids'} = $dbh->selectcol_arrayref(q{ + if (!defined $self->{'bugs_ids'}) { + $self->{'bugs_ids'} = $dbh->selectcol_arrayref( + q{ SELECT bug_id FROM bugs - WHERE component_id = ?}, undef, $self->id); - } - return $self->{'bugs_ids'}; + WHERE component_id = ?}, undef, $self->id + ); + } + return $self->{'bugs_ids'}; } sub default_assignee { - my $self = shift; + my $self = shift; - return $self->{'default_assignee'} - ||= new Bugzilla::User({ id => $self->{'initialowner'}, cache => 1 }); + return $self->{'default_assignee'} + ||= new Bugzilla::User({id => $self->{'initialowner'}, cache => 1}); } sub default_qa_contact { - my $self = shift; + my $self = shift; - return unless $self->{'initialqacontact'}; - return $self->{'default_qa_contact'} - ||= new Bugzilla::User({id => $self->{'initialqacontact'}, cache => 1 }); + return unless $self->{'initialqacontact'}; + return $self->{'default_qa_contact'} + ||= new Bugzilla::User({id => $self->{'initialqacontact'}, cache => 1}); } -sub flag_types { - my $self = shift; +## REDHAT EXTENSION START 876015 +sub default_docs_contact { + my $self = shift; - if (!defined $self->{'flag_types'}) { - my $flagtypes = Bugzilla::FlagType::match({ product_id => $self->product_id, - component_id => $self->id }); + return if !$self->{'initialdocscontact'}; - $self->{'flag_types'} = {}; - $self->{'flag_types'}->{'bug'} = - [grep { $_->target_type eq 'bug' } @$flagtypes]; - $self->{'flag_types'}->{'attachment'} = - [grep { $_->target_type eq 'attachment' } @$flagtypes]; - } - return $self->{'flag_types'}; + if (!defined $self->{'default_docs_contact'}) { + $self->{'default_docs_contact'} + = new Bugzilla::User($self->{'initialdocscontact'}); + } + return $self->{'default_docs_contact'}; +} +## REDHAT EXTENSION END 876015 + +sub flag_types { + my $self = shift; + + if (!defined $self->{'flag_types'}) { + my $flagtypes = Bugzilla::FlagType::match( + {product_id => $self->product_id, component_id => $self->id}); + + $self->{'flag_types'} = {}; + $self->{'flag_types'}->{'bug'} + = [grep { $_->target_type eq 'bug' } @$flagtypes]; + $self->{'flag_types'}->{'attachment'} + = [grep { $_->target_type eq 'attachment' } @$flagtypes]; + } + return $self->{'flag_types'}; } sub initial_cc { - my $self = shift; - my $dbh = Bugzilla->dbh; - - if (!defined $self->{'initial_cc'}) { - # If set_cc_list() has been called but data are not yet written - # into the DB, we want the new values defined by it. - my $cc_ids = $self->{cc_ids} - || $dbh->selectcol_arrayref('SELECT user_id FROM component_cc - WHERE component_id = ?', - undef, $self->id); - - $self->{'initial_cc'} = Bugzilla::User->new_from_list($cc_ids); - } - return $self->{'initial_cc'}; + my $self = shift; + my $dbh = Bugzilla->dbh; + + if (!defined $self->{'initial_cc'}) { + + # If set_cc_list() has been called but data are not yet written + # into the DB, we want the new values defined by it. + my $cc_ids = $self->{cc_ids} || $dbh->selectcol_arrayref( + 'SELECT user_id FROM component_cc + WHERE component_id = ?', undef, + $self->id + ); + + $self->{'initial_cc'} = Bugzilla::User->new_from_list($cc_ids); + } + return $self->{'initial_cc'}; } sub product { - my $self = shift; - if (!defined $self->{'product'}) { - require Bugzilla::Product; # We cannot |use| it. - $self->{'product'} = new Bugzilla::Product($self->product_id); - } - return $self->{'product'}; + my $self = shift; + if (!defined $self->{'product'}) { + require Bugzilla::Product; # We cannot |use| it. + $self->{'product'} = new Bugzilla::Product($self->product_id); + } + return $self->{'product'}; } ############################### @@ -405,8 +500,8 @@ sub product { ############################### sub description { return $_[0]->{'description'}; } -sub product_id { return $_[0]->{'product_id'}; } -sub is_active { return $_[0]->{'isactive'}; } +sub product_id { return $_[0]->{'product_id'}; } +sub is_active { return $_[0]->{'isactive'}; } ############################################## # Implement Bugzilla::Field::ChoiceInterface # @@ -416,11 +511,11 @@ use constant FIELD_NAME => 'component'; use constant is_default => 0; sub is_set_on_bug { - my ($self, $bug) = @_; - my $value = blessed($bug) ? $bug->component_id : $bug->{component}; - $value = $value->id if blessed($value); - return 0 unless $value; - return $value == $self->id ? 1 : 0; + my ($self, $bug) = @_; + my $value = blessed($bug) ? $bug->component_id : $bug->{component}; + $value = $value->id if blessed($value); + return 0 unless $value; + return $value == $self->id ? 1 : 0; } ############################### @@ -450,6 +545,7 @@ Bugzilla::Component - Bugzilla product component class. my $product_id = $component->product_id; my $default_assignee = $component->default_assignee; my $default_qa_contact = $component->default_qa_contact; + my $default_docs_contact = $component->default_docs_contact; my $initial_cc = $component->initial_cc; my $product = $component->product; my $bug_flag_types = $component->flag_types->{'bug'}; @@ -462,12 +558,14 @@ Bugzilla::Component - Bugzilla product component class. product => $product, initialowner => $user_login1, initialqacontact => $user_login2, + initialdocscontact => $user_login3, description => $description}); $component->set_name($new_name); $component->set_description($new_description); $component->set_default_assignee($new_login_name); $component->set_default_qa_contact($new_login_name); + $component->set_default_docs_contact($new_login_name); $component->set_cc_list(\@new_login_names); $component->update(); @@ -530,6 +628,16 @@ Component.pm represents a Product Component object. Params: none. + Returns: A Bugzilla::User object if the default Docs contact is defined for + the component. Otherwise, returns undef. + +=item C + + Description: Returns a user object that represents the default Docs contact for + the component. + + Params: none. + Returns: A Bugzilla::User object if the default QA contact is defined for the component. Otherwise, returns undef. @@ -596,6 +704,16 @@ Component.pm represents a Product Component object. Returns: Nothing. +=item C + + Description: Changes the default Docs contact of the component. + + Params: $new_docs_contact - login name of the new Docs contact of + the component (string). This user + account must already exist. + + Returns: Nothing. + =item C Description: Changes the list of users being in the CC list by default. @@ -642,6 +760,8 @@ Component.pm represents a Product Component object. The following keys are optional: initialqacontact - login name of the default QA contact (string), or an empty string to clear it. + initialdocscontact - login name of the default Docs contact (string), + or an empty string to clear it. initial_cc - an arrayref of login names to add to the CC list by default. diff --git a/Bugzilla/Config.pm b/Bugzilla/Config.pm index 458616701..ef528b179 100644 --- a/Bugzilla/Config.pm +++ b/Bugzilla/Config.pm @@ -25,316 +25,322 @@ use File::Basename; # Don't export localvars by default - people should have to explicitly # ask for it, as a (probably futile) attempt to stop code using it # when it shouldn't -%Bugzilla::Config::EXPORT_TAGS = - ( - admin => [qw(update_params SetParam write_params)], - ); +%Bugzilla::Config::EXPORT_TAGS + = (admin => [qw(update_params SetParam write_params)],); Exporter::export_ok_tags('admin'); # INITIALISATION CODE # Perl throws a warning if we use bz_locations() directly after do. our %params; + # Load in the param definitions sub _load_params { - my $panels = param_panels(); - my %hook_panels; - foreach my $panel (keys %$panels) { - my $module = $panels->{$panel}; - eval("require $module") || die $@; - my @new_param_list = $module->get_param_list(); - $hook_panels{lc($panel)} = { params => \@new_param_list }; - } - # This hook is also called in editparams.cgi. This call here is required - # to make SetParam work. - Bugzilla::Hook::process('config_modify_panels', - { panels => \%hook_panels }); - - foreach my $panel (keys %hook_panels) { - foreach my $item (@{$hook_panels{$panel}->{params}}) { - $params{$item->{'name'}} = $item; - } + my $panels = param_panels(); + my %hook_panels; + foreach my $panel (keys %$panels) { + my $module = $panels->{$panel}; + eval("require $module") || die $@; + my @new_param_list = $module->get_param_list(); + $hook_panels{lc($panel)} = {params => \@new_param_list}; + } + + # This hook is also called in editparams.cgi. This call here is required + # to make SetParam work. + Bugzilla::Hook::process('config_modify_panels', {panels => \%hook_panels}); + + foreach my $panel (keys %hook_panels) { + foreach my $item (@{$hook_panels{$panel}->{params}}) { + $params{$item->{'name'}} = $item; } + } } + # END INIT CODE # Subroutines go here sub param_panels { - my $param_panels = {}; - my $libpath = bz_locations()->{'libpath'}; - foreach my $item ((glob "$libpath/Bugzilla/Config/*.pm")) { - $item =~ m#/([^/]+)\.pm$#; - my $module = $1; - $param_panels->{$module} = "Bugzilla::Config::$module" unless $module eq 'Common'; - } - # Now check for any hooked params - Bugzilla::Hook::process('config_add_panels', - { panel_modules => $param_panels }); - return $param_panels; + my $param_panels = {}; + my $libpath = bz_locations()->{'libpath'}; + foreach my $item ((glob "$libpath/Bugzilla/Config/*.pm")) { + $item =~ m#/([^/]+)\.pm$#; + my $module = $1; + $param_panels->{$module} = "Bugzilla::Config::$module" + unless $module eq 'Common'; + } + + # Now check for any hooked params + Bugzilla::Hook::process('config_add_panels', {panel_modules => $param_panels}); + return $param_panels; } sub SetParam { - my ($name, $value) = @_; + my ($name, $value) = @_; - _load_params unless %params; - die "Unknown param $name" unless (exists $params{$name}); + _load_params unless %params; + die "Unknown param $name" unless (exists $params{$name}); - my $entry = $params{$name}; + my $entry = $params{$name}; - # sanity check the value + # sanity check the value - # XXX - This runs the checks. Which would be good, except that - # check_shadowdb creates the database as a side effect, and so the - # checker fails the second time around... - if ($name ne 'shadowdb' && exists $entry->{'checker'}) { - my $err = $entry->{'checker'}->($value, $entry); - die "Param $name is not valid: $err" unless $err eq ''; - } + # XXX - This runs the checks. Which would be good, except that + # check_shadowdb creates the database as a side effect, and so the + # checker fails the second time around... + if ($name ne 'shadowdb' && exists $entry->{'checker'}) { + my $err = $entry->{'checker'}->($value, $entry); + die "Param $name is not valid: $err" unless $err eq ''; + } - Bugzilla->params->{$name} = $value; + Bugzilla->params->{$name} = $value; } sub update_params { - my ($params) = @_; - my $answer = Bugzilla->installation_answers; - my $datadir = bz_locations()->{'datadir'}; - my $param; - - # If the old data/params file using Data::Dumper output still exists, - # read it. It will be deleted once the parameters are stored in the new - # data/params.json file. - my $old_file = "$datadir/params"; - - if (-e $old_file) { - require Safe; - my $s = new Safe; - - $s->rdo($old_file); - die "Error reading $old_file: $!" if $!; - die "Error evaluating $old_file: $@" if $@; - - # Now read the param back out from the sandbox. - $param = \%{ $s->varglob('param') }; + my ($params) = @_; + my $answer = Bugzilla->installation_answers; + my $datadir = bz_locations()->{'datadir'}; + my $param; + + # If the old data/params file using Data::Dumper output still exists, + # read it. It will be deleted once the parameters are stored in the new + # data/params.json file. + my $old_file = "$datadir/params"; + + if (-e $old_file) { + require Safe; + my $s = new Safe; + + $s->rdo($old_file); + die "Error reading $old_file: $!" if $!; + die "Error evaluating $old_file: $@" if $@; + + # Now read the param back out from the sandbox. + $param = \%{$s->varglob('param')}; + } + else { + # Rename params.js to params.json if checksetup.pl + # was executed with an earlier version of this change + rename "$old_file.js", "$old_file.json" + if -e "$old_file.js" && !-e "$old_file.json"; + + # Read the new data/params.json file. + $param = read_param_file(); + } + + my %new_params; + + # If we didn't return any param values, then this is a new installation. + my $new_install = !(keys %$param); + + # --- UPDATE OLD PARAMS --- + + # Change from usebrowserinfo to defaultplatform/defaultopsys combo + if (exists $param->{'usebrowserinfo'}) { + if (!$param->{'usebrowserinfo'}) { + if (!exists $param->{'defaultplatform'}) { + $new_params{'defaultplatform'} = 'Other'; + } + if (!exists $param->{'defaultopsys'}) { + $new_params{'defaultopsys'} = 'Other'; + } } - else { - # Rename params.js to params.json if checksetup.pl - # was executed with an earlier version of this change - rename "$old_file.js", "$old_file.json" - if -e "$old_file.js" && !-e "$old_file.json"; - - # Read the new data/params.json file. - $param = read_param_file(); + } + + # Change from a boolean for quips to multi-state + if (exists $param->{'usequip'} && !exists $param->{'enablequips'}) { + $new_params{'enablequips'} = $param->{'usequip'} ? 'on' : 'off'; + } + + # Change from old product groups to controls for group_control_map + # 2002-10-14 bug 147275 bugreport@peshkin.net + if (exists $param->{'usebuggroups'} && !exists $param->{'makeproductgroups'}) { + $new_params{'makeproductgroups'} = $param->{'usebuggroups'}; + } + + # Modularise auth code + if (exists $param->{'useLDAP'} && !exists $param->{'loginmethod'}) { + $new_params{'loginmethod'} = $param->{'useLDAP'} ? "LDAP" : "DB"; + } + + # set verify method to whatever loginmethod was + if (exists $param->{'loginmethod'} && !exists $param->{'user_verify_class'}) { + $new_params{'user_verify_class'} = $param->{'loginmethod'}; + } + + # Remove quip-display control from parameters + # and give it to users via User Settings (Bug 41972) + if (exists $param->{'enablequips'} + && !exists $param->{'quip_list_entry_control'}) + { + my $new_value; + ($param->{'enablequips'} eq 'on') && do { $new_value = 'open'; }; + ($param->{'enablequips'} eq 'approved') && do { $new_value = 'moderated'; }; + ($param->{'enablequips'} eq 'frozen') && do { $new_value = 'closed'; }; + ($param->{'enablequips'} eq 'off') && do { $new_value = 'closed'; }; + $new_params{'quip_list_entry_control'} = $new_value; + } + + # Old mail_delivery_method choices contained no uppercase characters + my $mta = $param->{'mail_delivery_method'}; + if ($mta) { + if ($mta !~ /[A-Z]/) { + my %translation = ( + 'sendmail' => 'Sendmail', + 'smtp' => 'SMTP', + 'qmail' => 'Qmail', + 'testfile' => 'Test', + 'none' => 'None' + ); + $param->{'mail_delivery_method'} = $translation{$mta}; } - my %new_params; - - # If we didn't return any param values, then this is a new installation. - my $new_install = !(keys %$param); - - # --- UPDATE OLD PARAMS --- - - # Change from usebrowserinfo to defaultplatform/defaultopsys combo - if (exists $param->{'usebrowserinfo'}) { - if (!$param->{'usebrowserinfo'}) { - if (!exists $param->{'defaultplatform'}) { - $new_params{'defaultplatform'} = 'Other'; - } - if (!exists $param->{'defaultopsys'}) { - $new_params{'defaultopsys'} = 'Other'; - } - } + # This will force the parameter to be reset to its default value. + delete $param->{'mail_delivery_method'} + if $param->{'mail_delivery_method'} eq 'Qmail'; + } + + # Convert the old "ssl" parameter to the new "ssl_redirect" parameter. + # Both "authenticated sessions" and "always" turn on "ssl_redirect" + # when upgrading. + if (exists $param->{'ssl'} and $param->{'ssl'} ne 'never') { + $new_params{'ssl_redirect'} = 1; + } + +# "specific_search_allow_empty_words" has been renamed to "search_allow_no_criteria". + if (exists $param->{'specific_search_allow_empty_words'}) { + $new_params{'search_allow_no_criteria'} + = $param->{'specific_search_allow_empty_words'}; + } + + # --- DEFAULTS FOR NEW PARAMS --- + + _load_params unless %params; + foreach my $name (keys %params) { + my $item = $params{$name}; + + ## REDHAT EXTENSION BEGIN + # If you supplied an answers file then use it + if (exists $answer->{$name}) { + $param->{$name} = $answer->{$name}; } - - # Change from a boolean for quips to multi-state - if (exists $param->{'usequip'} && !exists $param->{'enablequips'}) { - $new_params{'enablequips'} = $param->{'usequip'} ? 'on' : 'off'; + ## REDHAT EXTENSION END + + unless (exists $param->{$name}) { + print "New parameter: $name\n" unless $new_install; + if (exists $new_params{$name}) { + $param->{$name} = $new_params{$name}; + } + elsif (exists $answer->{$name}) { + $param->{$name} = $answer->{$name}; + } + else { + $param->{$name} = $item->{'default'}; + } } + } - # Change from old product groups to controls for group_control_map - # 2002-10-14 bug 147275 bugreport@peshkin.net - if (exists $param->{'usebuggroups'} && - !exists $param->{'makeproductgroups'}) - { - $new_params{'makeproductgroups'} = $param->{'usebuggroups'}; - } + $param->{'utf8'} = 1 if $new_install; - # Modularise auth code - if (exists $param->{'useLDAP'} && !exists $param->{'loginmethod'}) { - $new_params{'loginmethod'} = $param->{'useLDAP'} ? "LDAP" : "DB"; - } + # Bug 452525: OR based groups are on by default for new installations + $param->{'or_groups'} = 1 if $new_install; - # set verify method to whatever loginmethod was - if (exists $param->{'loginmethod'} - && !exists $param->{'user_verify_class'}) - { - $new_params{'user_verify_class'} = $param->{'loginmethod'}; - } + # --- REMOVE OLD PARAMS --- - # Remove quip-display control from parameters - # and give it to users via User Settings (Bug 41972) - if ( exists $param->{'enablequips'} - && !exists $param->{'quip_list_entry_control'}) - { - my $new_value; - ($param->{'enablequips'} eq 'on') && do {$new_value = 'open';}; - ($param->{'enablequips'} eq 'approved') && do {$new_value = 'moderated';}; - ($param->{'enablequips'} eq 'frozen') && do {$new_value = 'closed';}; - ($param->{'enablequips'} eq 'off') && do {$new_value = 'closed';}; - $new_params{'quip_list_entry_control'} = $new_value; - } + my %oldparams; - # Old mail_delivery_method choices contained no uppercase characters - my $mta = $param->{'mail_delivery_method'}; - if ($mta) { - if ($mta !~ /[A-Z]/) { - my %translation = ( - 'sendmail' => 'Sendmail', - 'smtp' => 'SMTP', - 'qmail' => 'Qmail', - 'testfile' => 'Test', - 'none' => 'None'); - $param->{'mail_delivery_method'} = $translation{$mta}; - } - # This will force the parameter to be reset to its default value. - delete $param->{'mail_delivery_method'} if $param->{'mail_delivery_method'} eq 'Qmail'; + # Remove any old params + foreach my $item (keys %$param) { + if (!exists $params{$item}) { + $oldparams{$item} = delete $param->{$item}; } - - # Convert the old "ssl" parameter to the new "ssl_redirect" parameter. - # Both "authenticated sessions" and "always" turn on "ssl_redirect" - # when upgrading. - if (exists $param->{'ssl'} and $param->{'ssl'} ne 'never') { - $new_params{'ssl_redirect'} = 1; + } + + # Write any old parameters to old-params.txt + my $old_param_file = "$datadir/old-params.txt"; + if (scalar(keys %oldparams)) { + my $op_file = new IO::File($old_param_file, '>>', 0600) + || die "Couldn't create $old_param_file: $!"; + + print "The following parameters are no longer used in Bugzilla,", + " and so have been\nmoved from your parameters file into", + " $old_param_file:\n"; + + my $comma = ""; + foreach my $item (keys %oldparams) { + print $op_file "\n\n$item:\n" . $oldparams{$item} . "\n"; + print "${comma}$item"; + $comma = ", "; } + print "\n"; + $op_file->close; + } - # "specific_search_allow_empty_words" has been renamed to "search_allow_no_criteria". - if (exists $param->{'specific_search_allow_empty_words'}) { - $new_params{'search_allow_no_criteria'} = $param->{'specific_search_allow_empty_words'}; - } - - # --- DEFAULTS FOR NEW PARAMS --- - - _load_params unless %params; - foreach my $name (keys %params) { - my $item = $params{$name}; - unless (exists $param->{$name}) { - print "New parameter: $name\n" unless $new_install; - if (exists $new_params{$name}) { - $param->{$name} = $new_params{$name}; - } - elsif (exists $answer->{$name}) { - $param->{$name} = $answer->{$name}; - } - else { - $param->{$name} = $item->{'default'}; - } - } - } - - $param->{'utf8'} = 1 if $new_install; + write_params($param); - # Bug 452525: OR based groups are on by default for new installations - $param->{'or_groups'} = 1 if $new_install; - - # --- REMOVE OLD PARAMS --- - - my %oldparams; - # Remove any old params - foreach my $item (keys %$param) { - if (!exists $params{$item}) { - $oldparams{$item} = delete $param->{$item}; - } - } + if (-e $old_file) { + unlink $old_file; + say "$old_file has been converted into $old_file.json, using the JSON format."; + } - # Write any old parameters to old-params.txt - my $old_param_file = "$datadir/old-params.txt"; - if (scalar(keys %oldparams)) { - my $op_file = new IO::File($old_param_file, '>>', 0600) - || die "Couldn't create $old_param_file: $!"; - - print "The following parameters are no longer used in Bugzilla,", - " and so have been\nmoved from your parameters file into", - " $old_param_file:\n"; - - my $comma = ""; - foreach my $item (keys %oldparams) { - print $op_file "\n\n$item:\n" . $oldparams{$item} . "\n"; - print "${comma}$item"; - $comma = ", "; - } - print "\n"; - $op_file->close; - } - - write_params($param); - - if (-e $old_file) { - unlink $old_file; - say "$old_file has been converted into $old_file.json, using the JSON format."; - } - - # Return deleted params and values so that checksetup.pl has a chance - # to convert old params to new data. - return %oldparams; + # Return deleted params and values so that checksetup.pl has a chance + # to convert old params to new data. + return %oldparams; } sub write_params { - my ($param_data) = @_; - $param_data ||= Bugzilla->params; - my $param_file = bz_locations()->{'datadir'} . '/params.json'; + my ($param_data) = @_; + $param_data ||= Bugzilla->params; + my $param_file = bz_locations()->{'datadir'} . '/params.json'; - my $json_data = JSON::XS->new->canonical->pretty->encode($param_data); - write_text($param_file, $json_data); + my $json_data = JSON::XS->new->canonical->pretty->encode($param_data); + write_text($param_file, $json_data); - # It's not common to edit parameters and loading - # Bugzilla::Install::Filesystem is slow. - require Bugzilla::Install::Filesystem; - Bugzilla::Install::Filesystem::fix_file_permissions($param_file); + # It's not common to edit parameters and loading + # Bugzilla::Install::Filesystem is slow. + require Bugzilla::Install::Filesystem; + Bugzilla::Install::Filesystem::fix_file_permissions($param_file); - # And now we have to reset the params cache so that Bugzilla will re-read - # them. - delete Bugzilla->request_cache->{params}; + # And now we have to reset the params cache so that Bugzilla will re-read + # them. + delete Bugzilla->request_cache->{params}; } sub read_param_file { - my %params; - my $file = bz_locations()->{'datadir'} . '/params.json'; - - if (-e $file) { - my $data = read_text($file); - trick_taint($data); - - # If params.json has been manually edited and e.g. some quotes are - # missing, we don't want JSON::XS to leak the content of the file - # to all users in its error message, so we have to eval'uate it. - %params = eval { %{JSON::XS->new->decode($data)} }; - if ($@) { - my $error_msg = (basename($0) eq 'checksetup.pl') ? - $@ : 'run checksetup.pl to see the details.'; - die "Error parsing $file: $error_msg"; - } - # JSON::XS doesn't detaint data for us. - foreach my $key (keys %params) { - if (ref($params{$key}) eq "ARRAY") { - foreach my $item (@{$params{$key}}) { - trick_taint($item); - } - } else { - trick_taint($params{$key}) if defined $params{$key}; - } - } - } - elsif ($ENV{'SERVER_SOFTWARE'}) { - # We're in a CGI, but the params file doesn't exist. We can't - # Template Toolkit, or even install_string, since checksetup - # might not have thrown an error. Bugzilla::CGI->new - # hasn't even been called yet, so we manually use CGI::Carp here - # so that the user sees the error. - require CGI::Carp; - CGI::Carp->import('fatalsToBrowser'); - die "The $file file does not exist." - . ' You probably need to run checksetup.pl.', + my $params; + my $file = bz_locations()->{'datadir'} . '/params.json'; + + if (-e $file) { + my $data = read_text($file); + trick_taint($data); + + # If params.json has been manually edited and e.g. some quotes are + # missing, we don't want JSON::XS to leak the content of the file + # to all users in its error message, so we have to eval'uate it. + + trick_taint($data); + + $params = eval { JSON::XS->new->decode($data) }; + if ($@) { + my $error_msg + = (basename($0) eq 'checksetup.pl') + ? $@ + : 'run checksetup.pl to see the details.'; + die "Error parsing $file: $error_msg"; } - return \%params; + } + elsif ($ENV{'SERVER_SOFTWARE'}) { + + # We're in a CGI, but the params file doesn't exist. We can't + # Template Toolkit, or even install_string, since checksetup + # might not have thrown an error. Bugzilla::CGI->new + # hasn't even been called yet, so we manually use CGI::Carp here + # so that the user sees the error. + require CGI::Carp; + CGI::Carp->import('fatalsToBrowser'); + die "The $file file does not exist." + . ' You probably need to run checksetup.pl.',; + } + return $params // {}; } 1; diff --git a/Bugzilla/Config/Admin.pm b/Bugzilla/Config/Admin.pm index 41d929298..fe19d7cf0 100644 --- a/Bugzilla/Config/Admin.pm +++ b/Bugzilla/Config/Admin.pm @@ -16,32 +16,21 @@ use Bugzilla::Config::Common; our $sortkey = 200; sub get_param_list { - my $class = shift; + my $class = shift; my @param_list = ( - { - name => 'allowbugdeletion', - type => 'b', - default => 0 - }, - - { - name => 'allowemailchange', - type => 'b', - default => 1 - }, - - { - name => 'allowuserdeletion', - type => 'b', - default => 0 - }, - - { - name => 'last_visit_keep_days', - type => 't', - default => 10, - checker => \&check_numeric - }); + {name => 'allowbugdeletion', type => 'b', default => 0}, + + {name => 'allowemailchange', type => 'b', default => 1}, + + {name => 'allowuserdeletion', type => 'b', default => 0}, + + { + name => 'last_visit_keep_days', + type => 't', + default => 10, + checker => \&check_numeric + } + ); return @param_list; } diff --git a/Bugzilla/Config/Advanced.pm b/Bugzilla/Config/Advanced.pm index 8356c3361..043a892d7 100644 --- a/Bugzilla/Config/Advanced.pm +++ b/Bugzilla/Config/Advanced.pm @@ -16,31 +16,18 @@ use Bugzilla::Config::Common; our $sortkey = 1700; use constant get_param_list => ( - { - name => 'cookiedomain', - type => 't', - default => '' - }, + {name => 'cookiedomain', type => 't', default => ''}, - { - name => 'inbound_proxies', - type => 't', - default => '', - checker => \&check_ip - }, + {name => 'inbound_proxies', type => 't', default => '', checker => \&check_ip}, - { - name => 'proxy_url', - type => 't', - default => '' - }, + {name => 'proxy_url', type => 't', default => ''}, { - name => 'strict_transport_security', - type => 's', - choices => ['off', 'this_domain_only', 'include_subdomains'], - default => 'off', - checker => \&check_multi + name => 'strict_transport_security', + type => 's', + choices => ['off', 'this_domain_only', 'include_subdomains'], + default => 'off', + checker => \&check_multi }, ); diff --git a/Bugzilla/Config/Attachment.pm b/Bugzilla/Config/Attachment.pm index 580ec46d9..0cf4b768a 100644 --- a/Bugzilla/Config/Attachment.pm +++ b/Bugzilla/Config/Attachment.pm @@ -16,48 +16,41 @@ use Bugzilla::Config::Common; our $sortkey = 400; sub get_param_list { - my $class = shift; + my $class = shift; my @param_list = ( - { - name => 'allow_attachment_display', - type => 'b', - default => 0 - }, - - { - name => 'attachment_base', - type => 't', - default => '', - checker => \&check_urlbase - }, - - { - name => 'allow_attachment_deletion', - type => 'b', - default => 0 - }, - - { - name => 'maxattachmentsize', - type => 't', - default => '1000', - checker => \&check_maxattachmentsize - }, - - # The maximum size (in bytes) for patches and non-patch attachments. - # The default limit is 1000KB, which is 24KB less than mysql's default - # maximum packet size (which determines how much data can be sent in a - # single mysql packet and thus how much data can be inserted into the - # database) to provide breathing space for the data in other fields of - # the attachment record as well as any mysql packet overhead (I don't - # know of any, but I suspect there may be some.) - - { - name => 'maxlocalattachment', - type => 't', - default => '0', - checker => \&check_numeric - } ); + {name => 'allow_attachment_display', type => 'b', default => 0}, + + { + name => 'attachment_base', + type => 't', + default => '', + checker => \&check_urlbase + }, + + {name => 'allow_attachment_deletion', type => 'b', default => 0}, + + { + name => 'maxattachmentsize', + type => 't', + default => '1000', + checker => \&check_maxattachmentsize + }, + + # The maximum size (in bytes) for patches and non-patch attachments. + # The default limit is 1000KB, which is 24KB less than mysql's default + # maximum packet size (which determines how much data can be sent in a + # single mysql packet and thus how much data can be inserted into the + # database) to provide breathing space for the data in other fields of + # the attachment record as well as any mysql packet overhead (I don't + # know of any, but I suspect there may be some.) + + { + name => 'maxlocalattachment', + type => 't', + default => '0', + checker => \&check_numeric + } + ); return @param_list; } diff --git a/Bugzilla/Config/Auth.pm b/Bugzilla/Config/Auth.pm index 78d719b15..09e81339f 100644 --- a/Bugzilla/Config/Auth.pm +++ b/Bugzilla/Config/Auth.pm @@ -16,111 +16,85 @@ use Bugzilla::Config::Common; our $sortkey = 300; sub get_param_list { - my $class = shift; + my $class = shift; my @param_list = ( - { - name => 'auth_env_id', - type => 't', - default => '', - }, - - { - name => 'auth_env_email', - type => 't', - default => '', - }, - - { - name => 'auth_env_realname', - type => 't', - default => '', - }, - - # XXX in the future: - # - # user_verify_class and user_info_class should have choices gathered from - # whatever sits in their respective directories - # - # rather than comma-separated lists, these two should eventually become - # arrays, but that requires alterations to editparams first - - { - name => 'user_info_class', - type => 's', - choices => [ 'CGI', 'Env', 'Env,CGI' ], - default => 'CGI', - checker => \&check_multi - }, - - { - name => 'user_verify_class', - type => 'o', - choices => [ 'DB', 'RADIUS', 'LDAP' ], - default => 'DB', - checker => \&check_user_verify_class - }, - - { - name => 'rememberlogin', - type => 's', - choices => ['on', 'defaulton', 'defaultoff', 'off'], - default => 'on', - checker => \&check_multi - }, - - { - name => 'requirelogin', - type => 'b', - default => '0' - }, - - { - name => 'webservice_email_filter', - type => 'b', - default => 0 - }, - - { - name => 'emailregexp', - type => 't', - default => q:^[\\w\\.\\+\\-=']+@[\\w\\.\\-]+\\.[\\w\\-]+$:, - checker => \&check_regexp - }, - - { - name => 'emailregexpdesc', - type => 'l', - default => 'A legal address must contain exactly one \'@\', and at least ' . - 'one \'.\' after the @.' - }, - - { - name => 'emailsuffix', - type => 't', - default => '' - }, - - { - name => 'createemailregexp', - type => 't', - default => q:.*:, - checker => \&check_regexp - }, - - { - name => 'password_complexity', - type => 's', - choices => [ 'no_constraints', 'mixed_letters', 'letters_numbers', - 'letters_numbers_specialchars' ], - default => 'no_constraints', - checker => \&check_multi - }, - - { - name => 'password_check_on_login', - type => 'b', - default => '1' - }, + {name => 'auth_env_id', type => 't', default => '',}, + + {name => 'auth_env_email', type => 't', default => '',}, + + {name => 'auth_env_realname', type => 't', default => '',}, + + # XXX in the future: + # + # user_verify_class and user_info_class should have choices gathered from + # whatever sits in their respective directories + # + # rather than comma-separated lists, these two should eventually become + # arrays, but that requires alterations to editparams first + + { + name => 'user_info_class', + type => 's', + choices => ['CGI', 'Env', 'Env,CGI'], + default => 'CGI', + checker => \&check_multi + }, + + { + name => 'user_verify_class', + type => 'o', + choices => ['DB', 'RADIUS', 'LDAP'], + default => 'DB', + checker => \&check_user_verify_class + }, + + { + name => 'rememberlogin', + type => 's', + choices => ['on', 'defaulton', 'defaultoff', 'off'], + default => 'on', + checker => \&check_multi + }, + + {name => 'requirelogin', type => 'b', default => '0'}, + + {name => 'webservice_email_filter', type => 'b', default => 0}, + + { + name => 'emailregexp', + type => 't', + default => q:^[\\w\\.\\+\\-=']+@[\\w\\.\\-]+\\.[\\w\\-]+$:, + checker => \&check_regexp + }, + + { + name => 'emailregexpdesc', + type => 'l', + default => 'A legal address must contain exactly one \'@\', and at least ' + . 'one \'.\' after the @.' + }, + + {name => 'emailsuffix', type => 't', default => ''}, + + { + name => 'createemailregexp', + type => 't', + default => q:.*:, + checker => \&check_regexp + }, + + { + name => 'password_complexity', + type => 's', + choices => [ + 'no_constraints', 'mixed_letters', + 'letters_numbers', 'letters_numbers_specialchars' + ], + default => 'no_constraints', + checker => \&check_multi + }, + + {name => 'password_check_on_login', type => 'b', default => '1'}, ); return @param_list; } diff --git a/Bugzilla/Config/BugChange.pm b/Bugzilla/Config/BugChange.pm index 0acdc0ce4..ad1cafefc 100644 --- a/Bugzilla/Config/BugChange.pm +++ b/Bugzilla/Config/BugChange.pm @@ -26,55 +26,33 @@ sub get_param_list { # and bug_status.is_open is not yet defined (hence the eval), so we use # the bug statuses above as they are still hardcoded. eval { - my @current_closed_states = map {$_->name} closed_bug_statuses(); - # If no closed state was found, use the default list above. - @closed_bug_statuses = @current_closed_states if scalar(@current_closed_states); + my @current_closed_states = map { $_->name } closed_bug_statuses(); + + # If no closed state was found, use the default list above. + @closed_bug_statuses = @current_closed_states if scalar(@current_closed_states); }; my @param_list = ( - { - name => 'duplicate_or_move_bug_status', - type => 's', - choices => \@closed_bug_statuses, - default => $closed_bug_statuses[0], - checker => \&check_bug_status - }, - - { - name => 'letsubmitterchoosepriority', - type => 'b', - default => 1 - }, - - { - name => 'letsubmitterchoosemilestone', - type => 'b', - default => 1 - }, - - { - name => 'musthavemilestoneonaccept', - type => 'b', - default => 0 - }, - - { - name => 'commentonchange_resolution', - type => 'b', - default => 0 - }, - - { - name => 'commentonduplicate', - type => 'b', - default => 0 - }, - - { - name => 'noresolveonopenblockers', - type => 'b', - default => 0, - } ); + { + name => 'duplicate_or_move_bug_status', + type => 's', + choices => \@closed_bug_statuses, + default => $closed_bug_statuses[0], + checker => \&check_bug_status + }, + + {name => 'letsubmitterchoosepriority', type => 'b', default => 1}, + + {name => 'letsubmitterchoosemilestone', type => 'b', default => 1}, + + {name => 'musthavemilestoneonaccept', type => 'b', default => 0}, + + {name => 'commentonchange_resolution', type => 'b', default => 0}, + + {name => 'commentonduplicate', type => 'b', default => 0}, + + {name => 'noresolveonopenblockers', type => 'b', default => 0,} + ); return @param_list; } diff --git a/Bugzilla/Config/BugFields.pm b/Bugzilla/Config/BugFields.pm index ef2faa64b..1659dc66a 100644 --- a/Bugzilla/Config/BugFields.pm +++ b/Bugzilla/Config/BugFields.pm @@ -25,73 +25,50 @@ sub get_param_list { my @legal_OS = @{get_legal_field_values('op_sys')}; my @param_list = ( - { - name => 'useclassification', - type => 'b', - default => 0 - }, - - { - name => 'usetargetmilestone', - type => 'b', - default => 0 - }, - - { - name => 'useqacontact', - type => 'b', - default => 0 - }, - - { - name => 'usestatuswhiteboard', - type => 'b', - default => 0 - }, - - { - name => 'use_see_also', - type => 'b', - default => 1 - }, - - { - name => 'defaultpriority', - type => 's', - choices => \@legal_priorities, - default => $legal_priorities[-1], - checker => \&check_priority - }, - - { - name => 'defaultseverity', - type => 's', - choices => \@legal_severities, - default => $legal_severities[-1], - checker => \&check_severity - }, - - { - name => 'defaultplatform', - type => 's', - choices => ['', @legal_platforms], - default => '', - checker => \&check_platform - }, - - { - name => 'defaultopsys', - type => 's', - choices => ['', @legal_OS], - default => '', - checker => \&check_opsys - }, - - { - name => 'collapsed_comment_tags', - type => 't', - default => 'obsolete, spam', - }); + {name => 'useclassification', type => 'b', default => 0}, + + {name => 'usetargetmilestone', type => 'b', default => 0}, + + {name => 'useqacontact', type => 'b', default => 0}, + + {name => 'usestatuswhiteboard', type => 'b', default => 0}, + + {name => 'use_see_also', type => 'b', default => 1}, + + { + name => 'defaultpriority', + type => 's', + choices => \@legal_priorities, + default => $legal_priorities[-1], + checker => \&check_priority + }, + + { + name => 'defaultseverity', + type => 's', + choices => \@legal_severities, + default => $legal_severities[-1], + checker => \&check_severity + }, + + { + name => 'defaultplatform', + type => 's', + choices => ['', @legal_platforms], + default => '', + checker => \&check_platform + }, + + { + name => 'defaultopsys', + type => 's', + choices => ['', @legal_OS], + default => '', + checker => \&check_opsys + }, + + {name => 'collapsed_comment_tags', type => 't', default => 'obsolete, spam',} + ); return @param_list; } diff --git a/Bugzilla/Config/Common.pm b/Bugzilla/Config/Common.pm index bd9b0bf84..a0077b5b2 100644 --- a/Bugzilla/Config/Common.pm +++ b/Bugzilla/Config/Common.pm @@ -21,392 +21,406 @@ use Bugzilla::Group; use Bugzilla::Status; use parent qw(Exporter); -@Bugzilla::Config::Common::EXPORT = - qw(check_multi check_numeric check_regexp check_url check_group - check_sslbase check_priority check_severity check_platform - check_opsys check_shadowdb check_urlbase check_webdotbase - check_user_verify_class check_ip check_font_file - check_mail_delivery_method check_notification check_utf8 - check_bug_status check_smtp_auth check_theschwartz_available - check_maxattachmentsize check_email check_smtp_ssl - check_comment_taggers_group check_smtp_server +@Bugzilla::Config::Common::EXPORT + = qw(check_multi check_numeric check_regexp check_url check_group + check_sslbase check_priority check_severity check_platform + check_opsys check_shadowdb check_urlbase check_webdotbase + check_user_verify_class check_ip check_font_file + check_mail_delivery_method check_notification check_utf8 + check_bug_status check_smtp_auth check_theschwartz_available + check_maxattachmentsize check_email check_smtp_ssl + check_comment_taggers_group check_smtp_server ); # Checking functions for the various values sub check_multi { - my ($value, $param) = (@_); + my ($value, $param) = (@_); - if ($param->{'type'} eq "s") { - unless (scalar(grep {$_ eq $value} (@{$param->{'choices'}}))) { - return "Invalid choice '$value' for single-select list param '$param->{'name'}'"; - } - - return ""; + if ($param->{'type'} eq "s") { + unless (scalar(grep { $_ eq $value } (@{$param->{'choices'}}))) { + return + "Invalid choice '$value' for single-select list param '$param->{'name'}'"; } - elsif ($param->{'type'} eq 'm' || $param->{'type'} eq 'o') { - if (ref($value) ne "ARRAY") { - $value = [split(',', $value)] - } - foreach my $chkParam (@$value) { - unless (scalar(grep {$_ eq $chkParam} (@{$param->{'choices'}}))) { - return "Invalid choice '$chkParam' for multi-select list param '$param->{'name'}'"; - } - } - - return ""; + + return ""; + } + elsif ($param->{'type'} eq 'm' || $param->{'type'} eq 'o') { + if (ref($value) ne "ARRAY") { + $value = [split(',', $value)]; } - else { - return "Invalid param type '$param->{'type'}' for check_multi(); " . - "contact your Bugzilla administrator"; + foreach my $chkParam (@$value) { + unless (scalar(grep { $_ eq $chkParam } (@{$param->{'choices'}}))) { + return + "Invalid choice '$chkParam' for multi-select list param '$param->{'name'}'"; + } } + + return ""; + } + else { + return "Invalid param type '$param->{'type'}' for check_multi(); " + . "contact your Bugzilla administrator"; + } } sub check_numeric { - my ($value) = (@_); - if ($value !~ /^[0-9]+$/) { - return "must be a numeric value"; - } - return ""; + my ($value) = (@_); + if ($value !~ /^[0-9]+$/) { + return "must be a numeric value"; + } + return ""; } sub check_regexp { - my ($value) = (@_); - eval { qr/$value/ }; - return $@; + my ($value) = (@_); + eval {qr/$value/}; + return $@; } sub check_email { - my ($value) = @_; - if ($value !~ $Email::Address::mailbox) { - return "must be a valid email address."; - } - return ""; + my ($value) = @_; + if ($value !~ $Email::Address::mailbox) { + return "must be a valid email address."; + } + return ""; } sub check_sslbase { - my $url = shift; - if ($url ne '') { - if ($url !~ m#^https://([^/]+).*/$#) { - return "must be a legal URL, that starts with https and ends with a slash."; - } - my $host = $1; - # Fall back to port 443 if for some reason getservbyname() fails. - my $port = getservbyname('https', 'tcp') || 443; - if ($host =~ /^(.+):(\d+)$/) { - $host = $1; - $port = $2; - } - local *SOCK; - my $proto = getprotobyname('tcp'); - socket(SOCK, PF_INET, SOCK_STREAM, $proto); - my $iaddr = inet_aton($host) || return "The host $host cannot be resolved"; - my $sin = sockaddr_in($port, $iaddr); - if (!connect(SOCK, $sin)) { - return "Failed to connect to $host:$port ($!); unable to enable SSL"; - } - close(SOCK); - } - return ""; + my $url = shift; + if ($url ne '') { + if ($url !~ m#^https://([^/]+).*/$#) { + return "must be a legal URL, that starts with https and ends with a slash."; + } + my $host = $1; + + # Fall back to port 443 if for some reason getservbyname() fails. + my $port = getservbyname('https', 'tcp') || 443; + if ($host =~ /^(.+):(\d+)$/) { + $host = $1; + $port = $2; + } + local *SOCK; + my $proto = getprotobyname('tcp'); + socket(SOCK, PF_INET, SOCK_STREAM, $proto); + my $iaddr = inet_aton($host) || return "The host $host cannot be resolved"; + my $sin = sockaddr_in($port, $iaddr); + if (!connect(SOCK, $sin)) { + return "Failed to connect to $host:$port ($!); unable to enable SSL"; + } + close(SOCK); + } + return ""; } sub check_ip { - my $inbound_proxies = shift; - my @proxies = split(/[\s,]+/, $inbound_proxies); - foreach my $proxy (@proxies) { - validate_ip($proxy) || return "$proxy is not a valid IPv4 or IPv6 address"; - } - return ""; + my $inbound_proxies = shift; + my @proxies = split(/[\s,]+/, $inbound_proxies); + foreach my $proxy (@proxies) { + validate_ip($proxy) || return "$proxy is not a valid IPv4 or IPv6 address"; + } + return ""; } sub check_utf8 { - my $utf8 = shift; - # You cannot turn off the UTF-8 parameter if you've already converted - # your tables to utf-8. - my $dbh = Bugzilla->dbh; - if ($dbh->isa('Bugzilla::DB::Mysql') && $dbh->bz_db_is_utf8 && !$utf8) { - return "You cannot disable UTF-8 support, because your MySQL database" - . " is encoded in UTF-8"; - } - return ""; + my $utf8 = shift; + + # You cannot turn off the UTF-8 parameter if you've already converted + # your tables to utf-8. + my $dbh = Bugzilla->dbh; + if ($dbh->isa('Bugzilla::DB::Mysql') && $dbh->bz_db_is_utf8 && !$utf8) { + return "You cannot disable UTF-8 support, because your MySQL database" + . " is encoded in UTF-8"; + } + return ""; } sub check_priority { - my ($value) = (@_); - my $legal_priorities = get_legal_field_values('priority'); - if (!grep($_ eq $value, @$legal_priorities)) { - return "Must be a legal priority value: one of " . - join(", ", @$legal_priorities); - } - return ""; + my ($value) = (@_); + my $legal_priorities = get_legal_field_values('priority'); + if (!grep($_ eq $value, @$legal_priorities)) { + return "Must be a legal priority value: one of " + . join(", ", @$legal_priorities); + } + return ""; } sub check_severity { - my ($value) = (@_); - my $legal_severities = get_legal_field_values('bug_severity'); - if (!grep($_ eq $value, @$legal_severities)) { - return "Must be a legal severity value: one of " . - join(", ", @$legal_severities); - } - return ""; + my ($value) = (@_); + my $legal_severities = get_legal_field_values('bug_severity'); + if (!grep($_ eq $value, @$legal_severities)) { + return "Must be a legal severity value: one of " + . join(", ", @$legal_severities); + } + return ""; } sub check_platform { - my ($value) = (@_); - my $legal_platforms = get_legal_field_values('rep_platform'); - if (!grep($_ eq $value, '', @$legal_platforms)) { - return "Must be empty or a legal platform value: one of " . - join(", ", @$legal_platforms); - } - return ""; + my ($value) = (@_); + my $legal_platforms = get_legal_field_values('rep_platform'); + if (!grep($_ eq $value, '', @$legal_platforms)) { + return "Must be empty or a legal platform value: one of " + . join(", ", @$legal_platforms); + } + return ""; } sub check_opsys { - my ($value) = (@_); - my $legal_OS = get_legal_field_values('op_sys'); - if (!grep($_ eq $value, '', @$legal_OS)) { - return "Must be empty or a legal operating system value: one of " . - join(", ", @$legal_OS); - } - return ""; + my ($value) = (@_); + my $legal_OS = get_legal_field_values('op_sys'); + if (!grep($_ eq $value, '', @$legal_OS)) { + return "Must be empty or a legal operating system value: one of " + . join(", ", @$legal_OS); + } + return ""; } sub check_bug_status { - my $bug_status = shift; - my @closed_bug_statuses = map {$_->name} closed_bug_statuses(); - if (!grep($_ eq $bug_status, @closed_bug_statuses)) { - return "Must be a valid closed status: one of " . join(', ', @closed_bug_statuses); - } - return ""; + my $bug_status = shift; + my @closed_bug_statuses = map { $_->name } closed_bug_statuses(); + if (!grep($_ eq $bug_status, @closed_bug_statuses)) { + return "Must be a valid closed status: one of " + . join(', ', @closed_bug_statuses); + } + return ""; } sub check_group { - my $group_name = shift; - return "" unless $group_name; - my $group = new Bugzilla::Group({'name' => $group_name}); - unless (defined $group) { - return "Must be an existing group name"; - } - return ""; + my $group_name = shift; + return "" unless $group_name; + my $group = new Bugzilla::Group({'name' => $group_name}); + unless (defined $group) { + return "Must be an existing group name"; + } + return ""; } sub check_shadowdb { - my ($value) = (@_); - $value = trim($value); - if ($value eq "") { - return ""; - } + my ($value) = (@_); + $value = trim($value); + if ($value eq "") { + return ""; + } - if (!Bugzilla->params->{'shadowdbhost'}) { - return "You need to specify a host when using a shadow database"; - } + if (!Bugzilla->params->{'shadowdbhost'}) { + return "You need to specify a host when using a shadow database"; + } - # Can't test existence of this because ConnectToDatabase uses the param, - # but we can't set this before testing.... - # This can really only be fixed after we can use the DBI more openly - return ""; + # Can't test existence of this because ConnectToDatabase uses the param, + # but we can't set this before testing.... + # This can really only be fixed after we can use the DBI more openly + return ""; } sub check_urlbase { - my ($url) = (@_); - if ($url && $url !~ m:^http.*/$:) { - return "must be a legal URL, that starts with http and ends with a slash."; - } - return ""; + my ($url) = (@_); + if ($url && $url !~ m:^http.*/$:) { + return "must be a legal URL, that starts with http and ends with a slash."; + } + return ""; } sub check_url { - my ($url) = (@_); - return '' if $url eq ''; # Allow empty URLs - if ($url !~ m:/$:) { - return 'must be a legal URL, absolute or relative, ending with a slash.'; - } - return ''; + my ($url) = (@_); + return '' if $url eq ''; # Allow empty URLs + if ($url !~ m:/$:) { + return 'must be a legal URL, absolute or relative, ending with a slash.'; + } + return ''; } sub check_webdotbase { - my ($value) = (@_); - $value = trim($value); - if ($value eq "") { - return ""; - } - if($value !~ /^https?:/) { - if(! -x $value) { - return "The file path \"$value\" is not a valid executable. Please specify the complete file path to 'dot' if you intend to generate graphs locally."; - } - # Check .htaccess allows access to generated images - my $webdotdir = bz_locations()->{'webdotdir'}; - if(-e "$webdotdir/.htaccess") { - open HTACCESS, "<", "$webdotdir/.htaccess"; - if(! grep(/ \\\.png\$/,)) { - return "Dependency graph images are not accessible.\nAssuming that you have not modified the file, delete $webdotdir/.htaccess and re-run checksetup.pl to rectify.\n"; - } - close HTACCESS; - } - } + my ($value) = (@_); + $value = trim($value); + if ($value eq "") { return ""; + } + if ($value !~ /^https?:/) { + if (!-x $value) { + return + "The file path \"$value\" is not a valid executable. Please specify the complete file path to 'dot' if you intend to generate graphs locally."; + } + + # Check .htaccess allows access to generated images + my $webdotdir = bz_locations()->{'webdotdir'}; + if (-e "$webdotdir/.htaccess") { + open HTACCESS, "<", "$webdotdir/.htaccess"; + if (!grep(/ \\\.png\$/, )) { + return + "Dependency graph images are not accessible.\nAssuming that you have not modified the file, delete $webdotdir/.htaccess and re-run checksetup.pl to rectify.\n"; + } + close HTACCESS; + } + } + return ""; } sub check_font_file { - my ($font) = @_; - $font = trim($font); - return '' unless $font; - - if ($font !~ /\.(ttf|otf)$/) { - return "The file must point to a TrueType or OpenType font file (its extension must be .ttf or .otf)" - } - if (! -f $font) { - return "The file '$font' cannot be found. Make sure you typed the full path to the file" - } - return ''; + my ($font) = @_; + $font = trim($font); + return '' unless $font; + + if ($font !~ /\.(ttf|otf)$/) { + return + "The file must point to a TrueType or OpenType font file (its extension must be .ttf or .otf)"; + } + if (!-f $font) { + return + "The file '$font' cannot be found. Make sure you typed the full path to the file"; + } + return ''; } sub check_user_verify_class { - # doeditparams traverses the list of params, and for each one it checks, - # then updates. This means that if one param checker wants to look at - # other params, it must be below that other one. So you can't have two - # params mutually dependent on each other. - # This means that if someone clears the LDAP config params after setting - # the login method as LDAP, we won't notice, but all logins will fail. - # So don't do that. - - my $params = Bugzilla->params; - my ($list, $entry) = @_; - $list || return 'You need to specify at least one authentication mechanism'; - for my $class (split /,\s*/, $list) { - my $res = check_multi($class, $entry); - return $res if $res; - if ($class eq 'RADIUS') { - if (!Bugzilla->feature('auth_radius')) { - return "RADIUS support is not available. Run checksetup.pl" - . " for more details"; - } - return "RADIUS servername (RADIUS_server) is missing" - if !$params->{"RADIUS_server"}; - return "RADIUS_secret is empty" if !$params->{"RADIUS_secret"}; - } - elsif ($class eq 'LDAP') { - if (!Bugzilla->feature('auth_ldap')) { - return "LDAP support is not available. Run checksetup.pl" - . " for more details"; - } - return "LDAP servername (LDAPserver) is missing" - if !$params->{"LDAPserver"}; - return "LDAPBaseDN is empty" if !$params->{"LDAPBaseDN"}; - } - } - return ""; + + # doeditparams traverses the list of params, and for each one it checks, + # then updates. This means that if one param checker wants to look at + # other params, it must be below that other one. So you can't have two + # params mutually dependent on each other. + # This means that if someone clears the LDAP config params after setting + # the login method as LDAP, we won't notice, but all logins will fail. + # So don't do that. + + my $params = Bugzilla->params; + my ($list, $entry) = @_; + $list || return 'You need to specify at least one authentication mechanism'; + for my $class (split /,\s*/, $list) { + my $res = check_multi($class, $entry); + return $res if $res; + if ($class eq 'RADIUS') { + if (!Bugzilla->feature('auth_radius')) { + return "RADIUS support is not available. Run checksetup.pl" + . " for more details"; + } + return "RADIUS servername (RADIUS_server) is missing" + if !$params->{"RADIUS_server"}; + return "RADIUS_secret is empty" if !$params->{"RADIUS_secret"}; + } + elsif ($class eq 'LDAP') { + if (!Bugzilla->feature('auth_ldap')) { + return "LDAP support is not available. Run checksetup.pl" . " for more details"; + } + return "LDAP servername (LDAPserver) is missing" if !$params->{"LDAPserver"}; + return "LDAPBaseDN is empty" if !$params->{"LDAPBaseDN"}; + } + } + return ""; } sub check_mail_delivery_method { - my $check = check_multi(@_); - return $check if $check; - my $mailer = shift; - if ($mailer eq 'Sendmail' and ON_WINDOWS) { - # look for sendmail.exe - return "Failed to locate " . SENDMAIL_EXE - unless -e SENDMAIL_EXE; - } - return ""; + my $check = check_multi(@_); + return $check if $check; + my $mailer = shift; + if ($mailer eq 'Sendmail' and ON_WINDOWS) { + + # look for sendmail.exe + return "Failed to locate " . SENDMAIL_EXE unless -e SENDMAIL_EXE; + } + return ""; } sub check_maxattachmentsize { - my $check = check_numeric(@_); - return $check if $check; - my $size = shift; - my $dbh = Bugzilla->dbh; - if ($dbh->isa('Bugzilla::DB::Mysql')) { - my (undef, $max_packet) = $dbh->selectrow_array( - q{SHOW VARIABLES LIKE 'max\_allowed\_packet'}); - my $byte_size = $size * 1024; - if ($max_packet < $byte_size) { - return "You asked for a maxattachmentsize of $byte_size bytes," - . " but the max_allowed_packet setting in MySQL currently" - . " only allows packets up to $max_packet bytes"; - } - } - return ""; + my $check = check_numeric(@_); + return $check if $check; + my $size = shift; + my $dbh = Bugzilla->dbh; + if ($dbh->isa('Bugzilla::DB::Mysql')) { + my (undef, $max_packet) + = $dbh->selectrow_array(q{SHOW VARIABLES LIKE 'max\_allowed\_packet'}); + my $byte_size = $size * 1024; + if ($max_packet < $byte_size) { + return + "You asked for a maxattachmentsize of $byte_size bytes," + . " but the max_allowed_packet setting in MySQL currently" + . " only allows packets up to $max_packet bytes"; + } + } + return ""; } sub check_notification { - my $option = shift; - my @current_version = - (BUGZILLA_VERSION =~ m/^(\d+)\.(\d+)(?:(rc|\.)(\d+))?\+?$/); - if ($current_version[1] % 2 && $option eq 'stable_branch_release') { - return "You are currently running a development snapshot, and so your " . - "installation is not based on a branch. If you want to be notified " . - "about the next stable release, you should select " . - "'latest_stable_release' instead"; - } - if ($option ne 'disabled' && !Bugzilla->feature('updates')) { - return "Some Perl modules are missing to get notifications about " . - "new releases. See the output of checksetup.pl for more information"; - } - return ""; + my $option = shift; + my @current_version + = (BUGZILLA_VERSION =~ m/^(\d+)\.(\d+)(?:(rc|\.)(\d+))?\+?$/); + if ($current_version[1] % 2 && $option eq 'stable_branch_release') { + return + "You are currently running a development snapshot, and so your " + . "installation is not based on a branch. If you want to be notified " + . "about the next stable release, you should select " + . "'latest_stable_release' instead"; + } + if ($option ne 'disabled' && !Bugzilla->feature('updates')) { + return "Some Perl modules are missing to get notifications about " + . "new releases. See the output of checksetup.pl for more information"; + } + return ""; } sub check_smtp_server { - my $host = shift; - my $port; - - return '' unless $host; - - if ($host =~ /:/) { - ($host, $port) = split(/:/, $host, 2); - unless ($port && detaint_natural($port)) { - return "Invalid port. It must be an integer (typically 25, 465 or 587)"; - } - } - trick_taint($host); - # Let's first try to connect using SSL. If this fails, we fall back to - # an unencrypted connection. - foreach my $method (['Net::SMTP::SSL', 465], ['Net::SMTP', 25]) { - my ($class, $default_port) = @$method; - next if $class eq 'Net::SMTP::SSL' && !Bugzilla->feature('smtp_ssl'); - eval "require $class"; - my $smtp = $class->new($host, Port => $port || $default_port, Timeout => 5); - if ($smtp) { - # The connection works! - $smtp->quit; - return ''; - } - } - return "Cannot connect to $host" . ($port ? " using port $port" : ""); + my $host = shift; + my $port; + + return '' unless $host; + + if ($host =~ /:/) { + ($host, $port) = split(/:/, $host, 2); + unless ($port && detaint_natural($port)) { + return "Invalid port. It must be an integer (typically 25, 465 or 587)"; + } + } + trick_taint($host); + + # Let's first try to connect using SSL. If this fails, we fall back to + # an unencrypted connection. + foreach my $method (['Net::SMTP::SSL', 465], ['Net::SMTP', 25]) { + my ($class, $default_port) = @$method; + next if $class eq 'Net::SMTP::SSL' && !Bugzilla->feature('smtp_ssl'); + eval "require $class"; + my $smtp = $class->new($host, Port => $port || $default_port, Timeout => 5); + if ($smtp) { + + # The connection works! + $smtp->quit; + return ''; + } + } + return "Cannot connect to $host" . ($port ? " using port $port" : ""); } sub check_smtp_auth { - my $username = shift; - if ($username and !Bugzilla->feature('smtp_auth')) { - return "SMTP Authentication is not available. Run checksetup.pl for" - . " more details"; - } - return ""; + my $username = shift; + if ($username and !Bugzilla->feature('smtp_auth')) { + return "SMTP Authentication is not available. Run checksetup.pl for" + . " more details"; + } + return ""; } sub check_smtp_ssl { - my $use_ssl = shift; - if ($use_ssl && !Bugzilla->feature('smtp_ssl')) { - return "SSL support is not available. Run checksetup.pl for more details"; - } - return ""; + my $use_ssl = shift; + if ($use_ssl && !Bugzilla->feature('smtp_ssl')) { + return "SSL support is not available. Run checksetup.pl for more details"; + } + return ""; } sub check_theschwartz_available { - my $use_queue = shift; - if ($use_queue && !Bugzilla->feature('jobqueue')) { - return "Using the job queue requires that you have certain Perl" - . " modules installed. See the output of checksetup.pl" - . " for more information"; - } - return ""; + my $use_queue = shift; + if ($use_queue && !Bugzilla->feature('jobqueue')) { + return + "Using the job queue requires that you have certain Perl" + . " modules installed. See the output of checksetup.pl" + . " for more information"; + } + return ""; } sub check_comment_taggers_group { - my $group_name = shift; - if ($group_name && !Bugzilla->feature('jsonrpc')) { - return "Comment tagging requires installation of the JSONRPC feature"; - } - return check_group($group_name); + my $group_name = shift; + if ($group_name && !Bugzilla->feature('jsonrpc')) { + return "Comment tagging requires installation of the JSONRPC feature"; + } + return check_group($group_name); } # OK, here are the parameter definitions themselves. @@ -467,13 +481,13 @@ sub check_comment_taggers_group { # } # # Here, 'b' is the default option, and 'a' and 'c' are other possible -# options, but only one at a time! +# options, but only one at a time! # # &check_multi should always be used as the param verification function # for list (single and multiple) parameter types. sub get_param_list { - return; + return; } 1; diff --git a/Bugzilla/Config/Core.pm b/Bugzilla/Config/Core.pm index 654e569ba..50af9a077 100644 --- a/Bugzilla/Config/Core.pm +++ b/Bugzilla/Config/Core.pm @@ -16,31 +16,13 @@ use Bugzilla::Config::Common; our $sortkey = 100; use constant get_param_list => ( - { - name => 'urlbase', - type => 't', - default => '', - checker => \&check_urlbase - }, - - { - name => 'ssl_redirect', - type => 'b', - default => 0 - }, - - { - name => 'sslbase', - type => 't', - default => '', - checker => \&check_sslbase - }, - - { - name => 'cookiepath', - type => 't', - default => '/' - }, + {name => 'urlbase', type => 't', default => '', checker => \&check_urlbase}, + + {name => 'ssl_redirect', type => 'b', default => 0}, + + {name => 'sslbase', type => 't', default => '', checker => \&check_sslbase}, + + {name => 'cookiepath', type => 't', default => '/'}, ); 1; diff --git a/Bugzilla/Config/DependencyGraph.pm b/Bugzilla/Config/DependencyGraph.pm index c815822f3..27bc9938d 100644 --- a/Bugzilla/Config/DependencyGraph.pm +++ b/Bugzilla/Config/DependencyGraph.pm @@ -16,21 +16,17 @@ use Bugzilla::Config::Common; our $sortkey = 800; sub get_param_list { - my $class = shift; + my $class = shift; my @param_list = ( - { - name => 'webdotbase', - type => 't', - default => '', - checker => \&check_webdotbase - }, + { + name => 'webdotbase', + type => 't', + default => '', + checker => \&check_webdotbase + }, - { - name => 'font_file', - type => 't', - default => '', - checker => \&check_font_file - }); + {name => 'font_file', type => 't', default => '', checker => \&check_font_file} + ); return @param_list; } diff --git a/Bugzilla/Config/General.pm b/Bugzilla/Config/General.pm index 380680590..322275aa0 100644 --- a/Bugzilla/Config/General.pm +++ b/Bugzilla/Config/General.pm @@ -17,39 +17,28 @@ our $sortkey = 150; use constant get_param_list => ( { - name => 'maintainer', - type => 't', - no_reset => '1', - default => '', - checker => \&check_email + name => 'maintainer', + type => 't', + no_reset => '1', + default => '', + checker => \&check_email }, - { - name => 'utf8', - type => 'b', - default => '0', - checker => \&check_utf8 - }, + {name => 'utf8', type => 'b', default => '0', checker => \&check_utf8}, - { - name => 'shutdownhtml', - type => 'l', - default => '' - }, + {name => 'shutdownhtml', type => 'l', default => ''}, - { - name => 'announcehtml', - type => 'l', - default => '' - }, + {name => 'announcehtml', type => 'l', default => ''}, { - name => 'upgrade_notification', - type => 's', - choices => ['development_snapshot', 'latest_stable_release', - 'stable_branch_release', 'disabled'], - default => 'latest_stable_release', - checker => \&check_notification + name => 'upgrade_notification', + type => 's', + choices => [ + 'development_snapshot', 'latest_stable_release', + 'stable_branch_release', 'disabled' + ], + default => 'latest_stable_release', + checker => \&check_notification }, ); diff --git a/Bugzilla/Config/GroupSecurity.pm b/Bugzilla/Config/GroupSecurity.pm index e827834a0..1df40cd26 100644 --- a/Bugzilla/Config/GroupSecurity.pm +++ b/Bugzilla/Config/GroupSecurity.pm @@ -20,84 +20,77 @@ sub get_param_list { my $class = shift; my @param_list = ( - { - name => 'makeproductgroups', - type => 'b', - default => 0 - }, - - { - name => 'chartgroup', - type => 's', - choices => \&_get_all_group_names, - default => 'editbugs', - checker => \&check_group - }, - - { - name => 'insidergroup', - type => 's', - choices => \&_get_all_group_names, - default => '', - checker => \&check_group - }, - - { - name => 'timetrackinggroup', - type => 's', - choices => \&_get_all_group_names, - default => 'editbugs', - checker => \&check_group - }, - - { - name => 'querysharegroup', - type => 's', - choices => \&_get_all_group_names, - default => 'editbugs', - checker => \&check_group - }, - - { - name => 'comment_taggers_group', - type => 's', - choices => \&_get_all_group_names, - default => 'editbugs', - checker => \&check_comment_taggers_group - }, - - { - name => 'debug_group', - type => 's', - choices => \&_get_all_group_names, - default => 'admin', - checker => \&check_group - }, - - { - name => 'usevisibilitygroups', - type => 'b', - default => 0 - }, - - { - name => 'strict_isolation', - type => 'b', - default => 0 - }, - - { - name => 'or_groups', - type => 'b', - default => 0 - } ); + {name => 'makeproductgroups', type => 'b', default => 0}, + + { + name => 'chartgroup', + type => 's', + choices => \&_get_all_group_names, + default => 'editbugs', + checker => \&check_group + }, + + { + name => 'insidergroup', + type => 's', + choices => \&_get_all_group_names, + default => '', + checker => \&check_group + }, + + { + name => 'timetrackinggroup', + type => 's', + choices => \&_get_all_group_names, + default => 'editbugs', + checker => \&check_group + }, + + { + name => 'querysharegroup', + type => 's', + choices => \&_get_all_group_names, + default => 'editbugs', + checker => \&check_group + }, + + { + name => 'comment_taggers_group', + type => 's', + choices => \&_get_all_group_names, + default => 'editbugs', + checker => \&check_comment_taggers_group + }, + + { + name => 'minor_update_group', + type => 's', + choices => \&_get_all_group_names, + default => '', + checker => \&check_group + }, + + { + name => 'debug_group', + type => 's', + choices => \&_get_all_group_names, + default => 'admin', + checker => \&check_group + }, + + {name => 'usevisibilitygroups', type => 'b', default => 0}, + + {name => 'strict_isolation', type => 'b', default => 0}, + + {name => 'or_groups', type => 'b', default => 0} + ); return @param_list; } sub _get_all_group_names { - my @group_names = map {$_->name} Bugzilla::Group->get_all; - unshift(@group_names, ''); - return \@group_names; + my @group_names = map { $_->name } Bugzilla::Group->get_all; + unshift(@group_names, ''); + return \@group_names; } 1; diff --git a/Bugzilla/Config/LDAP.pm b/Bugzilla/Config/LDAP.pm index 0bc8240df..75f58e141 100644 --- a/Bugzilla/Config/LDAP.pm +++ b/Bugzilla/Config/LDAP.pm @@ -16,49 +16,22 @@ use Bugzilla::Config::Common; our $sortkey = 1000; sub get_param_list { - my $class = shift; + my $class = shift; my @param_list = ( - { - name => 'LDAPserver', - type => 't', - default => '' - }, + {name => 'LDAPserver', type => 't', default => ''}, - { - name => 'LDAPstarttls', - type => 'b', - default => 0 - }, + {name => 'LDAPstarttls', type => 'b', default => 0}, - { - name => 'LDAPbinddn', - type => 't', - default => '' - }, + {name => 'LDAPbinddn', type => 't', default => ''}, - { - name => 'LDAPBaseDN', - type => 't', - default => '' - }, + {name => 'LDAPBaseDN', type => 't', default => ''}, - { - name => 'LDAPuidattribute', - type => 't', - default => 'uid' - }, + {name => 'LDAPuidattribute', type => 't', default => 'uid'}, - { - name => 'LDAPmailattribute', - type => 't', - default => 'mail' - }, + {name => 'LDAPmailattribute', type => 't', default => 'mail'}, - { - name => 'LDAPfilter', - type => 't', - default => '', - } ); + {name => 'LDAPfilter', type => 't', default => '',} + ); return @param_list; } diff --git a/Bugzilla/Config/MTA.pm b/Bugzilla/Config/MTA.pm index 467bdab3f..c7f8e5057 100644 --- a/Bugzilla/Config/MTA.pm +++ b/Bugzilla/Config/MTA.pm @@ -16,68 +16,43 @@ use Bugzilla::Config::Common; our $sortkey = 1200; sub get_param_list { - my $class = shift; + my $class = shift; my @param_list = ( - { - name => 'mail_delivery_method', - type => 's', - choices => ['Sendmail', 'SMTP', 'Test', 'None'], - default => 'Sendmail', - checker => \&check_mail_delivery_method - }, - - { - name => 'mailfrom', - type => 't', - default => 'bugzilla-daemon' - }, - - { - name => 'use_mailer_queue', - type => 'b', - default => 0, - checker => \&check_theschwartz_available, - }, - - { - name => 'smtpserver', - type => 't', - default => 'localhost', - checker => \&check_smtp_server - }, - { - name => 'smtp_username', - type => 't', - default => '', - checker => \&check_smtp_auth - }, - { - name => 'smtp_password', - type => 'p', - default => '' - }, - { - name => 'smtp_ssl', - type => 'b', - default => 0, - checker => \&check_smtp_ssl - }, - { - name => 'smtp_debug', - type => 'b', - default => 0 - }, - { - name => 'whinedays', - type => 't', - default => 7, - checker => \&check_numeric - }, - { - name => 'globalwatchers', - type => 't', - default => '', - }, ); + { + name => 'mail_delivery_method', + type => 's', + choices => ['Sendmail', 'SMTP', 'Test', 'None'], + default => 'Sendmail', + checker => \&check_mail_delivery_method + }, + + {name => 'mailfrom', type => 't', default => 'bugzilla-daemon'}, + + { + name => 'use_mailer_queue', + type => 'b', + default => 0, + checker => \&check_theschwartz_available, + }, + + { + name => 'smtpserver', + type => 't', + default => 'localhost', + checker => \&check_smtp_server + }, + { + name => 'smtp_username', + type => 't', + default => '', + checker => \&check_smtp_auth + }, + {name => 'smtp_password', type => 'p', default => ''}, + {name => 'smtp_ssl', type => 'b', default => 0, checker => \&check_smtp_ssl}, + {name => 'smtp_debug', type => 'b', default => 0}, + {name => 'whinedays', type => 't', default => 7, checker => \&check_numeric}, + {name => 'globalwatchers', type => 't', default => '',}, + ); return @param_list; } diff --git a/Bugzilla/Config/Memcached.pm b/Bugzilla/Config/Memcached.pm index 292803d86..5ab3364f9 100644 --- a/Bugzilla/Config/Memcached.pm +++ b/Bugzilla/Config/Memcached.pm @@ -17,16 +17,8 @@ our $sortkey = 1550; sub get_param_list { return ( - { - name => 'memcached_servers', - type => 't', - default => '' - }, - { - name => 'memcached_namespace', - type => 't', - default => 'bugzilla:', - }, + {name => 'memcached_servers', type => 't', default => ''}, + {name => 'memcached_namespace', type => 't', default => 'bugzilla:',}, ); } diff --git a/Bugzilla/Config/Query.pm b/Bugzilla/Config/Query.pm index f18bb90df..adfb4eaf4 100644 --- a/Bugzilla/Config/Query.pm +++ b/Bugzilla/Config/Query.pm @@ -16,47 +16,45 @@ use Bugzilla::Config::Common; our $sortkey = 1400; sub get_param_list { - my $class = shift; + my $class = shift; my @param_list = ( - { - name => 'quip_list_entry_control', - type => 's', - choices => ['open', 'moderated', 'closed'], - default => 'open', - checker => \&check_multi - }, - - { - name => 'mybugstemplate', - type => 't', - default => 'buglist.cgi?resolution=---&emailassigned_to1=1&emailreporter1=1&emailtype1=exact&email1=%userid%' - }, - - { - name => 'defaultquery', - type => 't', - default => 'resolution=---&emailassigned_to1=1&emailassigned_to2=1&emailreporter2=1&emailcc2=1&emailqa_contact2=1&emaillongdesc3=1&order=Importance&long_desc_type=substring' - }, - - { - name => 'search_allow_no_criteria', - type => 'b', - default => 1 - }, - - { - name => 'default_search_limit', - type => 't', - default => '500', - checker => \&check_numeric - }, - - { - name => 'max_search_results', - type => 't', - default => '10000', - checker => \&check_numeric - }, + { + name => 'quip_list_entry_control', + type => 's', + choices => ['open', 'moderated', 'closed'], + default => 'open', + checker => \&check_multi + }, + + { + name => 'mybugstemplate', + type => 't', + default => + 'buglist.cgi?resolution=---&emailassigned_to1=1&emailreporter1=1&emailtype1=exact&email1=%userid%' + }, + + { + name => 'defaultquery', + type => 't', + default => + 'resolution=---&emailassigned_to1=1&emailassigned_to2=1&emailreporter2=1&emailcc2=1&emailqa_contact2=1&emaillongdesc3=1&order=Importance&long_desc_type=substring' + }, + + {name => 'search_allow_no_criteria', type => 'b', default => 1}, + + { + name => 'default_search_limit', + type => 't', + default => '500', + checker => \&check_numeric + }, + + { + name => 'max_search_results', + type => 't', + default => '10000', + checker => \&check_numeric + }, ); return @param_list; } diff --git a/Bugzilla/Config/RADIUS.pm b/Bugzilla/Config/RADIUS.pm index 8e30b07a9..b0a5ddbf5 100644 --- a/Bugzilla/Config/RADIUS.pm +++ b/Bugzilla/Config/RADIUS.pm @@ -16,31 +16,15 @@ use Bugzilla::Config::Common; our $sortkey = 1100; sub get_param_list { - my $class = shift; + my $class = shift; my @param_list = ( - { - name => 'RADIUS_server', - type => 't', - default => '' - }, - - { - name => 'RADIUS_secret', - type => 't', - default => '' - }, - - { - name => 'RADIUS_NAS_IP', - type => 't', - default => '' - }, - - { - name => 'RADIUS_email_suffix', - type => 't', - default => '' - }, + {name => 'RADIUS_server', type => 't', default => ''}, + + {name => 'RADIUS_secret', type => 't', default => ''}, + + {name => 'RADIUS_NAS_IP', type => 't', default => ''}, + + {name => 'RADIUS_email_suffix', type => 't', default => ''}, ); return @param_list; } diff --git a/Bugzilla/Config/ShadowDB.pm b/Bugzilla/Config/ShadowDB.pm index 5dbbb5202..101e4678f 100644 --- a/Bugzilla/Config/ShadowDB.pm +++ b/Bugzilla/Config/ShadowDB.pm @@ -16,35 +16,23 @@ use Bugzilla::Config::Common; our $sortkey = 1500; sub get_param_list { - my $class = shift; + my $class = shift; my @param_list = ( - { - name => 'shadowdbhost', - type => 't', - default => '', - }, - - { - name => 'shadowdbport', - type => 't', - default => '3306', - checker => \&check_numeric, - }, - - { - name => 'shadowdbsock', - type => 't', - default => '', - }, - - # This entry must be _after_ the shadowdb{host,port,sock} settings so that - # they can be used in the validation here - { - name => 'shadowdb', - type => 't', - default => '', - checker => \&check_shadowdb - } ); + {name => 'shadowdbhost', type => 't', default => '',}, + + { + name => 'shadowdbport', + type => 't', + default => '3306', + checker => \&check_numeric, + }, + + {name => 'shadowdbsock', type => 't', default => '',}, + + # This entry must be _after_ the shadowdb{host,port,sock} settings so that + # they can be used in the validation here + {name => 'shadowdb', type => 't', default => '', checker => \&check_shadowdb} + ); return @param_list; } diff --git a/Bugzilla/Config/UserMatch.pm b/Bugzilla/Config/UserMatch.pm index 3f74a7c44..a1f8a3eb2 100644 --- a/Bugzilla/Config/UserMatch.pm +++ b/Bugzilla/Config/UserMatch.pm @@ -16,32 +16,21 @@ use Bugzilla::Config::Common; our $sortkey = 1600; sub get_param_list { - my $class = shift; + my $class = shift; my @param_list = ( - { - name => 'usemenuforusers', - type => 'b', - default => '0' - }, - - { - name => 'ajax_user_autocompletion', - type => 'b', - default => '1', - }, - - { - name => 'maxusermatches', - type => 't', - default => '1000', - checker => \&check_numeric - }, - - { - name => 'confirmuniqueusermatch', - type => 'b', - default => 1, - } ); + {name => 'usemenuforusers', type => 'b', default => '0'}, + + {name => 'ajax_user_autocompletion', type => 'b', default => '1',}, + + { + name => 'maxusermatches', + type => 't', + default => '1000', + checker => \&check_numeric + }, + + {name => 'confirmuniqueusermatch', type => 'b', default => 1,} + ); return @param_list; } diff --git a/Bugzilla/Constants.pm b/Bugzilla/Constants.pm index b4d22f8bf..8a39b30af 100644 --- a/Bugzilla/Constants.pm +++ b/Bugzilla/Constants.pm @@ -18,181 +18,190 @@ use File::Basename; use Memoize; @Bugzilla::Constants::EXPORT = qw( - BUGZILLA_VERSION - REST_DOC - - REMOTE_FILE - LOCAL_FILE + BUGZILLA_VERSION + REST_DOC - bz_locations - - CONCATENATE_ASSETS - - IS_NULL - NOT_NULL - - CONTROLMAPNA - CONTROLMAPSHOWN - CONTROLMAPDEFAULT - CONTROLMAPMANDATORY - - AUTH_OK - AUTH_NODATA - AUTH_ERROR - AUTH_LOGINFAILED - AUTH_DISABLED - AUTH_NO_SUCH_USER - AUTH_LOCKOUT - - USER_PASSWORD_MIN_LENGTH - - LOGIN_OPTIONAL - LOGIN_NORMAL - LOGIN_REQUIRED - - LOGOUT_ALL - LOGOUT_CURRENT - LOGOUT_KEEP_CURRENT - - GRANT_DIRECT - GRANT_REGEXP - - GROUP_MEMBERSHIP - GROUP_BLESS - GROUP_VISIBLE - - MAILTO_USER - MAILTO_GROUP - - DEFAULT_COLUMN_LIST - DEFAULT_QUERY_NAME - DEFAULT_MILESTONE - - SAVE_NUM_SEARCHES - - COMMENT_COLS - MAX_COMMENT_LENGTH - - MIN_COMMENT_TAG_LENGTH - MAX_COMMENT_TAG_LENGTH - - CMT_NORMAL - CMT_DUPE_OF - CMT_HAS_DUPE - CMT_ATTACHMENT_CREATED - CMT_ATTACHMENT_UPDATED - - THROW_ERROR - - RELATIONSHIPS - REL_ASSIGNEE REL_QA REL_REPORTER REL_CC REL_GLOBAL_WATCHER - REL_ANY - - 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_COMPONENT - - NEG_EVENTS - EVT_UNCONFIRMED EVT_CHANGED_BY_ME - - GLOBAL_EVENTS - EVT_FLAG_REQUESTED EVT_REQUESTED_FLAG - - ADMIN_GROUP_NAME - PER_PRODUCT_PRIVILEGES - - SENDMAIL_EXE - SENDMAIL_PATH - - FIELD_TYPE_UNKNOWN - FIELD_TYPE_FREETEXT - FIELD_TYPE_SINGLE_SELECT - FIELD_TYPE_MULTI_SELECT - FIELD_TYPE_TEXTAREA - FIELD_TYPE_DATETIME - FIELD_TYPE_DATE - FIELD_TYPE_BUG_ID - FIELD_TYPE_BUG_URLS - FIELD_TYPE_KEYWORDS - FIELD_TYPE_INTEGER - FIELD_TYPE_HIGHEST_PLUS_ONE - - EMPTY_DATETIME_REGEX - - ABNORMAL_SELECTS - - TIMETRACKING_FIELDS - - USAGE_MODE_BROWSER - USAGE_MODE_CMDLINE - USAGE_MODE_XMLRPC - USAGE_MODE_EMAIL - USAGE_MODE_JSON - USAGE_MODE_TEST - USAGE_MODE_REST - - ERROR_MODE_WEBPAGE - ERROR_MODE_DIE - ERROR_MODE_DIE_SOAP_FAULT - ERROR_MODE_JSON_RPC - ERROR_MODE_TEST - ERROR_MODE_REST - - COLOR_ERROR - COLOR_SUCCESS - - INSTALLATION_MODE_INTERACTIVE - INSTALLATION_MODE_NON_INTERACTIVE - - DB_MODULE - ROOT_USER - ON_WINDOWS - ON_ACTIVESTATE - - MAX_TOKEN_AGE - MAX_LOGINCOOKIE_AGE - MAX_SUDO_TOKEN_AGE - MAX_LOGIN_ATTEMPTS - LOGIN_LOCKOUT_INTERVAL - ACCOUNT_CHANGE_INTERVAL - MAX_STS_AGE - - SAFE_PROTOCOLS - LEGAL_CONTENT_TYPES - - MIN_SMALLINT - MAX_SMALLINT - MAX_INT_32 - - MAX_LEN_QUERY_NAME - MAX_CLASSIFICATION_SIZE - MAX_PRODUCT_SIZE - MAX_MILESTONE_SIZE - MAX_COMPONENT_SIZE - MAX_FIELD_VALUE_SIZE - MAX_FIELD_LONG_DESC_LENGTH - MAX_FREETEXT_LENGTH - MAX_BUG_URL_LENGTH - MAX_POSSIBLE_DUPLICATES - MAX_ATTACH_FILENAME_LENGTH - MAX_QUIP_LENGTH - MAX_WEBDOT_BUGS - - PASSWORD_DIGEST_ALGORITHM - PASSWORD_SALT_LENGTH - - CGI_URI_LIMIT - - PRIVILEGES_REQUIRED_NONE - PRIVILEGES_REQUIRED_REPORTER - PRIVILEGES_REQUIRED_ASSIGNEE - PRIVILEGES_REQUIRED_EMPOWERED - - AUDIT_CREATE - AUDIT_REMOVE - - MOST_FREQUENT_THRESHOLD + REMOTE_FILE + LOCAL_FILE + + bz_locations + + CONCATENATE_ASSETS + + IS_NULL + NOT_NULL + + CONTROLMAPNA + CONTROLMAPSHOWN + CONTROLMAPDEFAULT + CONTROLMAPMANDATORY + + AUTH_OK + AUTH_NODATA + AUTH_ERROR + AUTH_LOGINFAILED + AUTH_DISABLED + AUTH_NO_SUCH_USER + AUTH_LOCKOUT + AUTH_RH_RADIUS_LOGINFAILED + + USER_PASSWORD_MIN_LENGTH + + LOGIN_OPTIONAL + LOGIN_NORMAL + LOGIN_REQUIRED + + LOGOUT_ALL + LOGOUT_CURRENT + LOGOUT_KEEP_CURRENT + + GRANT_DIRECT + GRANT_REGEXP + + GROUP_MEMBERSHIP + GROUP_BLESS + GROUP_VISIBLE + + MAILTO_USER + MAILTO_GROUP + + DEFAULT_COLUMN_LIST + DEFAULT_QUERY_NAME + DEFAULT_MILESTONE + DEFAULT_RELEASE + + SAVE_NUM_SEARCHES + + COMMENT_COLS + MAX_COMMENT_LENGTH + + MIN_COMMENT_TAG_LENGTH + MAX_COMMENT_TAG_LENGTH + + CMT_NORMAL + CMT_DUPE_OF + CMT_HAS_DUPE + CMT_ATTACHMENT_CREATED + CMT_ATTACHMENT_UPDATED + + THROW_ERROR + + RELATIONSHIPS + REL_ASSIGNEE REL_QA REL_REPORTER REL_CC REL_GLOBAL_WATCHER REL_DOCS + REL_ANY + + 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_COMPONENT + + NEG_EVENTS + EVT_UNCONFIRMED EVT_CHANGED_BY_ME EVT_MINOR_UPDATE + + GLOBAL_EVENTS + EVT_FLAG_REQUESTED EVT_REQUESTED_FLAG + + ADMIN_GROUP_NAME + PER_PRODUCT_PRIVILEGES + + SENDMAIL_EXE + SENDMAIL_PATH + + FIELD_TYPE_UNKNOWN + FIELD_TYPE_FREETEXT + FIELD_TYPE_SINGLE_SELECT + FIELD_TYPE_MULTI_SELECT + FIELD_TYPE_ONE_SELECT + FIELD_TYPE_TEXTAREA + FIELD_TYPE_DATETIME + FIELD_TYPE_DATE + FIELD_TYPE_BUG_ID + FIELD_TYPE_BUG_URLS + FIELD_TYPE_KEYWORDS + FIELD_TYPE_INTEGER + FIELD_TYPE_HIGHEST_PLUS_ONE + + ACCESS_TYPE_VIEW + ACCESS_TYPE_EDIT + ACCESS_TYPE_ADMIN + + EMPTY_DATETIME_REGEX + + ABNORMAL_SELECTS + + TIMETRACKING_FIELDS + + USAGE_MODE_BROWSER + USAGE_MODE_CMDLINE + USAGE_MODE_XMLRPC + USAGE_MODE_EMAIL + USAGE_MODE_JSON + USAGE_MODE_TEST + USAGE_MODE_REST + + ERROR_MODE_WEBPAGE + ERROR_MODE_DIE + ERROR_MODE_DIE_SOAP_FAULT + ERROR_MODE_JSON_RPC + ERROR_MODE_TEST + ERROR_MODE_REST + + COLOR_ERROR + COLOR_SUCCESS + + INSTALLATION_MODE_INTERACTIVE + INSTALLATION_MODE_NON_INTERACTIVE + + DB_MODULE + ROOT_USER + ON_WINDOWS + ON_ACTIVESTATE + + MAX_TOKEN_AGE + MAX_LOGINCOOKIE_AGE + MAX_SUDO_TOKEN_AGE + MAX_LOGIN_ATTEMPTS + LOGIN_LOCKOUT_INTERVAL + ACCOUNT_CHANGE_INTERVAL + MAX_STS_AGE + + SAFE_PROTOCOLS + LEGAL_CONTENT_TYPES + + MIN_SMALLINT + MAX_SMALLINT + MAX_INT_32 + + MAX_LEN_QUERY_NAME + MAX_CLASSIFICATION_SIZE + MAX_PRODUCT_SIZE + MAX_MILESTONE_SIZE + MAX_RELEASE_SIZE + MAX_COMPONENT_SIZE + MAX_FIELD_VALUE_SIZE + MAX_FIELD_LONG_DESC_LENGTH + MAX_FREETEXT_LENGTH + MAX_BUG_URL_LENGTH + MAX_POSSIBLE_DUPLICATES + MAX_ATTACH_FILENAME_LENGTH + MAX_QUIP_LENGTH + MAX_WEBDOT_BUGS + + LOG4PERL_DEFAULT_CONFIG + PASSWORD_DIGEST_ALGORITHM + PASSWORD_SALT_LENGTH + + CGI_URI_LIMIT + + PRIVILEGES_REQUIRED_NONE + PRIVILEGES_REQUIRED_REPORTER + PRIVILEGES_REQUIRED_ASSIGNEE + PRIVILEGES_REQUIRED_EMPOWERED + + AUDIT_CREATE + AUDIT_REMOVE + + MOST_FREQUENT_THRESHOLD ); @Bugzilla::Constants::EXPORT_OK = qw(contenttypes); @@ -200,7 +209,7 @@ use Memoize; # CONSTANTS # # Bugzilla version -use constant BUGZILLA_VERSION => "5.0.4"; +use constant BUGZILLA_VERSION => "5.0.4.rh44"; # A base link to the current REST Documentation. We place it here # as it will need to be updated to whatever the current release is. @@ -208,7 +217,7 @@ use constant REST_DOC => 'https://bugzilla.readthedocs.org/en/5.0/api/'; # Location of the remote and local XML files to track new releases. use constant REMOTE_FILE => 'http://updates.bugzilla.org/bugzilla-update.xml'; -use constant LOCAL_FILE => 'bugzilla-update.xml'; # Relative to datadir. +use constant LOCAL_FILE => 'bugzilla-update.xml'; # Relative to datadir. # When true CSS and JavaScript assets will be concatanted and minified at # run-time, to reduce the number of requests required to render a page. @@ -229,9 +238,9 @@ use constant NOT_NULL => ' __NOT_NULL__ '; # # ControlMap constants for group_control_map. # membercontol:othercontrol => meaning -# Na:Na => Bugs in this product may not be restricted to this +# Na:Na => Bugs in this product may not be restricted to this # group. -# Shown:Na => Members of the group may restrict bugs +# Shown:Na => Members of the group may restrict bugs # in this product to this group. # Shown:Shown => Members of the group may restrict bugs # in this product to this group. @@ -253,46 +262,49 @@ use constant NOT_NULL => ' __NOT_NULL__ '; # Mandatory:Mandatory => Bug will be forced into this group regardless. # All other combinations are illegal. -use constant CONTROLMAPNA => 0; -use constant CONTROLMAPSHOWN => 1; -use constant CONTROLMAPDEFAULT => 2; +use constant CONTROLMAPNA => 0; +use constant CONTROLMAPSHOWN => 1; +use constant CONTROLMAPDEFAULT => 2; use constant CONTROLMAPMANDATORY => 3; # See Bugzilla::Auth for docs on AUTH_*, LOGIN_* and LOGOUT_* -use constant AUTH_OK => 0; -use constant AUTH_NODATA => 1; -use constant AUTH_ERROR => 2; -use constant AUTH_LOGINFAILED => 3; -use constant AUTH_DISABLED => 4; -use constant AUTH_NO_SUCH_USER => 5; -use constant AUTH_LOCKOUT => 6; +use constant AUTH_OK => 0; +use constant AUTH_NODATA => 1; +use constant AUTH_ERROR => 2; +use constant AUTH_LOGINFAILED => 3; +use constant AUTH_DISABLED => 4; +use constant AUTH_NO_SUCH_USER => 5; +use constant AUTH_LOCKOUT => 6; +## REDHAT EXTENSION 1262651 BEGIN +use constant AUTH_RH_RADIUS_LOGINFAILED => 20; +## REDHAT EXTENSION 1262651 END # The minimum length a password must have. -use constant USER_PASSWORD_MIN_LENGTH => 6; +use constant USER_PASSWORD_MIN_LENGTH => 8; use constant LOGIN_OPTIONAL => 0; -use constant LOGIN_NORMAL => 1; +use constant LOGIN_NORMAL => 1; use constant LOGIN_REQUIRED => 2; -use constant LOGOUT_ALL => 0; -use constant LOGOUT_CURRENT => 1; +use constant LOGOUT_ALL => 0; +use constant LOGOUT_CURRENT => 1; use constant LOGOUT_KEEP_CURRENT => 2; use constant GRANT_DIRECT => 0; use constant GRANT_REGEXP => 2; use constant GROUP_MEMBERSHIP => 0; -use constant GROUP_BLESS => 1; -use constant GROUP_VISIBLE => 2; +use constant GROUP_BLESS => 1; +use constant GROUP_VISIBLE => 2; -use constant MAILTO_USER => 0; +use constant MAILTO_USER => 0; use constant MAILTO_GROUP => 1; # The default list of columns for buglist.cgi use constant DEFAULT_COLUMN_LIST => ( - "product", "component", "assigned_to", - "bug_status", "resolution", "short_desc", "changeddate" + "product", "component", "assigned_to", "bug_status", + "resolution", "short_desc", "changeddate" ); # Used by query.cgi and buglist.cgi as the named-query name @@ -302,11 +314,17 @@ use constant DEFAULT_QUERY_NAME => '(Default query)'; # The default "defaultmilestone" created for products. use constant DEFAULT_MILESTONE => '---'; +# The default "defaultrelease" created for products. +use constant DEFAULT_RELEASE => '---'; + +## REDHAT EXTENSION START 840265 # How many of the user's most recent searches to save. -use constant SAVE_NUM_SEARCHES => 10; +use constant SAVE_NUM_SEARCHES => 50; +## REDHAT EXTENSION END 840265 # The column width for comment textareas and comments in bugmails. use constant COMMENT_COLS => 80; + # Used in _check_comment(). Gives the max length allowed for a comment. use constant MAX_COMMENT_LENGTH => 65535; @@ -315,9 +333,10 @@ use constant MIN_COMMENT_TAG_LENGTH => 3; use constant MAX_COMMENT_TAG_LENGTH => 24; # The type of bug comments. -use constant CMT_NORMAL => 0; -use constant CMT_DUPE_OF => 1; +use constant CMT_NORMAL => 0; +use constant CMT_DUPE_OF => 1; use constant CMT_HAS_DUPE => 2; + # Type 3 was CMT_POPULAR_VOTES, which moved to the Voting extension. # Type 4 was CMT_MOVED_TO, which moved to the OldBugMove extension. use constant CMT_ATTACHMENT_CREATED => 5; @@ -327,27 +346,31 @@ use constant CMT_ATTACHMENT_UPDATED => 6; # an error when the validation fails. use constant THROW_ERROR => 1; -use constant REL_ASSIGNEE => 0; -use constant REL_QA => 1; -use constant REL_REPORTER => 2; -use constant REL_CC => 3; +use constant REL_ASSIGNEE => 0; +use constant REL_QA => 1; +use constant REL_REPORTER => 2; +use constant REL_CC => 3; + # REL 4 was REL_VOTER, before it was moved ino an extension. -use constant REL_GLOBAL_WATCHER => 5; +use constant REL_GLOBAL_WATCHER => 5; + +## REDHAT EXTENSION START 876015 +use constant REL_DOCS => 9; +## REDHAT EXTENSION END 876015 # We need these strings for the X-Bugzilla-Reasons header # Note: this hash uses "," rather than "=>" to avoid auto-quoting of the LHS. # This should be accessed through Bugzilla::BugMail::relationships() instead # of being accessed directly. +## REDHAT EXTENSION 876015: Add REL_DOCS use constant RELATIONSHIPS => { - REL_ASSIGNEE , "AssignedTo", - REL_REPORTER , "Reporter", - REL_QA , "QAcontact", - REL_CC , "CC", - REL_GLOBAL_WATCHER, "GlobalWatcher" + REL_ASSIGNEE, "AssignedTo", REL_REPORTER, "Reporter", + REL_QA, "QAcontact", REL_DOCS, "Docscontact", + REL_CC, "CC", REL_GLOBAL_WATCHER, "GlobalWatcher" }; - + # Used for global events like EVT_FLAG_REQUESTED -use constant REL_ANY => 100; +use constant REL_ANY => 100; # There are two sorts of event - positive and negative. Positive events are # those for which the user says "I want mail if this happens." Negative events @@ -355,34 +378,35 @@ use constant REL_ANY => 100; # # Exactly when each event fires is defined in wants_bug_mail() in User.pm; I'm # not commenting them here in case the comments and the code get out of sync. -use constant EVT_OTHER => 0; -use constant EVT_ADDED_REMOVED => 1; -use constant EVT_COMMENT => 2; -use constant EVT_ATTACHMENT => 3; -use constant EVT_ATTACHMENT_DATA => 4; -use constant EVT_PROJ_MANAGEMENT => 5; -use constant EVT_OPENED_CLOSED => 6; -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_COMPONENT; - -use constant EVT_UNCONFIRMED => 50; -use constant EVT_CHANGED_BY_ME => 51; - -use constant NEG_EVENTS => EVT_UNCONFIRMED, EVT_CHANGED_BY_ME; +use constant EVT_OTHER => 0; +use constant EVT_ADDED_REMOVED => 1; +use constant EVT_COMMENT => 2; +use constant EVT_ATTACHMENT => 3; +use constant EVT_ATTACHMENT_DATA => 4; +use constant EVT_PROJ_MANAGEMENT => 5; +use constant EVT_OPENED_CLOSED => 6; +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_COMPONENT; + +use constant EVT_UNCONFIRMED => 50; +use constant EVT_CHANGED_BY_ME => 51; +use constant EVT_MINOR_UPDATE => 52; + +use constant NEG_EVENTS => EVT_UNCONFIRMED, EVT_CHANGED_BY_ME, EVT_MINOR_UPDATE; # These are the "global" flags, which aren't tied to a particular relationship. # and so use REL_ANY. -use constant EVT_FLAG_REQUESTED => 100; # Flag has been requested of me -use constant EVT_REQUESTED_FLAG => 101; # I have requested a flag +use constant EVT_FLAG_REQUESTED => 100; # Flag has been requested of me +use constant EVT_REQUESTED_FLAG => 101; # I have requested a flag use constant GLOBAL_EVENTS => EVT_FLAG_REQUESTED, EVT_REQUESTED_FLAG; @@ -390,10 +414,12 @@ use constant GLOBAL_EVENTS => EVT_FLAG_REQUESTED, EVT_REQUESTED_FLAG; use constant ADMIN_GROUP_NAME => 'admin'; # Privileges which can be per-product. -use constant PER_PRODUCT_PRIVILEGES => ('editcomponents', 'editbugs', 'canconfirm'); +use constant PER_PRODUCT_PRIVILEGES => + ('editcomponents', 'editbugs', 'canconfirm'); # Path to sendmail.exe (Windows only) use constant SENDMAIL_EXE => '/usr/lib/sendmail.exe'; + # Paths to search for the sendmail binary (non-Windows) use constant SENDMAIL_PATH => '/usr/lib:/usr/sbin:/usr/ucblib'; @@ -404,45 +430,57 @@ use constant SENDMAIL_PATH => '/usr/lib:/usr/sbin:/usr/ucblib'; # we do more than we would do for a standard integer type (f.e. we might # display a user picker). -use constant FIELD_TYPE_UNKNOWN => 0; -use constant FIELD_TYPE_FREETEXT => 1; +use constant FIELD_TYPE_UNKNOWN => 0; +use constant FIELD_TYPE_FREETEXT => 1; use constant FIELD_TYPE_SINGLE_SELECT => 2; -use constant FIELD_TYPE_MULTI_SELECT => 3; -use constant FIELD_TYPE_TEXTAREA => 4; -use constant FIELD_TYPE_DATETIME => 5; -use constant FIELD_TYPE_BUG_ID => 6; -use constant FIELD_TYPE_BUG_URLS => 7; -use constant FIELD_TYPE_KEYWORDS => 8; -use constant FIELD_TYPE_DATE => 9; -use constant FIELD_TYPE_INTEGER => 10; +use constant FIELD_TYPE_MULTI_SELECT => 3; +use constant FIELD_TYPE_TEXTAREA => 4; +use constant FIELD_TYPE_DATETIME => 5; +use constant FIELD_TYPE_BUG_ID => 6; +use constant FIELD_TYPE_BUG_URLS => 7; +use constant FIELD_TYPE_KEYWORDS => 8; +use constant FIELD_TYPE_DATE => 9; +use constant FIELD_TYPE_INTEGER => 10; +use constant FIELD_TYPE_ONE_SELECT => 11; + # Add new field types above this line, and change the below value in the # obvious fashion -use constant FIELD_TYPE_HIGHEST_PLUS_ONE => 11; +use constant FIELD_TYPE_HIGHEST_PLUS_ONE => 12; -use constant EMPTY_DATETIME_REGEX => qr/^[0\-:\sA-Za-z]+$/; +# What access does a user have to a field? +# It's important that higher numbers are more privileged as they cascade. +use constant ACCESS_TYPE_VIEW => 0; +use constant ACCESS_TYPE_EDIT => 1; +use constant ACCESS_TYPE_ADMIN => 2; + +use constant EMPTY_DATETIME_REGEX => qr/^[0\-:\sA-Za-z]+$/; # See the POD for Bugzilla::Field/is_abnormal to see why these are listed # here. use constant ABNORMAL_SELECTS => { - classification => 1, - component => 1, - product => 1, + classification => 1, + component => 1, + product => 1, + dependent_products => 1, ## REDHAT EXTENSION 1341100 }; # The fields from fielddefs that are blocked from non-timetracking users. # work_time is sometimes called actual_time. use constant TIMETRACKING_FIELDS => - qw(estimated_time remaining_time work_time actual_time percentage_complete); + qw(estimated_time remaining_time work_time actual_time percentage_complete); # The maximum number of days a token will remain valid. use constant MAX_TOKEN_AGE => 3; + # How many days a logincookie will remain valid if not used. -use constant MAX_LOGINCOOKIE_AGE => 30; +use constant MAX_LOGINCOOKIE_AGE => 7; + # How many seconds (default is 6 hours) a sudo cookie remains valid. use constant MAX_SUDO_TOKEN_AGE => 21600; # Maximum failed logins to lock account for this IP use constant MAX_LOGIN_ATTEMPTS => 5; + # If the maximum login attempts occur during this many minutes, the # account is locked. use constant LOGIN_LOCKOUT_INTERVAL => 30; @@ -456,36 +494,39 @@ use constant ACCOUNT_CHANGE_INTERVAL => 10; use constant MAX_STS_AGE => 604800; # Protocols which are considered as safe. -use constant SAFE_PROTOCOLS => ('afs', 'cid', 'ftp', 'gopher', 'http', 'https', - 'irc', 'ircs', 'mid', 'news', 'nntp', 'prospero', - 'telnet', 'view-source', 'wais'); +use constant SAFE_PROTOCOLS => ( + 'afs', 'cid', 'ftp', 'gopher', 'http', 'https', + 'irc', 'ircs', 'mid', 'news', 'nntp', 'prospero', + 'telnet', 'view-source', 'wais' +); # Valid MIME types for attachments. -use constant LEGAL_CONTENT_TYPES => ('application', 'audio', 'image', 'message', - 'model', 'multipart', 'text', 'video'); - -use constant contenttypes => - { - "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" , - }; +use constant LEGAL_CONTENT_TYPES => ( + 'application', 'audio', 'image', 'message', + 'model', 'multipart', 'text', 'video' +); + +use constant contenttypes => { + "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. -use constant USAGE_MODE_BROWSER => 0; -use constant USAGE_MODE_CMDLINE => 1; -use constant USAGE_MODE_XMLRPC => 2; -use constant USAGE_MODE_EMAIL => 3; -use constant USAGE_MODE_JSON => 4; -use constant USAGE_MODE_TEST => 5; -use constant USAGE_MODE_REST => 6; +use constant USAGE_MODE_BROWSER => 0; +use constant USAGE_MODE_CMDLINE => 1; +use constant USAGE_MODE_XMLRPC => 2; +use constant USAGE_MODE_EMAIL => 3; +use constant USAGE_MODE_JSON => 4; +use constant USAGE_MODE_TEST => 5; +use constant USAGE_MODE_REST => 6; # Error modes. Default set by Bugzilla->usage_mode (so ERROR_MODE_WEBPAGE # usually). Use with Bugzilla->error_mode. @@ -497,60 +538,76 @@ use constant ERROR_MODE_TEST => 4; use constant ERROR_MODE_REST => 5; # The ANSI colors of messages that command-line scripts use -use constant COLOR_ERROR => 'red'; +use constant COLOR_ERROR => 'red'; use constant COLOR_SUCCESS => 'green'; # The various modes that checksetup.pl can run in. -use constant INSTALLATION_MODE_INTERACTIVE => 0; +use constant INSTALLATION_MODE_INTERACTIVE => 0; use constant INSTALLATION_MODE_NON_INTERACTIVE => 1; # Data about what we require for different databases. use constant DB_MODULE => { - # MySQL 5.0.15 was the first production 5.0.x release. - 'mysql' => {db => 'Bugzilla::DB::Mysql', db_version => '5.0.15', - dbd => { - package => 'DBD-mysql', - module => 'DBD::mysql', - # Disallow development versions - blacklist => ['_'], - # For UTF-8 support. 4.001 makes sure that blobs aren't - # marked as UTF-8. - version => '4.001', - }, - name => 'MySQL'}, - # Also see Bugzilla::DB::Pg::bz_check_server_version, which has special - # code to require DBD::Pg 2.17.2 for PostgreSQL 9 and above. - 'pg' => {db => 'Bugzilla::DB::Pg', db_version => '8.03.0000', - dbd => { - package => 'DBD-Pg', - module => 'DBD::Pg', - # 2.7.0 fixes a problem with quoting strings - # containing backslashes in them. - version => '2.7.0', - }, - name => 'PostgreSQL'}, - 'oracle'=> {db => 'Bugzilla::DB::Oracle', db_version => '10.02.0', - dbd => { - package => 'DBD-Oracle', - module => 'DBD::Oracle', - version => '1.19', - }, - name => 'Oracle'}, - # SQLite 3.6.22 fixes a WHERE clause problem that may affect us. - sqlite => {db => 'Bugzilla::DB::Sqlite', db_version => '3.6.22', - dbd => { - package => 'DBD-SQLite', - module => 'DBD::SQLite', - # 1.29 is the version that contains 3.6.22. - version => '1.29', - }, - name => 'SQLite'}, + + # MySQL 5.0.15 was the first production 5.0.x release. + 'mysql' => { + db => 'Bugzilla::DB::Mysql', + db_version => '5.0.15', + dbd => { + package => 'DBD-mysql', + module => 'DBD::mysql', + + # Disallow development versions + blacklist => ['_'], + + # For UTF-8 support. 4.001 makes sure that blobs aren't + # marked as UTF-8. + version => '4.001', + }, + name => 'MySQL' + }, + + # Also see Bugzilla::DB::Pg::bz_check_server_version, which has special + # code to require DBD::Pg 2.17.2 for PostgreSQL 9 and above. + 'pg' => { + db => 'Bugzilla::DB::Pg', + db_version => '8.03.0000', + dbd => { + package => 'DBD-Pg', + module => 'DBD::Pg', + + # 2.7.0 fixes a problem with quoting strings + # containing backslashes in them. + version => '2.7.0', + }, + name => 'PostgreSQL' + }, + 'oracle' => { + db => 'Bugzilla::DB::Oracle', + db_version => '10.02.0', + dbd => {package => 'DBD-Oracle', module => 'DBD::Oracle', version => '1.19',}, + name => 'Oracle' + }, + + # SQLite 3.6.22 fixes a WHERE clause problem that may affect us. + sqlite => { + db => 'Bugzilla::DB::Sqlite', + db_version => '3.6.22', + dbd => { + package => 'DBD-SQLite', + module => 'DBD::SQLite', + + # 1.29 is the version that contains 3.6.22. + version => '1.29', + }, + name => 'SQLite' + }, }; # True if we're on Win32. use constant ON_WINDOWS => ($^O =~ /MSWin32/i) ? 1 : 0; + # True if we're using ActiveState Perl (as opposed to Strawberry) on Windows. -use constant ON_ACTIVESTATE => eval { &Win32::BuildNumber }; +use constant ON_ACTIVESTATE => eval {&Win32::BuildNumber}; # The user who should be considered "root" when we're giving # instructions to Bugzilla administrators. @@ -558,7 +615,7 @@ use constant ROOT_USER => ON_WINDOWS ? 'Administrator' : 'root'; use constant MIN_SMALLINT => -32768; use constant MAX_SMALLINT => 32767; -use constant MAX_INT_32 => 2147483647; +use constant MAX_INT_32 => 2147483647; # The longest that a saved search name can be. use constant MAX_LEN_QUERY_NAME => 64; @@ -572,6 +629,9 @@ use constant MAX_PRODUCT_SIZE => 64; # The longest milestone name allowed. use constant MAX_MILESTONE_SIZE => 64; +# The longest release name allowed. +use constant MAX_RELEASE_SIZE => 64; + # The longest component name allowed. use constant MAX_COMPONENT_SIZE => 64; @@ -584,6 +644,18 @@ use constant MAX_FIELD_LONG_DESC_LENGTH => 255; # Maximum length allowed for free text fields. use constant MAX_FREETEXT_LENGTH => 255; +## REDHAT EXTENSION BEGIN 406301 +# default log4perl config +use constant LOG4PERL_DEFAULT_CONFIG => q( +log4perl.rootLogger=DEBUG, LOGFILE +log4perl.appender.LOGFILE=Log::Log4perl::Appender::File +log4perl.appender.LOGFILE.filename=data/bz.log +log4perl.appender.LOGFILE.mode=append +log4perl.appender.LOGFILE.layout=PatternLayout +log4perl.appender.LOGFILE.layout.ConversionPattern=[%d.%r] [%H;%P] [%p] [%X{script}:%X{remote_addr}:%X{http_x_forwarded_for}:%X{userid}:%X{username}:%X{userlogin}] [%l] - %m%n +); +## REDHAT EXTENSION END 406301 + # The longest a bug URL in a BUG_URLS field can be. use constant MAX_BUG_URL_LENGTH => 255; @@ -607,6 +679,7 @@ use constant MAX_WEBDOT_BUGS => 2000; # Perl's "Digest" module. Note that if you change this, it won't take # effect until a user logs in or changes their password. use constant PASSWORD_DIGEST_ALGORITHM => 'SHA-256'; + # How long of a salt should we use? Note that if you change this, it # won't take effect until a user logs in or changes their password. use constant PASSWORD_SALT_LENGTH => 8; @@ -615,7 +688,9 @@ use constant PASSWORD_SALT_LENGTH => 8; # via POST such as buglist.cgi. This value determines whether the redirect # can be safely done or not based on the web server's URI length setting. # See http://support.microsoft.com/kb/208427 for why MSIE is different -use constant CGI_URI_LIMIT => ($ENV{'HTTP_USER_AGENT'} || '') =~ /MSIE/ ? 2083 : 8000; +use constant CGI_URI_LIMIT => ($ENV{'HTTP_USER_AGENT'} || '') =~ /MSIE/ + ? 2083 + : 8000; # If the user isn't allowed to change a field, we must tell them who can. # We store the required permission set into the $PrivilegesRequired @@ -636,75 +711,84 @@ use constant AUDIT_REMOVE => '__remove__'; use constant MOST_FREQUENT_THRESHOLD => 2; sub bz_locations { - # Force memoize() to re-compute data per project, to avoid - # sharing the same data across different installations. - return _bz_locations($ENV{'PROJECT'}); + + # Force memoize() to re-compute data per project, to avoid + # sharing the same data across different installations. + return _bz_locations($ENV{'PROJECT'}); } sub _bz_locations { - my $project = shift; - # We know that Bugzilla/Constants.pm must be in %INC at this point. - # So the only question is, what's the name of the directory - # above it? This is the most reliable way to get our current working - # directory under both mod_cgi and mod_perl. We call dirname twice - # to get the name of the directory above the "Bugzilla/" directory. - # - # Calling dirname twice like that won't work on VMS or AmigaOS - # but I doubt anybody runs Bugzilla on those. - # - # On mod_cgi this will be a relative path. On mod_perl it will be an - # absolute path. - my $libpath = dirname(dirname($INC{'Bugzilla/Constants.pm'})); - # We have to detaint $libpath, but we can't use Bugzilla::Util here. - $libpath =~ /(.*)/; - $libpath = $1; - - my ($localconfig, $datadir); - if ($project && $project =~ /^(\w+)$/) { - $project = $1; - $localconfig = "localconfig.$project"; - $datadir = "data/$project"; - } else { - $project = undef; - $localconfig = "localconfig"; - $datadir = "data"; - } - - $datadir = "$libpath/$datadir"; - # We have to return absolute paths for mod_perl. - # That means that if you modify these paths, they must be absolute paths. - return { - 'libpath' => $libpath, - 'ext_libpath' => "$libpath/lib", - # If you put the libraries in a different location than the CGIs, - # make sure this still points to the CGIs. - 'cgi_path' => $libpath, - 'templatedir' => "$libpath/template", - 'template_cache' => "$datadir/template", - 'project' => $project, - 'localconfig' => "$libpath/$localconfig", - 'datadir' => $datadir, - 'attachdir' => "$datadir/attachments", - 'skinsdir' => "$libpath/skins", - 'graphsdir' => "$libpath/graphs", - # $webdotdir must be in the web server's tree somewhere. Even if you use a - # local dot, we output images to there. Also, if $webdotdir is - # not relative to the bugzilla root directory, you'll need to - # change showdependencygraph.cgi to set image_url to the correct - # location. - # The script should really generate these graphs directly... - 'webdotdir' => "$datadir/webdot", - 'extensionsdir' => "$libpath/extensions", - 'assetsdir' => "$datadir/assets", - }; + my $project = shift; + + # We know that Bugzilla/Constants.pm must be in %INC at this point. + # So the only question is, what's the name of the directory + # above it? This is the most reliable way to get our current working + # directory under both mod_cgi and mod_perl. We call dirname twice + # to get the name of the directory above the "Bugzilla/" directory. + # + # Calling dirname twice like that won't work on VMS or AmigaOS + # but I doubt anybody runs Bugzilla on those. + # + # On mod_cgi this will be a relative path. On mod_perl it will be an + # absolute path. + my $libpath = dirname(dirname($INC{'Bugzilla/Constants.pm'})); + + # We have to detaint $libpath, but we can't use Bugzilla::Util here. + $libpath =~ /(.*)/; + $libpath = $1; + + my ($localconfig, $datadir); + if ($project && $project =~ /^(\w+)$/) { + $project = $1; + $localconfig = "localconfig.$project"; + $datadir = "data/$project"; + } + else { + $project = undef; + $localconfig = "localconfig"; + $datadir = "data"; + } + + $datadir = "$libpath/$datadir"; + + # We have to return absolute paths for mod_perl. + # That means that if you modify these paths, they must be absolute paths. + return { + 'libpath' => $libpath, + 'ext_libpath' => "$libpath/lib", + + # If you put the libraries in a different location than the CGIs, + # make sure this still points to the CGIs. + 'cgi_path' => $libpath, + 'templatedir' => "$libpath/template", + 'template_cache' => "$datadir/template", + 'project' => $project, + 'localconfig' => "$libpath/$localconfig", + 'datadir' => $datadir, + 'attachdir' => "$datadir/attachments", + 'skinsdir' => "$libpath/skins", + 'graphsdir' => "$libpath/graphs", + + # $webdotdir must be in the web server's tree somewhere. Even if you use a + # local dot, we output images to there. Also, if $webdotdir is + # not relative to the bugzilla root directory, you'll need to + # change showdependencygraph.cgi to set image_url to the correct + # location. + # The script should really generate these graphs directly... + 'webdotdir' => "$datadir/webdot", + 'extensionsdir' => "$libpath/extensions", + 'assetsdir' => "$datadir/assets", + }; } # This makes us not re-compute all the bz_locations data every time it's # called. -BEGIN { memoize('_bz_locations') }; +BEGIN { memoize('_bz_locations') } 1; +__END__ + =head1 B =over @@ -715,4 +799,6 @@ BEGIN { memoize('_bz_locations') }; =item bz_locations +=item LOG4PERL_DEFAULT_CONFIG + =back diff --git a/Bugzilla/DB.pm b/Bugzilla/DB.pm index 5bc83f9d6..abed6ea0c 100644 --- a/Bugzilla/DB.pm +++ b/Bugzilla/DB.pm @@ -26,14 +26,19 @@ use Bugzilla::Error; use Bugzilla::DB::Schema; use Bugzilla::Version; +## REDHAT EXTENSION 987269 +use File::Basename; + use List::Util qw(max); use Storable qw(dclone); +use Text::ParseWords qw(shellwords); +use Data::Dumper; ##################################################################### # Constants ##################################################################### -use constant BLOB_TYPE => DBI::SQL_BLOB; +use constant BLOB_TYPE => DBI::SQL_BLOB; use constant ISOLATION_LEVEL => 'REPEATABLE READ'; # Set default values for what used to be the enum types. These values @@ -46,14 +51,14 @@ use constant ISOLATION_LEVEL => 'REPEATABLE READ'; # Bugzilla with enums. After that, they are either controlled through # the Bugzilla UI or through the DB. use constant ENUM_DEFAULTS => { - bug_severity => ['blocker', 'critical', 'major', 'normal', - 'minor', 'trivial', 'enhancement'], - priority => ["Highest", "High", "Normal", "Low", "Lowest", "---"], - op_sys => ["All","Windows","Mac OS","Linux","Other"], - rep_platform => ["All","PC","Macintosh","Other"], - bug_status => ["UNCONFIRMED","CONFIRMED","IN_PROGRESS","RESOLVED", - "VERIFIED"], - resolution => ["","FIXED","INVALID","WONTFIX", "DUPLICATE","WORKSFORME"], + bug_severity => + ['blocker', 'critical', 'major', 'normal', 'minor', 'trivial', 'enhancement'], + priority => ["Highest", "High", "Normal", "Low", "Lowest", "---"], + op_sys => ["All", "Windows", "Mac OS", "Linux", "Other"], + rep_platform => ["All", "PC", "Macintosh", "Other"], + bug_status => + ["UNCONFIRMED", "CONFIRMED", "IN_PROGRESS", "RESOLVED", "VERIFIED"], + resolution => ["", "FIXED", "INVALID", "WONTFIX", "DUPLICATE", "WORKSFORME"], }; # The character that means "OR" in a boolean fulltext search. If empty, @@ -83,14 +88,14 @@ use constant WORD_END => '($|[^[:alnum:]])'; use constant INDEX_DROPS_REQUIRE_FK_DROPS => 1; ##################################################################### -# Overridden Superclass Methods +# Overridden Superclass Methods ##################################################################### sub quote { - my $self = shift; - my $retval = $self->SUPER::quote(@_); - trick_taint($retval) if defined $retval; - return $retval; + my $self = shift; + my $retval = $self->SUPER::quote(@_); + trick_taint($retval) if defined $retval; + return $retval; } ##################################################################### @@ -98,95 +103,172 @@ sub quote { ##################################################################### sub connect_shadow { - my $params = Bugzilla->params; - die "Tried to connect to non-existent shadowdb" - unless $params->{'shadowdb'}; + my $params = Bugzilla->params; + die "Tried to connect to non-existent shadowdb" unless $params->{'shadowdb'}; - # Instead of just passing in a new hashref, we locally modify the - # values of "localconfig", because some drivers access it while - # connecting. - my %connect_params = %{ Bugzilla->localconfig }; - $connect_params{db_host} = $params->{'shadowdbhost'}; - $connect_params{db_name} = $params->{'shadowdb'}; - $connect_params{db_port} = $params->{'shadowdbport'}; - $connect_params{db_sock} = $params->{'shadowdbsock'}; + # Instead of just passing in a new hashref, we locally modify the + # values of "localconfig", because some drivers access it while + # connecting. + my %connect_params = %{Bugzilla->localconfig}; + $connect_params{db_host} = $params->{'shadowdbhost'}; + $connect_params{db_name} = $params->{'shadowdb'}; + $connect_params{db_port} = $params->{'shadowdbport'}; + $connect_params{db_sock} = $params->{'shadowdbsock'}; - return _connect(\%connect_params); + return _connect(\%connect_params); } sub connect_main { - my $lc = Bugzilla->localconfig; - return _connect(Bugzilla->localconfig); + my $lc = Bugzilla->localconfig; + return _connect(Bugzilla->localconfig); } sub _connect { - my ($params) = @_; + my ($params) = @_; + + my $driver = $params->{db_driver}; + my $pkg_module = DB_MODULE->{lc($driver)}->{db}; - my $driver = $params->{db_driver}; - my $pkg_module = DB_MODULE->{lc($driver)}->{db}; + # do the actual import + eval("require $pkg_module") + || die( + "'$driver' is not a valid choice for \$db_driver in " . " localconfig: " . $@); - # do the actual import - eval ("require $pkg_module") - || die ("'$driver' is not a valid choice for \$db_driver in " - . " localconfig: " . $@); + # instantiate the correct DB specific module + my $dbh = $pkg_module->new($params); - # instantiate the correct DB specific module - my $dbh = $pkg_module->new($params); +## BUGBUG uncomment this to get profiling of the SQL +## $dbh->{Profile} = "!Statement:!MethodName:!MethodClass:!Caller2/DBI::ProfileDumper::Apache/Dir:data"; - return $dbh; + return $dbh; } sub _handle_error { - require Carp; - - # 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]); - return 0; # Now let DBI handle raising the error + require Carp; + +# Check for some common error cases in postgres database and pop up a nicer error message + if (!Bugzilla->request_cache->{in_error} + && lc(Bugzilla->localconfig->{db_driver}) eq 'pg') + { + ## REDHAT EXTENSION BEGIN 1174110 + # If a serialization error happened, tell the user to try again. + if ($_[0] =~ /could not serialize access due to concurrent update/) { + Bugzilla->request_cache->{in_error} = 1; + mail_db_error( + 'Serialization Error', + $_[0], + (Carp::longmess((Bugzilla->dbh->{Statement} || "No query statement found"),)), + Dumper($_[1]->{ParamValues}) + ); + + my $error_msg + = "
\n"
+        . Carp::longmess((Bugzilla->dbh->{Statement} || "No query statement found"))
+        . "\n
"; + if (i_am_cgi()) { + ## REDHAT EXTENSION 1276162 + ThrowUserError("db_sync_error_web", {query => $error_msg}); + } + else { # Cover RPC and console use cases + ThrowUserError("db_sync_error_other", {query => $error_msg}); + } + } + ## REDHAT EXTENSION END 1174110 + ## REDHAT EXTENSION BEGIN 1228512 + if ($_[0] =~ /invalid input syntax for type timestamp: (".*")\n/) { + Bugzilla->request_cache->{in_error} = 1; + ThrowUserError("db_invalid_timestamp_format", {timestamp => $1}); + } + ## REDHAT EXTENSION END 1228512 + ## REDHAT EXTENSION BEGIN 1250304 + if ($_[0] =~ /canceling statement due to statement timeout/) { + Bugzilla->request_cache->{in_error} = 1; + ## REDHAT EXTENSION START 1561831 + # Long queries can timeout the connection + Bugzilla->check_dbh(); + ## REDHAT EXTENSION START 1561831 + ThrowUserError("query_timeout", + {timeout => Bugzilla->params->{'long_query_timeout'}}); + } + ## REDHAT EXTENSION END 1250304 + ## RED HAT EXTENSION START 1584601 + if ($_[0] =~ /child connection forced to terminate due to client_idle_limit/) { + Bugzilla->check_dbh(); + ThrowUserError("request_timeout"); + } + ## RED HAT EXTENSION END 1584601 + ## RED HAT EXTENSION START 1302094 + if ($_[0] =~ m/ERROR:\s*invalid/) { + $_[0] =~ m{(ERROR:[^\[]*)}s; + my $txt = $1; + Bugzilla->logger->error($_[0]); + ThrowUserError("db_invalid_input", {err => $txt}); + } + ## RED HAT EXTENSION END 1302094 + } + + # Cut down the error string to a reasonable size + $_[0] = substr($_[0], 0, 2000) . ' ... ' . substr($_[0], -2000) + if length($_[0]) > 4000; + $_[0] = $_[0] . "\n
\n" . Carp::longmess() . "\n
"; + + if (!Bugzilla->request_cache->{in_error} + && Bugzilla->usage_mode == USAGE_MODE_BROWSER) + { + Bugzilla->request_cache->{in_error} = 1; +## REDHAT EXTENSION START 1174529 + my $error = 'db_error'; + $error = 'db_query_timout' + if ($_[0] =~ /canceling statement due to statement timeout/); +## REDHAT EXTENSION END 1174529 + Bugzilla->check_dbh(); ## RED HAT EXTENSION 1584601 + ThrowCodeError($error, {err_message => $_[0]}); + } + + return 0; # Now let DBI handle raising the error } sub bz_check_requirements { - my ($output) = @_; + my ($output) = @_; - my $lc = Bugzilla->localconfig; - my $db = DB_MODULE->{lc($lc->{db_driver})}; + my $lc = Bugzilla->localconfig; + my $db = DB_MODULE->{lc($lc->{db_driver})}; - # Only certain values are allowed for $db_driver. - if (!defined $db) { - die "$lc->{db_driver} is not a valid choice for \$db_driver in" - . bz_locations()->{'localconfig'}; - } + # Only certain values are allowed for $db_driver. + if (!defined $db) { + die "$lc->{db_driver} is not a valid choice for \$db_driver in" + . bz_locations()->{'localconfig'}; + } - # Check the existence and version of the DBD that we need. - my $dbd = $db->{dbd}; - _bz_check_dbd($db, $output); + # Check the existence and version of the DBD that we need. + my $dbd = $db->{dbd}; + _bz_check_dbd($db, $output); - # We don't try to connect to the actual database if $db_check is - # disabled. - unless ($lc->{db_check}) { - print "\n" if $output; - return; - } + # We don't try to connect to the actual database if $db_check is + # disabled. + unless ($lc->{db_check}) { + print "\n" if $output; + return; + } - # And now check the version of the database server itself. - my $dbh = _get_no_db_connection(); - $dbh->bz_check_server_version($db, $output); + # And now check the version of the database server itself. + my $dbh = _get_no_db_connection(); + $dbh->bz_check_server_version($db, $output); - print "\n" if $output; + print "\n" if $output; } sub _bz_check_dbd { - my ($db, $output) = @_; + my ($db, $output) = @_; - my $dbd = $db->{dbd}; - unless (have_vers($dbd, $output)) { - my $sql_server = $db->{name}; - my $command = install_command($dbd); - my $root = ROOT_USER; - my $dbd_mod = $dbd->{module}; - my $dbd_ver = $dbd->{version}; - die <{dbd}; + unless (have_vers($dbd, $output)) { + my $sql_server = $db->{name}; + my $command = install_command($dbd); + my $root = ROOT_USER; + my $dbd_mod = $dbd->{module}; + my $dbd_ver = $dbd->{version}; + die <bz_server_version; - $self->disconnect; + my $sql_vers = $self->bz_server_version; + $self->disconnect; - my $sql_want = $db->{db_version}; - my $version_ok = vers_cmp($sql_vers, $sql_want) > -1 ? 1 : 0; + my $sql_want = $db->{db_version}; + my $version_ok = vers_cmp($sql_vers, $sql_want) > -1 ? 1 : 0; - my $sql_server = $db->{name}; - if ($output) { - Bugzilla::Install::Requirements::_checking_for({ - package => $sql_server, wanted => $sql_want, - found => $sql_vers, ok => $version_ok }); - } + my $sql_server = $db->{name}; + if ($output) { + Bugzilla::Install::Requirements::_checking_for({ + package => $sql_server, + wanted => $sql_want, + found => $sql_vers, + ok => $version_ok + }); + } - # Check what version of the database server is installed and let - # the user know if the version is too old to be used with Bugzilla. - if (!$version_ok) { - die <localconfig->{db_name}; - - if (!$conn_success) { - $dbh = _get_no_db_connection(); - say "Creating database $db_name..."; - - # Try to create the DB, and if we fail print a friendly error. - my $success = eval { - my @sql = $dbh->_bz_schema->get_create_database_sql($db_name); - # This ends with 1 because this particular do doesn't always - # return something. - $dbh->do($_) foreach @sql; 1; - }; - if (!$success) { - my $error = $dbh->errstr || $@; - chomp($error); - die "The '$db_name' database could not be created.", - " The error returned was:\n\n $error\n\n", - _bz_connect_error_reasons(); - } + my $dbh; + + # See if we can connect to the actual Bugzilla database. + my $conn_success = eval { $dbh = connect_main() }; + my $db_name = Bugzilla->localconfig->{db_name}; + + if (!$conn_success) { + $dbh = _get_no_db_connection(); + say "Creating database $db_name..."; + + # Try to create the DB, and if we fail print a friendly error. + my $success = eval { + my @sql = $dbh->_bz_schema->get_create_database_sql($db_name); + + # This ends with 1 because this particular do doesn't always + # return something. + $dbh->do($_) foreach @sql; + 1; + }; + if (!$success) { + my $error = $dbh->errstr || $@; + chomp($error); + die "The '$db_name' database could not be created.", + " The error returned was:\n\n $error\n\n", _bz_connect_error_reasons(); } + } - $dbh->disconnect; + $dbh->disconnect; } # A helper for bz_create_database and bz_check_requirements. sub _get_no_db_connection { - my ($sql_server) = @_; - my $dbh; - my %connect_params = %{ Bugzilla->localconfig }; - $connect_params{db_name} = ''; - my $conn_success = eval { - $dbh = _connect(\%connect_params); - }; - if (!$conn_success) { - my $driver = $connect_params{db_driver}; - my $sql_server = DB_MODULE->{lc($driver)}->{name}; - # Can't use $dbh->errstr because $dbh is undef. - my $error = $DBI::errstr || $@; - chomp($error); - die "There was an error connecting to $sql_server:\n\n", - " $error\n\n", _bz_connect_error_reasons(), "\n"; - } - return $dbh; + my ($sql_server) = @_; + my $dbh; + my %connect_params = %{Bugzilla->localconfig}; + $connect_params{db_name} = ''; + my $conn_success = eval { $dbh = _connect(\%connect_params); }; + if (!$conn_success) { + my $driver = $connect_params{db_driver}; + my $sql_server = DB_MODULE->{lc($driver)}->{name}; + + # Can't use $dbh->errstr because $dbh is undef. + my $error = $DBI::errstr || $@; + chomp($error); + die "There was an error connecting to $sql_server:\n\n", " $error\n\n", + _bz_connect_error_reasons(), "\n"; + } + return $dbh; } # Just a helper because we have to re-use this text. # We don't use this in db_new because it gives away the database # username, and db_new errors can show up on CGIs. sub _bz_connect_error_reasons { - my $lc_file = bz_locations()->{'localconfig'}; - my $lc = Bugzilla->localconfig; - my $db = DB_MODULE->{lc($lc->{db_driver})}; - my $server = $db->{name}; + my $lc_file = bz_locations()->{'localconfig'}; + my $lc = Bugzilla->localconfig; + my $db = DB_MODULE->{lc($lc->{db_driver})}; + my $server = $db->{name}; -return <can($meth) - or die("Class $pkg does not define method $meth"); - } + my $pkg = shift; + + # do not check this module + if ($pkg ne __PACKAGE__) { + + # make sure all abstract methods are implemented + foreach my $meth (@_abstract_methods) { + $pkg->can($meth) or die("Class $pkg does not define method $meth"); } + } - # Now we want to call our superclass implementation. - # If our superclass is Exporter, which is using caller() to find - # a namespace to populate, we need to adjust for this extra call. - # All this can go when we stop using deprecated functions. - my $is_exporter = $pkg->isa('Exporter'); - $Exporter::ExportLevel++ if $is_exporter; - $pkg->SUPER::import(@_); - $Exporter::ExportLevel-- if $is_exporter; + # Now we want to call our superclass implementation. + # If our superclass is Exporter, which is using caller() to find + # a namespace to populate, we need to adjust for this extra call. + # All this can go when we stop using deprecated functions. + my $is_exporter = $pkg->isa('Exporter'); + $Exporter::ExportLevel++ if $is_exporter; + $pkg->SUPER::import(@_); + $Exporter::ExportLevel-- if $is_exporter; } sub sql_istrcmp { - my ($self, $left, $right, $op) = @_; - $op ||= "="; + my ($self, $left, $right, $op) = @_; + $op ||= "="; - return $self->sql_istring($left) . " $op " . $self->sql_istring($right); + return $self->sql_istring($left) . " $op " . $self->sql_istring($right); } sub sql_istring { - my ($self, $string) = @_; + my ($self, $string) = @_; - return "LOWER($string)"; + return "LOWER($string)"; } sub sql_iposition { - my ($self, $fragment, $text) = @_; - $fragment = $self->sql_istring($fragment); - $text = $self->sql_istring($text); - return $self->sql_position($fragment, $text); + my ($self, $fragment, $text) = @_; + $fragment = $self->sql_istring($fragment); + $text = $self->sql_istring($text); + return $self->sql_position($fragment, $text); } sub sql_position { - my ($self, $fragment, $text) = @_; + my ($self, $fragment, $text) = @_; - return "POSITION($fragment IN $text)"; + return "POSITION($fragment IN $text)"; } sub sql_like { - my ($self, $fragment, $column) = @_; + my ($self, $fragment, $column) = @_; - my $quoted = $self->quote($fragment); + my $quoted = $self->quote($fragment); - return $self->sql_position($quoted, $column) . " > 0"; + return $self->sql_position($quoted, $column) . " > 0"; } sub sql_ilike { - my ($self, $fragment, $column) = @_; + my ($self, $fragment, $column) = @_; - my $quoted = $self->quote($fragment); + my $quoted = $self->quote($fragment); - return $self->sql_iposition($quoted, $column) . " > 0"; + return $self->sql_iposition($quoted, $column) . " > 0"; } sub sql_not_ilike { - my ($self, $fragment, $column) = @_; + my ($self, $fragment, $column) = @_; - my $quoted = $self->quote($fragment); + my $quoted = $self->quote($fragment); - return $self->sql_iposition($quoted, $column) . " = 0"; + return $self->sql_iposition($quoted, $column) . " = 0"; } sub sql_group_by { - my ($self, $needed_columns, $optional_columns) = @_; + my ($self, $needed_columns, $optional_columns) = @_; + + my $expression = "GROUP BY $needed_columns"; + $expression .= ", " . $optional_columns if $optional_columns; - my $expression = "GROUP BY $needed_columns"; - $expression .= ", " . $optional_columns if $optional_columns; - - return $expression; + return $expression; } sub sql_string_concat { - my ($self, @params) = @_; - - return '(' . join(' || ', @params) . ')'; + my ($self, @params) = @_; + + return '(' . join(' || ', @params) . ')'; } sub sql_string_until { - my ($self, $string, $substring) = @_; + my ($self, $string, $substring) = @_; - my $position = $self->sql_position($substring, $string); - return "CASE WHEN $position != 0" - . " THEN SUBSTR($string, 1, $position - 1)" - . " ELSE $string END"; + my $position = $self->sql_position($substring, $string); + return + "CASE WHEN $position != 0" + . " THEN SUBSTR($string, 1, $position - 1)" + . " ELSE $string END"; } sub sql_in { - my ($self, $column_name, $in_list_ref, $negate) = @_; - return " $column_name " - . ($negate ? "NOT " : "") - . "IN (" . join(',', @$in_list_ref) . ") "; + my ($self, $column_name, $in_list_ref, $negate) = @_; + return + " $column_name " + . ($negate ? "NOT " : "") . "IN (" + . join(',', @$in_list_ref) . ") "; } sub sql_fulltext_search { - my ($self, $column, $text) = @_; - - # This is as close as we can get to doing full text search using - # standard ANSI SQL, without real full text search support. DB specific - # modules should override this, as this will be always much slower. - - # make the string lowercase to do case insensitive search - my $lower_text = lc($text); - - # split the text we're searching for into separate words. As a hack - # to allow quicksearch to work, if the field starts and ends with - # a double-quote, then we don't split it into words. We can't use - # Text::ParseWords here because it gets very confused by unbalanced - # quotes, which breaks searches like "don't try this" (because of the - # unbalanced single-quote in "don't"). - my @words; - if ($lower_text =~ /^"/ and $lower_text =~ /"$/) { - $lower_text =~ s/^"//; - $lower_text =~ s/"$//; - @words = ($lower_text); - } - else { - @words = split(/\s+/, $lower_text); - } - - # surround the words with wildcards and SQL quotes so we can use them - # in LIKE search clauses - @words = map($self->quote("\%$_\%"), @words); - - # untaint words, since they are safe to use now that we've quoted them - trick_taint($_) foreach @words; - - # turn the words into a set of LIKE search clauses - @words = map("LOWER($column) LIKE $_", @words); - - # search for occurrences of all specified words in the column - return join (" AND ", @words), "CASE WHEN (" . join(" AND ", @words) . ") THEN 1 ELSE 0 END"; + my ($self, $column, $text) = @_; + + # This is as close as we can get to doing full text search using + # standard ANSI SQL, without real full text search support. DB specific + # modules should override this, as this will be always much slower. + + # make the string lowercase to do case insensitive search + my $lower_text = lc($text); + + # split the text we're searching for into separate words. As a hack + # to allow quicksearch to work, if the field starts and ends with + # a double-quote, then we don't split it into words. We can't use + # Text::ParseWords here because it gets very confused by unbalanced + # quotes, which breaks searches like "don't try this" (because of the + # unbalanced single-quote in "don't"). + my @words; + if ($lower_text =~ /^"/ and $lower_text =~ /"$/) { + $lower_text =~ s/^"//; + $lower_text =~ s/"$//; + @words = ($lower_text); + } + else { + @words = split(/\s+/, $lower_text); + } + + # surround the words with wildcards and SQL quotes so we can use them + # in LIKE search clauses + @words = map($self->quote("\%$_\%"), @words); + + # untaint words, since they are safe to use now that we've quoted them + trick_taint($_) foreach @words; + + # turn the words into a set of LIKE search clauses + @words = map("LOWER($column) LIKE $_", @words); + + # search for occurrences of all specified words in the column + return join(" AND ", @words), + "CASE WHEN (" . join(" AND ", @words) . ") THEN 1 ELSE 0 END"; } ##################################################################### @@ -465,24 +554,27 @@ sub sql_fulltext_search { # XXX - Needs to be documented. sub bz_server_version { - my ($self) = @_; - return $self->get_info(18); # SQL_DBMS_VER + my ($self) = @_; + return $self->get_info(18); # SQL_DBMS_VER } sub bz_last_key { - my ($self, $table, $column) = @_; + my ($self, $table, $column) = @_; - return $self->last_insert_id(Bugzilla->localconfig->{db_name}, undef, - $table, $column); + return $self->last_insert_id(Bugzilla->localconfig->{db_name}, + undef, $table, $column); } sub bz_check_regexp { - my ($self, $pattern) = @_; + my ($self, $pattern) = @_; - eval { $self->do("SELECT " . $self->sql_regexp($self->quote("a"), $pattern, 1)) }; + eval { + $self->do("SELECT " . $self->sql_regexp($self->quote("a"), $pattern, 1)); + }; - $@ && ThrowUserError('illegal_regexp', - { value => $pattern, dberror => $self->errstr }); + $@ + && ThrowUserError('illegal_regexp', + {value => $pattern, dberror => $self->errstr}); } ##################################################################### @@ -490,99 +582,100 @@ sub bz_check_regexp { ##################################################################### sub bz_setup_database { - my ($self) = @_; - - # If we haven't ever stored a serialized schema, - # set up the bz_schema table and store it. - $self->_bz_init_schema_storage(); - - # We don't use bz_table_list here, because that uses _bz_real_schema. - # We actually want the table list from the ABSTRACT_SCHEMA in - # Bugzilla::DB::Schema. - my @desired_tables = $self->_bz_schema->get_table_list(); - my $bugs_exists = $self->bz_table_info('bugs'); - if (!$bugs_exists) { - say install_string('db_table_setup'); - } + my ($self) = @_; - foreach my $table_name (@desired_tables) { - $self->bz_add_table($table_name, { silently => !$bugs_exists }); - } + # If we haven't ever stored a serialized schema, + # set up the bz_schema table and store it. + $self->_bz_init_schema_storage(); + + # We don't use bz_table_list here, because that uses _bz_real_schema. + # We actually want the table list from the ABSTRACT_SCHEMA in + # Bugzilla::DB::Schema. + my @desired_tables = $self->_bz_schema->get_table_list(); + my $bugs_exists = $self->bz_table_info('bugs'); + if (!$bugs_exists) { + say install_string('db_table_setup'); + } + + foreach my $table_name (@desired_tables) { + $self->bz_add_table($table_name, {silently => !$bugs_exists}); + } } # This really just exists to get overridden in Bugzilla::DB::Mysql. sub bz_enum_initial_values { - return ENUM_DEFAULTS; + return ENUM_DEFAULTS; } sub bz_populate_enum_tables { - my ($self) = @_; + my ($self) = @_; - my $any_severities = $self->selectrow_array( - 'SELECT 1 FROM bug_severity ' . $self->sql_limit(1)); - print install_string('db_enum_setup'), "\n " if !$any_severities; + my $any_severities + = $self->selectrow_array('SELECT 1 FROM bug_severity ' . $self->sql_limit(1)); + print install_string('db_enum_setup'), "\n " if !$any_severities; - my $enum_values = $self->bz_enum_initial_values(); - while (my ($table, $values) = each %$enum_values) { - $self->_bz_populate_enum_table($table, $values); - } + my $enum_values = $self->bz_enum_initial_values(); + while (my ($table, $values) = each %$enum_values) { + $self->_bz_populate_enum_table($table, $values); + } - print "\n" if !$any_severities; + print "\n" if !$any_severities; } sub bz_setup_foreign_keys { - my ($self) = @_; - - # profiles_activity was the first table to get foreign keys, - # so if it doesn't have them, then we're setting up FKs - # for the first time, and should be quieter about it. - my $activity_fk = $self->bz_fk_info('profiles_activity', 'userid'); - my $any_fks = $activity_fk && $activity_fk->{created}; - if (!$any_fks) { - say get_text('install_fk_setup'); - } + my ($self) = @_; + + # profiles_activity was the first table to get foreign keys, + # so if it doesn't have them, then we're setting up FKs + # for the first time, and should be quieter about it. + my $activity_fk = $self->bz_fk_info('profiles_activity', 'userid'); + my $any_fks = $activity_fk && $activity_fk->{created}; + if (!$any_fks) { + say get_text('install_fk_setup'); + } + + my @tables = $self->bz_table_list(); + foreach my $table (@tables) { + my @columns = $self->bz_table_columns($table); + my %add_fks; + foreach my $column (@columns) { - my @tables = $self->bz_table_list(); - foreach my $table (@tables) { - my @columns = $self->bz_table_columns($table); - my %add_fks; - foreach my $column (@columns) { - # First we check for any FKs that have created => 0, - # in the _bz_real_schema. This also picks up FKs with - # created => 1, but bz_add_fks will ignore those. - my $fk = $self->bz_fk_info($table, $column); - # Then we check the abstract schema to see if there - # should be an FK on this column, but one wasn't set in the - # _bz_real_schema for some reason. We do this to handle - # various problems caused by upgrading from versions - # prior to 4.2, and also to handle problems caused - # by enabling an extension pre-4.2, disabling it for - # the 4.2 upgrade, and then re-enabling it later. - unless ($fk && $fk->{created}) { - my $standard_def = - $self->_bz_schema->get_column_abstract($table, $column); - if (exists $standard_def->{REFERENCES}) { - $fk = dclone($standard_def->{REFERENCES}); - } - } - - $add_fks{$column} = $fk if $fk; + # First we check for any FKs that have created => 0, + # in the _bz_real_schema. This also picks up FKs with + # created => 1, but bz_add_fks will ignore those. + my $fk = $self->bz_fk_info($table, $column); + + # Then we check the abstract schema to see if there + # should be an FK on this column, but one wasn't set in the + # _bz_real_schema for some reason. We do this to handle + # various problems caused by upgrading from versions + # prior to 4.2, and also to handle problems caused + # by enabling an extension pre-4.2, disabling it for + # the 4.2 upgrade, and then re-enabling it later. + unless ($fk && $fk->{created}) { + my $standard_def = $self->_bz_schema->get_column_abstract($table, $column); + if (exists $standard_def->{REFERENCES}) { + $fk = dclone($standard_def->{REFERENCES}); } - $self->bz_add_fks($table, \%add_fks, { silently => !$any_fks }); + } + + $add_fks{$column} = $fk if $fk; } + $self->bz_add_fks($table, \%add_fks, {silently => !$any_fks}); + } } # This is used by contrib/bzdbcopy.pl, mostly. sub bz_drop_foreign_keys { - my ($self) = @_; + my ($self) = @_; - my @tables = $self->bz_table_list(); - foreach my $table (@tables) { - my @columns = $self->bz_table_columns($table); - foreach my $column (@columns) { - $self->bz_drop_fk($table, $column); - } + my @tables = $self->bz_table_list(); + foreach my $table (@tables) { + my @columns = $self->bz_table_columns($table); + foreach my $column (@columns) { + $self->bz_drop_fk($table, $column); } + } } ##################################################################### @@ -590,119 +683,121 @@ sub bz_drop_foreign_keys { ##################################################################### sub bz_add_column { - my ($self, $table, $name, $new_def, $init_value) = @_; - - # You can't add a NOT NULL column to a table with - # no DEFAULT statement, unless you have an init_value. - # SERIAL types are an exception, though, because they can - # auto-populate. - if ( $new_def->{NOTNULL} && !exists $new_def->{DEFAULT} - && !defined $init_value && $new_def->{TYPE} !~ /SERIAL/) - { - ThrowCodeError('column_not_null_without_default', - { name => "$table.$name" }); + my ($self, $table, $name, $new_def, $init_value) = @_; + + # You can't add a NOT NULL column to a table with + # no DEFAULT statement, unless you have an init_value. + # SERIAL types are an exception, though, because they can + # auto-populate. + if ( $new_def->{NOTNULL} + && !exists $new_def->{DEFAULT} + && !defined $init_value + && $new_def->{TYPE} !~ /SERIAL/) + { + ThrowCodeError('column_not_null_without_default', {name => "$table.$name"}); + } + + my $current_def = $self->bz_column_info($table, $name); + + if (!$current_def) { + + # REFERENCES need to happen later and not be created right away + my $trimmed_def = dclone($new_def); + delete $trimmed_def->{REFERENCES}; + my @statements + = $self->_bz_real_schema->get_add_column_ddl($table, $name, $trimmed_def, + defined $init_value ? $self->quote($init_value) : undef); + print get_text('install_column_add', {column => $name, table => $table}) . "\n" + if Bugzilla->usage_mode == USAGE_MODE_CMDLINE; + foreach my $sql (@statements) { + $self->do($sql); } - my $current_def = $self->bz_column_info($table, $name); - - if (!$current_def) { - # REFERENCES need to happen later and not be created right away - my $trimmed_def = dclone($new_def); - delete $trimmed_def->{REFERENCES}; - my @statements = $self->_bz_real_schema->get_add_column_ddl( - $table, $name, $trimmed_def, - defined $init_value ? $self->quote($init_value) : undef); - print get_text('install_column_add', - { column => $name, table => $table }) . "\n" - if Bugzilla->usage_mode == USAGE_MODE_CMDLINE; - foreach my $sql (@statements) { - $self->do($sql); - } - - # To make things easier for callers, if they don't specify - # a REFERENCES item, we pull it from the _bz_schema if the - # column exists there and has a REFERENCES item. - # bz_setup_foreign_keys will then add this FK at the end of - # Install::DB. - my $col_abstract = - $self->_bz_schema->get_column_abstract($table, $name); - if (exists $col_abstract->{REFERENCES}) { - my $new_fk = dclone($col_abstract->{REFERENCES}); - $new_fk->{created} = 0; - $new_def->{REFERENCES} = $new_fk; - } - - $self->_bz_real_schema->set_column($table, $name, $new_def); - $self->_bz_store_real_schema; + # To make things easier for callers, if they don't specify + # a REFERENCES item, we pull it from the _bz_schema if the + # column exists there and has a REFERENCES item. + # bz_setup_foreign_keys will then add this FK at the end of + # Install::DB. + my $col_abstract = $self->_bz_schema->get_column_abstract($table, $name); + if (exists $col_abstract->{REFERENCES}) { + my $new_fk = dclone($col_abstract->{REFERENCES}); + $new_fk->{created} = 0; + $new_def->{REFERENCES} = $new_fk; } + + $self->_bz_real_schema->set_column($table, $name, $new_def); + $self->_bz_store_real_schema; + } } sub bz_add_fk { - my ($self, $table, $column, $def) = @_; - $self->bz_add_fks($table, { $column => $def }); + my ($self, $table, $column, $def) = @_; + $self->bz_add_fks($table, {$column => $def}); } sub bz_add_fks { - my ($self, $table, $column_fks, $options) = @_; - - my %add_these; - foreach my $column (keys %$column_fks) { - my $current_fk = $self->bz_fk_info($table, $column); - next if ($current_fk and $current_fk->{created}); - my $new_fk = $column_fks->{$column}; - $self->_check_references($table, $column, $new_fk); - $add_these{$column} = $new_fk; - if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE - and !$options->{silently}) - { - print get_text('install_fk_add', - { table => $table, column => $column, - fk => $new_fk }), "\n"; - } + my ($self, $table, $column_fks, $options) = @_; + + my %add_these; + foreach my $column (keys %$column_fks) { + my $current_fk = $self->bz_fk_info($table, $column); + next if ($current_fk and $current_fk->{created}); + my $new_fk = $column_fks->{$column}; + $self->_check_references($table, $column, $new_fk); + $add_these{$column} = $new_fk; + if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE and !$options->{silently}) { + print get_text( + 'install_fk_add', {table => $table, column => $column, fk => $new_fk} + ), + "\n"; } + } - return if !scalar(keys %add_these); + return if !scalar(keys %add_these); - my @sql = $self->_bz_real_schema->get_add_fks_sql($table, \%add_these); - $self->do($_) foreach @sql; + my @sql = $self->_bz_real_schema->get_add_fks_sql($table, \%add_these); + $self->do($_) foreach @sql; - foreach my $column (keys %add_these) { - my $fk_def = $add_these{$column}; - $fk_def->{created} = 1; - $self->_bz_real_schema->set_fk($table, $column, $fk_def); - } + foreach my $column (keys %add_these) { + my $fk_def = $add_these{$column}; + $fk_def->{created} = 1; + $self->_bz_real_schema->set_fk($table, $column, $fk_def); + } - $self->_bz_store_real_schema(); + $self->_bz_store_real_schema(); } sub bz_alter_column { - my ($self, $table, $name, $new_def, $set_nulls_to) = @_; + my ($self, $table, $name, $new_def, $set_nulls_to) = @_; - my $current_def = $self->bz_column_info($table, $name); + my $current_def = $self->bz_column_info($table, $name); - if (!$self->_bz_schema->columns_equal($current_def, $new_def)) { - # You can't change a column to be NOT NULL if you have no DEFAULT - # and no value for $set_nulls_to, if there are any NULL values - # in that column. - if ($new_def->{NOTNULL} && - !exists $new_def->{DEFAULT} && !defined $set_nulls_to) - { - # Check for NULLs - my $any_nulls = $self->selectrow_array( - "SELECT 1 FROM $table WHERE $name IS NULL"); - ThrowCodeError('column_not_null_no_default_alter', - { name => "$table.$name" }) if ($any_nulls); - } - # Preserve foreign key definitions in the Schema object when altering - # types. - if (my $fk = $self->bz_fk_info($table, $name)) { - $new_def->{REFERENCES} = $fk; - } - $self->bz_alter_column_raw($table, $name, $new_def, $current_def, - $set_nulls_to); - $self->_bz_real_schema->set_column($table, $name, $new_def); - $self->_bz_store_real_schema; + if (!$self->_bz_schema->columns_equal($current_def, $new_def)) { + + # You can't change a column to be NOT NULL if you have no DEFAULT + # and no value for $set_nulls_to, if there are any NULL values + # in that column. + if ( $new_def->{NOTNULL} + && !exists $new_def->{DEFAULT} + && !defined $set_nulls_to) + { + # Check for NULLs + my $any_nulls + = $self->selectrow_array("SELECT 1 FROM $table WHERE $name IS NULL"); + ThrowCodeError('column_not_null_no_default_alter', {name => "$table.$name"}) + if ($any_nulls); + } + + # Preserve foreign key definitions in the Schema object when altering + # types. + if (my $fk = $self->bz_fk_info($table, $name)) { + $new_def->{REFERENCES} = $fk; } + $self->bz_alter_column_raw($table, $name, $new_def, $current_def, + $set_nulls_to); + $self->_bz_real_schema->set_column($table, $name, $new_def); + $self->_bz_store_real_schema; + } } @@ -728,39 +823,40 @@ sub bz_alter_column { # Returns: nothing # sub bz_alter_column_raw { - my ($self, $table, $name, $new_def, $current_def, $set_nulls_to) = @_; - my @statements = $self->_bz_real_schema->get_alter_column_ddl( - $table, $name, $new_def, - defined $set_nulls_to ? $self->quote($set_nulls_to) : undef); - my $new_ddl = $self->_bz_schema->get_type_ddl($new_def); - say "Updating column $name in table $table ..."; - if (defined $current_def) { - my $old_ddl = $self->_bz_schema->get_type_ddl($current_def); - say "Old: $old_ddl"; - } - say "New: $new_ddl"; - $self->do($_) foreach (@statements); + my ($self, $table, $name, $new_def, $current_def, $set_nulls_to) = @_; + my @statements + = $self->_bz_real_schema->get_alter_column_ddl($table, $name, $new_def, + defined $set_nulls_to ? $self->quote($set_nulls_to) : undef); + my $new_ddl = $self->_bz_schema->get_type_ddl($new_def); + say "Updating column $name in table $table ..."; + if (defined $current_def) { + my $old_ddl = $self->_bz_schema->get_type_ddl($current_def); + say "Old: $old_ddl"; + } + say "New: $new_ddl"; + $self->do($_) foreach (@statements); } sub bz_alter_fk { - my ($self, $table, $column, $fk_def) = @_; - my $current_fk = $self->bz_fk_info($table, $column); - ThrowCodeError('column_alter_nonexistent_fk', - { table => $table, column => $column }) if !$current_fk; - $self->bz_drop_fk($table, $column); - $self->bz_add_fk($table, $column, $fk_def); + my ($self, $table, $column, $fk_def) = @_; + my $current_fk = $self->bz_fk_info($table, $column); + ThrowCodeError('column_alter_nonexistent_fk', + {table => $table, column => $column}) + if !$current_fk; + $self->bz_drop_fk($table, $column); + $self->bz_add_fk($table, $column, $fk_def); } sub bz_add_index { - my ($self, $table, $name, $definition) = @_; + my ($self, $table, $name, $definition) = @_; - my $index_exists = $self->bz_index_info($table, $name); + my $index_exists = $self->bz_index_info($table, $name); - if (!$index_exists) { - $self->bz_add_index_raw($table, $name, $definition); - $self->_bz_real_schema->set_index($table, $name, $definition); - $self->_bz_store_real_schema; - } + if (!$index_exists) { + $self->bz_add_index_raw($table, $name, $definition); + $self->_bz_real_schema->set_index($table, $name, $definition); + $self->_bz_store_real_schema; + } } # bz_add_index_raw($table, $name, $silent) @@ -780,36 +876,36 @@ sub bz_add_index { # Returns: nothing # sub bz_add_index_raw { - my ($self, $table, $name, $definition, $silent) = @_; - my @statements = $self->_bz_schema->get_add_index_ddl( - $table, $name, $definition); - print "Adding new index '$name' to the $table table ...\n" unless $silent; - $self->do($_) foreach (@statements); + my ($self, $table, $name, $definition, $silent) = @_; + my @statements + = $self->_bz_schema->get_add_index_ddl($table, $name, $definition); + print "Adding new index '$name' to the $table table ...\n" unless $silent; + $self->do($_) foreach (@statements); } sub bz_add_table { - my ($self, $name, $options) = @_; - - my $table_exists = $self->bz_table_info($name); - - if (!$table_exists) { - $self->_bz_add_table_raw($name, $options); - my $table_def = dclone($self->_bz_schema->get_table_abstract($name)); - - my %fields = @{$table_def->{FIELDS}}; - foreach my $col (keys %fields) { - # Foreign Key references have to be added by Install::DB after - # initial table creation, because column names have changed - # over history and it's impossible to keep track of that info - # in ABSTRACT_SCHEMA. - next unless exists $fields{$col}->{REFERENCES}; - $fields{$col}->{REFERENCES}->{created} = - $self->_bz_real_schema->FK_ON_CREATE; - } - - $self->_bz_real_schema->add_table($name, $table_def); - $self->_bz_store_real_schema; + my ($self, $name, $options) = @_; + + my $table_exists = $self->bz_table_info($name); + + if (!$table_exists) { + $self->_bz_add_table_raw($name, $options); + my $table_def = dclone($self->_bz_schema->get_table_abstract($name)); + + my %fields = @{$table_def->{FIELDS}}; + foreach my $col (keys %fields) { + + # Foreign Key references have to be added by Install::DB after + # initial table creation, because column names have changed + # over history and it's impossible to keep track of that info + # in ABSTRACT_SCHEMA. + next unless exists $fields{$col}->{REFERENCES}; + $fields{$col}->{REFERENCES}->{created} = $self->_bz_real_schema->FK_ON_CREATE; } + + $self->_bz_real_schema->add_table($name, $table_def); + $self->_bz_store_real_schema; + } } # _bz_add_table_raw($name) - Private @@ -821,164 +917,190 @@ sub bz_add_table { # _bz_init_schema_storage. Used when you don't # yet have a Schema object but you need to # add a table, for some reason. -# Params: $name - The name of the table you're creating. -# The definition for the table is pulled from +# Params: $name - The name of the table you're creating. +# The definition for the table is pulled from # _bz_schema. # Returns: nothing # sub _bz_add_table_raw { - my ($self, $name, $options) = @_; - my @statements = $self->_bz_schema->get_table_ddl($name); - if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE - and !$options->{silently}) - { - say install_string('db_table_new', { table => $name }); - } - $self->do($_) foreach (@statements); + my ($self, $name, $options) = @_; + my @statements = $self->_bz_schema->get_table_ddl($name); + if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE and !$options->{silently}) { + say install_string('db_table_new', {table => $name}); + } + $self->do($_) foreach (@statements); } sub _bz_add_field_table { - my ($self, $name, $schema_ref) = @_; - # We do nothing if the table already exists. - return if $self->bz_table_info($name); - - # Copy this so that we're not modifying the passed reference. - # (This avoids modifying a constant in Bugzilla::DB::Schema.) - my %table_schema = %$schema_ref; - my %indexes = @{ $table_schema{INDEXES} }; - my %fixed_indexes; - foreach my $key (keys %indexes) { - $fixed_indexes{$name . "_" . $key} = $indexes{$key}; - } - # INDEXES is supposed to be an arrayref, so we have to convert back. - my @indexes_array = %fixed_indexes; - $table_schema{INDEXES} = \@indexes_array; - # We add this to the abstract schema so that bz_add_table can find it. - $self->_bz_schema->add_table($name, \%table_schema); - $self->bz_add_table($name); + my ($self, $name, $schema_ref) = @_; + + # We do nothing if the table already exists. + return if $self->bz_table_info($name); + + # Copy this so that we're not modifying the passed reference. + # (This avoids modifying a constant in Bugzilla::DB::Schema.) + my %table_schema = %$schema_ref; + my %indexes = @{$table_schema{INDEXES}}; + my %fixed_indexes; + foreach my $key (keys %indexes) { + $fixed_indexes{$name . "_" . $key} = $indexes{$key}; + } + + # INDEXES is supposed to be an arrayref, so we have to convert back. + my @indexes_array = %fixed_indexes; + $table_schema{INDEXES} = \@indexes_array; + + # We add this to the abstract schema so that bz_add_table can find it. + $self->_bz_schema->add_table($name, \%table_schema); + $self->bz_add_table($name); } sub bz_add_field_tables { - my ($self, $field) = @_; - - $self->_bz_add_field_table($field->name, - $self->_bz_schema->FIELD_TABLE_SCHEMA, $field->type); - if ($field->type == FIELD_TYPE_MULTI_SELECT) { - my $ms_table = "bug_" . $field->name; - $self->_bz_add_field_table($ms_table, - $self->_bz_schema->MULTI_SELECT_VALUE_TABLE); - - $self->bz_add_fks($ms_table, - { bug_id => {TABLE => 'bugs', COLUMN => 'bug_id', - DELETE => 'CASCADE'}, - - value => {TABLE => $field->name, COLUMN => 'value'} }); - } + my ($self, $field) = @_; + + $self->_bz_add_field_table($field->name, $self->_bz_schema->FIELD_TABLE_SCHEMA, + $field->type); + if ($field->type == FIELD_TYPE_MULTI_SELECT) { + my $ms_table = "bug_" . $field->name; + $self->_bz_add_field_table($ms_table, + $self->_bz_schema->MULTI_SELECT_VALUE_TABLE); + + $self->bz_add_fks( + $ms_table, + { + bug_id => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'}, + + value => {TABLE => $field->name, COLUMN => 'value'} + } + ); + } + + if ($field->type == FIELD_TYPE_ONE_SELECT) { + my $ms_table = "bug_" . $field->name; + $self->_bz_add_field_table($ms_table, + $self->_bz_schema->SELECT_ONE_VALUE_TABLE); + + $self->bz_add_fks( + $ms_table, + { + bug_id => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'}, + value => {TABLE => $field->name, COLUMN => 'value'} + } + ); + } } sub bz_drop_field_tables { - my ($self, $field) = @_; - if ($field->type == FIELD_TYPE_MULTI_SELECT) { - $self->bz_drop_table('bug_' . $field->name); - } - $self->bz_drop_table($field->name); + my ($self, $field) = @_; + if ( $field->type == FIELD_TYPE_MULTI_SELECT + || $field->type == FIELD_TYPE_ONE_SELECT) + { + $self->bz_drop_table('bug_' . $field->name); + } + $self->bz_drop_table($field->name); } sub bz_drop_column { - my ($self, $table, $column) = @_; - - my $current_def = $self->bz_column_info($table, $column); - - if ($current_def) { - my @statements = $self->_bz_real_schema->get_drop_column_ddl( - $table, $column); - print get_text('install_column_drop', - { table => $table, column => $column }) . "\n" - if Bugzilla->usage_mode == USAGE_MODE_CMDLINE; - foreach my $sql (@statements) { - # Because this is a deletion, we don't want to die hard if - # we fail because of some local customization. If something - # is already gone, that's fine with us! - eval { $self->do($sql); } or warn "Failed SQL: [$sql] Error: $@"; - } - $self->_bz_real_schema->delete_column($table, $column); - $self->_bz_store_real_schema; + my ($self, $table, $column) = @_; + + my $current_def = $self->bz_column_info($table, $column); + + if ($current_def) { + my @statements = $self->_bz_real_schema->get_drop_column_ddl($table, $column); + print get_text('install_column_drop', {table => $table, column => $column}) + . "\n" + if Bugzilla->usage_mode == USAGE_MODE_CMDLINE; + foreach my $sql (@statements) { + + # Because this is a deletion, we don't want to die hard if + # we fail because of some local customization. If something + # is already gone, that's fine with us! + eval { $self->do($sql); } or warn "Failed SQL: [$sql] Error: $@"; } + $self->_bz_real_schema->delete_column($table, $column); + $self->_bz_store_real_schema; + } } sub bz_drop_fk { - my ($self, $table, $column) = @_; - - my $fk_def = $self->bz_fk_info($table, $column); - if ($fk_def and $fk_def->{created}) { - print get_text('install_fk_drop', - { table => $table, column => $column, fk => $fk_def }) - . "\n" if Bugzilla->usage_mode == USAGE_MODE_CMDLINE; - my @statements = - $self->_bz_real_schema->get_drop_fk_sql($table, $column, $fk_def); - foreach my $sql (@statements) { - # Because this is a deletion, we don't want to die hard if - # we fail because of some local customization. If something - # is already gone, that's fine with us! - eval { $self->do($sql); } or warn "Failed SQL: [$sql] Error: $@"; - } - # Under normal circumstances, we don't permanently drop the fk-- - # we want checksetup to re-create it again later. The only - # time that FKs get permanently dropped is if the column gets - # dropped. - $fk_def->{created} = 0; - $self->_bz_real_schema->set_fk($table, $column, $fk_def); - $self->_bz_store_real_schema; + my ($self, $table, $column) = @_; + + my $fk_def = $self->bz_fk_info($table, $column); + if ($fk_def and $fk_def->{created}) { + print get_text('install_fk_drop', + {table => $table, column => $column, fk => $fk_def}) + . "\n" + if Bugzilla->usage_mode == USAGE_MODE_CMDLINE; + my @statements + = $self->_bz_real_schema->get_drop_fk_sql($table, $column, $fk_def); + foreach my $sql (@statements) { + + # Because this is a deletion, we don't want to die hard if + # we fail because of some local customization. If something + # is already gone, that's fine with us! + eval { $self->do($sql); } or warn "Failed SQL: [$sql] Error: $@"; } + # Under normal circumstances, we don't permanently drop the fk-- + # we want checksetup to re-create it again later. The only + # time that FKs get permanently dropped is if the column gets + # dropped. + $fk_def->{created} = 0; + $self->_bz_real_schema->set_fk($table, $column, $fk_def); + $self->_bz_store_real_schema; + } + } sub bz_get_related_fks { - my ($self, $table, $column) = @_; - my @tables = $self->_bz_real_schema->get_table_list(); - my @related; - foreach my $check_table (@tables) { - my @columns = $self->bz_table_columns($check_table); - foreach my $check_column (@columns) { - my $fk = $self->bz_fk_info($check_table, $check_column); - if ($fk - and (($fk->{TABLE} eq $table and $fk->{COLUMN} eq $column) - or ($check_column eq $column and $check_table eq $table))) - { - push(@related, [$check_table, $check_column, $fk]); - } - } # foreach $column - } # foreach $table - - return \@related; + my ($self, $table, $column) = @_; + my @tables = $self->_bz_real_schema->get_table_list(); + my @related; + foreach my $check_table (@tables) { + my @columns = $self->bz_table_columns($check_table); + foreach my $check_column (@columns) { + my $fk = $self->bz_fk_info($check_table, $check_column); + if ( + $fk + and (($fk->{TABLE} eq $table and $fk->{COLUMN} eq $column) + or ($check_column eq $column and $check_table eq $table)) + ) + { + push(@related, [$check_table, $check_column, $fk]); + } + } # foreach $column + } # foreach $table + + return \@related; } sub bz_drop_related_fks { - my $self = shift; - my $related = $self->bz_get_related_fks(@_); - foreach my $item (@$related) { - my ($table, $column) = @$item; - $self->bz_drop_fk($table, $column); - } - return $related; + my $self = shift; + my $related = $self->bz_get_related_fks(@_); + foreach my $item (@$related) { + my ($table, $column) = @$item; + $self->bz_drop_fk($table, $column); + } + return $related; } sub bz_drop_index { - my ($self, $table, $name) = @_; + my ($self, $table, $name) = @_; - my $index_exists = $self->bz_index_info($table, $name); + my $index_exists = $self->bz_index_info($table, $name); - if ($index_exists) { - if ($self->INDEX_DROPS_REQUIRE_FK_DROPS) { - # We cannot delete an index used by a FK. - foreach my $column (@{$index_exists->{FIELDS}}) { - $self->bz_drop_related_fks($table, $column); - } - } - $self->bz_drop_index_raw($table, $name); - $self->_bz_real_schema->delete_index($table, $name); - $self->_bz_store_real_schema; + if ($index_exists) { + if ($self->INDEX_DROPS_REQUIRE_FK_DROPS) { + + # We cannot delete an index used by a FK. + foreach my $column (@{$index_exists->{FIELDS}}) { + $self->bz_drop_related_fks($table, $column); + } } + $self->bz_drop_index_raw($table, $name); + $self->_bz_real_schema->delete_index($table, $name); + $self->_bz_store_real_schema; + } } # bz_drop_index_raw($table, $name, $silent) @@ -987,7 +1109,7 @@ sub bz_drop_index { # Drops an index from the database # without updating any Schema object. Generally # should only be called by bz_drop_index. -# Used when either: (1) You don't yet have a Schema +# Used when either: (1) You don't yet have a Schema # object but you need to drop an index, for some reason. # (2) You need to drop an index that somehow got into the # database but doesn't exist in Schema. @@ -998,108 +1120,111 @@ sub bz_drop_index { # Returns: nothing # sub bz_drop_index_raw { - my ($self, $table, $name, $silent) = @_; - my @statements = $self->_bz_schema->get_drop_index_ddl( - $table, $name); - print "Removing index '$name' from the $table table...\n" unless $silent; - foreach my $sql (@statements) { - # Because this is a deletion, we don't want to die hard if - # we fail because of some local customization. If something - # is already gone, that's fine with us! - eval { $self->do($sql) } or warn "Failed SQL: [$sql] Error: $@"; - } + my ($self, $table, $name, $silent) = @_; + my @statements = $self->_bz_schema->get_drop_index_ddl($table, $name); + print "Removing index '$name' from the $table table...\n" unless $silent; + foreach my $sql (@statements) { + + # Because this is a deletion, we don't want to die hard if + # we fail because of some local customization. If something + # is already gone, that's fine with us! + eval { $self->do($sql) } or warn "Failed SQL: [$sql] Error: $@"; + } } sub bz_drop_table { - my ($self, $name) = @_; - - my $table_exists = $self->bz_table_info($name); - - if ($table_exists) { - my @statements = $self->_bz_schema->get_drop_table_ddl($name); - print get_text('install_table_drop', { name => $name }) . "\n" - if Bugzilla->usage_mode == USAGE_MODE_CMDLINE; - foreach my $sql (@statements) { - # Because this is a deletion, we don't want to die hard if - # we fail because of some local customization. If something - # is already gone, that's fine with us! - eval { $self->do($sql); } or warn "Failed SQL: [$sql] Error: $@"; - } - $self->_bz_real_schema->delete_table($name); - $self->_bz_store_real_schema; + my ($self, $name) = @_; + + my $table_exists = $self->bz_table_info($name); + + if ($table_exists) { + my @statements = $self->_bz_schema->get_drop_table_ddl($name); + print get_text('install_table_drop', {name => $name}) . "\n" + if Bugzilla->usage_mode == USAGE_MODE_CMDLINE; + foreach my $sql (@statements) { + + # Because this is a deletion, we don't want to die hard if + # we fail because of some local customization. If something + # is already gone, that's fine with us! + eval { $self->do($sql); } or warn "Failed SQL: [$sql] Error: $@"; } + $self->_bz_real_schema->delete_table($name); + $self->_bz_store_real_schema; + } } sub bz_fk_info { - my ($self, $table, $column) = @_; - my $col_info = $self->bz_column_info($table, $column); - return undef if !$col_info; - my $fk = $col_info->{REFERENCES}; - return $fk; + my ($self, $table, $column) = @_; + my $col_info = $self->bz_column_info($table, $column); + return undef if !$col_info; + my $fk = $col_info->{REFERENCES}; + return $fk; } sub bz_rename_column { - my ($self, $table, $old_name, $new_name) = @_; + my ($self, $table, $old_name, $new_name) = @_; - my $old_col_exists = $self->bz_column_info($table, $old_name); + my $old_col_exists = $self->bz_column_info($table, $old_name); - if ($old_col_exists) { - my $already_renamed = $self->bz_column_info($table, $new_name); - ThrowCodeError('db_rename_conflict', - { old => "$table.$old_name", - new => "$table.$new_name" }) if $already_renamed; - my @statements = $self->_bz_real_schema->get_rename_column_ddl( - $table, $old_name, $new_name); + if ($old_col_exists) { + my $already_renamed = $self->bz_column_info($table, $new_name); + ThrowCodeError('db_rename_conflict', + {old => "$table.$old_name", new => "$table.$new_name"}) + if $already_renamed; + my @statements + = $self->_bz_real_schema->get_rename_column_ddl($table, $old_name, $new_name); - print get_text('install_column_rename', - { old => "$table.$old_name", new => "$table.$new_name" }) - . "\n" if Bugzilla->usage_mode == USAGE_MODE_CMDLINE; + print get_text('install_column_rename', + {old => "$table.$old_name", new => "$table.$new_name"}) + . "\n" + if Bugzilla->usage_mode == USAGE_MODE_CMDLINE; - foreach my $sql (@statements) { - $self->do($sql); - } - $self->_bz_real_schema->rename_column($table, $old_name, $new_name); - $self->_bz_store_real_schema; + foreach my $sql (@statements) { + $self->do($sql); } + $self->_bz_real_schema->rename_column($table, $old_name, $new_name); + $self->_bz_store_real_schema; + } } sub bz_rename_table { - my ($self, $old_name, $new_name) = @_; - my $old_table = $self->bz_table_info($old_name); - return if !$old_table; - - my $new = $self->bz_table_info($new_name); - ThrowCodeError('db_rename_conflict', { old => $old_name, - new => $new_name }) if $new; - - # FKs will all have the wrong names unless we drop and then let them - # be re-created later. Under normal circumstances, checksetup.pl will - # automatically re-create these dropped FKs at the end of its DB upgrade - # run, so we don't need to re-create them in this method. - my @columns = $self->bz_table_columns($old_name); - foreach my $column (@columns) { - # these just return silently if there's no FK to drop - $self->bz_drop_fk($old_name, $column); - $self->bz_drop_related_fks($old_name, $column); - } - - my @sql = $self->_bz_real_schema->get_rename_table_sql($old_name, $new_name); - print get_text('install_table_rename', - { old => $old_name, new => $new_name }) . "\n" - if Bugzilla->usage_mode == USAGE_MODE_CMDLINE; - $self->do($_) foreach @sql; - $self->_bz_real_schema->rename_table($old_name, $new_name); - $self->_bz_store_real_schema; + my ($self, $old_name, $new_name) = @_; + my $old_table = $self->bz_table_info($old_name); + return if !$old_table; + + my $new = $self->bz_table_info($new_name); + ThrowCodeError('db_rename_conflict', {old => $old_name, new => $new_name}) + if $new; + + # FKs will all have the wrong names unless we drop and then let them + # be re-created later. Under normal circumstances, checksetup.pl will + # automatically re-create these dropped FKs at the end of its DB upgrade + # run, so we don't need to re-create them in this method. + my @columns = $self->bz_table_columns($old_name); + foreach my $column (@columns) { + + # these just return silently if there's no FK to drop + $self->bz_drop_fk($old_name, $column); + $self->bz_drop_related_fks($old_name, $column); + } + + my @sql = $self->_bz_real_schema->get_rename_table_sql($old_name, $new_name); + print get_text('install_table_rename', {old => $old_name, new => $new_name}) + . "\n" + if Bugzilla->usage_mode == USAGE_MODE_CMDLINE; + $self->do($_) foreach @sql; + $self->_bz_real_schema->rename_table($old_name, $new_name); + $self->_bz_store_real_schema; } sub bz_set_next_serial_value { - my ($self, $table, $column, $value) = @_; - if (!$value) { - $value = $self->selectrow_array("SELECT MAX($column) FROM $table") || 0; - $value++; - } - my @sql = $self->_bz_real_schema->get_set_serial_sql($table, $column, $value); - $self->do($_) foreach @sql; + my ($self, $table, $column, $value) = @_; + if (!$value) { + $value = $self->selectrow_array("SELECT MAX($column) FROM $table") || 0; + $value++; + } + my @sql = $self->_bz_real_schema->get_set_serial_sql($table, $column, $value); + $self->do($_) foreach @sql; } ##################################################################### @@ -1107,12 +1232,12 @@ sub bz_set_next_serial_value { ##################################################################### sub _bz_schema { - my ($self) = @_; - return $self->{private_bz_schema} if exists $self->{private_bz_schema}; - my @module_parts = split('::', ref $self); - my $module_name = pop @module_parts; - $self->{private_bz_schema} = Bugzilla::DB::Schema->new($module_name); - return $self->{private_bz_schema}; + my ($self) = @_; + return $self->{private_bz_schema} if exists $self->{private_bz_schema}; + my @module_parts = split('::', ref $self); + my $module_name = pop @module_parts; + $self->{private_bz_schema} = Bugzilla::DB::Schema->new($module_name); + return $self->{private_bz_schema}; } # _bz_get_initial_schema() @@ -1126,53 +1251,54 @@ sub _bz_schema { # Returns: A Schema object that can be serialized and written to disk # for _bz_init_schema_storage. sub _bz_get_initial_schema { - my ($self) = @_; - return $self->_bz_schema->get_empty_schema(); + my ($self) = @_; + return $self->_bz_schema->get_empty_schema(); } sub bz_column_info { - my ($self, $table, $column) = @_; - my $def = $self->_bz_real_schema->get_column_abstract($table, $column); - # We dclone it so callers can't modify the Schema. - $def = dclone($def) if defined $def; - return $def; + my ($self, $table, $column) = @_; + my $def = $self->_bz_real_schema->get_column_abstract($table, $column); + + # We dclone it so callers can't modify the Schema. + $def = dclone($def) if defined $def; + return $def; } sub bz_index_info { - my ($self, $table, $index) = @_; - my $index_def = - $self->_bz_real_schema->get_index_abstract($table, $index); - if (ref($index_def) eq 'ARRAY') { - $index_def = {FIELDS => $index_def, TYPE => ''}; - } - return $index_def; + my ($self, $table, $index) = @_; + my $index_def = $self->_bz_real_schema->get_index_abstract($table, $index); + if (ref($index_def) eq 'ARRAY') { + $index_def = {FIELDS => $index_def, TYPE => ''}; + } + return $index_def; } sub bz_table_info { - my ($self, $table) = @_; - return $self->_bz_real_schema->get_table_abstract($table); + my ($self, $table) = @_; + return $self->_bz_real_schema->get_table_abstract($table); } sub bz_table_columns { - my ($self, $table) = @_; - return $self->_bz_real_schema->get_table_columns($table); + my ($self, $table) = @_; + return $self->_bz_real_schema->get_table_columns($table); } sub bz_table_indexes { - my ($self, $table) = @_; - my $indexes = $self->_bz_real_schema->get_table_indexes_abstract($table); - my %return_indexes; - # We do this so that they're always hashes. - foreach my $name (keys %$indexes) { - $return_indexes{$name} = $self->bz_index_info($table, $name); - } - return \%return_indexes; + my ($self, $table) = @_; + my $indexes = $self->_bz_real_schema->get_table_indexes_abstract($table); + my %return_indexes; + + # We do this so that they're always hashes. + foreach my $name (keys %$indexes) { + $return_indexes{$name} = $self->bz_index_info($table, $name); + } + return \%return_indexes; } sub bz_table_list { - my ($self) = @_; - return $self->_bz_real_schema->get_table_list(); + my ($self) = @_; + return $self->_bz_real_schema->get_table_list(); } ##################################################################### @@ -1191,9 +1317,9 @@ sub bz_table_list { # Returns: An array of column names. # sub bz_table_columns_real { - my ($self, $table) = @_; - my $sth = $self->column_info(undef, undef, $table, '%'); - return @{ $self->selectcol_arrayref($sth, {Columns => [4]}) }; + my ($self, $table) = @_; + my $sth = $self->column_info(undef, undef, $table, '%'); + return @{$self->selectcol_arrayref($sth, {Columns => [4]})}; } # bz_table_list_real() @@ -1203,9 +1329,9 @@ sub bz_table_columns_real { # Params: none # Returns: An array containing table names. sub bz_table_list_real { - my ($self) = @_; - my $table_sth = $self->table_info(undef, undef, undef, "TABLE"); - return @{$self->selectcol_arrayref($table_sth, { Columns => [3] })}; + my ($self) = @_; + my $table_sth = $self->table_info(undef, undef, undef, "TABLE"); + return @{$self->selectcol_arrayref($table_sth, {Columns => [3]})}; } ##################################################################### @@ -1213,98 +1339,149 @@ sub bz_table_list_real { ##################################################################### sub bz_in_transaction { - return $_[0]->{private_bz_transaction_count} ? 1 : 0; + return $_[0]->{private_bz_transaction_count} ? 1 : 0; } +## REDHAT EXTENSION 1235135 +# Add unsafe so we can allow some inconsequential updates to go through. sub bz_start_transaction { - my ($self) = @_; - - if ($self->bz_in_transaction) { - $self->{private_bz_transaction_count}++; - } else { - # Turn AutoCommit off and start a new transaction - $self->begin_work(); - # REPEATABLE READ means "We work on a snapshot of the DB that - # is created when we execute our first SQL statement." It's - # what we need in Bugzilla to be safe, for what we do. - # Different DBs have different defaults for their isolation - # level, so we just set it here manually. - if ($self->ISOLATION_LEVEL) { - $self->do('SET TRANSACTION ISOLATION LEVEL ' - . $self->ISOLATION_LEVEL); - } - $self->{private_bz_transaction_count} = 1; + my ($self, $unsafe) = @_; + + if ($self->bz_in_transaction) { + $self->{private_bz_transaction_count}++; + } + else { + # Turn AutoCommit off and start a new transaction + $self->begin_work(); + + # REPEATABLE READ means "We work on a snapshot of the DB that + # is created when we execute our first SQL statement." It's + # what we need in Bugzilla to be safe, for what we do. + # Different DBs have different defaults for their isolation + # level, so we just set it here manually. +## REDHAT EXTENSION START 1235135 + if ($unsafe && $self->ISOLATION_LEVEL) { + $self->do('SET TRANSACTION ISOLATION LEVEL READ COMMITTED'); + } + elsif ($self->ISOLATION_LEVEL) { +## REDHAT EXTENSION END 1235135 + $self->do('SET TRANSACTION ISOLATION LEVEL ' . $self->ISOLATION_LEVEL); } + $self->{private_bz_transaction_count} = 1; + } } sub bz_commit_transaction { - my ($self) = @_; - - if ($self->{private_bz_transaction_count} > 1) { - $self->{private_bz_transaction_count}--; - } elsif ($self->bz_in_transaction) { - $self->commit(); - $self->{private_bz_transaction_count} = 0; - Bugzilla::Mailer->send_staged_mail(); - } else { - ThrowCodeError('not_in_transaction'); - } + my ($self) = @_; + + if ($self->{private_bz_transaction_count} > 1) { + $self->{private_bz_transaction_count}--; + } + elsif ($self->bz_in_transaction) { + $self->commit(); + $self->{private_bz_transaction_count} = 0; + Bugzilla::Mailer->send_staged_mail(); + } + else { + ThrowCodeError('not_in_transaction'); + } } sub bz_rollback_transaction { - my ($self) = @_; - - # Unlike start and commit, if we rollback at any point it happens - # instantly, even if we're in a nested transaction. - if (!$self->bz_in_transaction) { - ThrowCodeError("not_in_transaction"); - } else { - $self->rollback(); - $self->{private_bz_transaction_count} = 0; - } -} + my ($self) = @_; + + # Unlike start and commit, if we rollback at any point it happens + # instantly, even if we're in a nested transaction. + if (!$self->bz_in_transaction) { + ThrowCodeError("not_in_transaction"); + } + else { + $self->rollback(); + $self->{private_bz_transaction_count} = 0; + } +} + +## REDHAT EXTENSION START 590893 +sub bz_call_with_timeout { + my $self = shift; + my $sth = shift; + my @args = @_; + + ## REDHAT EXTENSION START 967692 + # If we are running from a script, don't do the timeout + my $script = basename($0); + if (remote_ip() eq '127.0.0.1' && $script ne 'whine.pl') { + return $sth->execute(@args); + } + ## REDHAT EXTENSION END 967692 + + ## REDHAT EXTENSION START 966505 + # Since bugzilla can now do multiple queries at once, we need to + # set the timeout to the remaining time. + my $start_time = Bugzilla->request_cache->{_rh_start_time} || $^T; + my $timeout = Bugzilla->params->{'long_query_timeout'} - time + $start_time; + return undef if $timeout <= 0; + ## REDHAT EXTENSION END 966505 + + ## REDHAT EXTENSION BEGIN 1250304 + $self->do("SET STATEMENT_TIMEOUT TO " . ($timeout * 1000)); + my $OK = $sth->execute(@args); + $self->do('SET STATEMENT_TIMEOUT TO 0'); + return ($OK); + ## REDHAT EXTENSION END 1250304 +} +## REDHAT EXTENSION END 590893 ##################################################################### # Subclass Helpers ##################################################################### sub db_new { - my ($class, $params) = @_; - my ($dsn, $user, $pass, $override_attrs) = - @$params{qw(dsn user pass attrs)}; - - # set up default attributes used to connect to the database - # (may be overridden by DB driver implementations) - my $attributes = { RaiseError => 0, - AutoCommit => 1, - PrintError => 0, - ShowErrorStatement => 1, - HandleError => \&_handle_error, - TaintIn => 1, - # See https://rt.perl.org/rt3/Public/Bug/Display.html?id=30933 - # for the reason to use NAME instead of NAME_lc (bug 253696). - FetchHashKeyName => 'NAME', - }; - - if ($override_attrs) { - foreach my $key (keys %$override_attrs) { - $attributes->{$key} = $override_attrs->{$key}; - } + my ($class, $params) = @_; + my ($dsn, $user, $pass, $override_attrs) = @$params{qw(dsn user pass attrs)}; + + # set up default attributes used to connect to the database + # (may be overridden by DB driver implementations) + my $attributes = { + RaiseError => 0, + AutoCommit => 1, + PrintError => 0, + ShowErrorStatement => 1, + HandleError => \&_handle_error, + TaintIn => 1, + + # See https://rt.perl.org/rt3/Public/Bug/Display.html?id=30933 + # for the reason to use NAME instead of NAME_lc (bug 253696). + FetchHashKeyName => 'NAME', + }; + + if ($override_attrs) { + foreach my $key (keys %$override_attrs) { + $attributes->{$key} = $override_attrs->{$key}; } + } + + # connect using our known info to the specified db + my $self = undef; + eval { $self = DBI->connect($dsn, $user, $pass, $attributes); }; + ## REDHAT EXTENSION BEGIN 853262 1575809 + if (!defined($self) || $@) { - # connect using our known info to the specified db - my $self = DBI->connect($dsn, $user, $pass, $attributes) - or die "\nCan't connect to the database.\nError: $DBI::errstr\n" - . " Is your database installed and up and running?\n Do you have" - . " the correct username and password selected in localconfig?\n\n"; + # Display an error to the page for the users. + die + "There was an error connecting to the database. The system administrator has been notified."; + } + ## REDHAT EXTENSION END 853262 1575809 - # RaiseError was only set to 0 so that we could catch the - # above "die" condition. - $self->{RaiseError} = 1; + # RaiseError was only set to 0 so that we could catch the + # above "die" condition. + $self->{RaiseError} = 1; - bless ($self, $class); + bless($self, $class); - return $self; + Bugzilla->request_cache->{in_error} = undef; + + return $self; } ##################################################################### @@ -1328,55 +1505,54 @@ These methods really are private. Do not override them in subclasses. =cut sub _bz_init_schema_storage { - my ($self) = @_; - - my $table_size; - eval { - $table_size = - $self->selectrow_array("SELECT COUNT(*) FROM bz_schema"); - }; + my ($self) = @_; + + my $table_size; + eval { $table_size = $self->selectrow_array("SELECT COUNT(*) FROM bz_schema"); }; + + if (!$table_size) { + my $init_schema = $self->_bz_get_initial_schema; + my $store_me = $init_schema->serialize_abstract(); + my $schema_version = $init_schema->SCHEMA_VERSION; + + # If table_size is not defined, then we hit an error reading the + # bz_schema table, which means it probably doesn't exist yet. So, + # we have to create it. If we failed above for some other reason, + # we'll see the failure here. + # However, we must create the table after we do get_initial_schema, + # because some versions of get_initial_schema read that the table + # exists and then add it to the Schema, where other versions don't. + if (!defined $table_size) { + $self->_bz_add_table_raw('bz_schema'); + } - if (!$table_size) { - my $init_schema = $self->_bz_get_initial_schema; - my $store_me = $init_schema->serialize_abstract(); - my $schema_version = $init_schema->SCHEMA_VERSION; - - # If table_size is not defined, then we hit an error reading the - # bz_schema table, which means it probably doesn't exist yet. So, - # we have to create it. If we failed above for some other reason, - # we'll see the failure here. - # However, we must create the table after we do get_initial_schema, - # because some versions of get_initial_schema read that the table - # exists and then add it to the Schema, where other versions don't. - if (!defined $table_size) { - $self->_bz_add_table_raw('bz_schema'); - } + say install_string('db_schema_init'); + my $sth = $self->prepare( + "INSERT INTO bz_schema " . " (schema_data, version) VALUES (?,?)"); + $sth->bind_param(1, $store_me, $self->BLOB_TYPE); + $sth->bind_param(2, $schema_version); + $sth->execute(); - say install_string('db_schema_init'); - my $sth = $self->prepare("INSERT INTO bz_schema " - ." (schema_data, version) VALUES (?,?)"); - $sth->bind_param(1, $store_me, $self->BLOB_TYPE); - $sth->bind_param(2, $schema_version); - $sth->execute(); - - # And now we have to update the on-disk schema to hold the bz_schema - # table, if the bz_schema table didn't exist when we were called. - if (!defined $table_size) { - $self->_bz_real_schema->add_table('bz_schema', - $self->_bz_schema->get_table_abstract('bz_schema')); - $self->_bz_store_real_schema; - } - } - # Sanity check - elsif ($table_size > 1) { - # We tell them to delete the newer one. Better to have checksetup - # run migration code too many times than to have it not run the - # correct migration code at all. - die "Attempted to initialize the schema but there are already " - . " $table_size copies of it stored.\nThis should never happen.\n" - . " Compare the rows of the bz_schema table and delete the " - . "newer one(s)."; + # And now we have to update the on-disk schema to hold the bz_schema + # table, if the bz_schema table didn't exist when we were called. + if (!defined $table_size) { + $self->_bz_real_schema->add_table('bz_schema', + $self->_bz_schema->get_table_abstract('bz_schema')); + $self->_bz_store_real_schema; } + } + + # Sanity check + elsif ($table_size > 1) { + + # We tell them to delete the newer one. Better to have checksetup + # run migration code too many times than to have it not run the + # correct migration code at all. + die "Attempted to initialize the schema but there are already " + . " $table_size copies of it stored.\nThis should never happen.\n" + . " Compare the rows of the bz_schema table and delete the " + . "newer one(s)."; + } } =item C<_bz_real_schema()> @@ -1390,24 +1566,23 @@ sub _bz_init_schema_storage { =cut sub _bz_real_schema { - my ($self) = @_; - return $self->{private_real_schema} if exists $self->{private_real_schema}; - - my $bz_schema; - unless ($bz_schema = Bugzilla->memcached->get({ key => 'bz_schema' })) { - $bz_schema = $self->selectrow_arrayref( - "SELECT schema_data, version FROM bz_schema" - ); - Bugzilla->memcached->set({ key => 'bz_schema', value => $bz_schema }); - } + my ($self) = @_; + return $self->{private_real_schema} if exists $self->{private_real_schema}; - (die "_bz_real_schema tried to read the bz_schema table but it's empty!") - if !$bz_schema; + my $bz_schema; + unless ($bz_schema = Bugzilla->memcached->get({key => 'bz_schema'})) { + $bz_schema + = $self->selectrow_arrayref("SELECT schema_data, version FROM bz_schema"); + Bugzilla->memcached->set({key => 'bz_schema', value => $bz_schema}); + } - $self->{private_real_schema} = - $self->_bz_schema->deserialize_abstract($bz_schema->[0], $bz_schema->[1]); + (die "_bz_real_schema tried to read the bz_schema table but it's empty!") + if !$bz_schema; - return $self->{private_real_schema}; + $self->{private_real_schema} + = $self->_bz_schema->deserialize_abstract($bz_schema->[0], $bz_schema->[1]); + + return $self->{private_real_schema}; } =item C<_bz_store_real_schema()> @@ -1427,106 +1602,135 @@ sub _bz_real_schema { =cut sub _bz_store_real_schema { - my ($self) = @_; - - # Make sure that there's a schema to update - my $table_size = $self->selectrow_array("SELECT COUNT(*) FROM bz_schema"); - - die "Attempted to update the bz_schema table but there's nothing " - . "there to update. Run checksetup." unless $table_size; - - # We want to store the current object, not one - # that we read from the database. So we use the actual hash - # member instead of the subroutine call. If the hash - # member is not defined, we will (and should) fail. - my $update_schema = $self->{private_real_schema}; - my $store_me = $update_schema->serialize_abstract(); - my $schema_version = $update_schema->SCHEMA_VERSION; - my $sth = $self->prepare("UPDATE bz_schema - SET schema_data = ?, version = ?"); - $sth->bind_param(1, $store_me, $self->BLOB_TYPE); - $sth->bind_param(2, $schema_version); - $sth->execute(); + my ($self) = @_; + + # Make sure that there's a schema to update + my $table_size = $self->selectrow_array("SELECT COUNT(*) FROM bz_schema"); - Bugzilla->memcached->clear({ key => 'bz_schema' }); + die "Attempted to update the bz_schema table but there's nothing " + . "there to update. Run checksetup." + unless $table_size; + + # We want to store the current object, not one + # that we read from the database. So we use the actual hash + # member instead of the subroutine call. If the hash + # member is not defined, we will (and should) fail. + my $update_schema = $self->{private_real_schema}; + my $store_me = $update_schema->serialize_abstract(); + my $schema_version = $update_schema->SCHEMA_VERSION; + my $sth = $self->prepare( + "UPDATE bz_schema + SET schema_data = ?, version = ?" + ); + $sth->bind_param(1, $store_me, $self->BLOB_TYPE); + $sth->bind_param(2, $schema_version); + $sth->execute(); + + Bugzilla->memcached->clear({key => 'bz_schema'}); } # For bz_populate_enum_tables sub _bz_populate_enum_table { - my ($self, $table, $valuelist) = @_; - - my $sql_table = $self->quote_identifier($table); - - # Check if there are any table entries - my $table_size = $self->selectrow_array("SELECT COUNT(*) FROM $sql_table"); - - # If the table is empty... - if (!$table_size) { - print " $table"; - my $insert = $self->prepare( - "INSERT INTO $sql_table (value,sortkey) VALUES (?,?)"); - my $sortorder = 0; - my $maxlen = max(map(length($_), @$valuelist)) + 2; - foreach my $value (@$valuelist) { - $sortorder += 100; - $insert->execute($value, $sortorder); - } + my ($self, $table, $valuelist) = @_; + + my $sql_table = $self->quote_identifier($table); + + # Check if there are any table entries + my $table_size = $self->selectrow_array("SELECT COUNT(*) FROM $sql_table"); + + # If the table is empty... + if (!$table_size) { + print " $table"; + my $insert + = $self->prepare("INSERT INTO $sql_table (value,sortkey) VALUES (?,?)"); + my $sortorder = 0; + my $maxlen = max(map(length($_), @$valuelist)) + 2; + foreach my $value (@$valuelist) { + $sortorder += 100; + $insert->execute($value, $sortorder); } + } } # This is used before adding a foreign key to a column, to make sure # that the database won't fail adding the key. sub _check_references { - my ($self, $table, $column, $fk) = @_; - my $foreign_table = $fk->{TABLE}; - my $foreign_column = $fk->{COLUMN}; - - # We use table aliases because sometimes we join a table to itself, - # and we can't use the same table name on both sides of the join. - # We also can't use the words "table" or "foreign" because those are - # reserved words. - my $bad_values = $self->selectcol_arrayref( - "SELECT DISTINCT tabl.$column + my ($self, $table, $column, $fk) = @_; + my $foreign_table = $fk->{TABLE}; + my $foreign_column = $fk->{COLUMN}; + + # We use table aliases because sometimes we join a table to itself, + # and we can't use the same table name on both sides of the join. + # We also can't use the words "table" or "foreign" because those are + # reserved words. + my $bad_values = $self->selectcol_arrayref( + "SELECT DISTINCT tabl.$column FROM $table AS tabl LEFT JOIN $foreign_table AS forn ON tabl.$column = forn.$foreign_column WHERE forn.$foreign_column IS NULL - AND tabl.$column IS NOT NULL"); - - if (@$bad_values) { - my $delete_action = $fk->{DELETE} || ''; - if ($delete_action eq 'CASCADE') { - $self->do("DELETE FROM $table WHERE $column IN (" - . join(',', ('?') x @$bad_values) . ")", - undef, @$bad_values); - if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) { - print "\n", get_text('install_fk_invalid_fixed', - { table => $table, column => $column, - foreign_table => $foreign_table, - foreign_column => $foreign_column, - 'values' => $bad_values, action => 'delete' }), "\n"; - } - } - elsif ($delete_action eq 'SET NULL') { - $self->do("UPDATE $table SET $column = NULL + AND tabl.$column IS NOT NULL" + ); + + if (@$bad_values) { + my $delete_action = $fk->{DELETE} || ''; + if ($delete_action eq 'CASCADE') { + $self->do( + "DELETE FROM $table WHERE $column IN (" . join(',', ('?') x @$bad_values) . ")", + undef, @$bad_values + ); + if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) { + print "\n", + get_text( + 'install_fk_invalid_fixed', + { + table => $table, + column => $column, + foreign_table => $foreign_table, + foreign_column => $foreign_column, + 'values' => $bad_values, + action => 'delete' + } + ), + "\n"; + } + } + elsif ($delete_action eq 'SET NULL') { + $self->do( + "UPDATE $table SET $column = NULL WHERE $column IN (" - . join(',', ('?') x @$bad_values) . ")", - undef, @$bad_values); - if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) { - print "\n", get_text('install_fk_invalid_fixed', - { table => $table, column => $column, - foreign_table => $foreign_table, - foreign_column => $foreign_column, - 'values' => $bad_values, action => 'null' }), "\n"; - } - } - else { - die "\n", get_text('install_fk_invalid', - { table => $table, column => $column, - foreign_table => $foreign_table, - foreign_column => $foreign_column, - 'values' => $bad_values }), "\n"; + . join(',', ('?') x @$bad_values) . ")", undef, @$bad_values + ); + if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) { + print "\n", + get_text( + 'install_fk_invalid_fixed', + { + table => $table, + column => $column, + foreign_table => $foreign_table, + foreign_column => $foreign_column, + 'values' => $bad_values, + action => 'null' + } + ), + "\n"; + } + } + else { + die "\n", + get_text( + 'install_fk_invalid', + { + table => $table, + column => $column, + foreign_table => $foreign_table, + foreign_column => $foreign_column, + 'values' => $bad_values } + ), + "\n"; } + } } 1; @@ -2847,4 +3051,6 @@ L =item bz_add_field_tables +=item bz_call_with_timeout + =back diff --git a/Bugzilla/DB/Mysql.pm b/Bugzilla/DB/Mysql.pm index d0915f1e6..97d6fc6ec 100644 --- a/Bugzilla/DB/Mysql.pm +++ b/Bugzilla/DB/Mysql.pm @@ -37,258 +37,264 @@ use List::Util qw(max); use Text::ParseWords; # This is how many comments of MAX_COMMENT_LENGTH we expect on a single bug. -# In reality, you could have a LOT more comments than this, because +# In reality, you could have a LOT more comments than this, because # MAX_COMMENT_LENGTH is big. use constant MAX_COMMENTS => 50; use constant FULLTEXT_OR => '|'; sub new { - my ($class, $params) = @_; - my ($user, $pass, $host, $dbname, $port, $sock) = - @$params{qw(db_user db_pass db_host db_name db_port db_sock)}; - - # construct the DSN from the parameters we got - my $dsn = "dbi:mysql:host=$host;database=$dbname"; - $dsn .= ";port=$port" if $port; - $dsn .= ";mysql_socket=$sock" if $sock; - - my %attrs = ( - mysql_enable_utf8 => Bugzilla->params->{'utf8'}, - # Needs to be explicitly specified for command-line processes. - mysql_auto_reconnect => 1, - ); - - # MySQL SSL options - my ($ssl_ca_file, $ssl_ca_path, $ssl_cert, $ssl_key) = - @$params{qw(db_mysql_ssl_ca_file db_mysql_ssl_ca_path - db_mysql_ssl_client_cert db_mysql_ssl_client_key)}; - if ($ssl_ca_file || $ssl_ca_path || $ssl_cert || $ssl_key) { - $attrs{'mysql_ssl'} = 1; - $attrs{'mysql_ssl_ca_file'} = $ssl_ca_file if $ssl_ca_file; - $attrs{'mysql_ssl_ca_path'} = $ssl_ca_path if $ssl_ca_path; - $attrs{'mysql_ssl_client_cert'} = $ssl_cert if $ssl_cert; - $attrs{'mysql_ssl_client_key'} = $ssl_key if $ssl_key; + my ($class, $params) = @_; + my ($user, $pass, $host, $dbname, $port, $sock) + = @$params{qw(db_user db_pass db_host db_name db_port db_sock)}; + + # construct the DSN from the parameters we got + my $dsn = "dbi:mysql:host=$host;database=$dbname"; + $dsn .= ";port=$port" if $port; + $dsn .= ";mysql_socket=$sock" if $sock; + + my %attrs = ( + mysql_enable_utf8 => Bugzilla->params->{'utf8'}, + + # Needs to be explicitly specified for command-line processes. + mysql_auto_reconnect => 1, + ); + + # MySQL SSL options + my ($ssl_ca_file, $ssl_ca_path, $ssl_cert, $ssl_key) + = @$params{qw(db_mysql_ssl_ca_file db_mysql_ssl_ca_path + db_mysql_ssl_client_cert db_mysql_ssl_client_key)}; + if ($ssl_ca_file || $ssl_ca_path || $ssl_cert || $ssl_key) { + $attrs{'mysql_ssl'} = 1; + $attrs{'mysql_ssl_ca_file'} = $ssl_ca_file if $ssl_ca_file; + $attrs{'mysql_ssl_ca_path'} = $ssl_ca_path if $ssl_ca_path; + $attrs{'mysql_ssl_client_cert'} = $ssl_cert if $ssl_cert; + $attrs{'mysql_ssl_client_key'} = $ssl_key if $ssl_key; + } + + my $self = $class->db_new( + {dsn => $dsn, user => $user, pass => $pass, attrs => \%attrs}); + + # This makes sure that if the tables are encoded as UTF-8, we + # return their data correctly. + $self->do("SET NAMES utf8") if Bugzilla->params->{'utf8'}; + + # all class local variables stored in DBI derived class needs to have + # a prefix 'private_'. See DBI documentation. + $self->{private_bz_tables_locked} = ""; + + # Needed by TheSchwartz + $self->{private_bz_dsn} = $dsn; + + bless($self, $class); + + # Check for MySQL modes. + my ($var, $sql_mode) + = $self->selectrow_array("SHOW VARIABLES LIKE 'sql\\_mode'"); + + # Disable ANSI and strict modes, else Bugzilla will crash. + if ($sql_mode) { + + # STRICT_TRANS_TABLE or STRICT_ALL_TABLES enable MySQL strict mode, + # causing bug 321645. TRADITIONAL sets these modes (among others) as + # well, so it has to be stipped as well + my $new_sql_mode = join(",", + grep { $_ !~ /^(?:ANSI|STRICT_(?:TRANS|ALL)_TABLES|TRADITIONAL)$/ } + split(/,/, $sql_mode)); + + if ($sql_mode ne $new_sql_mode) { + $self->do("SET SESSION sql_mode = ?", undef, $new_sql_mode); } + } - my $self = $class->db_new({ dsn => $dsn, user => $user, - pass => $pass, attrs => \%attrs }); - - # This makes sure that if the tables are encoded as UTF-8, we - # return their data correctly. - $self->do("SET NAMES utf8") if Bugzilla->params->{'utf8'}; - - # all class local variables stored in DBI derived class needs to have - # a prefix 'private_'. See DBI documentation. - $self->{private_bz_tables_locked} = ""; - - # Needed by TheSchwartz - $self->{private_bz_dsn} = $dsn; + # Allow large GROUP_CONCATs (largely for inserting comments + # into bugs_fulltext). + $self->do('SET SESSION group_concat_max_len = 128000000'); - bless ($self, $class); + # MySQL 5.5.2 and older have this variable set to true, which causes + # trouble, see bug 870369. + $self->do('SET SESSION sql_auto_is_null = 0'); - # Check for MySQL modes. - my ($var, $sql_mode) = $self->selectrow_array( - "SHOW VARIABLES LIKE 'sql\\_mode'"); - - # Disable ANSI and strict modes, else Bugzilla will crash. - if ($sql_mode) { - # STRICT_TRANS_TABLE or STRICT_ALL_TABLES enable MySQL strict mode, - # causing bug 321645. TRADITIONAL sets these modes (among others) as - # well, so it has to be stipped as well - my $new_sql_mode = - join(",", grep {$_ !~ /^(?:ANSI|STRICT_(?:TRANS|ALL)_TABLES|TRADITIONAL)$/} - split(/,/, $sql_mode)); - - if ($sql_mode ne $new_sql_mode) { - $self->do("SET SESSION sql_mode = ?", undef, $new_sql_mode); - } - } - - # Allow large GROUP_CONCATs (largely for inserting comments - # into bugs_fulltext). - $self->do('SET SESSION group_concat_max_len = 128000000'); - - # MySQL 5.5.2 and older have this variable set to true, which causes - # trouble, see bug 870369. - $self->do('SET SESSION sql_auto_is_null = 0'); - - return $self; + return $self; } # when last_insert_id() is supported on MySQL by lowest DBI/DBD version # required by Bugzilla, this implementation can be removed. sub bz_last_key { - my ($self) = @_; + my ($self) = @_; - my ($last_insert_id) = $self->selectrow_array('SELECT LAST_INSERT_ID()'); + my ($last_insert_id) = $self->selectrow_array('SELECT LAST_INSERT_ID()'); - return $last_insert_id; + return $last_insert_id; } sub sql_group_concat { - my ($self, $column, $separator, $sort, $order_by) = @_; - $separator = $self->quote(', ') if !defined $separator; - $sort = 1 if !defined $sort; - if ($order_by) { - $column .= " ORDER BY $order_by"; - } - elsif ($sort) { - my $sort_order = $column; - $sort_order =~ s/^DISTINCT\s+//i; - $column = "$column ORDER BY $sort_order"; - } - return "GROUP_CONCAT($column SEPARATOR $separator)"; + my ($self, $column, $separator, $sort, $order_by) = @_; + $separator = $self->quote(', ') if !defined $separator; + $sort = 1 if !defined $sort; + if ($order_by) { + $column .= " ORDER BY $order_by"; + } + elsif ($sort) { + my $sort_order = $column; + $sort_order =~ s/^DISTINCT\s+//i; + $column = "$column ORDER BY $sort_order"; + } + return "GROUP_CONCAT($column SEPARATOR $separator)"; } sub sql_regexp { - my ($self, $expr, $pattern, $nocheck, $real_pattern) = @_; - $real_pattern ||= $pattern; + my ($self, $expr, $pattern, $nocheck, $real_pattern) = @_; + $real_pattern ||= $pattern; - $self->bz_check_regexp($real_pattern) if !$nocheck; + $self->bz_check_regexp($real_pattern) if !$nocheck; - return "$expr REGEXP $pattern"; + return "$expr REGEXP $pattern"; } sub sql_not_regexp { - my ($self, $expr, $pattern, $nocheck, $real_pattern) = @_; - $real_pattern ||= $pattern; + my ($self, $expr, $pattern, $nocheck, $real_pattern) = @_; + $real_pattern ||= $pattern; - $self->bz_check_regexp($real_pattern) if !$nocheck; + $self->bz_check_regexp($real_pattern) if !$nocheck; - return "$expr NOT REGEXP $pattern"; + return "$expr NOT REGEXP $pattern"; } sub sql_limit { - my ($self, $limit, $offset) = @_; - - if (defined($offset)) { - return "LIMIT $offset, $limit"; - } else { - return "LIMIT $limit"; - } + my ($self, $limit, $offset) = @_; + + if (defined($offset)) { + return "LIMIT $offset, $limit"; + } + else { + return "LIMIT $limit"; + } } sub sql_string_concat { - my ($self, @params) = @_; - - return 'CONCAT(' . join(', ', @params) . ')'; + my ($self, @params) = @_; + + return 'CONCAT(' . join(', ', @params) . ')'; } sub sql_fulltext_search { - my ($self, $column, $text) = @_; - - # Add the boolean mode modifier if the search string contains - # boolean operators at the start or end of a word. - my $mode = ''; - if ($text =~ /(?:^|\W)[+\-<>~"()]/ || $text =~ /[()"*](?:$|\W)/) { - $mode = 'IN BOOLEAN MODE'; - - my @terms = split(quotemeta(FULLTEXT_OR), $text); - foreach my $term (@terms) { - # quote un-quoted compound words - my @words = quotewords('[\s()]+', 'delimiters', $term); - foreach my $word (@words) { - # match words that have non-word chars in the middle of them - if ($word =~ /\w\W+\w/ && $word !~ m/"/) { - $word = '"' . $word . '"'; - } - } - $term = join('', @words); + my ($self, $column, $text) = @_; + + # Add the boolean mode modifier if the search string contains + # boolean operators at the start or end of a word. + my $mode = ''; + if ($text =~ /(?:^|\W)[+\-<>~"()]/ || $text =~ /[()"*](?:$|\W)/) { + $mode = 'IN BOOLEAN MODE'; + + my @terms = split(quotemeta(FULLTEXT_OR), $text); + foreach my $term (@terms) { + + # quote un-quoted compound words + my @words = quotewords('[\s()]+', 'delimiters', $term); + foreach my $word (@words) { + + # match words that have non-word chars in the middle of them + if ($word =~ /\w\W+\w/ && $word !~ m/"/) { + $word = '"' . $word . '"'; } - $text = join(FULLTEXT_OR, @terms); + } + $term = join('', @words); } + $text = join(FULLTEXT_OR, @terms); + } - # quote the text for use in the MATCH AGAINST expression - $text = $self->quote($text); + # quote the text for use in the MATCH AGAINST expression + $text = $self->quote($text); - # untaint the text, since it's safe to use now that we've quoted it - trick_taint($text); + # untaint the text, since it's safe to use now that we've quoted it + trick_taint($text); - return "MATCH($column) AGAINST($text $mode)"; + return "MATCH($column) AGAINST($text $mode)"; } sub sql_istring { - my ($self, $string) = @_; - - return $string; + my ($self, $string) = @_; + + return $string; } sub sql_from_days { - my ($self, $days) = @_; + my ($self, $days) = @_; - return "FROM_DAYS($days)"; + return "FROM_DAYS($days)"; } sub sql_to_days { - my ($self, $date) = @_; + my ($self, $date) = @_; - return "TO_DAYS($date)"; + return "TO_DAYS($date)"; } sub sql_date_format { - my ($self, $date, $format) = @_; + my ($self, $date, $format) = @_; + + $format = "%Y.%m.%d %H:%i:%s" if !$format; - $format = "%Y.%m.%d %H:%i:%s" if !$format; - - return "DATE_FORMAT($date, " . $self->quote($format) . ")"; + return "DATE_FORMAT($date, " . $self->quote($format) . ")"; } sub sql_date_math { - my ($self, $date, $operator, $interval, $units) = @_; - - return "$date $operator INTERVAL $interval $units"; + my ($self, $date, $operator, $interval, $units) = @_; + + return "$date $operator INTERVAL $interval $units"; } sub sql_iposition { - my ($self, $fragment, $text) = @_; - return "INSTR($text, $fragment)"; + my ($self, $fragment, $text) = @_; + return "INSTR($text, $fragment)"; } sub sql_position { - my ($self, $fragment, $text) = @_; + my ($self, $fragment, $text) = @_; - return "INSTR(CAST($text AS BINARY), CAST($fragment AS BINARY))"; + return "INSTR(CAST($text AS BINARY), CAST($fragment AS BINARY))"; } sub sql_group_by { - my ($self, $needed_columns, $optional_columns) = @_; + my ($self, $needed_columns, $optional_columns) = @_; - # MySQL allows you to specify the minimal subset of columns to get - # a unique result. While it does allow specifying all columns as - # ANSI SQL requires, according to MySQL documentation, the fewer - # columns you specify, the faster the query runs. - return "GROUP BY $needed_columns"; + # MySQL allows you to specify the minimal subset of columns to get + # a unique result. While it does allow specifying all columns as + # ANSI SQL requires, according to MySQL documentation, the fewer + # columns you specify, the faster the query runs. + return "GROUP BY $needed_columns"; } sub bz_explain { - my ($self, $sql) = @_; - my $sth = $self->prepare("EXPLAIN $sql"); - $sth->execute(); - my $columns = $sth->{'NAME'}; - my $lengths = $sth->{'mysql_max_length'}; - my $format_string = '|'; - my $i = 0; - foreach my $column (@$columns) { - # Sometimes the column name is longer than the contents. - my $length = max($lengths->[$i], length($column)); - $format_string .= ' %-' . $length . 's |'; - $i++; - } - - my $first_row = sprintf($format_string, @$columns); - my @explain_rows = ($first_row, '-' x length($first_row)); - while (my $row = $sth->fetchrow_arrayref) { - my @fixed = map { defined $_ ? $_ : 'NULL' } @$row; - push(@explain_rows, sprintf($format_string, @fixed)); - } - - return join("\n", @explain_rows); + my ($self, $sql) = @_; + my $sth = $self->prepare("EXPLAIN $sql"); + $sth->execute(); + my $columns = $sth->{'NAME'}; + my $lengths = $sth->{'mysql_max_length'}; + my $format_string = '|'; + my $i = 0; + foreach my $column (@$columns) { + + # Sometimes the column name is longer than the contents. + my $length = max($lengths->[$i], length($column)); + $format_string .= ' %-' . $length . 's |'; + $i++; + } + + my $first_row = sprintf($format_string, @$columns); + my @explain_rows = ($first_row, '-' x length($first_row)); + while (my $row = $sth->fetchrow_arrayref) { + my @fixed = map { defined $_ ? $_ : 'NULL' } @$row; + push(@explain_rows, sprintf($format_string, @fixed)); + } + + return join("\n", @explain_rows); } sub _bz_get_initial_schema { - my ($self) = @_; - return $self->_bz_build_schema_from_disk(); + my ($self) = @_; + return $self->_bz_build_schema_from_disk(); } ##################################################################### @@ -296,493 +302,503 @@ sub _bz_get_initial_schema { ##################################################################### sub bz_check_server_version { - my $self = shift; + my $self = shift; - my $lc = Bugzilla->localconfig; - if (lc(Bugzilla->localconfig->{db_name}) eq 'mysql') { - die "It is not safe to run Bugzilla inside a database named 'mysql'.\n" - . " Please pick a different value for \$db_name in localconfig.\n"; - } + my $lc = Bugzilla->localconfig; + if (lc(Bugzilla->localconfig->{db_name}) eq 'mysql') { + die "It is not safe to run Bugzilla inside a database named 'mysql'.\n" + . " Please pick a different value for \$db_name in localconfig.\n"; + } - $self->SUPER::bz_check_server_version(@_); + $self->SUPER::bz_check_server_version(@_); } sub bz_setup_database { - my ($self) = @_; - - # The "comments" field of the bugs_fulltext table could easily exceed - # MySQL's default max_allowed_packet. Also, MySQL should never have - # a max_allowed_packet smaller than our max_attachment_size. So, we - # warn the user here if max_allowed_packet is too small. - my $min_max_allowed = MAX_COMMENTS * MAX_COMMENT_LENGTH; - my (undef, $current_max_allowed) = $self->selectrow_array( - q{SHOW VARIABLES LIKE 'max\_allowed\_packet'}); - # This parameter is not yet defined when the DB is being built for - # the very first time. The code below still works properly, however, - # because the default maxattachmentsize is smaller than $min_max_allowed. - my $max_attachment = (Bugzilla->params->{'maxattachmentsize'} || 0) * 1024; - my $needed_max_allowed = max($min_max_allowed, $max_attachment); - if ($current_max_allowed < $needed_max_allowed) { - warn install_string('max_allowed_packet', - { current => $current_max_allowed, - needed => $needed_max_allowed }) . "\n"; + my ($self) = @_; + + # The "comments" field of the bugs_fulltext table could easily exceed + # MySQL's default max_allowed_packet. Also, MySQL should never have + # a max_allowed_packet smaller than our max_attachment_size. So, we + # warn the user here if max_allowed_packet is too small. + my $min_max_allowed = MAX_COMMENTS * MAX_COMMENT_LENGTH; + my (undef, $current_max_allowed) + = $self->selectrow_array(q{SHOW VARIABLES LIKE 'max\_allowed\_packet'}); + + # This parameter is not yet defined when the DB is being built for + # the very first time. The code below still works properly, however, + # because the default maxattachmentsize is smaller than $min_max_allowed. + my $max_attachment = (Bugzilla->params->{'maxattachmentsize'} || 0) * 1024; + my $needed_max_allowed = max($min_max_allowed, $max_attachment); + if ($current_max_allowed < $needed_max_allowed) { + warn install_string('max_allowed_packet', + {current => $current_max_allowed, needed => $needed_max_allowed}) + . "\n"; + } + + # Make sure the installation has InnoDB turned on, or we're going to be + # doing silly things like making foreign keys on MyISAM tables, which is + # hard to fix later. We do this up here because none of the code below + # works if InnoDB is off. (Particularly if we've already converted the + # tables to InnoDB.) + my %engines = @{$self->selectcol_arrayref('SHOW ENGINES', {Columns => [1, 2]})}; + if (!$engines{InnoDB} || $engines{InnoDB} !~ /^(YES|DEFAULT)$/) { + die install_string('mysql_innodb_disabled'); + } + + + my ($sd_index_deleted, $longdescs_index_deleted); + my @tables = $self->bz_table_list_real(); + + # We want to convert tables to InnoDB, but it's possible that they have + # fulltext indexes on them, and conversion will fail unless we remove + # the indexes. + if (grep($_ eq 'bugs', @tables) and !grep($_ eq 'bugs_fulltext', @tables)) { + if ($self->bz_index_info_real('bugs', 'short_desc')) { + $self->bz_drop_index_raw('bugs', 'short_desc'); } - - # Make sure the installation has InnoDB turned on, or we're going to be - # doing silly things like making foreign keys on MyISAM tables, which is - # hard to fix later. We do this up here because none of the code below - # works if InnoDB is off. (Particularly if we've already converted the - # tables to InnoDB.) - my %engines = @{$self->selectcol_arrayref('SHOW ENGINES', {Columns => [1,2]})}; - if (!$engines{InnoDB} || $engines{InnoDB} !~ /^(YES|DEFAULT)$/) { - die install_string('mysql_innodb_disabled'); + if ($self->bz_index_info_real('bugs', 'bugs_short_desc_idx')) { + $self->bz_drop_index_raw('bugs', 'bugs_short_desc_idx'); + $sd_index_deleted = 1; # Used for later schema cleanup. } - - - my ($sd_index_deleted, $longdescs_index_deleted); - my @tables = $self->bz_table_list_real(); - # We want to convert tables to InnoDB, but it's possible that they have - # fulltext indexes on them, and conversion will fail unless we remove - # the indexes. - if (grep($_ eq 'bugs', @tables) - and !grep($_ eq 'bugs_fulltext', @tables)) - { - if ($self->bz_index_info_real('bugs', 'short_desc')) { - $self->bz_drop_index_raw('bugs', 'short_desc'); - } - if ($self->bz_index_info_real('bugs', 'bugs_short_desc_idx')) { - $self->bz_drop_index_raw('bugs', 'bugs_short_desc_idx'); - $sd_index_deleted = 1; # Used for later schema cleanup. - } + } + if (grep($_ eq 'longdescs', @tables) and !grep($_ eq 'bugs_fulltext', @tables)) + { + if ($self->bz_index_info_real('longdescs', 'thetext')) { + $self->bz_drop_index_raw('longdescs', 'thetext'); } - if (grep($_ eq 'longdescs', @tables) - and !grep($_ eq 'bugs_fulltext', @tables)) - { - if ($self->bz_index_info_real('longdescs', 'thetext')) { - $self->bz_drop_index_raw('longdescs', 'thetext'); - } - if ($self->bz_index_info_real('longdescs', 'longdescs_thetext_idx')) { - $self->bz_drop_index_raw('longdescs', 'longdescs_thetext_idx'); - $longdescs_index_deleted = 1; # For later schema cleanup. - } + if ($self->bz_index_info_real('longdescs', 'longdescs_thetext_idx')) { + $self->bz_drop_index_raw('longdescs', 'longdescs_thetext_idx'); + $longdescs_index_deleted = 1; # For later schema cleanup. } - - # Upgrade tables from MyISAM to InnoDB - my $db_name = Bugzilla->localconfig->{db_name}; - my $myisam_tables = $self->selectcol_arrayref( - 'SELECT TABLE_NAME FROM information_schema.TABLES - WHERE TABLE_SCHEMA = ? AND ENGINE = ?', - undef, $db_name, 'MyISAM'); - foreach my $should_be_myisam (Bugzilla::DB::Schema::Mysql::MYISAM_TABLES) { - @$myisam_tables = grep { $_ ne $should_be_myisam } @$myisam_tables; + } + + # Upgrade tables from MyISAM to InnoDB + my $db_name = Bugzilla->localconfig->{db_name}; + my $myisam_tables = $self->selectcol_arrayref( + 'SELECT TABLE_NAME FROM information_schema.TABLES + WHERE TABLE_SCHEMA = ? AND ENGINE = ?', undef, $db_name, 'MyISAM' + ); + foreach my $should_be_myisam (Bugzilla::DB::Schema::Mysql::MYISAM_TABLES) { + @$myisam_tables = grep { $_ ne $should_be_myisam } @$myisam_tables; + } + + if (scalar @$myisam_tables) { + print "Bugzilla now uses the InnoDB storage engine in MySQL for", + " most tables.\nConverting tables to InnoDB:\n"; + foreach my $table (@$myisam_tables) { + print "Converting table $table... "; + $self->do("ALTER TABLE $table ENGINE = InnoDB"); + print "done.\n"; } - - if (scalar @$myisam_tables) { - print "Bugzilla now uses the InnoDB storage engine in MySQL for", - " most tables.\nConverting tables to InnoDB:\n"; - foreach my $table (@$myisam_tables) { - print "Converting table $table... "; - $self->do("ALTER TABLE $table ENGINE = InnoDB"); - print "done.\n"; - } + } + + # Versions of Bugzilla before the existence of Bugzilla::DB::Schema did + # not provide explicit names for the table indexes. This means + # that our upgrades will not be reliable, because we look for the name + # of the index, not what fields it is on, when doing upgrades. + # (using the name is much better for cross-database compatibility + # and general reliability). It's also very important that our + # Schema object be consistent with what is on the disk. + # + # While we're at it, we also fix some inconsistent index naming + # from the original checkin of Bugzilla::DB::Schema. + + # We check for the existence of a particular "short name" index that + # has existed at least since Bugzilla 2.8, and probably earlier. + # For fixing the inconsistent naming of Schema indexes, + # we also check for one of those inconsistently-named indexes. + if ( + grep($_ eq 'bugs', @tables) + && ( $self->bz_index_info_real('bugs', 'assigned_to') + || $self->bz_index_info_real('flags', 'flags_bidattid_idx')) + ) + { + + # This is a check unrelated to the indexes, to see if people are + # upgrading from 2.18 or below, but somehow have a bz_schema table + # already. This only happens if they have done a mysqldump into + # a database without doing a DROP DATABASE first. + # We just do the check here since this check is a reliable way + # of telling that we are upgrading from a version pre-2.20. + if (grep($_ eq 'bz_schema', $self->bz_table_list_real())) { + die install_string('bz_schema_exists_before_220'); } - - # Versions of Bugzilla before the existence of Bugzilla::DB::Schema did - # not provide explicit names for the table indexes. This means - # that our upgrades will not be reliable, because we look for the name - # of the index, not what fields it is on, when doing upgrades. - # (using the name is much better for cross-database compatibility - # and general reliability). It's also very important that our - # Schema object be consistent with what is on the disk. - # - # While we're at it, we also fix some inconsistent index naming - # from the original checkin of Bugzilla::DB::Schema. - - # We check for the existence of a particular "short name" index that - # has existed at least since Bugzilla 2.8, and probably earlier. - # For fixing the inconsistent naming of Schema indexes, - # we also check for one of those inconsistently-named indexes. - if (grep($_ eq 'bugs', @tables) - && ($self->bz_index_info_real('bugs', 'assigned_to') - || $self->bz_index_info_real('flags', 'flags_bidattid_idx')) ) - { - # This is a check unrelated to the indexes, to see if people are - # upgrading from 2.18 or below, but somehow have a bz_schema table - # already. This only happens if they have done a mysqldump into - # a database without doing a DROP DATABASE first. - # We just do the check here since this check is a reliable way - # of telling that we are upgrading from a version pre-2.20. - if (grep($_ eq 'bz_schema', $self->bz_table_list_real())) { - die install_string('bz_schema_exists_before_220'); - } + my $bug_count = $self->selectrow_array("SELECT COUNT(*) FROM bugs"); - my $bug_count = $self->selectrow_array("SELECT COUNT(*) FROM bugs"); - # We estimate one minute for each 3000 bugs, plus 3 minutes just - # to handle basic MySQL stuff. - my $rename_time = int($bug_count / 3000) + 3; - # And 45 minutes for every 15,000 attachments, per some experiments. - my ($attachment_count) = - $self->selectrow_array("SELECT COUNT(*) FROM attachments"); - $rename_time += int(($attachment_count * 45) / 15000); - # If we're going to take longer than 5 minutes, we let the user know - # and allow them to abort. - if ($rename_time > 5) { - print "\n", install_string('mysql_index_renaming', - { minutes => $rename_time }); - # Wait 45 seconds for them to respond. - sleep(45) unless Bugzilla->installation_answers->{NO_PAUSE}; - } - print "Renaming indexes...\n"; - - # We can't be interrupted, because of how the "if" - # works above. - local $SIG{INT} = 'IGNORE'; - local $SIG{TERM} = 'IGNORE'; - local $SIG{PIPE} = 'IGNORE'; - - # Certain indexes had names in Schema that did not easily conform - # to a standard. We store those names here, so that they - # can be properly renamed. - # Also, sometimes an old mysqldump would incorrectly rename - # unique indexes to "PRIMARY", so we address that here, also. - my $bad_names = { - # 'when' is a possible leftover from Bugzillas before 2.8 - bugs_activity => ['when', 'bugs_activity_bugid_idx', - 'bugs_activity_bugwhen_idx'], - cc => ['PRIMARY'], - longdescs => ['longdescs_bugid_idx', - 'longdescs_bugwhen_idx'], - flags => ['flags_bidattid_idx'], - flaginclusions => ['flaginclusions_tpcid_idx'], - flagexclusions => ['flagexclusions_tpc_id_idx'], - keywords => ['PRIMARY'], - milestones => ['PRIMARY'], - profiles_activity => ['profiles_activity_when_idx'], - group_control_map => ['group_control_map_gid_idx', 'PRIMARY'], - user_group_map => ['PRIMARY'], - group_group_map => ['PRIMARY'], - email_setting => ['PRIMARY'], - bug_group_map => ['PRIMARY'], - category_group_map => ['PRIMARY'], - watch => ['PRIMARY'], - namedqueries => ['PRIMARY'], - series_data => ['PRIMARY'], - # series_categories is dealt with below, not here. - }; - - # The series table is broken and needs to have one index - # dropped before we begin the renaming, because it had a - # useless index on it that would cause a naming conflict here. - if (grep($_ eq 'series', @tables)) { - my $dropname; - # This is what the bad index was called before Schema. - if ($self->bz_index_info_real('series', 'creator_2')) { - $dropname = 'creator_2'; - } - # This is what the bad index is called in Schema. - elsif ($self->bz_index_info_real('series', 'series_creator_idx')) { - $dropname = 'series_creator_idx'; - } - $self->bz_drop_index_raw('series', $dropname) if $dropname; - } + # We estimate one minute for each 3000 bugs, plus 3 minutes just + # to handle basic MySQL stuff. + my $rename_time = int($bug_count / 3000) + 3; - # The email_setting table also had the same problem. - if( grep($_ eq 'email_setting', @tables) - && $self->bz_index_info_real('email_setting', - 'email_settings_user_id_idx') ) - { - $self->bz_drop_index_raw('email_setting', - 'email_settings_user_id_idx'); - } - - # Go through all the tables. - foreach my $table (@tables) { - # Will contain the names of old indexes as keys, and the - # definition of the new indexes as a value. The values - # include an extra hash key, NAME, with the new name of - # the index. - my %rename_indexes; - # And go through all the columns on each table. - my @columns = $self->bz_table_columns_real($table); - - # We also want to fix the silly naming of unique indexes - # that happened when we first checked-in Bugzilla::DB::Schema. - if ($table eq 'series_categories') { - # The series_categories index had a nonstandard name. - push(@columns, 'series_cats_unique_idx'); - } - elsif ($table eq 'email_setting') { - # The email_setting table had a similar problem. - push(@columns, 'email_settings_unique_idx'); - } - else { - push(@columns, "${table}_unique_idx"); - } - # And this is how we fix the other inconsistent Schema naming. - push(@columns, @{$bad_names->{$table}}) - if (exists $bad_names->{$table}); - foreach my $column (@columns) { - # If we have an index named after this column, it's an - # old-style-name index. - if (my $index = $self->bz_index_info_real($table, $column)) { - # Fix the name to fit in with the new naming scheme. - $index->{NAME} = $table . "_" . - $index->{FIELDS}->[0] . "_idx"; - print "Renaming index $column to " - . $index->{NAME} . "...\n"; - $rename_indexes{$column} = $index; - } # if - } # foreach column - - my @rename_sql = $self->_bz_schema->get_rename_indexes_ddl( - $table, %rename_indexes); - $self->do($_) foreach (@rename_sql); - - } # foreach table - } # if old-name indexes - - # If there are no tables, but the DB isn't utf8 and it should be, - # then we should alter the database to be utf8. We know it should be - # if the utf8 parameter is true or there are no params at all. - # This kind of situation happens when people create the database - # themselves, and if we don't do this they will get the big - # scary WARNING statement about conversion to UTF8. - if ( !$self->bz_db_is_utf8 && !@tables - && (Bugzilla->params->{'utf8'} || !scalar keys %{Bugzilla->params}) ) - { - $self->_alter_db_charset_to_utf8(); - } + # And 45 minutes for every 15,000 attachments, per some experiments. + my ($attachment_count) + = $self->selectrow_array("SELECT COUNT(*) FROM attachments"); + $rename_time += int(($attachment_count * 45) / 15000); - # And now we create the tables and the Schema object. - $self->SUPER::bz_setup_database(); + # If we're going to take longer than 5 minutes, we let the user know + # and allow them to abort. + if ($rename_time > 5) { + print "\n", install_string('mysql_index_renaming', {minutes => $rename_time}); - if ($sd_index_deleted) { - $self->_bz_real_schema->delete_index('bugs', 'bugs_short_desc_idx'); - $self->_bz_store_real_schema; + # Wait 45 seconds for them to respond. + sleep(45) unless Bugzilla->installation_answers->{NO_PAUSE}; } - if ($longdescs_index_deleted) { - $self->_bz_real_schema->delete_index('longdescs', - 'longdescs_thetext_idx'); - $self->_bz_store_real_schema; + print "Renaming indexes...\n"; + + # We can't be interrupted, because of how the "if" + # works above. + local $SIG{INT} = 'IGNORE'; + local $SIG{TERM} = 'IGNORE'; + local $SIG{PIPE} = 'IGNORE'; + + # Certain indexes had names in Schema that did not easily conform + # to a standard. We store those names here, so that they + # can be properly renamed. + # Also, sometimes an old mysqldump would incorrectly rename + # unique indexes to "PRIMARY", so we address that here, also. + my $bad_names = { + + # 'when' is a possible leftover from Bugzillas before 2.8 + bugs_activity => + ['when', 'bugs_activity_bugid_idx', 'bugs_activity_bugwhen_idx'], + cc => ['PRIMARY'], + longdescs => ['longdescs_bugid_idx', 'longdescs_bugwhen_idx'], + flags => ['flags_bidattid_idx'], + flaginclusions => ['flaginclusions_tpcid_idx'], + flagexclusions => ['flagexclusions_tpc_id_idx'], + keywords => ['PRIMARY'], + milestones => ['PRIMARY'], + profiles_activity => ['profiles_activity_when_idx'], + group_control_map => ['group_control_map_gid_idx', 'PRIMARY'], + user_group_map => ['PRIMARY'], + group_group_map => ['PRIMARY'], + email_setting => ['PRIMARY'], + bug_group_map => ['PRIMARY'], + category_group_map => ['PRIMARY'], + watch => ['PRIMARY'], + namedqueries => ['PRIMARY'], + series_data => ['PRIMARY'], + + # series_categories is dealt with below, not here. + }; + + # The series table is broken and needs to have one index + # dropped before we begin the renaming, because it had a + # useless index on it that would cause a naming conflict here. + if (grep($_ eq 'series', @tables)) { + my $dropname; + + # This is what the bad index was called before Schema. + if ($self->bz_index_info_real('series', 'creator_2')) { + $dropname = 'creator_2'; + } + + # This is what the bad index is called in Schema. + elsif ($self->bz_index_info_real('series', 'series_creator_idx')) { + $dropname = 'series_creator_idx'; + } + $self->bz_drop_index_raw('series', $dropname) if $dropname; } - # The old timestamp fields need to be adjusted here instead of in - # checksetup. Otherwise the UPDATE statements inside of bz_add_column - # will cause accidental timestamp updates. - # The code that does this was moved here from checksetup. - - # 2002-08-14 - bbaetz@student.usyd.edu.au - bug 153578 - # attachments creation time needs to be a datetime, not a timestamp - my $attach_creation = - $self->bz_column_info("attachments", "creation_ts"); - if ($attach_creation && $attach_creation->{TYPE} =~ /^TIMESTAMP/i) { - print "Fixing creation time on attachments...\n"; + # The email_setting table also had the same problem. + if (grep($_ eq 'email_setting', @tables) + && $self->bz_index_info_real('email_setting', 'email_settings_user_id_idx')) + { + $self->bz_drop_index_raw('email_setting', 'email_settings_user_id_idx'); + } - my $sth = $self->prepare("SELECT COUNT(attach_id) FROM attachments"); - $sth->execute(); - my ($attach_count) = $sth->fetchrow_array(); + # Go through all the tables. + foreach my $table (@tables) { - if ($attach_count > 1000) { - print "This may take a while...\n"; - } - my $i = 0; - - # This isn't just as simple as changing the field type, because - # the creation_ts was previously updated when an attachment was made - # obsolete from the attachment creation screen. So we have to go - # and recreate these times from the comments.. - $sth = $self->prepare("SELECT bug_id, attach_id, submitter_id " . - "FROM attachments"); - $sth->execute(); - - # Restrict this as much as possible in order to avoid false - # positives, and keep the db search time down - my $sth2 = $self->prepare("SELECT bug_when FROM longdescs - WHERE bug_id=? AND who=? - AND thetext LIKE ? - ORDER BY bug_when " . $self->sql_limit(1)); - while (my ($bug_id, $attach_id, $submitter_id) - = $sth->fetchrow_array()) - { - $sth2->execute($bug_id, $submitter_id, - "Created an attachment (id=$attach_id)%"); - my ($when) = $sth2->fetchrow_array(); - if ($when) { - $self->do("UPDATE attachments " . - "SET creation_ts='$when' " . - "WHERE attach_id=$attach_id"); - } else { - print "Warning - could not determine correct creation" - . " time for attachment $attach_id on bug $bug_id\n"; - } - ++$i; - print "Converted $i of $attach_count attachments\n" if !($i % 1000); - } - print "Done - converted $i attachments\n"; + # Will contain the names of old indexes as keys, and the + # definition of the new indexes as a value. The values + # include an extra hash key, NAME, with the new name of + # the index. + my %rename_indexes; + + # And go through all the columns on each table. + my @columns = $self->bz_table_columns_real($table); + + # We also want to fix the silly naming of unique indexes + # that happened when we first checked-in Bugzilla::DB::Schema. + if ($table eq 'series_categories') { + + # The series_categories index had a nonstandard name. + push(@columns, 'series_cats_unique_idx'); + } + elsif ($table eq 'email_setting') { + + # The email_setting table had a similar problem. + push(@columns, 'email_settings_unique_idx'); + } + else { + push(@columns, "${table}_unique_idx"); + } + + # And this is how we fix the other inconsistent Schema naming. + push(@columns, @{$bad_names->{$table}}) if (exists $bad_names->{$table}); + foreach my $column (@columns) { + + # If we have an index named after this column, it's an + # old-style-name index. + if (my $index = $self->bz_index_info_real($table, $column)) { + + # Fix the name to fit in with the new naming scheme. + $index->{NAME} = $table . "_" . $index->{FIELDS}->[0] . "_idx"; + print "Renaming index $column to " . $index->{NAME} . "...\n"; + $rename_indexes{$column} = $index; + } # if + } # foreach column + + my @rename_sql + = $self->_bz_schema->get_rename_indexes_ddl($table, %rename_indexes); + $self->do($_) foreach (@rename_sql); + + } # foreach table + } # if old-name indexes + + # If there are no tables, but the DB isn't utf8 and it should be, + # then we should alter the database to be utf8. We know it should be + # if the utf8 parameter is true or there are no params at all. + # This kind of situation happens when people create the database + # themselves, and if we don't do this they will get the big + # scary WARNING statement about conversion to UTF8. + if ( !$self->bz_db_is_utf8 + && !@tables + && (Bugzilla->params->{'utf8'} || !scalar keys %{Bugzilla->params})) + { + $self->_alter_db_charset_to_utf8(); + } + + # And now we create the tables and the Schema object. + $self->SUPER::bz_setup_database(); + + if ($sd_index_deleted) { + $self->_bz_real_schema->delete_index('bugs', 'bugs_short_desc_idx'); + $self->_bz_store_real_schema; + } + if ($longdescs_index_deleted) { + $self->_bz_real_schema->delete_index('longdescs', 'longdescs_thetext_idx'); + $self->_bz_store_real_schema; + } + + # The old timestamp fields need to be adjusted here instead of in + # checksetup. Otherwise the UPDATE statements inside of bz_add_column + # will cause accidental timestamp updates. + # The code that does this was moved here from checksetup. + + # 2002-08-14 - bbaetz@student.usyd.edu.au - bug 153578 + # attachments creation time needs to be a datetime, not a timestamp + my $attach_creation = $self->bz_column_info("attachments", "creation_ts"); + if ($attach_creation && $attach_creation->{TYPE} =~ /^TIMESTAMP/i) { + print "Fixing creation time on attachments...\n"; + + my $sth = $self->prepare("SELECT COUNT(attach_id) FROM attachments"); + $sth->execute(); + my ($attach_count) = $sth->fetchrow_array(); - $self->bz_alter_column("attachments", "creation_ts", - {TYPE => 'DATETIME', NOTNULL => 1}); + if ($attach_count > 1000) { + print "This may take a while...\n"; } + my $i = 0; - # 2004-08-29 - Tomas.Kopal@altap.cz, bug 257303 - # Change logincookies.lastused type from timestamp to datetime - my $login_lastused = $self->bz_column_info("logincookies", "lastused"); - if ($login_lastused && $login_lastused->{TYPE} =~ /^TIMESTAMP/i) { - $self->bz_alter_column('logincookies', 'lastused', - { TYPE => 'DATETIME', NOTNULL => 1}); - } + # This isn't just as simple as changing the field type, because + # the creation_ts was previously updated when an attachment was made + # obsolete from the attachment creation screen. So we have to go + # and recreate these times from the comments.. + $sth = $self->prepare( + "SELECT bug_id, attach_id, submitter_id " . "FROM attachments"); + $sth->execute(); - # 2005-01-17 - Tomas.Kopal@altap.cz, bug 257315 - # Change bugs.delta_ts type from timestamp to datetime - my $bugs_deltats = $self->bz_column_info("bugs", "delta_ts"); - if ($bugs_deltats && $bugs_deltats->{TYPE} =~ /^TIMESTAMP/i) { - $self->bz_alter_column('bugs', 'delta_ts', - {TYPE => 'DATETIME', NOTNULL => 1}); + # Restrict this as much as possible in order to avoid false + # positives, and keep the db search time down + my $sth2 = $self->prepare( + "SELECT bug_when FROM longdescs + WHERE bug_id=? AND who=? + AND thetext LIKE ? + ORDER BY bug_when " . $self->sql_limit(1) + ); + while (my ($bug_id, $attach_id, $submitter_id) = $sth->fetchrow_array()) { + $sth2->execute($bug_id, $submitter_id, + "Created an attachment (id=$attach_id)%"); + my ($when) = $sth2->fetchrow_array(); + if ($when) { + $self->do("UPDATE attachments " + . "SET creation_ts='$when' " + . "WHERE attach_id=$attach_id"); + } + else { + print "Warning - could not determine correct creation" + . " time for attachment $attach_id on bug $bug_id\n"; + } + ++$i; + print "Converted $i of $attach_count attachments\n" if !($i % 1000); } - - # 2005-09-24 - bugreport@peshkin.net, bug 307602 - # Make sure that default 4G table limit is overridden - my $attach_data_create = $self->selectrow_array( - 'SELECT CREATE_OPTIONS FROM information_schema.TABLES - WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?', - undef, $db_name, 'attach_data'); - if ($attach_data_create !~ /MAX_ROWS/i) { - print "Converting attach_data maximum size to 100G...\n"; - $self->do("ALTER TABLE attach_data + print "Done - converted $i attachments\n"; + + $self->bz_alter_column("attachments", "creation_ts", + {TYPE => 'DATETIME', NOTNULL => 1}); + } + + # 2004-08-29 - Tomas.Kopal@altap.cz, bug 257303 + # Change logincookies.lastused type from timestamp to datetime + my $login_lastused = $self->bz_column_info("logincookies", "lastused"); + if ($login_lastused && $login_lastused->{TYPE} =~ /^TIMESTAMP/i) { + $self->bz_alter_column('logincookies', 'lastused', + {TYPE => 'DATETIME', NOTNULL => 1}); + } + + # 2005-01-17 - Tomas.Kopal@altap.cz, bug 257315 + # Change bugs.delta_ts type from timestamp to datetime + my $bugs_deltats = $self->bz_column_info("bugs", "delta_ts"); + if ($bugs_deltats && $bugs_deltats->{TYPE} =~ /^TIMESTAMP/i) { + $self->bz_alter_column('bugs', 'delta_ts', {TYPE => 'DATETIME', NOTNULL => 1}); + } + + # 2005-09-24 - bugreport@peshkin.net, bug 307602 + # Make sure that default 4G table limit is overridden + my $attach_data_create = $self->selectrow_array( + 'SELECT CREATE_OPTIONS FROM information_schema.TABLES + WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?', undef, $db_name, 'attach_data' + ); + if ($attach_data_create !~ /MAX_ROWS/i) { + print "Converting attach_data maximum size to 100G...\n"; + $self->do( + "ALTER TABLE attach_data AVG_ROW_LENGTH=1000000, - MAX_ROWS=100000"); - } - - # Convert the database to UTF-8 if the utf8 parameter is on. - # We check if any table isn't utf8, because lots of crazy - # partial-conversion situations can happen, and this handles anything - # that could come up (including having the DB charset be utf8 but not - # the table charsets. - # - # TABLE_COLLATION IS NOT NULL prevents us from trying to convert views. - my $non_utf8_tables = $self->selectrow_array( - "SELECT 1 FROM information_schema.TABLES + MAX_ROWS=100000" + ); + } + + # Convert the database to UTF-8 if the utf8 parameter is on. + # We check if any table isn't utf8, because lots of crazy + # partial-conversion situations can happen, and this handles anything + # that could come up (including having the DB charset be utf8 but not + # the table charsets. + # + # TABLE_COLLATION IS NOT NULL prevents us from trying to convert views. + my $non_utf8_tables = $self->selectrow_array( + "SELECT 1 FROM information_schema.TABLES WHERE TABLE_SCHEMA = ? AND TABLE_COLLATION IS NOT NULL AND TABLE_COLLATION NOT LIKE 'utf8%' - LIMIT 1", undef, $db_name); - - if (Bugzilla->params->{'utf8'} && $non_utf8_tables) { - print "\n", install_string('mysql_utf8_conversion'); - - if (!Bugzilla->installation_answers->{NO_PAUSE}) { - if (Bugzilla->installation_mode == - INSTALLATION_MODE_NON_INTERACTIVE) - { - die install_string('continue_without_answers'), "\n"; - } - else { - print "\n " . install_string('enter_or_ctrl_c'); - getc; - } - } - - print "Converting table storage format to UTF-8. This may take a", - " while.\n"; - foreach my $table ($self->bz_table_list_real) { - my $info_sth = $self->prepare("SHOW FULL COLUMNS FROM $table"); - $info_sth->execute(); - my (@binary_sql, @utf8_sql); - while (my $column = $info_sth->fetchrow_hashref) { - # Our conversion code doesn't work on enum fields, but they - # all go away later in checksetup anyway. - next if $column->{Type} =~ /enum/i; - - # If this particular column isn't stored in utf-8 - if ($column->{Collation} - && $column->{Collation} ne 'NULL' - && $column->{Collation} !~ /utf8/) - { - my $name = $column->{Field}; - - print "$table.$name needs to be converted to UTF-8...\n"; - - # These will be automatically re-created at the end - # of checksetup. - $self->bz_drop_related_fks($table, $name); - - my $col_info = - $self->bz_column_info_real($table, $name); - # CHANGE COLUMN doesn't take PRIMARY KEY - delete $col_info->{PRIMARYKEY}; - my $sql_def = $self->_bz_schema->get_type_ddl($col_info); - # We don't want MySQL to actually try to *convert* - # from our current charset to UTF-8, we just want to - # transfer the bytes directly. This is how we do that. - - # The CHARACTER SET part of the definition has to come - # right after the type, which will always come first. - my ($binary, $utf8) = ($sql_def, $sql_def); - my $type = $self->_bz_schema->convert_type($col_info->{TYPE}); - $binary =~ s/(\Q$type\E)/$1 CHARACTER SET binary/; - $utf8 =~ s/(\Q$type\E)/$1 CHARACTER SET utf8/; - push(@binary_sql, "MODIFY COLUMN $name $binary"); - push(@utf8_sql, "MODIFY COLUMN $name $utf8"); - } - } # foreach column - - if (@binary_sql) { - my %indexes = %{ $self->bz_table_indexes($table) }; - foreach my $index_name (keys %indexes) { - my $index = $indexes{$index_name}; - if ($index->{TYPE} and $index->{TYPE} eq 'FULLTEXT') { - $self->bz_drop_index($table, $index_name); - } - else { - delete $indexes{$index_name}; - } - } - - print "Converting the $table table to UTF-8...\n"; - my $bin = "ALTER TABLE $table " . join(', ', @binary_sql); - my $utf = "ALTER TABLE $table " . join(', ', @utf8_sql, - 'DEFAULT CHARACTER SET utf8'); - $self->do($bin); - $self->do($utf); - - # Re-add any removed FULLTEXT indexes. - foreach my $index (keys %indexes) { - $self->bz_add_index($table, $index, $indexes{$index}); - } - } - else { - $self->do("ALTER TABLE $table DEFAULT CHARACTER SET utf8"); - } - - } # foreach my $table (@tables) + LIMIT 1", undef, $db_name + ); + + if (Bugzilla->params->{'utf8'} && $non_utf8_tables) { + print "\n", install_string('mysql_utf8_conversion'); + + if (!Bugzilla->installation_answers->{NO_PAUSE}) { + if (Bugzilla->installation_mode == INSTALLATION_MODE_NON_INTERACTIVE) { + die install_string('continue_without_answers'), "\n"; + } + else { + print "\n " . install_string('enter_or_ctrl_c'); + getc; + } } - # Sometimes you can have a situation where all the tables are utf8, - # but the database isn't. (This tends to happen when you've done - # a mysqldump.) So we have this change outside of the above block, - # so that it just happens silently if no actual *table* conversion - # needs to happen. - if (Bugzilla->params->{'utf8'} && !$self->bz_db_is_utf8) { - $self->_alter_db_charset_to_utf8(); - } + print "Converting table storage format to UTF-8. This may take a", " while.\n"; + foreach my $table ($self->bz_table_list_real) { + my $info_sth = $self->prepare("SHOW FULL COLUMNS FROM $table"); + $info_sth->execute(); + my (@binary_sql, @utf8_sql); + while (my $column = $info_sth->fetchrow_hashref) { + + # Our conversion code doesn't work on enum fields, but they + # all go away later in checksetup anyway. + next if $column->{Type} =~ /enum/i; + + # If this particular column isn't stored in utf-8 + if ( $column->{Collation} + && $column->{Collation} ne 'NULL' + && $column->{Collation} !~ /utf8/) + { + my $name = $column->{Field}; - $self->_fix_defaults(); + print "$table.$name needs to be converted to UTF-8...\n"; - # Bug 451735 highlighted a bug in bz_drop_index() which didn't - # check for FKs before trying to delete an index. Consequently, - # the series_creator_idx index was considered to be deleted - # despite it was still present in the DB. That's why we have to - # force the deletion, bypassing the DB schema. - if (!$self->bz_index_info('series', 'series_category_idx')) { - if (!$self->bz_index_info('series', 'series_creator_idx') - && $self->bz_index_info_real('series', 'series_creator_idx')) - { - foreach my $column (qw(creator category subcategory name)) { - $self->bz_drop_related_fks('series', $column); - } - $self->bz_drop_index_raw('series', 'series_creator_idx'); + # These will be automatically re-created at the end + # of checksetup. + $self->bz_drop_related_fks($table, $name); + + my $col_info = $self->bz_column_info_real($table, $name); + + # CHANGE COLUMN doesn't take PRIMARY KEY + delete $col_info->{PRIMARYKEY}; + my $sql_def = $self->_bz_schema->get_type_ddl($col_info); + + # We don't want MySQL to actually try to *convert* + # from our current charset to UTF-8, we just want to + # transfer the bytes directly. This is how we do that. + + # The CHARACTER SET part of the definition has to come + # right after the type, which will always come first. + my ($binary, $utf8) = ($sql_def, $sql_def); + my $type = $self->_bz_schema->convert_type($col_info->{TYPE}); + $binary =~ s/(\Q$type\E)/$1 CHARACTER SET binary/; + $utf8 =~ s/(\Q$type\E)/$1 CHARACTER SET utf8/; + push(@binary_sql, "MODIFY COLUMN $name $binary"); + push(@utf8_sql, "MODIFY COLUMN $name $utf8"); } + } # foreach column + + if (@binary_sql) { + my %indexes = %{$self->bz_table_indexes($table)}; + foreach my $index_name (keys %indexes) { + my $index = $indexes{$index_name}; + if ($index->{TYPE} and $index->{TYPE} eq 'FULLTEXT') { + $self->bz_drop_index($table, $index_name); + } + else { + delete $indexes{$index_name}; + } + } + + print "Converting the $table table to UTF-8...\n"; + my $bin = "ALTER TABLE $table " . join(', ', @binary_sql); + my $utf + = "ALTER TABLE $table " . join(', ', @utf8_sql, 'DEFAULT CHARACTER SET utf8'); + $self->do($bin); + $self->do($utf); + + # Re-add any removed FULLTEXT indexes. + foreach my $index (keys %indexes) { + $self->bz_add_index($table, $index, $indexes{$index}); + } + } + else { + $self->do("ALTER TABLE $table DEFAULT CHARACTER SET utf8"); + } + + } # foreach my $table (@tables) + } + + # Sometimes you can have a situation where all the tables are utf8, + # but the database isn't. (This tends to happen when you've done + # a mysqldump.) So we have this change outside of the above block, + # so that it just happens silently if no actual *table* conversion + # needs to happen. + if (Bugzilla->params->{'utf8'} && !$self->bz_db_is_utf8) { + $self->_alter_db_charset_to_utf8(); + } + + $self->_fix_defaults(); + + # Bug 451735 highlighted a bug in bz_drop_index() which didn't + # check for FKs before trying to delete an index. Consequently, + # the series_creator_idx index was considered to be deleted + # despite it was still present in the DB. That's why we have to + # force the deletion, bypassing the DB schema. + if (!$self->bz_index_info('series', 'series_category_idx')) { + if (!$self->bz_index_info('series', 'series_creator_idx') + && $self->bz_index_info_real('series', 'series_creator_idx')) + { + foreach my $column (qw(creator category subcategory name)) { + $self->bz_drop_related_fks('series', $column); + } + $self->bz_drop_index_raw('series', 'series_creator_idx'); } + } } # When you import a MySQL 3/4 mysqldump into MySQL 5, columns that @@ -792,100 +808,109 @@ sub bz_setup_database { # looks like. So we remove defaults from columns that aren't supposed # to have them sub _fix_defaults { - my $self = shift; - my $maj_version = substr($self->bz_server_version, 0, 1); - return if $maj_version < 5; - - # The oldest column that could have this problem is bugs.assigned_to, - # so if it doesn't have the problem, we just skip doing this entirely. - my $assi_def = $self->_bz_raw_column_info('bugs', 'assigned_to'); - my $assi_default = $assi_def->{COLUMN_DEF}; - # This "ne ''" thing is necessary because _raw_column_info seems to - # return COLUMN_DEF as an empty string for columns that don't have - # a default. - return unless (defined $assi_default && $assi_default ne ''); - - my %fix_columns; - foreach my $table ($self->_bz_real_schema->get_table_list()) { - foreach my $column ($self->bz_table_columns($table)) { - my $abs_def = $self->bz_column_info($table, $column); - # BLOB/TEXT columns never have defaults - next if $abs_def->{TYPE} =~ /BLOB|TEXT/i; - if (!defined $abs_def->{DEFAULT}) { - # Get the exact default from the database without any - # "fixing" by bz_column_info_real. - my $raw_info = $self->_bz_raw_column_info($table, $column); - my $raw_default = $raw_info->{COLUMN_DEF}; - if (defined $raw_default) { - if ($raw_default eq '') { - # Only (var)char columns can have empty strings as - # defaults, so if we got an empty string for some - # other default type, then it's bogus. - next unless $abs_def->{TYPE} =~ /char/i; - $raw_default = "''"; - } - $fix_columns{$table} ||= []; - push(@{ $fix_columns{$table} }, $column); - print "$table.$column has incorrect DB default: $raw_default\n"; - } - } - } # foreach $column - } # foreach $table - - print "Fixing defaults...\n"; - foreach my $table (reverse sort keys %fix_columns) { - my @alters = map("ALTER COLUMN $_ DROP DEFAULT", - @{ $fix_columns{$table} }); - my $sql = "ALTER TABLE $table " . join(',', @alters); - $self->do($sql); - } + my $self = shift; + my $maj_version = substr($self->bz_server_version, 0, 1); + return if $maj_version < 5; + + # The oldest column that could have this problem is bugs.assigned_to, + # so if it doesn't have the problem, we just skip doing this entirely. + my $assi_def = $self->_bz_raw_column_info('bugs', 'assigned_to'); + my $assi_default = $assi_def->{COLUMN_DEF}; + + # This "ne ''" thing is necessary because _raw_column_info seems to + # return COLUMN_DEF as an empty string for columns that don't have + # a default. + return unless (defined $assi_default && $assi_default ne ''); + + my %fix_columns; + foreach my $table ($self->_bz_real_schema->get_table_list()) { + foreach my $column ($self->bz_table_columns($table)) { + my $abs_def = $self->bz_column_info($table, $column); + + # BLOB/TEXT columns never have defaults + next if $abs_def->{TYPE} =~ /BLOB|TEXT/i; + if (!defined $abs_def->{DEFAULT}) { + + # Get the exact default from the database without any + # "fixing" by bz_column_info_real. + my $raw_info = $self->_bz_raw_column_info($table, $column); + my $raw_default = $raw_info->{COLUMN_DEF}; + if (defined $raw_default) { + if ($raw_default eq '') { + + # Only (var)char columns can have empty strings as + # defaults, so if we got an empty string for some + # other default type, then it's bogus. + next unless $abs_def->{TYPE} =~ /char/i; + $raw_default = "''"; + } + $fix_columns{$table} ||= []; + push(@{$fix_columns{$table}}, $column); + print "$table.$column has incorrect DB default: $raw_default\n"; + } + } + } # foreach $column + } # foreach $table + + print "Fixing defaults...\n"; + foreach my $table (reverse sort keys %fix_columns) { + my @alters = map("ALTER COLUMN $_ DROP DEFAULT", @{$fix_columns{$table}}); + my $sql = "ALTER TABLE $table " . join(',', @alters); + $self->do($sql); + } } sub _alter_db_charset_to_utf8 { - my $self = shift; - my $db_name = Bugzilla->localconfig->{db_name}; - $self->do("ALTER DATABASE $db_name CHARACTER SET utf8"); + my $self = shift; + my $db_name = Bugzilla->localconfig->{db_name}; + $self->do("ALTER DATABASE $db_name CHARACTER SET utf8"); } sub bz_db_is_utf8 { - my $self = shift; - my $db_collation = $self->selectrow_arrayref( - "SHOW VARIABLES LIKE 'character_set_database'"); - # First column holds the variable name, second column holds the value. - return $db_collation->[1] =~ /utf8/ ? 1 : 0; + my $self = shift; + my $db_collation + = $self->selectrow_arrayref("SHOW VARIABLES LIKE 'character_set_database'"); + + # First column holds the variable name, second column holds the value. + return $db_collation->[1] =~ /utf8/ ? 1 : 0; } sub bz_enum_initial_values { - my ($self) = @_; - my %enum_values = %{$self->ENUM_DEFAULTS}; - # Get a complete description of the 'bugs' table; with DBD::MySQL - # there isn't a column-by-column way of doing this. Could use - # $dbh->column_info, but it would go slower and we would have to - # use the undocumented mysql_type_name accessor to get the type - # of each row. - my $sth = $self->prepare("DESCRIBE bugs"); - $sth->execute(); - # Look for the particular columns we are interested in. - while (my ($thiscol, $thistype) = $sth->fetchrow_array()) { - if (defined $enum_values{$thiscol}) { - # this is a column of interest. - my @value_list; - if ($thistype and ($thistype =~ /^enum\(/)) { - # it has an enum type; get the set of values. - while ($thistype =~ /'([^']*)'(.*)/) { - push(@value_list, $1); - $thistype = $2; - } - } - if (@value_list) { - # record the enum values found. - $enum_values{$thiscol} = \@value_list; - } + my ($self) = @_; + my %enum_values = %{$self->ENUM_DEFAULTS}; + + # Get a complete description of the 'bugs' table; with DBD::MySQL + # there isn't a column-by-column way of doing this. Could use + # $dbh->column_info, but it would go slower and we would have to + # use the undocumented mysql_type_name accessor to get the type + # of each row. + my $sth = $self->prepare("DESCRIBE bugs"); + $sth->execute(); + + # Look for the particular columns we are interested in. + while (my ($thiscol, $thistype) = $sth->fetchrow_array()) { + if (defined $enum_values{$thiscol}) { + + # this is a column of interest. + my @value_list; + if ($thistype and ($thistype =~ /^enum\(/)) { + + # it has an enum type; get the set of values. + while ($thistype =~ /'([^']*)'(.*)/) { + push(@value_list, $1); + $thistype = $2; } + } + if (@value_list) { + + # record the enum values found. + $enum_values{$thiscol} = \@value_list; + } } + } - return \%enum_values; + return \%enum_values; } ##################################################################### @@ -916,29 +941,29 @@ backwards-compatibility anyway, for versions of Bugzilla before 2.20. =cut sub bz_column_info_real { - my ($self, $table, $column) = @_; - my $col_data = $self->_bz_raw_column_info($table, $column); - return $self->_bz_schema->column_info_to_column($col_data); + my ($self, $table, $column) = @_; + my $col_data = $self->_bz_raw_column_info($table, $column); + return $self->_bz_schema->column_info_to_column($col_data); } sub _bz_raw_column_info { - my ($self, $table, $column) = @_; - - # DBD::mysql does not support selecting a specific column, - # so we have to get all the columns on the table and find - # the one we want. - my $info_sth = $self->column_info(undef, undef, $table, '%'); - - # Don't use fetchall_hashref as there's a Win32 DBI bug (292821) - my $col_data; - while ($col_data = $info_sth->fetchrow_hashref) { - last if $col_data->{'COLUMN_NAME'} eq $column; - } - - if (!defined $col_data) { - return undef; - } - return $col_data; + my ($self, $table, $column) = @_; + + # DBD::mysql does not support selecting a specific column, + # so we have to get all the columns on the table and find + # the one we want. + my $info_sth = $self->column_info(undef, undef, $table, '%'); + + # Don't use fetchall_hashref as there's a Win32 DBI bug (292821) + my $col_data; + while ($col_data = $info_sth->fetchrow_hashref) { + last if $col_data->{'COLUMN_NAME'} eq $column; + } + + if (!defined $col_data) { + return undef; + } + return $col_data; } =item C @@ -952,42 +977,43 @@ sub _bz_raw_column_info { =cut sub bz_index_info_real { - my ($self, $table, $index) = @_; - - my $sth = $self->prepare("SHOW INDEX FROM $table"); - $sth->execute; - - my @fields; - my $index_type; - # $raw_def will be an arrayref containing the following information: - # 0 = name of the table that the index is on - # 1 = 0 if unique, 1 if not unique - # 2 = name of the index - # 3 = seq_in_index (The order of the current field in the index). - # 4 = Name of ONE column that the index is on - # 5 = 'Collation' of the index. Usually 'A'. - # 6 = Cardinality. Either a number or undef. - # 7 = sub_part. Usually undef. Sometimes 1. - # 8 = "packed". Usually undef. - # 9 = Null. Sometimes undef, sometimes 'YES'. - # 10 = Index_type. The type of the index. Usually either 'BTREE' or 'FULLTEXT' - # 11 = 'Comment.' Usually undef. - while (my $raw_def = $sth->fetchrow_arrayref) { - if ($raw_def->[2] eq $index) { - push(@fields, $raw_def->[4]); - # No index can be both UNIQUE and FULLTEXT, that's why - # this is written this way. - $index_type = $raw_def->[1] ? '' : 'UNIQUE'; - $index_type = $raw_def->[10] eq 'FULLTEXT' - ? 'FULLTEXT' : $index_type; - } + my ($self, $table, $index) = @_; + + my $sth = $self->prepare("SHOW INDEX FROM $table"); + $sth->execute; + + my @fields; + my $index_type; + + # $raw_def will be an arrayref containing the following information: + # 0 = name of the table that the index is on + # 1 = 0 if unique, 1 if not unique + # 2 = name of the index + # 3 = seq_in_index (The order of the current field in the index). + # 4 = Name of ONE column that the index is on + # 5 = 'Collation' of the index. Usually 'A'. + # 6 = Cardinality. Either a number or undef. + # 7 = sub_part. Usually undef. Sometimes 1. + # 8 = "packed". Usually undef. + # 9 = Null. Sometimes undef, sometimes 'YES'. + # 10 = Index_type. The type of the index. Usually either 'BTREE' or 'FULLTEXT' + # 11 = 'Comment.' Usually undef. + while (my $raw_def = $sth->fetchrow_arrayref) { + if ($raw_def->[2] eq $index) { + push(@fields, $raw_def->[4]); + + # No index can be both UNIQUE and FULLTEXT, that's why + # this is written this way. + $index_type = $raw_def->[1] ? '' : 'UNIQUE'; + $index_type = $raw_def->[10] eq 'FULLTEXT' ? 'FULLTEXT' : $index_type; } + } - my $retval; - if (scalar(@fields)) { - $retval = {FIELDS => \@fields, TYPE => $index_type}; - } - return $retval; + my $retval; + if (scalar(@fields)) { + $retval = {FIELDS => \@fields, TYPE => $index_type}; + } + return $retval; } =item C @@ -1000,10 +1026,11 @@ sub bz_index_info_real { =cut sub bz_index_list_real { - my ($self, $table) = @_; - my $sth = $self->prepare("SHOW INDEX FROM $table"); - # Column 3 of a SHOW INDEX statement contains the name of the index. - return @{ $self->selectcol_arrayref($sth, {Columns => [3]}) }; + my ($self, $table) = @_; + my $sth = $self->prepare("SHOW INDEX FROM $table"); + + # Column 3 of a SHOW INDEX statement contains the name of the index. + return @{$self->selectcol_arrayref($sth, {Columns => [3]})}; } ##################################################################### @@ -1027,34 +1054,33 @@ this code does. # bz_column_info_real function would be very difficult to create # properly for any other DB besides MySQL. sub _bz_build_schema_from_disk { - my ($self) = @_; - - my $schema = $self->_bz_schema->get_empty_schema(); - - my @tables = $self->bz_table_list_real(); - if (@tables) { - print "Building Schema object from database...\n"; + my ($self) = @_; + + my $schema = $self->_bz_schema->get_empty_schema(); + + my @tables = $self->bz_table_list_real(); + if (@tables) { + print "Building Schema object from database...\n"; + } + foreach my $table (@tables) { + $schema->add_table($table); + my @columns = $self->bz_table_columns_real($table); + foreach my $column (@columns) { + my $type_info = $self->bz_column_info_real($table, $column); + $schema->set_column($table, $column, $type_info); } - foreach my $table (@tables) { - $schema->add_table($table); - my @columns = $self->bz_table_columns_real($table); - foreach my $column (@columns) { - my $type_info = $self->bz_column_info_real($table, $column); - $schema->set_column($table, $column, $type_info); - } - my @indexes = $self->bz_index_list_real($table); - foreach my $index (@indexes) { - unless ($index eq 'PRIMARY') { - my $index_info = $self->bz_index_info_real($table, $index); - ($index_info = $index_info->{FIELDS}) - if (!$index_info->{TYPE}); - $schema->set_index($table, $index, $index_info); - } - } + my @indexes = $self->bz_index_list_real($table); + foreach my $index (@indexes) { + unless ($index eq 'PRIMARY') { + my $index_info = $self->bz_index_info_real($table, $index); + ($index_info = $index_info->{FIELDS}) if (!$index_info->{TYPE}); + $schema->set_index($table, $index, $index_info); + } } + } - return $schema; + return $schema; } 1; diff --git a/Bugzilla/DB/Oracle.pm b/Bugzilla/DB/Oracle.pm index 7424019ac..7dd7ab784 100644 --- a/Bugzilla/DB/Oracle.pm +++ b/Bugzilla/DB/Oracle.pm @@ -38,461 +38,473 @@ use Bugzilla::Util; ##################################################################### # Constants ##################################################################### -use constant EMPTY_STRING => '__BZ_EMPTY_STR__'; +use constant EMPTY_STRING => '__BZ_EMPTY_STR__'; use constant ISOLATION_LEVEL => 'READ COMMITTED'; -use constant BLOB_TYPE => { ora_type => ORA_BLOB }; +use constant BLOB_TYPE => {ora_type => ORA_BLOB}; + # The max size allowed for LOB fields, in kilobytes. use constant MIN_LONG_READ_LEN => 32 * 1024; -use constant FULLTEXT_OR => ' OR '; +use constant FULLTEXT_OR => ' OR '; sub new { - my ($class, $params) = @_; - my ($user, $pass, $host, $dbname, $port) = - @$params{qw(db_user db_pass db_host db_name db_port)}; - - # You can never connect to Oracle without a DB name, - # and there is no default DB. - $dbname ||= Bugzilla->localconfig->{db_name}; - - # Set the language enviroment - $ENV{'NLS_LANG'} = '.AL32UTF8' if Bugzilla->params->{'utf8'}; - - # construct the DSN from the parameters we got - my $dsn = "dbi:Oracle:host=$host;sid=$dbname"; - $dsn .= ";port=$port" if $port; - my $attrs = { FetchHashKeyName => 'NAME_lc', - LongReadLen => max(Bugzilla->params->{'maxattachmentsize'} || 0, - MIN_LONG_READ_LEN) * 1024, - }; - my $self = $class->db_new({ dsn => $dsn, user => $user, - pass => $pass, attrs => $attrs }); - # Needed by TheSchwartz - $self->{private_bz_dsn} = $dsn; - - bless ($self, $class); - - # Set the session's default date format to match MySQL - $self->do("ALTER SESSION SET NLS_DATE_FORMAT='YYYY-MM-DD HH24:MI:SS'"); - $self->do("ALTER SESSION SET NLS_TIMESTAMP_FORMAT='YYYY-MM-DD HH24:MI:SS'"); - $self->do("ALTER SESSION SET NLS_LENGTH_SEMANTICS='CHAR'") - if Bugzilla->params->{'utf8'}; - # To allow case insensitive query. - $self->do("ALTER SESSION SET NLS_COMP='ANSI'"); - $self->do("ALTER SESSION SET NLS_SORT='BINARY_AI'"); - return $self; + my ($class, $params) = @_; + my ($user, $pass, $host, $dbname, $port) + = @$params{qw(db_user db_pass db_host db_name db_port)}; + + # You can never connect to Oracle without a DB name, + # and there is no default DB. + $dbname ||= Bugzilla->localconfig->{db_name}; + + # Set the language enviroment + $ENV{'NLS_LANG'} = '.AL32UTF8' if Bugzilla->params->{'utf8'}; + + # construct the DSN from the parameters we got + my $dsn = "dbi:Oracle:host=$host;sid=$dbname"; + $dsn .= ";port=$port" if $port; + my $attrs = { + FetchHashKeyName => 'NAME_lc', + LongReadLen => + max(Bugzilla->params->{'maxattachmentsize'} || 0, MIN_LONG_READ_LEN) * 1024, + }; + my $self = $class->db_new( + {dsn => $dsn, user => $user, pass => $pass, attrs => $attrs}); + + # Needed by TheSchwartz + $self->{private_bz_dsn} = $dsn; + + bless($self, $class); + + # Set the session's default date format to match MySQL + $self->do("ALTER SESSION SET NLS_DATE_FORMAT='YYYY-MM-DD HH24:MI:SS'"); + $self->do("ALTER SESSION SET NLS_TIMESTAMP_FORMAT='YYYY-MM-DD HH24:MI:SS'"); + $self->do("ALTER SESSION SET NLS_LENGTH_SEMANTICS='CHAR'") + if Bugzilla->params->{'utf8'}; + + # To allow case insensitive query. + $self->do("ALTER SESSION SET NLS_COMP='ANSI'"); + $self->do("ALTER SESSION SET NLS_SORT='BINARY_AI'"); + return $self; } sub bz_last_key { - my ($self, $table, $column) = @_; + my ($self, $table, $column) = @_; - my $seq = $table . "_" . $column . "_SEQ"; - my ($last_insert_id) = $self->selectrow_array("SELECT $seq.CURRVAL " - . " FROM DUAL"); - return $last_insert_id; + my $seq = $table . "_" . $column . "_SEQ"; + my ($last_insert_id) + = $self->selectrow_array("SELECT $seq.CURRVAL " . " FROM DUAL"); + return $last_insert_id; } sub bz_check_regexp { - my ($self, $pattern) = @_; + my ($self, $pattern) = @_; - eval { $self->do("SELECT 1 FROM DUAL WHERE " - . $self->sql_regexp($self->quote("a"), $pattern, 1)) }; + eval { + $self->do("SELECT 1 FROM DUAL WHERE " + . $self->sql_regexp($self->quote("a"), $pattern, 1)); + }; - $@ && ThrowUserError('illegal_regexp', - { value => $pattern, dberror => $self->errstr }); + $@ + && ThrowUserError('illegal_regexp', + {value => $pattern, dberror => $self->errstr}); } -sub bz_explain { - my ($self, $sql) = @_; - my $sth = $self->prepare("EXPLAIN PLAN FOR $sql"); - $sth->execute(); - my $explain = $self->selectcol_arrayref( - "SELECT PLAN_TABLE_OUTPUT FROM TABLE(DBMS_XPLAN.DISPLAY)"); - return join("\n", @$explain); -} +sub bz_explain { + my ($self, $sql) = @_; + my $sth = $self->prepare("EXPLAIN PLAN FOR $sql"); + $sth->execute(); + my $explain = $self->selectcol_arrayref( + "SELECT PLAN_TABLE_OUTPUT FROM TABLE(DBMS_XPLAN.DISPLAY)"); + return join("\n", @$explain); +} sub sql_group_concat { - my ($self, $text, $separator) = @_; - $separator = $self->quote(', ') if !defined $separator; - my ($distinct, $rest) = $text =~/^(\s*DISTINCT\s|)(.+)$/i; - return "group_concat($distinct T_CLOB_DELIM(NVL($rest, ' '), $separator))"; + my ($self, $text, $separator) = @_; + $separator = $self->quote(', ') if !defined $separator; + my ($distinct, $rest) = $text =~ /^(\s*DISTINCT\s|)(.+)$/i; + return "group_concat($distinct T_CLOB_DELIM(NVL($rest, ' '), $separator))"; } sub sql_regexp { - my ($self, $expr, $pattern, $nocheck, $real_pattern) = @_; - $real_pattern ||= $pattern; + my ($self, $expr, $pattern, $nocheck, $real_pattern) = @_; + $real_pattern ||= $pattern; - $self->bz_check_regexp($real_pattern) if !$nocheck; + $self->bz_check_regexp($real_pattern) if !$nocheck; - return "REGEXP_LIKE($expr, $pattern)"; + return "REGEXP_LIKE($expr, $pattern)"; } sub sql_not_regexp { - my ($self, $expr, $pattern, $nocheck, $real_pattern) = @_; - $real_pattern ||= $pattern; + my ($self, $expr, $pattern, $nocheck, $real_pattern) = @_; + $real_pattern ||= $pattern; - $self->bz_check_regexp($real_pattern) if !$nocheck; + $self->bz_check_regexp($real_pattern) if !$nocheck; - return "NOT REGEXP_LIKE($expr, $pattern)" + return "NOT REGEXP_LIKE($expr, $pattern)"; } sub sql_limit { - my ($self, $limit, $offset) = @_; + my ($self, $limit, $offset) = @_; - if(defined $offset) { - return "/* LIMIT $limit $offset */"; - } - return "/* LIMIT $limit */"; + if (defined $offset) { + return "/* LIMIT $limit $offset */"; + } + return "/* LIMIT $limit */"; } sub sql_string_concat { - my ($self, @params) = @_; + my ($self, @params) = @_; - return 'CONCAT(' . join(', ', @params) . ')'; + return 'CONCAT(' . join(', ', @params) . ')'; } sub sql_to_days { - my ($self, $date) = @_; + my ($self, $date) = @_; - return " TO_CHAR(TO_DATE($date),'J') "; + return " TO_CHAR(TO_DATE($date),'J') "; } -sub sql_from_days{ - my ($self, $date) = @_; - return " TO_DATE($date,'J') "; +sub sql_from_days { + my ($self, $date) = @_; + + return " TO_DATE($date,'J') "; } sub sql_fulltext_search { - my ($self, $column, $text) = @_; - state $label = 0; - $text = $self->quote($text); - trick_taint($text); - $label++; - return "CONTAINS($column,$text,$label) > 0", "SCORE($label)"; + my ($self, $column, $text) = @_; + state $label = 0; + $text = $self->quote($text); + trick_taint($text); + $label++; + return "CONTAINS($column,$text,$label) > 0", "SCORE($label)"; } sub sql_date_format { - my ($self, $date, $format) = @_; - - $format = "%Y.%m.%d %H:%i:%s" if !$format; + my ($self, $date, $format) = @_; + + $format = "%Y.%m.%d %H:%i:%s" if !$format; - $format =~ s/\%Y/YYYY/g; - $format =~ s/\%y/YY/g; - $format =~ s/\%m/MM/g; - $format =~ s/\%d/DD/g; - $format =~ s/\%a/Dy/g; - $format =~ s/\%H/HH24/g; - $format =~ s/\%i/MI/g; - $format =~ s/\%s/SS/g; + $format =~ s/\%Y/YYYY/g; + $format =~ s/\%y/YY/g; + $format =~ s/\%m/MM/g; + $format =~ s/\%d/DD/g; + $format =~ s/\%a/Dy/g; + $format =~ s/\%H/HH24/g; + $format =~ s/\%i/MI/g; + $format =~ s/\%s/SS/g; - return "TO_CHAR($date, " . $self->quote($format) . ")"; + return "TO_CHAR($date, " . $self->quote($format) . ")"; } sub sql_date_math { - my ($self, $date, $operator, $interval, $units) = @_; - my $time_sql; - if ($units =~ /YEAR|MONTH/i) { - $time_sql = "NUMTOYMINTERVAL($interval,'$units')"; - } else{ - $time_sql = "NUMTODSINTERVAL($interval,'$units')"; - } - return "$date $operator $time_sql"; + my ($self, $date, $operator, $interval, $units) = @_; + my $time_sql; + if ($units =~ /YEAR|MONTH/i) { + $time_sql = "NUMTOYMINTERVAL($interval,'$units')"; + } + else { + $time_sql = "NUMTODSINTERVAL($interval,'$units')"; + } + return "$date $operator $time_sql"; } sub sql_position { - my ($self, $fragment, $text) = @_; - return "INSTR($text, $fragment)"; + my ($self, $fragment, $text) = @_; + return "INSTR($text, $fragment)"; } sub sql_in { - my ($self, $column_name, $in_list_ref, $negate) = @_; - my @in_list = @$in_list_ref; - return $self->SUPER::sql_in($column_name, $in_list_ref, $negate) if $#in_list < 1000; - my @in_str; - while (@in_list) { - my $length = $#in_list + 1; - my $splice = $length > 1000 ? 1000 : $length; - my @sub_in_list = splice(@in_list, 0, $splice); - push(@in_str, - $self->SUPER::sql_in($column_name, \@sub_in_list, $negate)); - } - return "( " . join(" OR ", @in_str) . " )"; + my ($self, $column_name, $in_list_ref, $negate) = @_; + my @in_list = @$in_list_ref; + return $self->SUPER::sql_in($column_name, $in_list_ref, $negate) + if $#in_list < 1000; + my @in_str; + while (@in_list) { + my $length = $#in_list + 1; + my $splice = $length > 1000 ? 1000 : $length; + my @sub_in_list = splice(@in_list, 0, $splice); + push(@in_str, $self->SUPER::sql_in($column_name, \@sub_in_list, $negate)); + } + return "( " . join(" OR ", @in_str) . " )"; } sub _bz_add_field_table { - my ($self, $name, $schema_ref, $type) = @_; - $self->SUPER::_bz_add_field_table($name, $schema_ref); - if (defined($type) && $type == FIELD_TYPE_MULTI_SELECT) { - my $uk_name = "UK_" . $self->_bz_schema->_hash_identifier($name . '_value'); - $self->do("ALTER TABLE $name ADD CONSTRAINT $uk_name UNIQUE(value)"); - } + my ($self, $name, $schema_ref, $type) = @_; + $self->SUPER::_bz_add_field_table($name, $schema_ref); + if (defined($type) && ($type == FIELD_TYPE_MULTI_SELECT || $type == FIELD_TYPE_ONE_SELECT)) { + my $uk_name = "UK_" . $self->_bz_schema->_hash_identifier($name . '_value'); + $self->do("ALTER TABLE $name ADD CONSTRAINT $uk_name UNIQUE(value)"); + } } sub bz_drop_table { - my ($self, $name) = @_; - my $table_exists = $self->bz_table_info($name); - if ($table_exists) { - $self->_bz_drop_fks($name); - $self->SUPER::bz_drop_table($name); - } + my ($self, $name) = @_; + my $table_exists = $self->bz_table_info($name); + if ($table_exists) { + $self->_bz_drop_fks($name); + $self->SUPER::bz_drop_table($name); + } } -# Dropping all FKs for a specified table. +# Dropping all FKs for a specified table. sub _bz_drop_fks { - my ($self, $table) = @_; - my @columns = $self->bz_table_columns($table); - foreach my $column (@columns) { - $self->bz_drop_fk($table, $column); - } + my ($self, $table) = @_; + my @columns = $self->bz_table_columns($table); + foreach my $column (@columns) { + $self->bz_drop_fk($table, $column); + } } sub _fix_empty { - my ($string) = @_; - $string = '' if $string eq EMPTY_STRING; - return $string; + my ($string) = @_; + $string = '' if $string eq EMPTY_STRING; + return $string; } sub _fix_arrayref { - my ($row) = @_; - return undef if !defined $row; - foreach my $field (@$row) { - $field = _fix_empty($field) if defined $field; - } - return $row; + my ($row) = @_; + return undef if !defined $row; + foreach my $field (@$row) { + $field = _fix_empty($field) if defined $field; + } + return $row; } sub _fix_hashref { - my ($row) = @_; - return undef if !defined $row; - foreach my $value (values %$row) { - $value = _fix_empty($value) if defined $value; - } - return $row; + my ($row) = @_; + return undef if !defined $row; + foreach my $value (values %$row) { + $value = _fix_empty($value) if defined $value; + } + return $row; } sub adjust_statement { - my ($sql) = @_; - - if ($sql =~ /^CREATE OR REPLACE.*/i){ - return $sql; - } - - # We can't just assume any occurrence of "''" in $sql is an empty - # string, since "''" can occur inside a string literal as a way of - # escaping a single "'" in the literal. Therefore we must be trickier... - - # split the statement into parts by single-quotes. The negative value - # at the end to the split operator from dropping trailing empty strings - # (e.g., when $sql ends in "''") - my @parts = split /'/, $sql, -1; - - if( !(@parts % 2) ) { - # Either the string is empty or the quotes are mismatched - # Returning input unmodified. - return $sql; + my ($sql) = @_; + + if ($sql =~ /^CREATE OR REPLACE.*/i) { + return $sql; + } + + # We can't just assume any occurrence of "''" in $sql is an empty + # string, since "''" can occur inside a string literal as a way of + # escaping a single "'" in the literal. Therefore we must be trickier... + + # split the statement into parts by single-quotes. The negative value + # at the end to the split operator from dropping trailing empty strings + # (e.g., when $sql ends in "''") + my @parts = split /'/, $sql, -1; + + if (!(@parts % 2)) { + + # Either the string is empty or the quotes are mismatched + # Returning input unmodified. + return $sql; + } + + # We already verified that we have an odd number of parts. If we take + # the first part off now, we know we're entering the loop with an even + # number of parts + my @result; + my $part = shift @parts; + + # Oracle requires a FROM clause in all SELECT statements, so append + # "FROM dual" to queries without one (e.g., "SELECT NOW()") + my $is_select = ($part =~ m/^\s*SELECT\b/io); + my $has_from = ($part =~ m/\bFROM\b/io) if $is_select; + + # Oracle includes the time in CURRENT_DATE. + $part =~ s/\bCURRENT_DATE\b/TRUNC(CURRENT_DATE)/io; + + # Oracle use SUBSTR instead of SUBSTRING + $part =~ s/\bSUBSTRING\b/SUBSTR/io; + + # Oracle need no 'AS' + $part =~ s/\bAS\b//ig; + + # Oracle doesn't have LIMIT, so if we find the LIMIT comment, wrap the + # query with "SELECT * FROM (...) WHERE rownum < $limit" + my ($limit, $offset) = ($part =~ m{/\* LIMIT (\d*) (\d*) \*/}o); + + push @result, $part; + while (@parts) { + my $string = shift @parts; + my $nonstring = shift @parts; + + # if the non-string part is zero-length and there are more parts left, + # then this is an escaped quote inside a string literal + while (!(length $nonstring) && @parts) { + + # we know it's safe to remove two parts at a time, since we + # entered the loop with an even number of parts + $string .= "''" . shift @parts; + $nonstring = shift @parts; } - # We already verified that we have an odd number of parts. If we take - # the first part off now, we know we're entering the loop with an even - # number of parts - my @result; - my $part = shift @parts; - - # Oracle requires a FROM clause in all SELECT statements, so append - # "FROM dual" to queries without one (e.g., "SELECT NOW()") - my $is_select = ($part =~ m/^\s*SELECT\b/io); - my $has_from = ($part =~ m/\bFROM\b/io) if $is_select; + # Look for a FROM if this is a SELECT and we haven't found one yet + $has_from = ($nonstring =~ m/\bFROM\b/io) if ($is_select and !$has_from); # Oracle includes the time in CURRENT_DATE. - $part =~ s/\bCURRENT_DATE\b/TRUNC(CURRENT_DATE)/io; + $nonstring =~ s/\bCURRENT_DATE\b/TRUNC(CURRENT_DATE)/io; # Oracle use SUBSTR instead of SUBSTRING - $part =~ s/\bSUBSTRING\b/SUBSTR/io; - + $nonstring =~ s/\bSUBSTRING\b/SUBSTR/io; + # Oracle need no 'AS' - $part =~ s/\bAS\b//ig; - - # Oracle doesn't have LIMIT, so if we find the LIMIT comment, wrap the - # query with "SELECT * FROM (...) WHERE rownum < $limit" - my ($limit,$offset) = ($part =~ m{/\* LIMIT (\d*) (\d*) \*/}o); - - push @result, $part; - while( @parts ) { - my $string = shift @parts; - my $nonstring = shift @parts; - - # if the non-string part is zero-length and there are more parts left, - # then this is an escaped quote inside a string literal - while( !(length $nonstring) && @parts ) { - # we know it's safe to remove two parts at a time, since we - # entered the loop with an even number of parts - $string .= "''" . shift @parts; - $nonstring = shift @parts; - } + $nonstring =~ s/\bAS\b//ig; - # Look for a FROM if this is a SELECT and we haven't found one yet - $has_from = ($nonstring =~ m/\bFROM\b/io) - if ($is_select and !$has_from); + # Look for a LIMIT clause + ($limit) = ($nonstring =~ m(/\* LIMIT (\d*) \*/)o); - # Oracle includes the time in CURRENT_DATE. - $nonstring =~ s/\bCURRENT_DATE\b/TRUNC(CURRENT_DATE)/io; + if (!length($string)) { + push @result, EMPTY_STRING; + push @result, $nonstring; + } + else { + push @result, $string; + push @result, $nonstring; + } + } - # Oracle use SUBSTR instead of SUBSTRING - $nonstring =~ s/\bSUBSTRING\b/SUBSTR/io; + my $new_sql = join "'", @result; - # Oracle need no 'AS' - $nonstring =~ s/\bAS\b//ig; + # Append "FROM dual" if this is a SELECT without a FROM clause + $new_sql .= " FROM DUAL" if ($is_select and !$has_from); - # Look for a LIMIT clause - ($limit) = ($nonstring =~ m(/\* LIMIT (\d*) \*/)o); + # Wrap the query with a "WHERE rownum <= ..." if we found LIMIT - if(!length($string)){ - push @result, EMPTY_STRING; - push @result, $nonstring; - } else { - push @result, $string; - push @result, $nonstring; - } + if (defined($limit)) { + if ($new_sql !~ /\bWHERE\b/) { + $new_sql = $new_sql . " WHERE 1=1"; } - - my $new_sql = join "'", @result; - - # Append "FROM dual" if this is a SELECT without a FROM clause - $new_sql .= " FROM DUAL" if ($is_select and !$has_from); - - # Wrap the query with a "WHERE rownum <= ..." if we found LIMIT - - if (defined($limit)) { - if ($new_sql !~ /\bWHERE\b/) { - $new_sql = $new_sql." WHERE 1=1"; - } - my ($before_where, $after_where) = split(/\bWHERE\b/i, $new_sql, 2); - if (defined($offset)) { - my ($before_from, $after_from) = split(/\bFROM\b/i, $new_sql, 2); - $before_where = "$before_from FROM ($before_from," - . " ROW_NUMBER() OVER (ORDER BY 1) R " - . " FROM $after_from ) "; - $after_where = " R BETWEEN $offset+1 AND $limit+$offset"; - } else { - $after_where = " rownum <=$limit AND ".$after_where; - } - $new_sql = $before_where." WHERE ".$after_where; + my ($before_where, $after_where) = split(/\bWHERE\b/i, $new_sql, 2); + if (defined($offset)) { + my ($before_from, $after_from) = split(/\bFROM\b/i, $new_sql, 2); + $before_where + = "$before_from FROM ($before_from," + . " ROW_NUMBER() OVER (ORDER BY 1) R " + . " FROM $after_from ) "; + $after_where = " R BETWEEN $offset+1 AND $limit+$offset"; + } + else { + $after_where = " rownum <=$limit AND " . $after_where; } - return $new_sql; + $new_sql = $before_where . " WHERE " . $after_where; + } + return $new_sql; } sub do { - my $self = shift; - my $sql = shift; - $sql = adjust_statement($sql); - unshift @_, $sql; - return $self->SUPER::do(@_); + my $self = shift; + my $sql = shift; + $sql = adjust_statement($sql); + unshift @_, $sql; + return $self->SUPER::do(@_); } sub selectrow_array { - my $self = shift; - my $stmt = shift; - my $new_stmt = (ref $stmt) ? $stmt : adjust_statement($stmt); - unshift @_, $new_stmt; - if ( wantarray ) { - my @row = $self->SUPER::selectrow_array(@_); - _fix_arrayref(\@row); - return @row; - } else { - my $row = $self->SUPER::selectrow_array(@_); - $row = _fix_empty($row) if defined $row; - return $row; - } + my $self = shift; + my $stmt = shift; + my $new_stmt = (ref $stmt) ? $stmt : adjust_statement($stmt); + unshift @_, $new_stmt; + if (wantarray) { + my @row = $self->SUPER::selectrow_array(@_); + _fix_arrayref(\@row); + return @row; + } + else { + my $row = $self->SUPER::selectrow_array(@_); + $row = _fix_empty($row) if defined $row; + return $row; + } } sub selectrow_arrayref { - my $self = shift; - my $stmt = shift; - my $new_stmt = (ref $stmt) ? $stmt : adjust_statement($stmt); - unshift @_, $new_stmt; - my $ref = $self->SUPER::selectrow_arrayref(@_); - return undef if !defined $ref; + my $self = shift; + my $stmt = shift; + my $new_stmt = (ref $stmt) ? $stmt : adjust_statement($stmt); + unshift @_, $new_stmt; + my $ref = $self->SUPER::selectrow_arrayref(@_); + return undef if !defined $ref; - _fix_arrayref($ref); - return $ref; + _fix_arrayref($ref); + return $ref; } sub selectrow_hashref { - my $self = shift; - my $stmt = shift; - my $new_stmt = (ref $stmt) ? $stmt : adjust_statement($stmt); - unshift @_, $new_stmt; - my $ref = $self->SUPER::selectrow_hashref(@_); - return undef if !defined $ref; + my $self = shift; + my $stmt = shift; + my $new_stmt = (ref $stmt) ? $stmt : adjust_statement($stmt); + unshift @_, $new_stmt; + my $ref = $self->SUPER::selectrow_hashref(@_); + return undef if !defined $ref; - _fix_hashref($ref); - return $ref; + _fix_hashref($ref); + return $ref; } sub selectall_arrayref { - my $self = shift; - my $stmt = shift; - my $new_stmt = (ref $stmt) ? $stmt : adjust_statement($stmt); - unshift @_, $new_stmt; - my $ref = $self->SUPER::selectall_arrayref(@_); - return undef if !defined $ref; - - foreach my $row (@$ref) { - if (ref($row) eq 'ARRAY') { - _fix_arrayref($row); - } - elsif (ref($row) eq 'HASH') { - _fix_hashref($row); - } + my $self = shift; + my $stmt = shift; + my $new_stmt = (ref $stmt) ? $stmt : adjust_statement($stmt); + unshift @_, $new_stmt; + my $ref = $self->SUPER::selectall_arrayref(@_); + return undef if !defined $ref; + + foreach my $row (@$ref) { + if (ref($row) eq 'ARRAY') { + _fix_arrayref($row); } + elsif (ref($row) eq 'HASH') { + _fix_hashref($row); + } + } - return $ref; + return $ref; } sub selectall_hashref { - my $self = shift; - my $stmt = shift; - my $new_stmt = (ref $stmt) ? $stmt : adjust_statement($stmt); - unshift @_, $new_stmt; - my $rows = $self->SUPER::selectall_hashref(@_); - return undef if !defined $rows; - foreach my $row (values %$rows) { - _fix_hashref($row); - } - return $rows; + my $self = shift; + my $stmt = shift; + my $new_stmt = (ref $stmt) ? $stmt : adjust_statement($stmt); + unshift @_, $new_stmt; + my $rows = $self->SUPER::selectall_hashref(@_); + return undef if !defined $rows; + foreach my $row (values %$rows) { + _fix_hashref($row); + } + return $rows; } sub selectcol_arrayref { - my $self = shift; - my $stmt = shift; - my $new_stmt = (ref $stmt) ? $stmt : adjust_statement($stmt); - unshift @_, $new_stmt; - my $ref = $self->SUPER::selectcol_arrayref(@_); - return undef if !defined $ref; - _fix_arrayref($ref); - return $ref; + my $self = shift; + my $stmt = shift; + my $new_stmt = (ref $stmt) ? $stmt : adjust_statement($stmt); + unshift @_, $new_stmt; + my $ref = $self->SUPER::selectcol_arrayref(@_); + return undef if !defined $ref; + _fix_arrayref($ref); + return $ref; } sub prepare { - my $self = shift; - my $sql = shift; - my $new_sql = adjust_statement($sql); - unshift @_, $new_sql; - return bless $self->SUPER::prepare(@_), - 'Bugzilla::DB::Oracle::st'; + my $self = shift; + my $sql = shift; + my $new_sql = adjust_statement($sql); + unshift @_, $new_sql; + return bless $self->SUPER::prepare(@_), 'Bugzilla::DB::Oracle::st'; } sub prepare_cached { - my $self = shift; - my $sql = shift; - my $new_sql = adjust_statement($sql); - unshift @_, $new_sql; - return bless $self->SUPER::prepare_cached(@_), - 'Bugzilla::DB::Oracle::st'; + my $self = shift; + my $sql = shift; + my $new_sql = adjust_statement($sql); + unshift @_, $new_sql; + return bless $self->SUPER::prepare_cached(@_), 'Bugzilla::DB::Oracle::st'; } sub quote_identifier { - my ($self,$id) = @_; - return $id; + my ($self, $id) = @_; + return $id; } ##################################################################### @@ -500,20 +512,22 @@ sub quote_identifier { ##################################################################### sub bz_table_columns_real { - my ($self, $table) = @_; - $table = uc($table); - my $cols = $self->selectcol_arrayref( - "SELECT LOWER(COLUMN_NAME) FROM USER_TAB_COLUMNS WHERE - TABLE_NAME = ? ORDER BY COLUMN_NAME", undef, $table); - return @$cols; + my ($self, $table) = @_; + $table = uc($table); + my $cols = $self->selectcol_arrayref( + "SELECT LOWER(COLUMN_NAME) FROM USER_TAB_COLUMNS WHERE + TABLE_NAME = ? ORDER BY COLUMN_NAME", undef, $table + ); + return @$cols; } sub bz_table_list_real { - my ($self) = @_; - my $tables = $self->selectcol_arrayref( - "SELECT LOWER(TABLE_NAME) FROM USER_TABLES WHERE - TABLE_NAME NOT LIKE ? ORDER BY TABLE_NAME", undef, 'DR$%'); - return @$tables; + my ($self) = @_; + my $tables = $self->selectcol_arrayref( + "SELECT LOWER(TABLE_NAME) FROM USER_TABLES WHERE + TABLE_NAME NOT LIKE ? ORDER BY TABLE_NAME", undef, 'DR$%' + ); + return @$tables; } ##################################################################### @@ -521,32 +535,37 @@ sub bz_table_list_real { ##################################################################### sub bz_setup_database { - my $self = shift; - - # Create a function that returns SYSDATE to emulate MySQL's "NOW()". - # Function NOW() is used widely in Bugzilla SQLs, but Oracle does not - # have that function, So we have to create one ourself. - $self->do("CREATE OR REPLACE FUNCTION NOW " - . " RETURN DATE IS BEGIN RETURN SYSDATE; END;"); - $self->do("CREATE OR REPLACE FUNCTION CHAR_LENGTH(COLUMN_NAME VARCHAR2)" - . " RETURN NUMBER IS BEGIN RETURN LENGTH(COLUMN_NAME); END;"); - - # Create types for group_concat - my $type_exists = $self->selectrow_array("SELECT 1 FROM user_types - WHERE type_name = 'T_GROUP_CONCAT'"); - $self->do("DROP TYPE T_GROUP_CONCAT") if $type_exists; - $self->do("CREATE OR REPLACE TYPE T_CLOB_DELIM AS OBJECT " - . "( p_CONTENT CLOB, p_DELIMITER VARCHAR2(256)" - . ", MAP MEMBER FUNCTION T_CLOB_DELIM_ToVarchar return VARCHAR2" - . ");"); - $self->do("CREATE OR REPLACE TYPE BODY T_CLOB_DELIM IS + my $self = shift; + + # Create a function that returns SYSDATE to emulate MySQL's "NOW()". + # Function NOW() is used widely in Bugzilla SQLs, but Oracle does not + # have that function, So we have to create one ourself. + $self->do("CREATE OR REPLACE FUNCTION NOW " + . " RETURN DATE IS BEGIN RETURN SYSDATE; END;"); + $self->do("CREATE OR REPLACE FUNCTION CHAR_LENGTH(COLUMN_NAME VARCHAR2)" + . " RETURN NUMBER IS BEGIN RETURN LENGTH(COLUMN_NAME); END;"); + + # Create types for group_concat + my $type_exists = $self->selectrow_array( + "SELECT 1 FROM user_types + WHERE type_name = 'T_GROUP_CONCAT'" + ); + $self->do("DROP TYPE T_GROUP_CONCAT") if $type_exists; + $self->do("CREATE OR REPLACE TYPE T_CLOB_DELIM AS OBJECT " + . "( p_CONTENT CLOB, p_DELIMITER VARCHAR2(256)" + . ", MAP MEMBER FUNCTION T_CLOB_DELIM_ToVarchar return VARCHAR2" + . ");"); + $self->do( + "CREATE OR REPLACE TYPE BODY T_CLOB_DELIM IS MAP MEMBER FUNCTION T_CLOB_DELIM_ToVarchar return VARCHAR2 is BEGIN RETURN p_CONTENT; END; - END;"); + END;" + ); - $self->do("CREATE OR REPLACE TYPE T_GROUP_CONCAT AS OBJECT + $self->do( + "CREATE OR REPLACE TYPE T_GROUP_CONCAT AS OBJECT ( CLOB_CONTENT CLOB, DELIMITER VARCHAR2(256), STATIC FUNCTION ODCIAGGREGATEINITIALIZE( @@ -564,9 +583,11 @@ sub bz_setup_database { MEMBER FUNCTION ODCIAGGREGATEMERGE( SELF IN OUT NOCOPY T_GROUP_CONCAT, CTX2 IN T_GROUP_CONCAT) - RETURN NUMBER);"); + RETURN NUMBER);" + ); - $self->do("CREATE OR REPLACE TYPE BODY T_GROUP_CONCAT IS + $self->do( + "CREATE OR REPLACE TYPE BODY T_GROUP_CONCAT IS STATIC FUNCTION ODCIAGGREGATEINITIALIZE( SCTX IN OUT NOCOPY T_GROUP_CONCAT) RETURN NUMBER IS @@ -610,110 +631,117 @@ sub bz_setup_database { DBMS_LOB.APPEND(SELF.CLOB_CONTENT, CTX2.CLOB_CONTENT); RETURN ODCICONST.SUCCESS; END; - END;"); + END;" + ); - # Create user-defined aggregate function group_concat - $self->do("CREATE OR REPLACE FUNCTION GROUP_CONCAT(P_INPUT T_CLOB_DELIM) + # Create user-defined aggregate function group_concat + $self->do( + "CREATE OR REPLACE FUNCTION GROUP_CONCAT(P_INPUT T_CLOB_DELIM) RETURN CLOB - DETERMINISTIC PARALLEL_ENABLE AGGREGATE USING T_GROUP_CONCAT;"); - - # Create a WORLD_LEXER named BZ_LEX for multilingual fulltext search - my $lexer = $self->selectcol_arrayref( - "SELECT pre_name FROM CTXSYS.CTX_PREFERENCES WHERE pre_name = ? AND - pre_owner = ?", - undef,'BZ_LEX',uc(Bugzilla->localconfig->{db_user})); - if(!@$lexer) { - $self->do("BEGIN CTX_DDL.CREATE_PREFERENCE - ('BZ_LEX', 'WORLD_LEXER'); END;"); - } - - $self->SUPER::bz_setup_database(@_); - - my $sth = $self->prepare("SELECT OBJECT_NAME FROM USER_OBJECTS WHERE OBJECT_NAME = ?"); - my @tables = $self->bz_table_list_real(); - - foreach my $table (@tables) { - my @columns = $self->bz_table_columns_real($table); - foreach my $column (@columns) { - my $def = $self->bz_column_info($table, $column); - # bz_add_column() before Bugzilla 4.2.3 didn't handle primary keys - # correctly (bug 731156). We have to add missing sequences and - # triggers ourselves. - if ($def->{TYPE} =~ /SERIAL/i) { - my $sequence = "${table}_${column}_SEQ"; - my $exists = $self->selectrow_array($sth, undef, $sequence); - if (!$exists) { - my @sql = $self->_get_create_seq_ddl($table, $column); - $self->do($_) foreach @sql; - } - } - - if ($def->{REFERENCES}) { - my $references = $def->{REFERENCES}; - my $update = $references->{UPDATE} || 'CASCADE'; - my $to_table = $references->{TABLE}; - my $to_column = $references->{COLUMN}; - my $fk_name = $self->_bz_schema->_get_fk_name($table, - $column, - $references); - # bz_rename_table didn't rename the trigger correctly. - if ($table eq 'bug_tag' && $to_table eq 'tags') { - $to_table = 'tag'; - } - if ( $update =~ /CASCADE/i ){ - my $trigger_name = uc($fk_name . "_UC"); - my $exist_trigger = $self->selectcol_arrayref($sth, undef, $trigger_name); - if(@$exist_trigger) { - $self->do("DROP TRIGGER $trigger_name"); - } - - my $tr_str = "CREATE OR REPLACE TRIGGER $trigger_name" - . " AFTER UPDATE OF $to_column ON $to_table " - . " REFERENCING " - . " NEW AS NEW " - . " OLD AS OLD " - . " FOR EACH ROW " - . " BEGIN " - . " UPDATE $table" - . " SET $column = :NEW.$to_column" - . " WHERE $column = :OLD.$to_column;" - . " END $trigger_name;"; - $self->do($tr_str); - } - } + DETERMINISTIC PARALLEL_ENABLE AGGREGATE USING T_GROUP_CONCAT;" + ); + + # Create a WORLD_LEXER named BZ_LEX for multilingual fulltext search + my $lexer = $self->selectcol_arrayref( + "SELECT pre_name FROM CTXSYS.CTX_PREFERENCES WHERE pre_name = ? AND + pre_owner = ?", undef, 'BZ_LEX', uc(Bugzilla->localconfig->{db_user}) + ); + if (!@$lexer) { + $self->do( + "BEGIN CTX_DDL.CREATE_PREFERENCE + ('BZ_LEX', 'WORLD_LEXER'); END;" + ); + } + + $self->SUPER::bz_setup_database(@_); + + my $sth = $self->prepare( + "SELECT OBJECT_NAME FROM USER_OBJECTS WHERE OBJECT_NAME = ?"); + my @tables = $self->bz_table_list_real(); + + foreach my $table (@tables) { + my @columns = $self->bz_table_columns_real($table); + foreach my $column (@columns) { + my $def = $self->bz_column_info($table, $column); + + # bz_add_column() before Bugzilla 4.2.3 didn't handle primary keys + # correctly (bug 731156). We have to add missing sequences and + # triggers ourselves. + if ($def->{TYPE} =~ /SERIAL/i) { + my $sequence = "${table}_${column}_SEQ"; + my $exists = $self->selectrow_array($sth, undef, $sequence); + if (!$exists) { + my @sql = $self->_get_create_seq_ddl($table, $column); + $self->do($_) foreach @sql; } + } + + if ($def->{REFERENCES}) { + my $references = $def->{REFERENCES}; + my $update = $references->{UPDATE} || 'CASCADE'; + my $to_table = $references->{TABLE}; + my $to_column = $references->{COLUMN}; + my $fk_name = $self->_bz_schema->_get_fk_name($table, $column, $references); + + # bz_rename_table didn't rename the trigger correctly. + if ($table eq 'bug_tag' && $to_table eq 'tags') { + $to_table = 'tag'; + } + if ($update =~ /CASCADE/i) { + my $trigger_name = uc($fk_name . "_UC"); + my $exist_trigger = $self->selectcol_arrayref($sth, undef, $trigger_name); + if (@$exist_trigger) { + $self->do("DROP TRIGGER $trigger_name"); + } + + my $tr_str + = "CREATE OR REPLACE TRIGGER $trigger_name" + . " AFTER UPDATE OF $to_column ON $to_table " + . " REFERENCING " + . " NEW AS NEW " + . " OLD AS OLD " + . " FOR EACH ROW " + . " BEGIN " + . " UPDATE $table" + . " SET $column = :NEW.$to_column" + . " WHERE $column = :OLD.$to_column;" + . " END $trigger_name;"; + $self->do($tr_str); + } + } } + } - # Drop the trigger which causes bug 541553 - my $trigger_name = "PRODUCTS_MILESTONEURL"; - my $exist_trigger = $self->selectcol_arrayref($sth, undef, $trigger_name); - if(@$exist_trigger) { - $self->do("DROP TRIGGER $trigger_name"); - } + # Drop the trigger which causes bug 541553 + my $trigger_name = "PRODUCTS_MILESTONEURL"; + my $exist_trigger = $self->selectcol_arrayref($sth, undef, $trigger_name); + if (@$exist_trigger) { + $self->do("DROP TRIGGER $trigger_name"); + } } # These two methods have been copied from Bugzilla::DB::Schema::Oracle. sub _get_create_seq_ddl { - my ($self, $table, $column) = @_; + my ($self, $table, $column) = @_; - my $seq_name = "${table}_${column}_SEQ"; - my $seq_sql = "CREATE SEQUENCE $seq_name INCREMENT BY 1 START WITH 1 " . - "NOMAXVALUE NOCYCLE NOCACHE"; - my $trigger_sql = $self->_get_create_trigger_ddl($table, $column, $seq_name); - return ($seq_sql, $trigger_sql); + my $seq_name = "${table}_${column}_SEQ"; + my $seq_sql = "CREATE SEQUENCE $seq_name INCREMENT BY 1 START WITH 1 " + . "NOMAXVALUE NOCYCLE NOCACHE"; + my $trigger_sql = $self->_get_create_trigger_ddl($table, $column, $seq_name); + return ($seq_sql, $trigger_sql); } sub _get_create_trigger_ddl { - my ($self, $table, $column, $seq_name) = @_; + my ($self, $table, $column, $seq_name) = @_; - my $trigger_sql = "CREATE OR REPLACE TRIGGER ${table}_${column}_TR " - . " BEFORE INSERT ON $table " - . " FOR EACH ROW " - . " BEGIN " - . " SELECT ${seq_name}.NEXTVAL " - . " INTO :NEW.$column FROM DUAL; " - . " END;"; - return $trigger_sql; + my $trigger_sql + = "CREATE OR REPLACE TRIGGER ${table}_${column}_TR " + . " BEFORE INSERT ON $table " + . " FOR EACH ROW " + . " BEGIN " + . " SELECT ${seq_name}.NEXTVAL " + . " INTO :NEW.$column FROM DUAL; " . " END;"; + return $trigger_sql; } ############################################################################ @@ -725,68 +753,69 @@ use strict; use warnings; use parent -norequire, qw(DBI::st); - + sub fetchrow_arrayref { - my $self = shift; - my $ref = $self->SUPER::fetchrow_arrayref(@_); - return undef if !defined $ref; - Bugzilla::DB::Oracle::_fix_arrayref($ref); - return $ref; + my $self = shift; + my $ref = $self->SUPER::fetchrow_arrayref(@_); + return undef if !defined $ref; + Bugzilla::DB::Oracle::_fix_arrayref($ref); + return $ref; } sub fetchrow_array { - my $self = shift; - if ( wantarray ) { - my @row = $self->SUPER::fetchrow_array(@_); - Bugzilla::DB::Oracle::_fix_arrayref(\@row); - return @row; - } else { - my $row = $self->SUPER::fetchrow_array(@_); - $row = Bugzilla::DB::Oracle::_fix_empty($row) if defined $row; - return $row; - } + my $self = shift; + if (wantarray) { + my @row = $self->SUPER::fetchrow_array(@_); + Bugzilla::DB::Oracle::_fix_arrayref(\@row); + return @row; + } + else { + my $row = $self->SUPER::fetchrow_array(@_); + $row = Bugzilla::DB::Oracle::_fix_empty($row) if defined $row; + return $row; + } } sub fetchrow_hashref { - my $self = shift; - my $ref = $self->SUPER::fetchrow_hashref(@_); - return undef if !defined $ref; - Bugzilla::DB::Oracle::_fix_hashref($ref); - return $ref; + my $self = shift; + my $ref = $self->SUPER::fetchrow_hashref(@_); + return undef if !defined $ref; + Bugzilla::DB::Oracle::_fix_hashref($ref); + return $ref; } sub fetchall_arrayref { - my $self = shift; - my $ref = $self->SUPER::fetchall_arrayref(@_); - return undef if !defined $ref; - foreach my $row (@$ref) { - if (ref($row) eq 'ARRAY') { - Bugzilla::DB::Oracle::_fix_arrayref($row); - } - elsif (ref($row) eq 'HASH') { - Bugzilla::DB::Oracle::_fix_hashref($row); - } + my $self = shift; + my $ref = $self->SUPER::fetchall_arrayref(@_); + return undef if !defined $ref; + foreach my $row (@$ref) { + if (ref($row) eq 'ARRAY') { + Bugzilla::DB::Oracle::_fix_arrayref($row); } - return $ref; + elsif (ref($row) eq 'HASH') { + Bugzilla::DB::Oracle::_fix_hashref($row); + } + } + return $ref; } sub fetchall_hashref { - my $self = shift; - my $ref = $self->SUPER::fetchall_hashref(@_); - return undef if !defined $ref; - foreach my $row (values %$ref) { - Bugzilla::DB::Oracle::_fix_hashref($row); - } - return $ref; + my $self = shift; + my $ref = $self->SUPER::fetchall_hashref(@_); + return undef if !defined $ref; + foreach my $row (values %$ref) { + Bugzilla::DB::Oracle::_fix_hashref($row); + } + return $ref; } sub fetch { - my $self = shift; - my $row = $self->SUPER::fetch(@_); - if ($row) { - Bugzilla::DB::Oracle::_fix_arrayref($row); - } - return $row; + my $self = shift; + my $row = $self->SUPER::fetch(@_); + if ($row) { + Bugzilla::DB::Oracle::_fix_arrayref($row); + } + return $row; } 1; diff --git a/Bugzilla/DB/Pg.pm b/Bugzilla/DB/Pg.pm index cbf8d7af1..a80391ff7 100644 --- a/Bugzilla/DB/Pg.pm +++ b/Bugzilla/DB/Pg.pm @@ -28,262 +28,370 @@ use warnings; use Bugzilla::Error; use Bugzilla::Version; use DBD::Pg; +use Bugzilla::Util qw(trick_taint); # This module extends the DB interface via inheritance use parent qw(Bugzilla::DB); -use constant BLOB_TYPE => { pg_type => DBD::Pg::PG_BYTEA }; +use constant BLOB_TYPE => {pg_type => DBD::Pg::PG_BYTEA}; sub new { - my ($class, $params) = @_; - my ($user, $pass, $host, $dbname, $port) = - @$params{qw(db_user db_pass db_host db_name db_port)}; + my ($class, $params) = @_; + my ($user, $pass, $host, $dbname, $port) + = @$params{qw(db_user db_pass db_host db_name db_port)}; - # The default database name for PostgreSQL. We have - # to connect to SOME database, even if we have - # no $dbname parameter. - $dbname ||= 'template1'; + # The default database name for PostgreSQL. We have + # to connect to SOME database, even if we have + # no $dbname parameter. + $dbname ||= 'template1'; - # construct the DSN from the parameters we got - my $dsn = "dbi:Pg:dbname=$dbname"; - $dsn .= ";host=$host" if $host; - $dsn .= ";port=$port" if $port; + # construct the DSN from the parameters we got + my $dsn = "dbi:Pg:dbname=$dbname"; + $dsn .= ";host=$host" if $host; + $dsn .= ";port=$port" if $port; - # This stops Pg from printing out lots of "NOTICE" messages when - # creating tables. - $dsn .= ";options='-c client_min_messages=warning'"; + # This stops Pg from printing out lots of "NOTICE" messages when + # creating tables. + $dsn .= ";options='-c client_min_messages=warning'"; - my $attrs = { pg_enable_utf8 => Bugzilla->params->{'utf8'} }; + my $attrs = {pg_enable_utf8 => Bugzilla->params->{'utf8'}}; - my $self = $class->db_new({ dsn => $dsn, user => $user, - pass => $pass, attrs => $attrs }); + my $self = $class->db_new( + {dsn => $dsn, user => $user, pass => $pass, attrs => $attrs}); - # all class local variables stored in DBI derived class needs to have - # a prefix 'private_'. See DBI documentation. - $self->{private_bz_tables_locked} = ""; - # Needed by TheSchwartz - $self->{private_bz_dsn} = $dsn; + # all class local variables stored in DBI derived class needs to have + # a prefix 'private_'. See DBI documentation. + $self->{private_bz_tables_locked} = ""; - bless ($self, $class); + # Needed by TheSchwartz + $self->{private_bz_dsn} = $dsn; - return $self; + bless($self, $class); + + return $self; } # if last_insert_id is supported on PostgreSQL by lowest DBI/DBD version # supported by Bugzilla, this implementation can be removed. sub bz_last_key { - my ($self, $table, $column) = @_; + my ($self, $table, $column) = @_; - my $seq = $table . "_" . $column . "_seq"; - my ($last_insert_id) = $self->selectrow_array("SELECT CURRVAL('$seq')"); + my $seq = $table . "_" . $column . "_seq"; + my ($last_insert_id) = $self->selectrow_array("SELECT CURRVAL('$seq')"); - return $last_insert_id; + return $last_insert_id; } -sub sql_group_concat { - my ($self, $text, $separator, $sort, $order_by) = @_; - $sort = 1 if !defined $sort; - $separator = $self->quote(', ') if !defined $separator; - - # PostgreSQL 8.x doesn't support STRING_AGG - if (vers_cmp($self->bz_server_version, 9) < 0) { - my $sql = "ARRAY_ACCUM($text)"; - if ($sort) { - $sql = "ARRAY_SORT($sql)"; - } - return "ARRAY_TO_STRING($sql, $separator)"; - } +## REDHAT EXTENSION 1255227 BEGIN - if ($order_by && $text =~ /^DISTINCT\s*(.+)$/i) { - # Since Postgres (quite rightly) doesn't support "SELECT DISTINCT x - # ORDER BY y", we need to sort the list, and then get the unique - # values - return "ARRAY_TO_STRING(ANYARRAY_UNIQ(ARRAY_AGG($1 ORDER BY $order_by)), $separator)"; - } +=head2 bz_start_transaction - # Determine the ORDER BY clause (if any) - if ($order_by) { - $order_by = " ORDER BY $order_by"; - } - elsif ($sort) { - # We don't include the DISTINCT keyword in an order by - $text =~ /^(?:DISTINCT\s*)?(.+)$/i; - $order_by = " ORDER BY $1"; - } +Customize how transactions start for RHBZ 1255227 - return "STRING_AGG(${text}::text, $separator${order_by}::text)" +=cut + +sub bz_start_transaction { + my $self = shift; + + unless ($self->bz_in_transaction) { + $self->do('ROLLBACK'); + } + + return $self->SUPER::bz_start_transaction(@_); +} +## REDHAT EXTENSION 1255227 END + +sub sql_group_concat { + my ($self, $text, $separator, $sort, $order_by) = @_; + $sort = 1 if !defined $sort; + $separator = $self->quote(', ') if !defined $separator; + + # PostgreSQL 8.x doesn't support STRING_AGG + if (vers_cmp($self->bz_server_version, 9) < 0) { + my $sql = "ARRAY_ACCUM($text)"; + if ($sort) { + $sql = "ARRAY_SORT($sql)"; + } + return "ARRAY_TO_STRING($sql, $separator)"; + } + + if ($order_by && $text =~ /^DISTINCT\s*(.+)$/i) { + + # Since Postgres (quite rightly) doesn't support "SELECT DISTINCT x + # ORDER BY y", we need to sort the list, and then get the unique + # values + return + "ARRAY_TO_STRING(ANYARRAY_UNIQ(ARRAY_AGG($1 ORDER BY $order_by)), $separator)"; + } + + # Determine the ORDER BY clause (if any) + if ($order_by) { + $order_by = " ORDER BY $order_by"; + } + elsif ($sort) { + + # We don't include the DISTINCT keyword in an order by + $text =~ /^(?:DISTINCT\s*)?(.+)$/i; + $order_by = " ORDER BY $1"; + } + + return "STRING_AGG(${text}::text, $separator${order_by}::text)"; } sub sql_istring { - my ($self, $string) = @_; + my ($self, $string) = @_; - return "LOWER(${string}::text)"; + return "LOWER(${string}::text)"; } sub sql_position { - my ($self, $fragment, $text) = @_; + my ($self, $fragment, $text) = @_; - return "POSITION(${fragment}::text IN ${text}::text)"; + return "POSITION(${fragment}::text IN ${text}::text)"; } sub sql_like { - my ($self, $fragment, $column, $not) = @_; - $not //= ''; + my ($self, $fragment, $column, $not) = @_; + $not //= ''; - return "${column}::text $not LIKE " . $self->sql_like_escape($fragment) . " ESCAPE '|'"; + return + "${column}::text $not LIKE " + . $self->sql_like_escape($fragment) + . " ESCAPE '|'"; } sub sql_ilike { - my ($self, $fragment, $column, $not) = @_; - $not //= ''; + my ($self, $fragment, $column, $not) = @_; + $not //= ''; - return "${column}::text $not ILIKE " . $self->sql_like_escape($fragment) . " ESCAPE '|'"; + return + "${column}::text $not ILIKE " + . $self->sql_like_escape($fragment) + . " ESCAPE '|'"; } sub sql_not_ilike { - return shift->sql_ilike(@_, 'NOT'); + return shift->sql_ilike(@_, 'NOT'); } # Escapes any % or _ characters which are special in a LIKE match. # Also performs a $dbh->quote to escape any quote characters. sub sql_like_escape { - my ($self, $fragment) = @_; + my ($self, $fragment) = @_; - $fragment =~ s/\|/\|\|/g; # escape the escape character if it appears - $fragment =~ s/%/\|%/g; # percent and underscore are the special match - $fragment =~ s/_/\|_/g; # characters in SQL. + $fragment =~ s/\|/\|\|/g; # escape the escape character if it appears + $fragment =~ s/%/\|%/g; # percent and underscore are the special match + $fragment =~ s/_/\|_/g; # characters in SQL. - return $self->quote("%$fragment%"); + return $self->quote("%$fragment%"); } sub sql_regexp { - my ($self, $expr, $pattern, $nocheck, $real_pattern) = @_; - $real_pattern ||= $pattern; + my ($self, $expr, $pattern, $nocheck, $real_pattern) = @_; + $real_pattern ||= $pattern; - $self->bz_check_regexp($real_pattern) if !$nocheck; + $self->bz_check_regexp($real_pattern) if !$nocheck; - return "${expr}::text ~* $pattern"; + return "${expr}::text ~* $pattern"; } sub sql_not_regexp { - my ($self, $expr, $pattern, $nocheck, $real_pattern) = @_; - $real_pattern ||= $pattern; + my ($self, $expr, $pattern, $nocheck, $real_pattern) = @_; + $real_pattern ||= $pattern; - $self->bz_check_regexp($real_pattern) if !$nocheck; + $self->bz_check_regexp($real_pattern) if !$nocheck; - return "${expr}::text !~* $pattern" + return "${expr}::text !~* $pattern"; } sub sql_limit { - my ($self, $limit, $offset) = @_; - - if (defined($offset)) { - return "LIMIT $limit OFFSET $offset"; - } else { - return "LIMIT $limit"; - } + my ($self, $limit, $offset) = @_; + + if (defined($offset)) { + return "LIMIT $limit OFFSET $offset"; + } + else { + return "LIMIT $limit"; + } } sub sql_from_days { - my ($self, $days) = @_; + my ($self, $days) = @_; - return "TO_TIMESTAMP('$days', 'J')::date"; + return "TO_TIMESTAMP('$days', 'J')::date"; } sub sql_to_days { - my ($self, $date) = @_; + my ($self, $date) = @_; - return "TO_CHAR(${date}::date, 'J')::int"; + return "TO_CHAR(${date}::date, 'J')::int"; } sub sql_date_format { - my ($self, $date, $format) = @_; - - $format = "%Y.%m.%d %H:%i:%s" if !$format; - - $format =~ s/\%Y/YYYY/g; - $format =~ s/\%y/YY/g; - $format =~ s/\%m/MM/g; - $format =~ s/\%d/DD/g; - $format =~ s/\%a/Dy/g; - $format =~ s/\%H/HH24/g; - $format =~ s/\%i/MI/g; - $format =~ s/\%s/SS/g; - - return "TO_CHAR($date, " . $self->quote($format) . ")"; + my ($self, $date, $format) = @_; + + $format = "%Y.%m.%d %H:%i:%s" if !$format; + + $format =~ s/\%Y/YYYY/g; + $format =~ s/\%y/YY/g; + $format =~ s/\%m/MM/g; + $format =~ s/\%d/DD/g; + $format =~ s/\%a/Dy/g; + $format =~ s/\%H/HH24/g; + $format =~ s/\%i/MI/g; + $format =~ s/\%s/SS/g; + + return "TO_CHAR($date, " . $self->quote($format) . ")"; } sub sql_date_math { - my ($self, $date, $operator, $interval, $units) = @_; - - return "$date $operator $interval * INTERVAL '1 $units'"; + my ($self, $date, $operator, $interval, $units) = @_; + + return "$date $operator $interval * INTERVAL '1 $units'"; } sub sql_string_concat { - my ($self, @params) = @_; - - # Postgres 7.3 does not support concatenating of different types, so we - # need to cast both parameters to text. Version 7.4 seems to handle this - # properly, so when we stop support 7.3, this can be removed. - return '(CAST(' . join(' AS text) || CAST(', @params) . ' AS text))'; + my ($self, @params) = @_; + + # Postgres 7.3 does not support concatenating of different types, so we + # need to cast both parameters to text. Version 7.4 seems to handle this + # properly, so when we stop support 7.3, this can be removed. + return '(CAST(' . join(' AS text) || CAST(', @params) . ' AS text))'; } # Tell us whether or not a particular sequence exists in the DB. sub bz_sequence_exists { - my ($self, $seq_name) = @_; - my $exists = $self->selectrow_array( - 'SELECT 1 FROM pg_statio_user_sequences WHERE relname = ?', - undef, $seq_name); - return $exists || 0; + my ($self, $seq_name) = @_; + my $exists + = $self->selectrow_array( + 'SELECT 1 FROM pg_statio_user_sequences WHERE relname = ?', + undef, $seq_name); + return $exists || 0; } sub bz_explain { - my ($self, $sql) = @_; - my $explain = $self->selectcol_arrayref("EXPLAIN ANALYZE $sql"); - return join("\n", @$explain); + my ($self, $sql) = @_; + my $explain = $self->selectcol_arrayref("EXPLAIN ANALYZE $sql"); + return join("\n", @$explain); +} + + +sub _postgres_fts_escape { + my ($self, $message) = @_; + + my $map = { + ':' => 'BZESCAPECOLON', + ';' => 'BZESCAPECOMMA', + '|' => 'BZESCAPEPIPE', + '/' => 'BZESCAPESLASH', + '\\' => 'BZESCAPEBSLASH', + '(' => 'BZESCAPEOB', + ')' => 'BZESCAPECB', + '{' => 'BZESCAPEOP', + '}' => 'BZESCAPECP', + '[' => 'BZESCAPESBO', + ']' => 'BZESCAPESBC', + '<' => 'BZESCAPELT', + '>' => 'BZESCAPEGT', + '\'' => 'BZESCAPESQ', + '"' => 'BZESCAPEDQ', + '=' => 'BZESCAPEEQ', + ',' => 'BZESCAPECOMMA', + '~' => 'BZESCAPETIL', + '!' => 'BZESCAPEEXC', + '@' => 'BZESCAPEAT', + '#' => 'BZESCAPEHAS', + '$' => 'BZESCAEPEDOL', + '%' => 'BZESCAPEPC', + '^' => 'BZESCAPECAR', + '&' => 'BZESCAPEAMP', + '*' => 'BZESCAPEAST', + '-' => 'BZESCAPEDASH', + '+' => 'BZESCAPEPLU', + }; + + while (my ($key, $value) = each(%$map)) { + my $qkey = quotemeta $key; + my $qval = ' ' . $value . ' '; + $message =~ s/$qkey/$qval/g; + } + chomp $message; + $message =~ s/^\s+//; + + return $message; } + +## REDHAT EXTENSION 1191022 BEGIN +sub sql_fulltext_search { + my ($self, $column, $text) = @_; + + my $fts_escaped = lc $self->_postgres_fts_escape($text); + + my $anded = join " & ", split /\s+/, $fts_escaped; + + my $quoted = $self->quote($anded); + trick_taint($quoted); + + my $query = "${column}_vect @@ TO_TSQUERY($quoted)"; + my $rank = "ts_rank_cd(${column}_vect, TO_TSQUERY($quoted))"; + + return ($query, $rank); +} +## REDHAT EXTENSION 1191022 END + ##################################################################### # Custom Database Setup ##################################################################### sub bz_check_server_version { - my $self = shift; - my ($db) = @_; - my $server_version = $self->SUPER::bz_check_server_version(@_); - my ($major_version, $minor_version) = $server_version =~ /^0*(\d+)\.0*(\d+)/; - # Pg 9.0 requires DBD::Pg 2.17.2 in order to properly read bytea values. - # Pg 9.2 requires DBD::Pg 2.19.3 as spclocation no longer exists. - if ($major_version >= 9) { - local $db->{dbd}->{version} = ($minor_version >= 2) ? '2.19.3' : '2.17.2'; - local $db->{name} = $db->{name} . " ${major_version}.$minor_version"; - Bugzilla::DB::_bz_check_dbd(@_); - } + my $self = shift; + my ($db) = @_; + my $server_version = $self->SUPER::bz_check_server_version(@_); + my ($major_version, $minor_version) = $server_version =~ /^0*(\d+)\.0*(\d+)/; + + # Pg 9.0 requires DBD::Pg 2.17.2 in order to properly read bytea values. + # Pg 9.2 requires DBD::Pg 2.19.3 as spclocation no longer exists. + if ($major_version >= 9) { + local $db->{dbd}->{version} = ($minor_version >= 2) ? '2.19.3' : '2.17.2'; + local $db->{name} = $db->{name} . " ${major_version}.$minor_version"; + Bugzilla::DB::_bz_check_dbd(@_); + } } sub bz_setup_database { - my $self = shift; - $self->SUPER::bz_setup_database(@_); - - my ($has_plpgsql) = $self->selectrow_array("SELECT COUNT(*) FROM pg_language WHERE lanname = 'plpgsql'"); - $self->do('CREATE LANGUAGE plpgsql') unless $has_plpgsql; - - if (vers_cmp($self->bz_server_version, 9) < 0) { - # Custom Functions for Postgres 8 - my $function = 'array_accum'; - my $array_accum = $self->selectrow_array( - 'SELECT 1 FROM pg_proc WHERE proname = ?', undef, $function); - if (!$array_accum) { - print "Creating function $function...\n"; - $self->do("CREATE AGGREGATE array_accum ( + my $self = shift; + $self->SUPER::bz_setup_database(@_); + + my ($has_plpgsql) + = $self->selectrow_array( + "SELECT COUNT(*) FROM pg_language WHERE lanname = 'plpgsql'"); + $self->do('CREATE LANGUAGE plpgsql') unless $has_plpgsql; + ## BUGBUG you need super user on the DB to do this. + ## This is just here so it blows up if they are missing. + $self->do('CREATE EXTENSION IF NOT EXISTS plperl'); + $self->do('CREATE EXTENSION IF NOT EXISTS pg_trgm'); + + if (vers_cmp($self->bz_server_version, 9) < 0) { + + # Custom Functions for Postgres 8 + my $function = 'array_accum'; + my $array_accum + = $self->selectrow_array('SELECT 1 FROM pg_proc WHERE proname = ?', + undef, $function); + if (!$array_accum) { + print "Creating function $function...\n"; + $self->do( + "CREATE AGGREGATE array_accum ( SFUNC = array_append, BASETYPE = anyelement, STYPE = anyarray, INITCOND = '{}' - )"); - } + )" + ); + } - $self->do(<<'END'); + $self->do(<<'END'); CREATE OR REPLACE FUNCTION array_sort(ANYARRAY) RETURNS ANYARRAY LANGUAGE SQL IMMUTABLE STRICT @@ -296,31 +404,32 @@ SELECT ARRAY( ); $$; END - } - else { - # Custom functions for Postgres 9.0+ - - # -Copyright © 2013 Joshua D. Burns (JDBurnZ) and Message In Action LLC - # JDBurnZ: https://github.com/JDBurnZ - # Message In Action: https://www.messageinaction.com - # - #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. - $self->do(q| + } + else { + # Custom functions for Postgres 9.0+ + + # -Copyright © 2013 Joshua D. Burns (JDBurnZ) and Message In Action LLC + # JDBurnZ: https://github.com/JDBurnZ + # Message In Action: https://www.messageinaction.com + # + #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. + $self->do( + q| DROP FUNCTION IF EXISTS anyarray_uniq(anyarray); CREATE OR REPLACE FUNCTION anyarray_uniq(with_array anyarray) RETURNS anyarray AS $BODY$ @@ -345,135 +454,293 @@ END RETURN return_array; END; $BODY$ LANGUAGE plpgsql; - |); - } - - # PostgreSQL doesn't like having *any* index on the thetext - # field, because it can't have index data longer than 2770 - # characters on that field. - $self->bz_drop_index('longdescs', 'longdescs_thetext_idx'); - # Same for all the comments fields in the fulltext table. - $self->bz_drop_index('bugs_fulltext', 'bugs_fulltext_comments_idx'); - $self->bz_drop_index('bugs_fulltext', - 'bugs_fulltext_comments_noprivate_idx'); - - # PostgreSQL also wants an index for calling LOWER on - # login_name, which we do with sql_istrcmp all over the place. - $self->bz_add_index('profiles', 'profiles_login_name_lower_idx', - {FIELDS => ['LOWER(login_name)'], TYPE => 'UNIQUE'}); - - # Now that Bugzilla::Object uses sql_istrcmp, other tables - # also need a LOWER() index. - _fix_case_differences('fielddefs', 'name'); - $self->bz_add_index('fielddefs', 'fielddefs_name_lower_idx', - {FIELDS => ['LOWER(name)'], TYPE => 'UNIQUE'}); - _fix_case_differences('keyworddefs', 'name'); - $self->bz_add_index('keyworddefs', 'keyworddefs_name_lower_idx', - {FIELDS => ['LOWER(name)'], TYPE => 'UNIQUE'}); - _fix_case_differences('products', 'name'); - $self->bz_add_index('products', 'products_name_lower_idx', - {FIELDS => ['LOWER(name)'], TYPE => 'UNIQUE'}); - - # bz_rename_column and bz_rename_table didn't correctly rename - # the sequence. - $self->_fix_bad_sequence('fielddefs', 'id', 'fielddefs_fieldid_seq', 'fielddefs_id_seq'); - # If the 'tags' table still exists, then bz_rename_table() - # will fix the sequence for us. - if (!$self->bz_table_info('tags')) { - my $res = $self->_fix_bad_sequence('tag', 'id', 'tags_id_seq', 'tag_id_seq'); - # If $res is true, then the sequence has been renamed, meaning that - # the primary key must be renamed too. - if ($res) { - $self->do('ALTER INDEX tags_pkey RENAME TO tag_pkey'); - } + | + ); + } + + # PostgreSQL doesn't like having *any* index on the thetext + # field, because it can't have index data longer than 2770 + # characters on that field. + $self->bz_drop_index('longdescs', 'longdescs_thetext_idx'); + + # Same for all the comments fields in the fulltext table. + $self->bz_drop_index('bugs_fulltext', 'bugs_fulltext_comments_idx'); + $self->bz_drop_index('bugs_fulltext', 'bugs_fulltext_comments_noprivate_idx'); + + # PostgreSQL also wants an index for calling LOWER on + # login_name, which we do with sql_istrcmp all over the place. + $self->bz_add_index( + 'profiles', + 'profiles_login_name_lower_idx', + {FIELDS => ['LOWER(login_name)'], TYPE => 'UNIQUE'} + ); + + # Now that Bugzilla::Object uses sql_istrcmp, other tables + # also need a LOWER() index. + _fix_case_differences('fielddefs', 'name'); + $self->bz_add_index('fielddefs', 'fielddefs_name_lower_idx', + {FIELDS => ['LOWER(name)'], TYPE => 'UNIQUE'}); + _fix_case_differences('keyworddefs', 'name'); + $self->bz_add_index('keyworddefs', 'keyworddefs_name_lower_idx', + {FIELDS => ['LOWER(name)'], TYPE => 'UNIQUE'}); + _fix_case_differences('products', 'name'); + $self->bz_add_index('products', 'products_name_lower_idx', + {FIELDS => ['LOWER(name)'], TYPE => 'UNIQUE'}); + + # bz_rename_column and bz_rename_table didn't correctly rename + # the sequence. + $self->_fix_bad_sequence('fielddefs', 'id', 'fielddefs_fieldid_seq', + 'fielddefs_id_seq'); + + # If the 'tags' table still exists, then bz_rename_table() + # will fix the sequence for us. + if (!$self->bz_table_info('tags')) { + my $res = $self->_fix_bad_sequence('tag', 'id', 'tags_id_seq', 'tag_id_seq'); + + # If $res is true, then the sequence has been renamed, meaning that + # the primary key must be renamed too. + if ($res) { + $self->do('ALTER INDEX tags_pkey RENAME TO tag_pkey'); } - - # Certain sequences got upgraded before we required Pg 8.3, and - # so they were not properly associated with their columns. - my @tables = $self->bz_table_list_real; - foreach my $table (@tables) { - my @columns = $self->bz_table_columns_real($table); - foreach my $column (@columns) { - # All our SERIAL pks have "id" in their name at the end. - next unless $column =~ /id$/; - my $sequence = "${table}_${column}_seq"; - if ($self->bz_sequence_exists($sequence)) { - my $is_associated = $self->selectrow_array( - 'SELECT pg_get_serial_sequence(?,?)', - undef, $table, $column); - next if $is_associated; - print "Fixing $sequence to be associated" - . " with $table.$column...\n"; - $self->do("ALTER SEQUENCE $sequence OWNED BY $table.$column"); - # In order to produce an exactly identical schema to what - # a brand-new checksetup.pl run would produce, we also need - # to re-set the default on this column. - $self->do("ALTER TABLE $table + } + + # Certain sequences got upgraded before we required Pg 8.3, and + # so they were not properly associated with their columns. + my @tables = $self->bz_table_list_real; + foreach my $table (@tables) { + my @columns = $self->bz_table_columns_real($table); + foreach my $column (@columns) { + + # All our SERIAL pks have "id" in their name at the end. + next unless $column =~ /id$/; + my $sequence = "${table}_${column}_seq"; + if ($self->bz_sequence_exists($sequence)) { + my $is_associated = $self->selectrow_array('SELECT pg_get_serial_sequence(?,?)', + undef, $table, $column); + next if $is_associated; + print "Fixing $sequence to be associated" . " with $table.$column...\n"; + $self->do("ALTER SEQUENCE $sequence OWNED BY $table.$column"); + + # In order to produce an exactly identical schema to what + # a brand-new checksetup.pl run would produce, we also need + # to re-set the default on this column. + $self->do( + "ALTER TABLE $table ALTER COLUMN $column - SET DEFAULT nextval('$sequence')"); - } - } + SET DEFAULT nextval('$sequence')" + ); + } } + } + + ## REDHAT EXTENSION 1191022 BEGIN + # These columns are needed to hold full text search data. + $self->bz_add_column('bugs_fulltext', 'comments_vect', {TYPE => 'TSVECTOR'}); + $self->bz_add_column('bugs_fulltext', 'comments_noprivate_vect', + {TYPE => 'TSVECTOR'}); + $self->bz_add_column('bugs_fulltext', 'short_desc_vect', {TYPE => 'TSVECTOR'}); + + # These triggers are to manage the inserting and updating of full text records. + $self->do(<<'SQLDATA'); + DROP TRIGGER IF EXISTS pg_fts_comments ON bugs_fulltext; + DROP TRIGGER IF EXISTS pg_fts_comments_noprivate ON bugs_fulltext; + DROP TRIGGER IF EXISTS pg_fts_short_desc ON bugs_fulltext; + DROP TRIGGER IF EXISTS pg_fts_insert ON bugs_fulltext; + + CREATE OR REPLACE FUNCTION escape_special_tokens(text) RETURNS text AS $$ + + my $message = $_[0]; + + my $map = { + ':' => 'BZESCAPECOLON', + ';' => 'BZESCAPECOMMA', + '|' => 'BZESCAPEPIPE', + '/' => 'BZESCAPESLASH', + '\\' => 'BZESCAPEBSLASH', + '(' => 'BZESCAPEOB', + ')' => 'BZESCAPECB', + '{' => 'BZESCAPEOP', + '}' => 'BZESCAPECP', + '[' => 'BZESCAPESBO', + ']' => 'BZESCAPESBC', + '<' => 'BZESCAPELT', + '>' => 'BZESCAPEGT', + '\'' => 'BZESCAPESQ', + '"' => 'BZESCAPEDQ', + '=' => 'BZESCAPEEQ', + ',' => 'BZESCAPECOMMA', + '~' => 'BZESCAPETIL', + '!' => 'BZESCAPEEXC', + '@' => 'BZESCAPEAT', + '#' => 'BZESCAPEHAS', + '$' => 'BZESCAEPEDOL', + '%' => 'BZESCAPEPC', + '^' => 'BZESCAPECAR', + '&' => 'BZESCAPEAMP', + '*' => 'BZESCAPEAST', + '-' => 'BZESCAPEDASH', + '+' => 'BZESCAPEPLU', + }; + + while(my ($key, $value) = each(%$map)) { + my $qkey = quotemeta $key; + my $qval = ' ' . $value . ' '; + $message =~ s/$qkey/$qval/g + } + + return $message; + + $$ LANGUAGE plperl; + + CREATE OR REPLACE FUNCTION bugzilla_fts_update_trigger() RETURNS trigger AS $$ + BEGIN + IF NEW.comments IS NOT NULL + THEN + NEW.comments_vect := to_tsvector('english', quote_literal(escape_special_tokens(NEW.comments))); + END IF; + + IF NEW.comments_noprivate IS NOT NULL + THEN + NEW.comments_noprivate_vect := + to_tsvector('english', quote_literal(escape_special_tokens(NEW.comments_noprivate))); + END IF; + + IF NEW.short_desc IS NOT NULL + THEN + NEW.short_desc_vect := to_tsvector('english', quote_literal(escape_special_tokens(NEW.short_desc))); + END IF; + + RETURN NEW; + END; + $$ + LANGUAGE plpgsql; + + CREATE TRIGGER pg_fts_insert + BEFORE INSERT + ON bugs_fulltext + FOR EACH ROW + EXECUTE PROCEDURE bugzilla_fts_update_trigger(); + + CREATE TRIGGER pg_fts_comments + BEFORE UPDATE OF comments + ON bugs_fulltext + FOR EACH ROW + EXECUTE PROCEDURE bugzilla_fts_update_trigger(); + + CREATE TRIGGER pg_fts_comments_noprivate + BEFORE UPDATE OF comments_noprivate + ON bugs_fulltext + FOR EACH ROW + EXECUTE PROCEDURE bugzilla_fts_update_trigger(); + + CREATE TRIGGER pg_fts_short_desc + BEFORE UPDATE OF short_desc + ON bugs_fulltext + FOR EACH ROW + EXECUTE PROCEDURE bugzilla_fts_update_trigger(); + +SQLDATA + ## REDHAT EXTENSION 1191022 END + $self->do(<<'SQLDATA'); +DO +$$ +BEGIN + IF NOT EXISTS ( + SELECT * + FROM pg_class c + JOIN pg_namespace n ON n.oid = c.relnamespace + WHERE c.relname = 'profiles_login_name_trgm_idx' + AND n.nspname = 'public' + ) THEN + CREATE INDEX profiles_login_name_trgm_idx ON public.profiles USING gin (to_tsvector('english', login_name)); + END IF; +END +$$; +SQLDATA + + return; } sub _fix_bad_sequence { - my ($self, $table, $column, $old_seq, $new_seq) = @_; - if ($self->bz_column_info($table, $column) - && $self->bz_sequence_exists($old_seq)) - { - print "Fixing $old_seq sequence...\n"; - $self->do("ALTER SEQUENCE $old_seq RENAME TO $new_seq"); - $self->do("ALTER TABLE $table ALTER COLUMN $column - SET DEFAULT NEXTVAL('$new_seq')"); - return 1; - } - return 0; + my ($self, $table, $column, $old_seq, $new_seq) = @_; + if ( $self->bz_column_info($table, $column) + && $self->bz_sequence_exists($old_seq)) + { + print "Fixing $old_seq sequence...\n"; + $self->do("ALTER SEQUENCE $old_seq RENAME TO $new_seq"); + $self->do( + "ALTER TABLE $table ALTER COLUMN $column + SET DEFAULT NEXTVAL('$new_seq')" + ); + return 1; + } + return 0; } # Renames things that differ only in case. sub _fix_case_differences { - my ($table, $field) = @_; - my $dbh = Bugzilla->dbh; - - my $duplicates = $dbh->selectcol_arrayref( - "SELECT DISTINCT LOWER($field) FROM $table - GROUP BY LOWER($field) HAVING COUNT(LOWER($field)) > 1"); - - foreach my $name (@$duplicates) { - my $dups = $dbh->selectcol_arrayref( - "SELECT $field FROM $table WHERE LOWER($field) = ?", - undef, $name); - my $primary = shift @$dups; - foreach my $dup (@$dups) { - my $new_name = "${dup}_"; - # Make sure the new name isn't *also* a duplicate. - while (1) { - last if (!$dbh->selectrow_array( - "SELECT 1 FROM $table WHERE LOWER($field) = ?", - undef, lc($new_name))); - $new_name .= "_"; - } - print "$table '$primary' and '$dup' have names that differ", - " only in case.\nRenaming '$dup' to '$new_name'...\n"; - $dbh->do("UPDATE $table SET $field = ? WHERE $field = ?", - undef, $new_name, $dup); - } + my ($table, $field) = @_; + my $dbh = Bugzilla->dbh; + + my $duplicates = $dbh->selectcol_arrayref( + "SELECT DISTINCT LOWER($field) FROM $table + GROUP BY LOWER($field) HAVING COUNT(LOWER($field)) > 1" + ); + + foreach my $name (@$duplicates) { + my $dups + = $dbh->selectcol_arrayref( + "SELECT $field FROM $table WHERE LOWER($field) = ?", + undef, $name); + my $primary = shift @$dups; + foreach my $dup (@$dups) { + my $new_name = "${dup}_"; + + # Make sure the new name isn't *also* a duplicate. + while (1) { + last + if (!$dbh->selectrow_array( + "SELECT 1 FROM $table WHERE LOWER($field) = ?", + undef, lc($new_name) + )); + $new_name .= "_"; + } + print "$table '$primary' and '$dup' have names that differ", + " only in case.\nRenaming '$dup' to '$new_name'...\n"; + $dbh->do("UPDATE $table SET $field = ? WHERE $field = ?", + undef, $new_name, $dup); } + } } ##################################################################### # Custom Schema Information Functions ##################################################################### -# Pg includes the PostgreSQL system tables in table_list_real, so +# Pg includes the PostgreSQL system tables in table_list_real, so # we need to remove those. sub bz_table_list_real { - my $self = shift; + my $self = shift; + + my @full_table_list = $self->SUPER::bz_table_list_real(@_); + + # All PostgreSQL system tables start with "pg_" or "sql_" + my @table_list = grep(!/(^pg_)|(^sql_)/, @full_table_list); + return @table_list; +} - my @full_table_list = $self->SUPER::bz_table_list_real(@_); - # All PostgreSQL system tables start with "pg_" or "sql_" - my @table_list = grep(!/(^pg_)|(^sql_)/, @full_table_list); - return @table_list; +sub quote { + my $self = shift; + my $str = shift; + my $decode = 0; + if (utf8::is_utf8($str)) { + utf8::encode($str); + $decode = 1; + } + my $retval = $self->SUPER::quote($str, @_); + trick_taint($retval) if defined $retval; + utf8::decode($retval) if defined $retval && $decode; + return $retval; } 1; @@ -515,8 +782,12 @@ percent characters and quoted. =over +=item quote + =item sql_date_format +=item sql_fulltext_search + =item bz_explain =item bz_sequence_exists diff --git a/Bugzilla/DB/Schema.pm b/Bugzilla/DB/Schema.pm index d1c1dc7e9..b6f0365c4 100644 --- a/Bugzilla/DB/Schema.pm +++ b/Bugzilla/DB/Schema.pm @@ -29,6 +29,7 @@ use Digest::MD5 qw(md5_hex); use Hash::Util qw(lock_value unlock_hash lock_keys unlock_keys); use List::MoreUtils qw(firstidx natatime); use Safe; + # Historical, needed for SCHEMA_VERSION = '1.00' use Storable qw(dclone freeze thaw); @@ -197,1596 +198,1576 @@ update this column in this table." =cut -use constant SCHEMA_VERSION => 3; -use constant ADD_COLUMN => 'ADD COLUMN'; +use constant SCHEMA_VERSION => 3; +use constant ADD_COLUMN => 'ADD COLUMN'; + # Multiple FKs can be added using ALTER TABLE ADD CONSTRAINT in one # SQL statement. This isn't true for all databases. use constant MULTIPLE_FKS_IN_ALTER => 1; + # This is a reasonable default that's true for both PostgreSQL and MySQL. use constant MAX_IDENTIFIER_LEN => 63; use constant FIELD_TABLE_SCHEMA => { + FIELDS => [ + id => {TYPE => 'SMALLSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + value => {TYPE => 'varchar(64)', NOTNULL => 1}, + sortkey => {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0}, + isactive => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}, + visibility_value_id => {TYPE => 'INT2'}, + ], + + # Note that bz_add_field_table should prepend the table name + # to these index names. + INDEXES => [ + value_idx => {FIELDS => ['value'], TYPE => 'UNIQUE'}, + sortkey_idx => ['sortkey', 'value'], + visibility_value_id_idx => ['visibility_value_id'], + ], +}; + +use constant ABSTRACT_SCHEMA => { + + # BUG-RELATED TABLES + # ------------------ + + # General Bug Information + # ----------------------- + bugs => { FIELDS => [ - id => {TYPE => 'SMALLSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - value => {TYPE => 'varchar(64)', NOTNULL => 1}, - sortkey => {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0}, - isactive => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'TRUE'}, - visibility_value_id => {TYPE => 'INT2'}, + bug_id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + assigned_to => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid'} + }, + bug_file_loc => {TYPE => 'MEDIUMTEXT', NOTNULL => 1, DEFAULT => "''"}, + bug_severity => {TYPE => 'varchar(64)', NOTNULL => 1}, + bug_status => {TYPE => 'varchar(64)', NOTNULL => 1}, + creation_ts => {TYPE => 'DATETIME'}, + delta_ts => {TYPE => 'DATETIME', NOTNULL => 1}, + short_desc => {TYPE => 'varchar(255)', NOTNULL => 1}, + op_sys => {TYPE => 'varchar(64)', NOTNULL => 1}, + priority => {TYPE => 'varchar(64)', NOTNULL => 1}, + product_id => { + TYPE => 'INT2', + NOTNULL => 1, + REFERENCES => {TABLE => 'products', COLUMN => 'id'} + }, + rep_platform => {TYPE => 'varchar(64)', NOTNULL => 1}, + reporter => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid'} + }, + version => {TYPE => 'varchar(64)', NOTNULL => 1}, + component_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'components', COLUMN => 'id'} + }, + resolution => {TYPE => 'varchar(64)', NOTNULL => 1, DEFAULT => "''"}, + target_milestone => {TYPE => 'varchar(64)', NOTNULL => 1, DEFAULT => "'---'"}, + qa_contact => + {TYPE => 'INT3', REFERENCES => {TABLE => 'profiles', COLUMN => 'userid'}}, + status_whiteboard => {TYPE => 'MEDIUMTEXT', NOTNULL => 1, DEFAULT => "''"}, + lastdiffed => {TYPE => 'DATETIME'}, + everconfirmed => {TYPE => 'BOOLEAN', NOTNULL => 1}, + reporter_accessible => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}, + cclist_accessible => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}, + estimated_time => {TYPE => 'decimal(7,2)', NOTNULL => 1, DEFAULT => '0'}, + remaining_time => {TYPE => 'decimal(7,2)', NOTNULL => 1, DEFAULT => '0'}, + deadline => {TYPE => 'DATETIME'}, ], - # Note that bz_add_field_table should prepend the table name - # to these index names. INDEXES => [ - value_idx => {FIELDS => ['value'], TYPE => 'UNIQUE'}, - sortkey_idx => ['sortkey', 'value'], - visibility_value_id_idx => ['visibility_value_id'], + bugs_assigned_to_idx => ['assigned_to'], + bugs_creation_ts_idx => ['creation_ts'], + bugs_delta_ts_idx => ['delta_ts'], + bugs_bug_severity_idx => ['bug_severity'], + bugs_bug_status_idx => ['bug_status'], + bugs_op_sys_idx => ['op_sys'], + bugs_priority_idx => ['priority'], + bugs_product_id_idx => ['product_id'], + bugs_reporter_idx => ['reporter'], + bugs_version_idx => ['version'], + bugs_component_id_idx => ['component_id'], + bugs_resolution_idx => ['resolution'], + bugs_target_milestone_idx => ['target_milestone'], + bugs_qa_contact_idx => ['qa_contact'], ], -}; + }, -use constant ABSTRACT_SCHEMA => { + bugs_fulltext => { + FIELDS => [ + bug_id => { + TYPE => 'INT3', + NOTNULL => 1, + PRIMARYKEY => 1, + REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'} + }, + short_desc => {TYPE => 'varchar(255)', NOTNULL => 1}, + + # Comments are stored all together in one column for searching. + # This allows us to examine all comments together when deciding + # the relevance of a bug in fulltext search. + comments => {TYPE => 'LONGTEXT'}, + comments_noprivate => {TYPE => 'LONGTEXT'}, + ], + INDEXES => [ + bugs_fulltext_short_desc_idx => {FIELDS => ['short_desc'], TYPE => 'FULLTEXT'}, + bugs_fulltext_comments_idx => {FIELDS => ['comments'], TYPE => 'FULLTEXT'}, + bugs_fulltext_comments_noprivate_idx => + {FIELDS => ['comments_noprivate'], TYPE => 'FULLTEXT'}, + ], + }, - # BUG-RELATED TABLES - # ------------------ - - # General Bug Information - # ----------------------- - bugs => { - FIELDS => [ - bug_id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - assigned_to => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid'}}, - bug_file_loc => {TYPE => 'MEDIUMTEXT', - NOTNULL => 1, DEFAULT => "''"}, - bug_severity => {TYPE => 'varchar(64)', NOTNULL => 1}, - bug_status => {TYPE => 'varchar(64)', NOTNULL => 1}, - creation_ts => {TYPE => 'DATETIME'}, - delta_ts => {TYPE => 'DATETIME', NOTNULL => 1}, - short_desc => {TYPE => 'varchar(255)', NOTNULL => 1}, - op_sys => {TYPE => 'varchar(64)', NOTNULL => 1}, - priority => {TYPE => 'varchar(64)', NOTNULL => 1}, - product_id => {TYPE => 'INT2', NOTNULL => 1, - REFERENCES => {TABLE => 'products', - COLUMN => 'id'}}, - rep_platform => {TYPE => 'varchar(64)', NOTNULL => 1}, - reporter => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid'}}, - version => {TYPE => 'varchar(64)', NOTNULL => 1}, - component_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'components', - COLUMN => 'id'}}, - resolution => {TYPE => 'varchar(64)', - NOTNULL => 1, DEFAULT => "''"}, - target_milestone => {TYPE => 'varchar(64)', - NOTNULL => 1, DEFAULT => "'---'"}, - qa_contact => {TYPE => 'INT3', - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid'}}, - status_whiteboard => {TYPE => 'MEDIUMTEXT', NOTNULL => 1, - DEFAULT => "''"}, - lastdiffed => {TYPE => 'DATETIME'}, - everconfirmed => {TYPE => 'BOOLEAN', NOTNULL => 1}, - reporter_accessible => {TYPE => 'BOOLEAN', - NOTNULL => 1, DEFAULT => 'TRUE'}, - cclist_accessible => {TYPE => 'BOOLEAN', - NOTNULL => 1, DEFAULT => 'TRUE'}, - estimated_time => {TYPE => 'decimal(7,2)', - NOTNULL => 1, DEFAULT => '0'}, - remaining_time => {TYPE => 'decimal(7,2)', - NOTNULL => 1, DEFAULT => '0'}, - deadline => {TYPE => 'DATETIME'}, - ], - INDEXES => [ - bugs_assigned_to_idx => ['assigned_to'], - bugs_creation_ts_idx => ['creation_ts'], - bugs_delta_ts_idx => ['delta_ts'], - bugs_bug_severity_idx => ['bug_severity'], - bugs_bug_status_idx => ['bug_status'], - bugs_op_sys_idx => ['op_sys'], - bugs_priority_idx => ['priority'], - bugs_product_id_idx => ['product_id'], - bugs_reporter_idx => ['reporter'], - bugs_version_idx => ['version'], - bugs_component_id_idx => ['component_id'], - bugs_resolution_idx => ['resolution'], - bugs_target_milestone_idx => ['target_milestone'], - bugs_qa_contact_idx => ['qa_contact'], - ], - }, - - bugs_fulltext => { - FIELDS => [ - bug_id => {TYPE => 'INT3', NOTNULL => 1, PRIMARYKEY => 1, - REFERENCES => {TABLE => 'bugs', - COLUMN => 'bug_id', - DELETE => 'CASCADE'}}, - short_desc => {TYPE => 'varchar(255)', NOTNULL => 1}, - # Comments are stored all together in one column for searching. - # This allows us to examine all comments together when deciding - # the relevance of a bug in fulltext search. - comments => {TYPE => 'LONGTEXT'}, - comments_noprivate => {TYPE => 'LONGTEXT'}, - ], - INDEXES => [ - bugs_fulltext_short_desc_idx => {FIELDS => ['short_desc'], - TYPE => 'FULLTEXT'}, - bugs_fulltext_comments_idx => {FIELDS => ['comments'], - TYPE => 'FULLTEXT'}, - bugs_fulltext_comments_noprivate_idx => { - FIELDS => ['comments_noprivate'], TYPE => 'FULLTEXT'}, - ], - }, - - bugs_activity => { - FIELDS => [ - id => {TYPE => 'INTSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - bug_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'bugs', - COLUMN => 'bug_id', - DELETE => 'CASCADE'}}, - attach_id => {TYPE => 'INT3', - REFERENCES => {TABLE => 'attachments', - COLUMN => 'attach_id', - DELETE => 'CASCADE'}}, - who => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid'}}, - bug_when => {TYPE => 'DATETIME', NOTNULL => 1}, - fieldid => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'fielddefs', - COLUMN => 'id'}}, - added => {TYPE => 'varchar(255)'}, - removed => {TYPE => 'varchar(255)'}, - comment_id => {TYPE => 'INT4', - REFERENCES => { TABLE => 'longdescs', - COLUMN => 'comment_id', - DELETE => 'CASCADE'}}, - ], - INDEXES => [ - bugs_activity_bug_id_idx => ['bug_id'], - bugs_activity_who_idx => ['who'], - bugs_activity_bug_when_idx => ['bug_when'], - bugs_activity_fieldid_idx => ['fieldid'], - bugs_activity_added_idx => ['added'], - bugs_activity_removed_idx => ['removed'], - ], - }, - - bugs_aliases => { - FIELDS => [ - alias => {TYPE => 'varchar(40)', NOTNULL => 1}, - bug_id => {TYPE => 'INT3', - REFERENCES => {TABLE => 'bugs', - COLUMN => 'bug_id', - DELETE => 'CASCADE'}}, - ], - INDEXES => [ - bugs_aliases_bug_id_idx => ['bug_id'], - bugs_aliases_alias_idx => {FIELDS => ['alias'], - TYPE => 'UNIQUE'}, - ], - }, - - cc => { - FIELDS => [ - bug_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'bugs', - COLUMN => 'bug_id', - DELETE => 'CASCADE'}}, - who => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE'}}, - ], - INDEXES => [ - cc_bug_id_idx => {FIELDS => [qw(bug_id who)], - TYPE => 'UNIQUE'}, - cc_who_idx => ['who'], - ], - }, - - longdescs => { - FIELDS => [ - comment_id => {TYPE => 'INTSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - bug_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'bugs', - COLUMN => 'bug_id', - DELETE => 'CASCADE'}}, - who => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid'}}, - bug_when => {TYPE => 'DATETIME', NOTNULL => 1}, - work_time => {TYPE => 'decimal(7,2)', NOTNULL => 1, - DEFAULT => '0'}, - thetext => {TYPE => 'LONGTEXT', NOTNULL => 1}, - isprivate => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - already_wrapped => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - type => {TYPE => 'INT2', NOTNULL => 1, - DEFAULT => '0'}, - extra_data => {TYPE => 'varchar(255)'} - ], - INDEXES => [ - longdescs_bug_id_idx => [qw(bug_id work_time)], - longdescs_who_idx => [qw(who bug_id)], - longdescs_bug_when_idx => ['bug_when'], - ], - }, - - longdescs_tags => { - FIELDS => [ - id => { TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1 }, - comment_id => { TYPE => 'INT4', - REFERENCES => { TABLE => 'longdescs', - COLUMN => 'comment_id', - DELETE => 'CASCADE' }}, - tag => { TYPE => 'varchar(24)', NOTNULL => 1 }, - ], - INDEXES => [ - longdescs_tags_idx => { FIELDS => ['comment_id', 'tag'], TYPE => 'UNIQUE' }, - ], - }, - - longdescs_tags_weights => { - FIELDS => [ - id => { TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1 }, - tag => { TYPE => 'varchar(24)', NOTNULL => 1 }, - weight => { TYPE => 'INT3', NOTNULL => 1 }, - ], - INDEXES => [ - longdescs_tags_weights_tag_idx => { FIELDS => ['tag'], TYPE => 'UNIQUE' }, - ], - }, - - longdescs_tags_activity => { - FIELDS => [ - id => { TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1 }, - bug_id => { TYPE => 'INT3', NOTNULL => 1, - REFERENCES => { TABLE => 'bugs', - COLUMN => 'bug_id', - DELETE => 'CASCADE' }}, - comment_id => { TYPE => 'INT4', - REFERENCES => { TABLE => 'longdescs', - COLUMN => 'comment_id', - DELETE => 'CASCADE' }}, - who => { TYPE => 'INT3', NOTNULL => 1, - REFERENCES => { TABLE => 'profiles', - COLUMN => 'userid' }}, - bug_when => { TYPE => 'DATETIME', NOTNULL => 1 }, - added => { TYPE => 'varchar(24)' }, - removed => { TYPE => 'varchar(24)' }, - ], - INDEXES => [ - longdescs_tags_activity_bug_id_idx => ['bug_id'], - ], - }, - - dependencies => { - FIELDS => [ - blocked => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'bugs', - COLUMN => 'bug_id', - DELETE => 'CASCADE'}}, - dependson => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'bugs', - COLUMN => 'bug_id', - DELETE => 'CASCADE'}}, - ], - INDEXES => [ - dependencies_blocked_idx => {FIELDS => [qw(blocked dependson)], - TYPE => 'UNIQUE'}, - dependencies_dependson_idx => ['dependson'], - ], - }, - - attachments => { - FIELDS => [ - attach_id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - bug_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'bugs', - COLUMN => 'bug_id', - DELETE => 'CASCADE'}}, - creation_ts => {TYPE => 'DATETIME', NOTNULL => 1}, - modification_time => {TYPE => 'DATETIME', NOTNULL => 1}, - description => {TYPE => 'TINYTEXT', NOTNULL => 1}, - mimetype => {TYPE => 'TINYTEXT', NOTNULL => 1}, - ispatch => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - filename => {TYPE => 'varchar(255)', NOTNULL => 1}, - submitter_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid'}}, - isobsolete => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - isprivate => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - ], - INDEXES => [ - attachments_bug_id_idx => ['bug_id'], - attachments_creation_ts_idx => ['creation_ts'], - attachments_modification_time_idx => ['modification_time'], - attachments_submitter_id_idx => ['submitter_id', 'bug_id'], - ], - }, - attach_data => { - FIELDS => [ - id => {TYPE => 'INT3', NOTNULL => 1, - PRIMARYKEY => 1, - REFERENCES => {TABLE => 'attachments', - COLUMN => 'attach_id', - DELETE => 'CASCADE'}}, - thedata => {TYPE => 'LONGBLOB', NOTNULL => 1}, - ], - }, - - duplicates => { - FIELDS => [ - dupe_of => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'bugs', - COLUMN => 'bug_id', - DELETE => 'CASCADE'}}, - dupe => {TYPE => 'INT3', NOTNULL => 1, - PRIMARYKEY => 1, - REFERENCES => {TABLE => 'bugs', - COLUMN => 'bug_id', - DELETE => 'CASCADE'}}, - ], - }, - - bug_see_also => { - FIELDS => [ - id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - bug_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'bugs', - COLUMN => 'bug_id', - DELETE => 'CASCADE'}}, - value => {TYPE => 'varchar(255)', NOTNULL => 1}, - class => {TYPE => 'varchar(255)', NOTNULL => 1, DEFAULT => "''"}, - ], - INDEXES => [ - bug_see_also_bug_id_idx => {FIELDS => [qw(bug_id value)], - TYPE => 'UNIQUE'}, - ], - }, - - # Auditing - # -------- - - audit_log => { - FIELDS => [ - user_id => {TYPE => 'INT3', - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'SET NULL'}}, - class => {TYPE => 'varchar(255)', NOTNULL => 1}, - object_id => {TYPE => 'INT4', NOTNULL => 1}, - field => {TYPE => 'varchar(64)', NOTNULL => 1}, - removed => {TYPE => 'MEDIUMTEXT'}, - added => {TYPE => 'MEDIUMTEXT'}, - at_time => {TYPE => 'DATETIME', NOTNULL => 1}, - ], - INDEXES => [ - audit_log_class_idx => ['class', 'at_time'], - ], - }, - - # Keywords - # -------- - - keyworddefs => { - FIELDS => [ - id => {TYPE => 'SMALLSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - name => {TYPE => 'varchar(64)', NOTNULL => 1}, - description => {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, - ], - INDEXES => [ - keyworddefs_name_idx => {FIELDS => ['name'], - TYPE => 'UNIQUE'}, - ], - }, - - keywords => { - FIELDS => [ - bug_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'bugs', - COLUMN => 'bug_id', - DELETE => 'CASCADE'}}, - keywordid => {TYPE => 'INT2', NOTNULL => 1, - REFERENCES => {TABLE => 'keyworddefs', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - - ], - INDEXES => [ - keywords_bug_id_idx => {FIELDS => [qw(bug_id keywordid)], - TYPE => 'UNIQUE'}, - keywords_keywordid_idx => ['keywordid'], - ], - }, - - # Flags - # ----- - - # "flags" stores one record for each flag on each bug/attachment. - flags => { - FIELDS => [ - id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - type_id => {TYPE => 'INT2', NOTNULL => 1, - REFERENCES => {TABLE => 'flagtypes', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - status => {TYPE => 'char(1)', NOTNULL => 1}, - bug_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'bugs', - COLUMN => 'bug_id', - DELETE => 'CASCADE'}}, - attach_id => {TYPE => 'INT3', - REFERENCES => {TABLE => 'attachments', - COLUMN => 'attach_id', - DELETE => 'CASCADE'}}, - creation_date => {TYPE => 'DATETIME', NOTNULL => 1}, - modification_date => {TYPE => 'DATETIME'}, - setter_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid'}}, - requestee_id => {TYPE => 'INT3', - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid'}}, - ], - INDEXES => [ - flags_bug_id_idx => [qw(bug_id attach_id)], - flags_setter_id_idx => ['setter_id'], - flags_requestee_id_idx => ['requestee_id'], - flags_type_id_idx => ['type_id'], - ], - }, - - # "flagtypes" defines the types of flags that can be set. - flagtypes => { - FIELDS => [ - id => {TYPE => 'SMALLSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - name => {TYPE => 'varchar(50)', NOTNULL => 1}, - description => {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, - cc_list => {TYPE => 'varchar(200)'}, - target_type => {TYPE => 'char(1)', NOTNULL => 1, - DEFAULT => "'b'"}, - is_active => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'TRUE'}, - is_requestable => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - is_requesteeble => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - is_multiplicable => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - sortkey => {TYPE => 'INT2', NOTNULL => 1, - DEFAULT => '0'}, - grant_group_id => {TYPE => 'INT3', - REFERENCES => {TABLE => 'groups', - COLUMN => 'id', - DELETE => 'SET NULL'}}, - request_group_id => {TYPE => 'INT3', - REFERENCES => {TABLE => 'groups', - COLUMN => 'id', - DELETE => 'SET NULL'}}, - ], - }, - - # "flaginclusions" and "flagexclusions" specify the products/components - # a bug/attachment must belong to in order for flags of a given type - # to be set for them. - flaginclusions => { - FIELDS => [ - type_id => {TYPE => 'INT2', NOTNULL => 1, - REFERENCES => {TABLE => 'flagtypes', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - product_id => {TYPE => 'INT2', - REFERENCES => {TABLE => 'products', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - component_id => {TYPE => 'INT3', - REFERENCES => {TABLE => 'components', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - ], - INDEXES => [ - flaginclusions_type_id_idx => { FIELDS => [qw(type_id product_id component_id)], - TYPE => 'UNIQUE' }, - ], - }, - - flagexclusions => { - FIELDS => [ - type_id => {TYPE => 'INT2', NOTNULL => 1, - REFERENCES => {TABLE => 'flagtypes', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - product_id => {TYPE => 'INT2', - REFERENCES => {TABLE => 'products', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - component_id => {TYPE => 'INT3', - REFERENCES => {TABLE => 'components', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - ], - INDEXES => [ - flagexclusions_type_id_idx => { FIELDS => [qw(type_id product_id component_id)], - TYPE => 'UNIQUE' }, - ], - }, - - # General Field Information - # ------------------------- - - fielddefs => { - FIELDS => [ - id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - name => {TYPE => 'varchar(64)', NOTNULL => 1}, - type => {TYPE => 'INT2', NOTNULL => 1, - DEFAULT => FIELD_TYPE_UNKNOWN}, - custom => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - description => {TYPE => 'TINYTEXT', NOTNULL => 1}, - long_desc => {TYPE => 'varchar(255)', NOTNULL => 1, DEFAULT => "''"}, - mailhead => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - sortkey => {TYPE => 'INT2', NOTNULL => 1}, - obsolete => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - enter_bug => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - buglist => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - visibility_field_id => {TYPE => 'INT3', - REFERENCES => {TABLE => 'fielddefs', - COLUMN => 'id'}}, - value_field_id => {TYPE => 'INT3', - REFERENCES => {TABLE => 'fielddefs', - COLUMN => 'id'}}, - reverse_desc => {TYPE => 'TINYTEXT'}, - is_mandatory => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - is_numeric => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - ], - INDEXES => [ - fielddefs_name_idx => {FIELDS => ['name'], - TYPE => 'UNIQUE'}, - fielddefs_sortkey_idx => ['sortkey'], - fielddefs_value_field_id_idx => ['value_field_id'], - fielddefs_is_mandatory_idx => ['is_mandatory'], - ], - }, - - # Field Visibility Information - # ------------------------- - - field_visibility => { - FIELDS => [ - field_id => {TYPE => 'INT3', - REFERENCES => {TABLE => 'fielddefs', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - value_id => {TYPE => 'INT2', NOTNULL => 1} - ], - INDEXES => [ - field_visibility_field_id_idx => { - FIELDS => [qw(field_id value_id)], - TYPE => 'UNIQUE' - }, - ], - }, - - # Per-product Field Values - # ------------------------ - - versions => { - FIELDS => [ - id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - value => {TYPE => 'varchar(64)', NOTNULL => 1}, - product_id => {TYPE => 'INT2', NOTNULL => 1, - REFERENCES => {TABLE => 'products', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - isactive => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'TRUE'}, - ], - INDEXES => [ - versions_product_id_idx => {FIELDS => [qw(product_id value)], - TYPE => 'UNIQUE'}, - ], - }, - - milestones => { - FIELDS => [ - id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - product_id => {TYPE => 'INT2', NOTNULL => 1, - REFERENCES => {TABLE => 'products', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - value => {TYPE => 'varchar(64)', NOTNULL => 1}, - sortkey => {TYPE => 'INT2', NOTNULL => 1, - DEFAULT => 0}, - isactive => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'TRUE'}, - ], - INDEXES => [ - milestones_product_id_idx => {FIELDS => [qw(product_id value)], - TYPE => 'UNIQUE'}, - ], - }, - - # Global Field Values - # ------------------- - - bug_status => { - FIELDS => [ - @{ dclone(FIELD_TABLE_SCHEMA->{FIELDS}) }, - is_open => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}, - - ], - INDEXES => [ - bug_status_value_idx => {FIELDS => ['value'], - TYPE => 'UNIQUE'}, - bug_status_sortkey_idx => ['sortkey', 'value'], - bug_status_visibility_value_id_idx => ['visibility_value_id'], - ], - }, - - resolution => { - FIELDS => dclone(FIELD_TABLE_SCHEMA->{FIELDS}), - INDEXES => [ - resolution_value_idx => {FIELDS => ['value'], - TYPE => 'UNIQUE'}, - resolution_sortkey_idx => ['sortkey', 'value'], - resolution_visibility_value_id_idx => ['visibility_value_id'], - ], - }, - - bug_severity => { - FIELDS => dclone(FIELD_TABLE_SCHEMA->{FIELDS}), - INDEXES => [ - bug_severity_value_idx => {FIELDS => ['value'], - TYPE => 'UNIQUE'}, - bug_severity_sortkey_idx => ['sortkey', 'value'], - bug_severity_visibility_value_id_idx => ['visibility_value_id'], - ], - }, - - priority => { - FIELDS => dclone(FIELD_TABLE_SCHEMA->{FIELDS}), - INDEXES => [ - priority_value_idx => {FIELDS => ['value'], - TYPE => 'UNIQUE'}, - priority_sortkey_idx => ['sortkey', 'value'], - priority_visibility_value_id_idx => ['visibility_value_id'], - ], - }, - - rep_platform => { - FIELDS => dclone(FIELD_TABLE_SCHEMA->{FIELDS}), - INDEXES => [ - rep_platform_value_idx => {FIELDS => ['value'], - TYPE => 'UNIQUE'}, - rep_platform_sortkey_idx => ['sortkey', 'value'], - rep_platform_visibility_value_id_idx => ['visibility_value_id'], - ], - }, - - op_sys => { - FIELDS => dclone(FIELD_TABLE_SCHEMA->{FIELDS}), - INDEXES => [ - op_sys_value_idx => {FIELDS => ['value'], - TYPE => 'UNIQUE'}, - op_sys_sortkey_idx => ['sortkey', 'value'], - op_sys_visibility_value_id_idx => ['visibility_value_id'], - ], - }, - - status_workflow => { - FIELDS => [ - # On bug creation, there is no old value. - old_status => {TYPE => 'INT2', - REFERENCES => {TABLE => 'bug_status', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - new_status => {TYPE => 'INT2', NOTNULL => 1, - REFERENCES => {TABLE => 'bug_status', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - require_comment => {TYPE => 'INT1', NOTNULL => 1, DEFAULT => 0}, - ], - INDEXES => [ - status_workflow_idx => {FIELDS => ['old_status', 'new_status'], - TYPE => 'UNIQUE'}, - ], - }, - - # USER INFO - # --------- - - # General User Information - # ------------------------ - - profiles => { - FIELDS => [ - userid => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - login_name => {TYPE => 'varchar(255)', NOTNULL => 1}, - cryptpassword => {TYPE => 'varchar(128)'}, - realname => {TYPE => 'varchar(255)', NOTNULL => 1, - DEFAULT => "''"}, - disabledtext => {TYPE => 'MEDIUMTEXT', NOTNULL => 1, - DEFAULT => "''"}, - disable_mail => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - mybugslink => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'TRUE'}, - extern_id => {TYPE => 'varchar(64)'}, - is_enabled => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'TRUE'}, - last_seen_date => {TYPE => 'DATETIME'}, - ], - INDEXES => [ - profiles_login_name_idx => {FIELDS => ['login_name'], - TYPE => 'UNIQUE'}, - profiles_extern_id_idx => {FIELDS => ['extern_id'], - TYPE => 'UNIQUE'} - ], - }, - - profile_search => { - FIELDS => [ - id => {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, - user_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE'}}, - bug_list => {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, - list_order => {TYPE => 'MEDIUMTEXT'}, - ], - INDEXES => [ - profile_search_user_id_idx => [qw(user_id)], - ], - }, - - profiles_activity => { - FIELDS => [ - id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - userid => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE'}}, - who => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid'}}, - profiles_when => {TYPE => 'DATETIME', NOTNULL => 1}, - fieldid => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'fielddefs', - COLUMN => 'id'}}, - oldvalue => {TYPE => 'TINYTEXT'}, - newvalue => {TYPE => 'TINYTEXT'}, - ], - INDEXES => [ - profiles_activity_userid_idx => ['userid'], - profiles_activity_profiles_when_idx => ['profiles_when'], - profiles_activity_fieldid_idx => ['fieldid'], - ], - }, - - email_setting => { - FIELDS => [ - user_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE'}}, - relationship => {TYPE => 'INT1', NOTNULL => 1}, - event => {TYPE => 'INT1', NOTNULL => 1}, - ], - INDEXES => [ - email_setting_user_id_idx => - {FIELDS => [qw(user_id relationship event)], - TYPE => 'UNIQUE'}, - ], - }, - - email_bug_ignore => { - FIELDS => [ - user_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE'}}, - bug_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'bugs', - COLUMN => 'bug_id', - DELETE => 'CASCADE'}}, - ], - INDEXES => [ - email_bug_ignore_user_id_idx => {FIELDS => [qw(user_id bug_id)], - TYPE => 'UNIQUE'}, - ], - }, - - watch => { - FIELDS => [ - watcher => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE'}}, - watched => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE'}}, - ], - INDEXES => [ - watch_watcher_idx => {FIELDS => [qw(watcher watched)], - TYPE => 'UNIQUE'}, - watch_watched_idx => ['watched'], - ], - }, - - namedqueries => { - FIELDS => [ - id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - userid => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE'}}, - name => {TYPE => 'varchar(64)', NOTNULL => 1}, - query => {TYPE => 'LONGTEXT', NOTNULL => 1}, - ], - INDEXES => [ - namedqueries_userid_idx => {FIELDS => [qw(userid name)], - TYPE => 'UNIQUE'}, - ], - }, - - namedqueries_link_in_footer => { - 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 => [ - namedqueries_link_in_footer_id_idx => {FIELDS => [qw(namedquery_id user_id)], - TYPE => 'UNIQUE'}, - namedqueries_link_in_footer_userid_idx => ['user_id'], - ], - }, - - tag => { - FIELDS => [ - id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, - name => {TYPE => 'varchar(64)', NOTNULL => 1}, - user_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE'}}, - ], - INDEXES => [ - tag_user_id_idx => {FIELDS => [qw(user_id name)], TYPE => 'UNIQUE'}, - ], - }, - - bug_tag => { - FIELDS => [ - bug_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'bugs', - COLUMN => 'bug_id', - DELETE => 'CASCADE'}}, - tag_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'tag', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - ], - INDEXES => [ - bug_tag_bug_id_idx => {FIELDS => [qw(bug_id tag_id)], TYPE => 'UNIQUE'}, - ], - }, - - reports => { - FIELDS => [ - id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - user_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE'}}, - name => {TYPE => 'varchar(64)', NOTNULL => 1}, - query => {TYPE => 'LONGTEXT', NOTNULL => 1}, - ], - INDEXES => [ - reports_user_id_idx => {FIELDS => [qw(user_id name)], - TYPE => 'UNIQUE'}, - ], - }, - - component_cc => { - - FIELDS => [ - user_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE'}}, - component_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'components', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - ], - INDEXES => [ - component_cc_user_id_idx => {FIELDS => [qw(component_id user_id)], - TYPE => 'UNIQUE'}, - ], - }, - - # Authentication - # -------------- - - logincookies => { - FIELDS => [ - cookie => {TYPE => 'varchar(16)', NOTNULL => 1, - PRIMARYKEY => 1}, - userid => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE'}}, - ipaddr => {TYPE => 'varchar(40)'}, - lastused => {TYPE => 'DATETIME', NOTNULL => 1}, - ], - INDEXES => [ - logincookies_lastused_idx => ['lastused'], - ], - }, - - login_failure => { - FIELDS => [ - user_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE'}}, - login_time => {TYPE => 'DATETIME', NOTNULL => 1}, - ip_addr => {TYPE => 'varchar(40)', NOTNULL => 1}, - ], - INDEXES => [ - # We do lookups by every item in the table simultaneously, but - # having an index with all three items would be the same size as - # the table. So instead we have an index on just the smallest item, - # to speed lookups. - login_failure_user_id_idx => ['user_id'], - ], - }, - - - # "tokens" stores the tokens users receive when a password or email - # change is requested. Tokens provide an extra measure of security - # for these changes. - tokens => { - FIELDS => [ - userid => {TYPE => 'INT3', REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE'}}, - issuedate => {TYPE => 'DATETIME', NOTNULL => 1} , - token => {TYPE => 'varchar(16)', NOTNULL => 1, - PRIMARYKEY => 1}, - tokentype => {TYPE => 'varchar(16)', NOTNULL => 1} , - eventdata => {TYPE => 'TINYTEXT'}, - ], - INDEXES => [ - tokens_userid_idx => ['userid'], - ], - }, - - # GROUPS - # ------ - - groups => { - FIELDS => [ - id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - name => {TYPE => 'varchar(255)', NOTNULL => 1}, - description => {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, - isbuggroup => {TYPE => 'BOOLEAN', NOTNULL => 1}, - userregexp => {TYPE => 'TINYTEXT', NOTNULL => 1, - DEFAULT => "''"}, - isactive => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'TRUE'}, - icon_url => {TYPE => 'TINYTEXT'}, - ], - INDEXES => [ - groups_name_idx => {FIELDS => ['name'], TYPE => 'UNIQUE'}, - ], - }, - - group_control_map => { - FIELDS => [ - group_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'groups', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - product_id => {TYPE => 'INT2', NOTNULL => 1, - REFERENCES => {TABLE => 'products', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - entry => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - membercontrol => {TYPE => 'INT1', NOTNULL => 1, - DEFAULT => CONTROLMAPNA}, - othercontrol => {TYPE => 'INT1', NOTNULL => 1, - DEFAULT => CONTROLMAPNA}, - canedit => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - editcomponents => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - editbugs => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - canconfirm => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - ], - INDEXES => [ - group_control_map_product_id_idx => - {FIELDS => [qw(product_id group_id)], TYPE => 'UNIQUE'}, - group_control_map_group_id_idx => ['group_id'], - ], - }, - - # "user_group_map" determines the groups that a user belongs to - # directly or due to regexp and which groups can be blessed by a user. - # - # grant_type: - # if GRANT_DIRECT - record was explicitly granted - # if GRANT_DERIVED - record was derived from expanding a group hierarchy - # if GRANT_REGEXP - record was created by evaluating a regexp - user_group_map => { - FIELDS => [ - user_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE'}}, - group_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'groups', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - isbless => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - grant_type => {TYPE => 'INT1', NOTNULL => 1, - DEFAULT => GRANT_DIRECT}, - ], - INDEXES => [ - user_group_map_user_id_idx => - {FIELDS => [qw(user_id group_id grant_type isbless)], - TYPE => 'UNIQUE'}, - ], - }, - - # This table determines which groups are made a member of another - # group, given the ability to bless another group, or given - # visibility to another groups existence and membership - # grant_type: - # if GROUP_MEMBERSHIP - member groups are made members of grantor - # if GROUP_BLESS - member groups may grant membership in grantor - # if GROUP_VISIBLE - member groups may see grantor group - group_group_map => { - FIELDS => [ - member_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'groups', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - grantor_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'groups', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - grant_type => {TYPE => 'INT1', NOTNULL => 1, - DEFAULT => GROUP_MEMBERSHIP}, - ], - INDEXES => [ - group_group_map_member_id_idx => - {FIELDS => [qw(member_id grantor_id grant_type)], - TYPE => 'UNIQUE'}, - ], - }, - - # This table determines which groups a user must be a member of - # in order to see a bug. - bug_group_map => { - FIELDS => [ - bug_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'bugs', - COLUMN => 'bug_id', - DELETE => 'CASCADE'}}, - group_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'groups', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - ], - INDEXES => [ - bug_group_map_bug_id_idx => - {FIELDS => [qw(bug_id group_id)], TYPE => 'UNIQUE'}, - bug_group_map_group_id_idx => ['group_id'], - ], - }, - - # This table determines which groups a user must be a member of - # in order to see a named query somebody else shares. - namedquery_group_map => { - FIELDS => [ - namedquery_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'namedqueries', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - group_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'groups', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - ], - INDEXES => [ - namedquery_group_map_namedquery_id_idx => - {FIELDS => [qw(namedquery_id)], TYPE => 'UNIQUE'}, - namedquery_group_map_group_id_idx => ['group_id'], - ], - }, - - category_group_map => { - FIELDS => [ - category_id => {TYPE => 'INT2', NOTNULL => 1, - REFERENCES => {TABLE => 'series_categories', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - group_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'groups', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - ], - INDEXES => [ - category_group_map_category_id_idx => - {FIELDS => [qw(category_id group_id)], TYPE => 'UNIQUE'}, - ], - }, - - - # PRODUCTS - # -------- - - classifications => { - FIELDS => [ - id => {TYPE => 'SMALLSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - name => {TYPE => 'varchar(64)', NOTNULL => 1}, - description => {TYPE => 'MEDIUMTEXT'}, - sortkey => {TYPE => 'INT2', NOTNULL => 1, DEFAULT => '0'}, - ], - INDEXES => [ - classifications_name_idx => {FIELDS => ['name'], - TYPE => 'UNIQUE'}, - ], - }, - - products => { - FIELDS => [ - id => {TYPE => 'SMALLSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - name => {TYPE => 'varchar(64)', NOTNULL => 1}, - classification_id => {TYPE => 'INT2', NOTNULL => 1, - DEFAULT => '1', - REFERENCES => {TABLE => 'classifications', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - description => {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, - isactive => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 1}, - defaultmilestone => {TYPE => 'varchar(64)', - NOTNULL => 1, DEFAULT => "'---'"}, - allows_unconfirmed => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'TRUE'}, - ], - INDEXES => [ - products_name_idx => {FIELDS => ['name'], - TYPE => 'UNIQUE'}, - ], - }, - - components => { - FIELDS => [ - id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - name => {TYPE => 'varchar(64)', NOTNULL => 1}, - product_id => {TYPE => 'INT2', NOTNULL => 1, - REFERENCES => {TABLE => 'products', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - initialowner => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid'}}, - initialqacontact => {TYPE => 'INT3', - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'SET NULL'}}, - description => {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, - isactive => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'TRUE'}, - ], - INDEXES => [ - components_product_id_idx => {FIELDS => [qw(product_id name)], - TYPE => 'UNIQUE'}, - components_name_idx => ['name'], - ], - }, - - # CHARTS - # ------ - - series => { - FIELDS => [ - series_id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - creator => {TYPE => 'INT3', - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE'}}, - category => {TYPE => 'INT2', NOTNULL => 1, - REFERENCES => {TABLE => 'series_categories', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - subcategory => {TYPE => 'INT2', NOTNULL => 1, - REFERENCES => {TABLE => 'series_categories', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - name => {TYPE => 'varchar(64)', NOTNULL => 1}, - frequency => {TYPE => 'INT2', NOTNULL => 1}, - query => {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, - is_public => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - ], - INDEXES => [ - series_creator_idx => ['creator'], - series_category_idx => {FIELDS => [qw(category subcategory name)], - TYPE => 'UNIQUE'}, - ], - }, - - series_data => { - FIELDS => [ - series_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'series', - COLUMN => 'series_id', - DELETE => 'CASCADE'}}, - series_date => {TYPE => 'DATETIME', NOTNULL => 1}, - series_value => {TYPE => 'INT3', NOTNULL => 1}, - ], - INDEXES => [ - series_data_series_id_idx => - {FIELDS => [qw(series_id series_date)], - TYPE => 'UNIQUE'}, - ], - }, - - series_categories => { - FIELDS => [ - id => {TYPE => 'SMALLSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - name => {TYPE => 'varchar(64)', NOTNULL => 1}, - ], - INDEXES => [ - series_categories_name_idx => {FIELDS => ['name'], - TYPE => 'UNIQUE'}, - ], - }, - - # WHINE SYSTEM - # ------------ - - whine_queries => { - FIELDS => [ - id => {TYPE => 'MEDIUMSERIAL', PRIMARYKEY => 1, - NOTNULL => 1}, - eventid => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'whine_events', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - query_name => {TYPE => 'varchar(64)', NOTNULL => 1, - DEFAULT => "''"}, - sortkey => {TYPE => 'INT2', NOTNULL => 1, - DEFAULT => '0'}, - onemailperbug => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - title => {TYPE => 'varchar(128)', NOTNULL => 1, - DEFAULT => "''"}, - ], - INDEXES => [ - whine_queries_eventid_idx => ['eventid'], - ], - }, - - whine_schedules => { - FIELDS => [ - id => {TYPE => 'MEDIUMSERIAL', PRIMARYKEY => 1, - NOTNULL => 1}, - eventid => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'whine_events', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - run_day => {TYPE => 'varchar(32)'}, - run_time => {TYPE => 'varchar(32)'}, - run_next => {TYPE => 'DATETIME'}, - mailto => {TYPE => 'INT3', NOTNULL => 1}, - mailto_type => {TYPE => 'INT2', NOTNULL => 1, DEFAULT => '0'}, - ], - INDEXES => [ - whine_schedules_run_next_idx => ['run_next'], - whine_schedules_eventid_idx => ['eventid'], - ], - }, - - whine_events => { - FIELDS => [ - id => {TYPE => 'MEDIUMSERIAL', PRIMARYKEY => 1, - NOTNULL => 1}, - owner_userid => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE'}}, - subject => {TYPE => 'varchar(128)'}, - body => {TYPE => 'MEDIUMTEXT'}, - mailifnobugs => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - ], - }, - - # QUIPS - # ----- - - quips => { - FIELDS => [ - quipid => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - userid => {TYPE => 'INT3', - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'SET NULL'}}, - quip => {TYPE => 'varchar(512)', NOTNULL => 1}, - approved => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'TRUE'}, - ], - }, - - # SETTINGS - # -------- - # setting - each global setting will have exactly one entry - # in this table. - # setting_value - stores the list of acceptable values for each - # setting, and a sort index that controls the order - # in which the values are displayed. - # profile_setting - If a user has chosen to use a value other than the - # global default for a given setting, it will be - # stored in this table. Note: even if a setting is - # later changed so is_enabled = false, the stored - # value will remain in case it is ever enabled again. - # - setting => { - FIELDS => [ - name => {TYPE => 'varchar(32)', NOTNULL => 1, - PRIMARYKEY => 1}, - default_value => {TYPE => 'varchar(32)', NOTNULL => 1}, - is_enabled => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'TRUE'}, - subclass => {TYPE => 'varchar(32)'}, - ], - }, - - setting_value => { - FIELDS => [ - name => {TYPE => 'varchar(32)', NOTNULL => 1, - REFERENCES => {TABLE => 'setting', - COLUMN => 'name', - DELETE => 'CASCADE'}}, - value => {TYPE => 'varchar(32)', NOTNULL => 1}, - sortindex => {TYPE => 'INT2', NOTNULL => 1}, - ], - INDEXES => [ - setting_value_nv_unique_idx => {FIELDS => [qw(name value)], - TYPE => 'UNIQUE'}, - setting_value_ns_unique_idx => {FIELDS => [qw(name sortindex)], - TYPE => 'UNIQUE'}, - ], - }, - - profile_setting => { - FIELDS => [ - user_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE'}}, - setting_name => {TYPE => 'varchar(32)', NOTNULL => 1, - REFERENCES => {TABLE => 'setting', - COLUMN => 'name', - DELETE => 'CASCADE'}}, - setting_value => {TYPE => 'varchar(32)', NOTNULL => 1}, - ], - INDEXES => [ - profile_setting_value_unique_idx => {FIELDS => [qw(user_id setting_name)], - TYPE => 'UNIQUE'}, - ], - }, - - # BUGMAIL - # ------- - - mail_staging => { - FIELDS => [ - id => {TYPE => 'INTSERIAL', PRIMARYKEY => 1, NOTNULL => 1}, - message => {TYPE => 'LONGBLOB', NOTNULL => 1}, - ], - }, - - # THESCHWARTZ TABLES - # ------------------ - # Note: In the standard TheSchwartz schema, most integers are unsigned, - # but we didn't implement unsigned ints for Bugzilla schemas, so we - # just create signed ints, which should be fine. - - ts_funcmap => { - FIELDS => [ - funcid => {TYPE => 'INTSERIAL', PRIMARYKEY => 1, NOTNULL => 1}, - funcname => {TYPE => 'varchar(255)', NOTNULL => 1}, - ], - INDEXES => [ - ts_funcmap_funcname_idx => {FIELDS => ['funcname'], - TYPE => 'UNIQUE'}, - ], - }, - - ts_job => { - FIELDS => [ - # In a standard TheSchwartz schema, this is a BIGINT, but we - # don't have those and I didn't want to add them just for this. - jobid => {TYPE => 'INTSERIAL', PRIMARYKEY => 1, - NOTNULL => 1}, - funcid => {TYPE => 'INT4', NOTNULL => 1}, - # In standard TheSchwartz, this is a MEDIUMBLOB. - arg => {TYPE => 'LONGBLOB'}, - uniqkey => {TYPE => 'varchar(255)'}, - insert_time => {TYPE => 'INT4'}, - run_after => {TYPE => 'INT4', NOTNULL => 1}, - grabbed_until => {TYPE => 'INT4', NOTNULL => 1}, - priority => {TYPE => 'INT2'}, - coalesce => {TYPE => 'varchar(255)'}, - ], - INDEXES => [ - ts_job_funcid_idx => {FIELDS => [qw(funcid uniqkey)], - TYPE => 'UNIQUE'}, - # In a standard TheSchewartz schema, these both go in the other - # direction, but there's no reason to have three indexes that - # all start with the same column, and our naming scheme doesn't - # allow it anyhow. - ts_job_run_after_idx => [qw(run_after funcid)], - ts_job_coalesce_idx => [qw(coalesce funcid)], - ], - }, - - ts_note => { - FIELDS => [ - # This is a BIGINT in standard TheSchwartz schemas. - jobid => {TYPE => 'INT4', NOTNULL => 1}, - notekey => {TYPE => 'varchar(255)'}, - value => {TYPE => 'LONGBLOB'}, - ], - INDEXES => [ - ts_note_jobid_idx => {FIELDS => [qw(jobid notekey)], - TYPE => 'UNIQUE'}, - ], - }, - - ts_error => { - FIELDS => [ - error_time => {TYPE => 'INT4', NOTNULL => 1}, - jobid => {TYPE => 'INT4', NOTNULL => 1}, - message => {TYPE => 'varchar(255)', NOTNULL => 1}, - funcid => {TYPE => 'INT4', NOTNULL => 1, DEFAULT => 0}, - ], - INDEXES => [ - ts_error_funcid_idx => [qw(funcid error_time)], - ts_error_error_time_idx => ['error_time'], - ts_error_jobid_idx => ['jobid'], - ], - }, - - ts_exitstatus => { - FIELDS => [ - jobid => {TYPE => 'INTSERIAL', PRIMARYKEY => 1, - NOTNULL => 1}, - funcid => {TYPE => 'INT4', NOTNULL => 1, DEFAULT => 0}, - status => {TYPE => 'INT2'}, - completion_time => {TYPE => 'INT4'}, - delete_after => {TYPE => 'INT4'}, - ], - INDEXES => [ - ts_exitstatus_funcid_idx => ['funcid'], - ts_exitstatus_delete_after_idx => ['delete_after'], - ], - }, - - # SCHEMA STORAGE - # -------------- - - bz_schema => { - FIELDS => [ - schema_data => {TYPE => 'LONGBLOB', NOTNULL => 1}, - version => {TYPE => 'decimal(3,2)', NOTNULL => 1}, - ], - }, - - bug_user_last_visit => { - FIELDS => [ - id => {TYPE => 'INTSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - user_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE'}}, - bug_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'bugs', - COLUMN => 'bug_id', - DELETE => 'CASCADE'}}, - last_visit_ts => {TYPE => 'DATETIME', NOTNULL => 1}, - ], - INDEXES => [ - bug_user_last_visit_idx => {FIELDS => ['user_id', 'bug_id'], - TYPE => 'UNIQUE'}, - bug_user_last_visit_last_visit_ts_idx => ['last_visit_ts'], - ], - }, - - user_api_keys => { - FIELDS => [ - id => {TYPE => 'INTSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - user_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE'}}, - api_key => {TYPE => 'VARCHAR(40)', NOTNULL => 1}, - description => {TYPE => 'VARCHAR(255)'}, - revoked => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'FALSE'}, - last_used => {TYPE => 'DATETIME'}, - ], - INDEXES => [ - user_api_keys_api_key_idx => {FIELDS => ['api_key'], TYPE => 'UNIQUE'}, - user_api_keys_user_id_idx => ['user_id'], - ], - }, -}; + bugs_activity => { + FIELDS => [ + id => {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + bug_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'} + }, + attach_id => { + TYPE => 'INT3', + REFERENCES => + {TABLE => 'attachments', COLUMN => 'attach_id', DELETE => 'CASCADE'} + }, + who => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid'} + }, + bug_when => {TYPE => 'DATETIME', NOTNULL => 1}, + fieldid => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'fielddefs', COLUMN => 'id'} + }, + added => {TYPE => 'varchar(255)'}, + removed => {TYPE => 'varchar(255)'}, + comment_id => { + TYPE => 'INT4', + REFERENCES => + {TABLE => 'longdescs', COLUMN => 'comment_id', DELETE => 'CASCADE'} + }, + ], + INDEXES => [ + bugs_activity_bug_id_idx => ['bug_id'], + bugs_activity_who_idx => ['who'], + bugs_activity_bug_when_idx => ['bug_when'], + bugs_activity_fieldid_idx => ['fieldid'], + bugs_activity_added_idx => ['added'], + bugs_activity_removed_idx => ['removed'], + ], + }, -# Foreign Keys are added in Bugzilla::DB::bz_add_field_tables -use constant MULTI_SELECT_VALUE_TABLE => { + bugs_aliases => { + FIELDS => [ + alias => {TYPE => 'varchar(40)', NOTNULL => 1}, + bug_id => { + TYPE => 'INT3', + REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'} + }, + ], + INDEXES => [ + bugs_aliases_bug_id_idx => ['bug_id'], + bugs_aliases_alias_idx => {FIELDS => ['alias'], TYPE => 'UNIQUE'}, + ], + }, + + cc => { + FIELDS => [ + bug_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'} + }, + who => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'} + }, + ], + INDEXES => [ + cc_bug_id_idx => {FIELDS => [qw(bug_id who)], TYPE => 'UNIQUE'}, + cc_who_idx => ['who'], + ], + }, + + longdescs => { + FIELDS => [ + comment_id => {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + bug_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'} + }, + who => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid'} + }, + bug_when => {TYPE => 'DATETIME', NOTNULL => 1}, + work_time => {TYPE => 'decimal(7,2)', NOTNULL => 1, DEFAULT => '0'}, + thetext => {TYPE => 'LONGTEXT', NOTNULL => 1}, + isprivate => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + already_wrapped => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + type => {TYPE => 'INT2', NOTNULL => 1, DEFAULT => '0'}, + extra_data => {TYPE => 'varchar(255)'} + ], + INDEXES => [ + longdescs_bug_id_idx => [qw(bug_id work_time)], + longdescs_who_idx => [qw(who bug_id)], + longdescs_bug_when_idx => ['bug_when'], + ], + }, + + longdescs_tags => { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + comment_id => { + TYPE => 'INT4', + REFERENCES => + {TABLE => 'longdescs', COLUMN => 'comment_id', DELETE => 'CASCADE'} + }, + tag => {TYPE => 'varchar(24)', NOTNULL => 1}, + ], + INDEXES => + [longdescs_tags_idx => {FIELDS => ['comment_id', 'tag'], TYPE => 'UNIQUE'},], + }, + + longdescs_tags_weights => { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + tag => {TYPE => 'varchar(24)', NOTNULL => 1}, + weight => {TYPE => 'INT3', NOTNULL => 1}, + ], + INDEXES => + [longdescs_tags_weights_tag_idx => {FIELDS => ['tag'], TYPE => 'UNIQUE'},], + }, + + longdescs_tags_activity => { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + bug_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'} + }, + comment_id => { + TYPE => 'INT4', + REFERENCES => + {TABLE => 'longdescs', COLUMN => 'comment_id', DELETE => 'CASCADE'} + }, + who => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid'} + }, + bug_when => {TYPE => 'DATETIME', NOTNULL => 1}, + added => {TYPE => 'varchar(24)'}, + removed => {TYPE => 'varchar(24)'}, + ], + INDEXES => [longdescs_tags_activity_bug_id_idx => ['bug_id'],], + }, + + dependencies => { + FIELDS => [ + blocked => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'} + }, + dependson => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'} + }, + ], + INDEXES => [ + dependencies_blocked_idx => + {FIELDS => [qw(blocked dependson)], TYPE => 'UNIQUE'}, + dependencies_dependson_idx => ['dependson'], + ], + }, + + attachments => { + FIELDS => [ + attach_id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + bug_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'} + }, + creation_ts => {TYPE => 'DATETIME', NOTNULL => 1}, + modification_time => {TYPE => 'DATETIME', NOTNULL => 1}, + description => {TYPE => 'TINYTEXT', NOTNULL => 1}, + mimetype => {TYPE => 'TINYTEXT', NOTNULL => 1}, + ispatch => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + filename => {TYPE => 'varchar(255)', NOTNULL => 1}, + submitter_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid'} + }, + isobsolete => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + isprivate => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + ], + INDEXES => [ + attachments_bug_id_idx => ['bug_id'], + attachments_creation_ts_idx => ['creation_ts'], + attachments_modification_time_idx => ['modification_time'], + attachments_submitter_id_idx => ['submitter_id', 'bug_id'], + ], + }, + attach_data => { + FIELDS => [ + id => { + TYPE => 'INT3', + NOTNULL => 1, + PRIMARYKEY => 1, + REFERENCES => + {TABLE => 'attachments', COLUMN => 'attach_id', DELETE => 'CASCADE'} + }, + thedata => {TYPE => 'LONGBLOB', NOTNULL => 1}, + ], + }, + + duplicates => { + FIELDS => [ + dupe_of => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'} + }, + dupe => { + TYPE => 'INT3', + NOTNULL => 1, + PRIMARYKEY => 1, + REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'} + }, + ], + }, + + bug_see_also => { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + bug_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'} + }, + value => {TYPE => 'varchar(255)', NOTNULL => 1}, + class => {TYPE => 'varchar(255)', NOTNULL => 1, DEFAULT => "''"}, + ], + INDEXES => [ + bug_see_also_bug_id_idx => {FIELDS => [qw(bug_id value)], TYPE => 'UNIQUE'}, + ], + }, + + # Auditing + # -------- + + audit_log => { + FIELDS => [ + user_id => { + TYPE => 'INT3', + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'SET NULL'} + }, + class => {TYPE => 'varchar(255)', NOTNULL => 1}, + object_id => {TYPE => 'INT4', NOTNULL => 1}, + field => {TYPE => 'varchar(64)', NOTNULL => 1}, + removed => {TYPE => 'MEDIUMTEXT'}, + added => {TYPE => 'MEDIUMTEXT'}, + at_time => {TYPE => 'DATETIME', NOTNULL => 1}, + ], + INDEXES => [audit_log_class_idx => ['class', 'at_time'],], + }, + + # Keywords + # -------- + + keyworddefs => { + FIELDS => [ + id => {TYPE => 'SMALLSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + name => {TYPE => 'varchar(64)', NOTNULL => 1}, + description => {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, + ], + INDEXES => [keyworddefs_name_idx => {FIELDS => ['name'], TYPE => 'UNIQUE'},], + }, + + keywords => { + FIELDS => [ + bug_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'} + }, + keywordid => { + TYPE => 'INT2', + NOTNULL => 1, + REFERENCES => {TABLE => 'keyworddefs', COLUMN => 'id', DELETE => 'CASCADE'} + }, + + ], + INDEXES => [ + keywords_bug_id_idx => {FIELDS => [qw(bug_id keywordid)], TYPE => 'UNIQUE'}, + keywords_keywordid_idx => ['keywordid'], + ], + }, + + # Flags + # ----- + + # "flags" stores one record for each flag on each bug/attachment. + flags => { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + type_id => { + TYPE => 'INT2', + NOTNULL => 1, + REFERENCES => {TABLE => 'flagtypes', COLUMN => 'id', DELETE => 'CASCADE'} + }, + status => {TYPE => 'char(1)', NOTNULL => 1}, + bug_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'} + }, + attach_id => { + TYPE => 'INT3', + REFERENCES => + {TABLE => 'attachments', COLUMN => 'attach_id', DELETE => 'CASCADE'} + }, + creation_date => {TYPE => 'DATETIME', NOTNULL => 1}, + modification_date => {TYPE => 'DATETIME'}, + setter_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid'} + }, + requestee_id => + {TYPE => 'INT3', REFERENCES => {TABLE => 'profiles', COLUMN => 'userid'}}, + ], + INDEXES => [ + flags_bug_id_idx => [qw(bug_id attach_id)], + flags_setter_id_idx => ['setter_id'], + flags_requestee_id_idx => ['requestee_id'], + flags_type_id_idx => ['type_id'], + ], + }, + + # "flagtypes" defines the types of flags that can be set. + flagtypes => { + FIELDS => [ + id => {TYPE => 'SMALLSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + name => {TYPE => 'varchar(50)', NOTNULL => 1}, + description => {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, + cc_list => {TYPE => 'varchar(200)'}, + target_type => {TYPE => 'char(1)', NOTNULL => 1, DEFAULT => "'b'"}, + is_active => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}, + is_requestable => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + is_requesteeble => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + is_multiplicable => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + sortkey => {TYPE => 'INT2', NOTNULL => 1, DEFAULT => '0'}, + grant_group_id => { + TYPE => 'INT3', + REFERENCES => {TABLE => 'groups', COLUMN => 'id', DELETE => 'SET NULL'} + }, + request_group_id => { + TYPE => 'INT3', + REFERENCES => {TABLE => 'groups', COLUMN => 'id', DELETE => 'SET NULL'} + }, + ], + }, + + # "flaginclusions" and "flagexclusions" specify the products/components + # a bug/attachment must belong to in order for flags of a given type + # to be set for them. + flaginclusions => { + FIELDS => [ + type_id => { + TYPE => 'INT2', + NOTNULL => 1, + REFERENCES => {TABLE => 'flagtypes', COLUMN => 'id', DELETE => 'CASCADE'} + }, + product_id => { + TYPE => 'INT2', + REFERENCES => {TABLE => 'products', COLUMN => 'id', DELETE => 'CASCADE'} + }, + component_id => { + TYPE => 'INT3', + REFERENCES => {TABLE => 'components', COLUMN => 'id', DELETE => 'CASCADE'} + }, + ], + INDEXES => [ + flaginclusions_type_id_idx => + {FIELDS => [qw(type_id product_id component_id)], TYPE => 'UNIQUE'}, + ], + }, + + flagexclusions => { + FIELDS => [ + type_id => { + TYPE => 'INT2', + NOTNULL => 1, + REFERENCES => {TABLE => 'flagtypes', COLUMN => 'id', DELETE => 'CASCADE'} + }, + product_id => { + TYPE => 'INT2', + REFERENCES => {TABLE => 'products', COLUMN => 'id', DELETE => 'CASCADE'} + }, + component_id => { + TYPE => 'INT3', + REFERENCES => {TABLE => 'components', COLUMN => 'id', DELETE => 'CASCADE'} + }, + ], + INDEXES => [ + flagexclusions_type_id_idx => + {FIELDS => [qw(type_id product_id component_id)], TYPE => 'UNIQUE'}, + ], + }, + + # General Field Information + # ------------------------- + + fielddefs => { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + name => {TYPE => 'varchar(64)', NOTNULL => 1}, + type => {TYPE => 'INT2', NOTNULL => 1, DEFAULT => FIELD_TYPE_UNKNOWN}, + custom => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + description => {TYPE => 'TINYTEXT', NOTNULL => 1}, + long_desc => {TYPE => 'varchar(255)', NOTNULL => 1, DEFAULT => "''"}, + mailhead => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + sortkey => {TYPE => 'INT2', NOTNULL => 1}, + obsolete => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + enter_bug => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + buglist => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + visibility_field_id => + {TYPE => 'INT3', REFERENCES => {TABLE => 'fielddefs', COLUMN => 'id'}}, + value_field_id => + {TYPE => 'INT3', REFERENCES => {TABLE => 'fielddefs', COLUMN => 'id'}}, + reverse_desc => {TYPE => 'TINYTEXT'}, + is_mandatory => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + is_numeric => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + ], + INDEXES => [ + fielddefs_name_idx => {FIELDS => ['name'], TYPE => 'UNIQUE'}, + fielddefs_sortkey_idx => ['sortkey'], + fielddefs_value_field_id_idx => ['value_field_id'], + fielddefs_is_mandatory_idx => ['is_mandatory'], + ], + }, + + # Field Access Information + fielddefs_access_groups => { + FIELDS => [ + field_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'fielddefs', COLUMN => 'id', DELETE => 'CASCADE'} + }, + group_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'groups', COLUMN => 'id', DELETE => 'CASCADE'} + }, + access => {TYPE => 'INT2', NOTNULL => 1, DEFAULT => ACCESS_TYPE_VIEW}, + ], + INDEXES => + [fielddefs_accessability_groups_idx => ['field_id', 'group_id', 'access'],], + }, + + # Field Visibility Information + # ------------------------- + + field_visibility => { + FIELDS => [ + field_id => { + TYPE => 'INT3', + REFERENCES => {TABLE => 'fielddefs', COLUMN => 'id', DELETE => 'CASCADE'} + }, + value_id => {TYPE => 'INT2', NOTNULL => 1} + ], + INDEXES => [ + field_visibility_field_id_idx => + {FIELDS => [qw(field_id value_id)], TYPE => 'UNIQUE'}, + ], + }, + + # Per-product Field Values + # ------------------------ + + versions => { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + value => {TYPE => 'varchar(64)', NOTNULL => 1}, + product_id => { + TYPE => 'INT2', + NOTNULL => 1, + REFERENCES => {TABLE => 'products', COLUMN => 'id', DELETE => 'CASCADE'} + }, + isactive => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}, + ], + INDEXES => [ + versions_product_id_idx => {FIELDS => [qw(product_id value)], TYPE => 'UNIQUE'}, + ], + }, + + milestones => { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + product_id => { + TYPE => 'INT2', + NOTNULL => 1, + REFERENCES => {TABLE => 'products', COLUMN => 'id', DELETE => 'CASCADE'} + }, + value => {TYPE => 'varchar(64)', NOTNULL => 1}, + sortkey => {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0}, + isactive => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}, + ], + INDEXES => [ + milestones_product_id_idx => + {FIELDS => [qw(product_id value)], TYPE => 'UNIQUE'}, + ], + }, + + # Global Field Values + # ------------------- + + bug_status => { + FIELDS => [ + @{dclone(FIELD_TABLE_SCHEMA->{FIELDS})}, + is_open => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}, + + ], + INDEXES => [ + bug_status_value_idx => {FIELDS => ['value'], TYPE => 'UNIQUE'}, + bug_status_sortkey_idx => ['sortkey', 'value'], + bug_status_visibility_value_id_idx => ['visibility_value_id'], + ], + }, + + resolution => { + FIELDS => dclone(FIELD_TABLE_SCHEMA->{FIELDS}), + INDEXES => [ + resolution_value_idx => {FIELDS => ['value'], TYPE => 'UNIQUE'}, + resolution_sortkey_idx => ['sortkey', 'value'], + resolution_visibility_value_id_idx => ['visibility_value_id'], + ], + }, + + bug_severity => { + FIELDS => dclone(FIELD_TABLE_SCHEMA->{FIELDS}), + INDEXES => [ + bug_severity_value_idx => {FIELDS => ['value'], TYPE => 'UNIQUE'}, + bug_severity_sortkey_idx => ['sortkey', 'value'], + bug_severity_visibility_value_id_idx => ['visibility_value_id'], + ], + }, + + priority => { + FIELDS => dclone(FIELD_TABLE_SCHEMA->{FIELDS}), + INDEXES => [ + priority_value_idx => {FIELDS => ['value'], TYPE => 'UNIQUE'}, + priority_sortkey_idx => ['sortkey', 'value'], + priority_visibility_value_id_idx => ['visibility_value_id'], + ], + }, + + rep_platform => { + FIELDS => dclone(FIELD_TABLE_SCHEMA->{FIELDS}), + INDEXES => [ + rep_platform_value_idx => {FIELDS => ['value'], TYPE => 'UNIQUE'}, + rep_platform_sortkey_idx => ['sortkey', 'value'], + rep_platform_visibility_value_id_idx => ['visibility_value_id'], + ], + }, + + op_sys => { + FIELDS => dclone(FIELD_TABLE_SCHEMA->{FIELDS}), + INDEXES => [ + op_sys_value_idx => {FIELDS => ['value'], TYPE => 'UNIQUE'}, + op_sys_sortkey_idx => ['sortkey', 'value'], + op_sys_visibility_value_id_idx => ['visibility_value_id'], + ], + }, + + status_workflow => { + FIELDS => [ + + # On bug creation, there is no old value. + old_status => { + TYPE => 'INT2', + REFERENCES => {TABLE => 'bug_status', COLUMN => 'id', DELETE => 'CASCADE'} + }, + new_status => { + TYPE => 'INT2', + NOTNULL => 1, + REFERENCES => {TABLE => 'bug_status', COLUMN => 'id', DELETE => 'CASCADE'} + }, + require_comment => {TYPE => 'INT1', NOTNULL => 1, DEFAULT => 0}, + ], + INDEXES => [ + status_workflow_idx => + {FIELDS => ['old_status', 'new_status'], TYPE => 'UNIQUE'}, + ], + }, + + # USER INFO + # --------- + + # General User Information + # ------------------------ + + profiles => { + FIELDS => [ + userid => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + login_name => {TYPE => 'varchar(255)', NOTNULL => 1}, + cryptpassword => {TYPE => 'varchar(128)'}, + realname => {TYPE => 'varchar(255)', NOTNULL => 1, DEFAULT => "''"}, + disabledtext => {TYPE => 'MEDIUMTEXT', NOTNULL => 1, DEFAULT => "''"}, + disable_mail => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + mybugslink => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}, + extern_id => {TYPE => 'varchar(64)'}, + is_enabled => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}, + last_seen_date => {TYPE => 'DATETIME'}, + ], + INDEXES => [ + profiles_login_name_idx => {FIELDS => ['login_name'], TYPE => 'UNIQUE'}, + profiles_extern_id_idx => {FIELDS => ['extern_id'], TYPE => 'UNIQUE'} + ], + }, + + profile_search => { + FIELDS => [ + id => {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + user_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'} + }, + bug_list => {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, + list_order => {TYPE => 'MEDIUMTEXT'}, + ], + INDEXES => [profile_search_user_id_idx => [qw(user_id)],], + }, + + profiles_activity => { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + userid => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'} + }, + who => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid'} + }, + profiles_when => {TYPE => 'DATETIME', NOTNULL => 1}, + fieldid => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'fielddefs', COLUMN => 'id'} + }, + oldvalue => {TYPE => 'TINYTEXT'}, + newvalue => {TYPE => 'TINYTEXT'}, + ], + INDEXES => [ + profiles_activity_userid_idx => ['userid'], + profiles_activity_profiles_when_idx => ['profiles_when'], + profiles_activity_fieldid_idx => ['fieldid'], + ], + }, + + email_setting => { + FIELDS => [ + user_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'} + }, + relationship => {TYPE => 'INT1', NOTNULL => 1}, + event => {TYPE => 'INT1', NOTNULL => 1}, + ], + INDEXES => [ + email_setting_user_id_idx => + {FIELDS => [qw(user_id relationship event)], TYPE => 'UNIQUE'}, + ], + }, + + email_bug_ignore => { + FIELDS => [ + user_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'} + }, + bug_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'} + }, + ], + INDEXES => [ + email_bug_ignore_user_id_idx => + {FIELDS => [qw(user_id bug_id)], TYPE => 'UNIQUE'}, + ], + }, + + watch => { + FIELDS => [ + watcher => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'} + }, + watched => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'} + }, + ], + INDEXES => [ + watch_watcher_idx => {FIELDS => [qw(watcher watched)], TYPE => 'UNIQUE'}, + watch_watched_idx => ['watched'], + ], + }, + + namedqueries => { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + userid => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'} + }, + name => {TYPE => 'varchar(64)', NOTNULL => 1}, + query => {TYPE => 'LONGTEXT', NOTNULL => 1}, + ], + INDEXES => + [namedqueries_userid_idx => {FIELDS => [qw(userid name)], TYPE => 'UNIQUE'},], + }, + + namedqueries_link_in_footer => { + 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 => [ + namedqueries_link_in_footer_id_idx => + {FIELDS => [qw(namedquery_id user_id)], TYPE => 'UNIQUE'}, + namedqueries_link_in_footer_userid_idx => ['user_id'], + ], + }, + + tag => { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + name => {TYPE => 'varchar(64)', NOTNULL => 1}, + user_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'} + }, + ], + INDEXES => + [tag_user_id_idx => {FIELDS => [qw(user_id name)], TYPE => 'UNIQUE'},], + }, + + bug_tag => { + FIELDS => [ + bug_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'} + }, + tag_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'tag', COLUMN => 'id', DELETE => 'CASCADE'} + }, + ], + INDEXES => + [bug_tag_bug_id_idx => {FIELDS => [qw(bug_id tag_id)], TYPE => 'UNIQUE'},], + }, + + reports => { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + user_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'} + }, + name => {TYPE => 'varchar(64)', NOTNULL => 1}, + query => {TYPE => 'LONGTEXT', NOTNULL => 1}, + ], + INDEXES => + [reports_user_id_idx => {FIELDS => [qw(user_id name)], TYPE => 'UNIQUE'},], + }, + + component_cc => { + + FIELDS => [ + user_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'} + }, + component_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'components', COLUMN => 'id', DELETE => 'CASCADE'} + }, + ], + INDEXES => [ + component_cc_user_id_idx => + {FIELDS => [qw(component_id user_id)], TYPE => 'UNIQUE'}, + ], + }, + + # Authentication + # -------------- + + logincookies => { + FIELDS => [ + cookie => {TYPE => 'varchar(22)', NOTNULL => 1}, + userid => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'} + }, + ipaddr => {TYPE => 'varchar(40)'}, + lastused => {TYPE => 'DATETIME', NOTNULL => 1}, + id => {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + ], + INDEXES => [ + logincookies_lastused_idx => ['lastused'], + logincookies_cookie_idx => {FIELDS => ['cookie'], TYPE => 'UNIQUE'}, + ], + }, + + login_failure => { + FIELDS => [ + user_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'} + }, + login_time => {TYPE => 'DATETIME', NOTNULL => 1}, + ip_addr => {TYPE => 'varchar(40)', NOTNULL => 1}, + ], + INDEXES => [ + + # We do lookups by every item in the table simultaneously, but + # having an index with all three items would be the same size as + # the table. So instead we have an index on just the smallest item, + # to speed lookups. + login_failure_user_id_idx => ['user_id'], + ], + }, + + + # "tokens" stores the tokens users receive when a password or email + # change is requested. Tokens provide an extra measure of security + # for these changes. + tokens => { FIELDS => [ - bug_id => {TYPE => 'INT3', NOTNULL => 1}, - value => {TYPE => 'varchar(64)', NOTNULL => 1}, + userid => { + TYPE => 'INT3', + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'} + }, + issuedate => {TYPE => 'DATETIME', NOTNULL => 1}, + token => {TYPE => 'varchar(16)', NOTNULL => 1, PRIMARYKEY => 1}, + tokentype => {TYPE => 'varchar(16)', NOTNULL => 1}, + eventdata => {TYPE => 'TINYTEXT'}, + ], + INDEXES => [tokens_userid_idx => ['userid'],], + }, + + # GROUPS + # ------ + + groups => { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + name => {TYPE => 'varchar(255)', NOTNULL => 1}, + description => {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, + isbuggroup => {TYPE => 'BOOLEAN', NOTNULL => 1}, + userregexp => {TYPE => 'TINYTEXT', NOTNULL => 1, DEFAULT => "''"}, + isactive => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}, + icon_url => {TYPE => 'TINYTEXT'}, + ], + INDEXES => [groups_name_idx => {FIELDS => ['name'], TYPE => 'UNIQUE'},], + }, + + group_control_map => { + FIELDS => [ + group_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'groups', COLUMN => 'id', DELETE => 'CASCADE'} + }, + product_id => { + TYPE => 'INT2', + NOTNULL => 1, + REFERENCES => {TABLE => 'products', COLUMN => 'id', DELETE => 'CASCADE'} + }, + entry => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + membercontrol => {TYPE => 'INT1', NOTNULL => 1, DEFAULT => CONTROLMAPNA}, + othercontrol => {TYPE => 'INT1', NOTNULL => 1, DEFAULT => CONTROLMAPNA}, + canedit => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + editcomponents => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + editbugs => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + canconfirm => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + ], + INDEXES => [ + group_control_map_product_id_idx => + {FIELDS => [qw(product_id group_id)], TYPE => 'UNIQUE'}, + group_control_map_group_id_idx => ['group_id'], + ], + }, + + # "user_group_map" determines the groups that a user belongs to + # directly or due to regexp and which groups can be blessed by a user. + # + # grant_type: + # if GRANT_DIRECT - record was explicitly granted + # if GRANT_DERIVED - record was derived from expanding a group hierarchy + # if GRANT_REGEXP - record was created by evaluating a regexp + user_group_map => { + FIELDS => [ + user_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'} + }, + group_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'groups', COLUMN => 'id', DELETE => 'CASCADE'} + }, + isbless => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + grant_type => {TYPE => 'INT1', NOTNULL => 1, DEFAULT => GRANT_DIRECT}, + ], + INDEXES => [ + user_group_map_user_id_idx => + {FIELDS => [qw(user_id group_id grant_type isbless)], TYPE => 'UNIQUE'}, + ], + }, + + # This table determines which groups are made a member of another + # group, given the ability to bless another group, or given + # visibility to another groups existence and membership + # grant_type: + # if GROUP_MEMBERSHIP - member groups are made members of grantor + # if GROUP_BLESS - member groups may grant membership in grantor + # if GROUP_VISIBLE - member groups may see grantor group + group_group_map => { + FIELDS => [ + member_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'groups', COLUMN => 'id', DELETE => 'CASCADE'} + }, + grantor_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'groups', COLUMN => 'id', DELETE => 'CASCADE'} + }, + grant_type => {TYPE => 'INT1', NOTNULL => 1, DEFAULT => GROUP_MEMBERSHIP}, + ], + INDEXES => [ + group_group_map_member_id_idx => + {FIELDS => [qw(member_id grantor_id grant_type)], TYPE => 'UNIQUE'}, + ], + }, + + # This table determines which groups a user must be a member of + # in order to see a bug. + bug_group_map => { + FIELDS => [ + bug_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'} + }, + group_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'groups', COLUMN => 'id', DELETE => 'CASCADE'} + }, + ], + INDEXES => [ + bug_group_map_bug_id_idx => {FIELDS => [qw(bug_id group_id)], TYPE => 'UNIQUE'}, + bug_group_map_group_id_idx => ['group_id'], + ], + }, + + # This table determines which groups a user must be a member of + # in order to see a named query somebody else shares. + namedquery_group_map => { + FIELDS => [ + namedquery_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'namedqueries', COLUMN => 'id', DELETE => 'CASCADE'} + }, + group_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'groups', COLUMN => 'id', DELETE => 'CASCADE'} + }, + ], + INDEXES => [ + namedquery_group_map_namedquery_id_idx => + {FIELDS => [qw(namedquery_id)], TYPE => 'UNIQUE'}, + namedquery_group_map_group_id_idx => ['group_id'], + ], + }, + + category_group_map => { + FIELDS => [ + category_id => { + TYPE => 'INT2', + NOTNULL => 1, + REFERENCES => + {TABLE => 'series_categories', COLUMN => 'id', DELETE => 'CASCADE'} + }, + group_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'groups', COLUMN => 'id', DELETE => 'CASCADE'} + }, + ], + INDEXES => [ + category_group_map_category_id_idx => + {FIELDS => [qw(category_id group_id)], TYPE => 'UNIQUE'}, + ], + }, + + + # PRODUCTS + # -------- + + classifications => { + FIELDS => [ + id => {TYPE => 'SMALLSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + name => {TYPE => 'varchar(64)', NOTNULL => 1}, + description => {TYPE => 'MEDIUMTEXT'}, + sortkey => {TYPE => 'INT2', NOTNULL => 1, DEFAULT => '0'}, + ], + INDEXES => + [classifications_name_idx => {FIELDS => ['name'], TYPE => 'UNIQUE'},], + }, + + products => { + FIELDS => [ + id => {TYPE => 'SMALLSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + name => {TYPE => 'varchar(64)', NOTNULL => 1}, + classification_id => { + TYPE => 'INT2', + NOTNULL => 1, + DEFAULT => '1', + REFERENCES => {TABLE => 'classifications', COLUMN => 'id', DELETE => 'CASCADE'} + }, + description => {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, + isactive => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 1}, + defaultmilestone => {TYPE => 'varchar(64)', NOTNULL => 1, DEFAULT => "'---'"}, + allows_unconfirmed => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}, + ], + INDEXES => [products_name_idx => {FIELDS => ['name'], TYPE => 'UNIQUE'},], + }, + + components => { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + name => {TYPE => 'varchar(64)', NOTNULL => 1}, + product_id => { + TYPE => 'INT2', + NOTNULL => 1, + REFERENCES => {TABLE => 'products', COLUMN => 'id', DELETE => 'CASCADE'} + }, + initialowner => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid'} + }, + initialqacontact => { + TYPE => 'INT3', + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'SET NULL'} + }, + description => {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, + isactive => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}, + ], + INDEXES => [ + components_product_id_idx => + {FIELDS => [qw(product_id name)], TYPE => 'UNIQUE'}, + components_name_idx => ['name'], + ], + }, + + # CHARTS + # ------ + + series => { + FIELDS => [ + series_id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + creator => { + TYPE => 'INT3', + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'} + }, + category => { + TYPE => 'INT2', + NOTNULL => 1, + REFERENCES => + {TABLE => 'series_categories', COLUMN => 'id', DELETE => 'CASCADE'} + }, + subcategory => { + TYPE => 'INT2', + NOTNULL => 1, + REFERENCES => + {TABLE => 'series_categories', COLUMN => 'id', DELETE => 'CASCADE'} + }, + name => {TYPE => 'varchar(64)', NOTNULL => 1}, + frequency => {TYPE => 'INT2', NOTNULL => 1}, + query => {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, + is_public => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + ], + INDEXES => [ + series_creator_idx => ['creator'], + series_category_idx => + {FIELDS => [qw(category subcategory name)], TYPE => 'UNIQUE'}, + ], + }, + + series_data => { + FIELDS => [ + series_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'series', COLUMN => 'series_id', DELETE => 'CASCADE'} + }, + series_date => {TYPE => 'DATETIME', NOTNULL => 1}, + series_value => {TYPE => 'INT3', NOTNULL => 1}, + ], + INDEXES => [ + series_data_series_id_idx => + {FIELDS => [qw(series_id series_date)], TYPE => 'UNIQUE'}, + ], + }, + + series_categories => { + FIELDS => [ + id => {TYPE => 'SMALLSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + name => {TYPE => 'varchar(64)', NOTNULL => 1}, + ], + INDEXES => + [series_categories_name_idx => {FIELDS => ['name'], TYPE => 'UNIQUE'},], + }, + + # WHINE SYSTEM + # ------------ + + whine_queries => { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', PRIMARYKEY => 1, NOTNULL => 1}, + eventid => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'whine_events', COLUMN => 'id', DELETE => 'CASCADE'} + }, + query_name => {TYPE => 'varchar(64)', NOTNULL => 1, DEFAULT => "''"}, + sortkey => {TYPE => 'INT2', NOTNULL => 1, DEFAULT => '0'}, + onemailperbug => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + title => {TYPE => 'varchar(128)', NOTNULL => 1, DEFAULT => "''"}, + ], + INDEXES => [whine_queries_eventid_idx => ['eventid'],], + }, + + whine_schedules => { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', PRIMARYKEY => 1, NOTNULL => 1}, + eventid => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'whine_events', COLUMN => 'id', DELETE => 'CASCADE'} + }, + run_day => {TYPE => 'varchar(32)'}, + run_time => {TYPE => 'varchar(32)'}, + run_next => {TYPE => 'DATETIME'}, + mailto => {TYPE => 'INT3', NOTNULL => 1}, + mailto_type => {TYPE => 'INT2', NOTNULL => 1, DEFAULT => '0'}, + ], + INDEXES => [ + whine_schedules_run_next_idx => ['run_next'], + whine_schedules_eventid_idx => ['eventid'], + ], + }, + + whine_events => { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', PRIMARYKEY => 1, NOTNULL => 1}, + owner_userid => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'} + }, + subject => {TYPE => 'varchar(128)'}, + body => {TYPE => 'MEDIUMTEXT'}, + mailifnobugs => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + ], + }, + + # QUIPS + # ----- + + quips => { + FIELDS => [ + quipid => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + userid => { + TYPE => 'INT3', + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'SET NULL'} + }, + quip => {TYPE => 'varchar(512)', NOTNULL => 1}, + approved => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}, + ], + }, + + # SETTINGS + # -------- + # setting - each global setting will have exactly one entry + # in this table. + # setting_value - stores the list of acceptable values for each + # setting, and a sort index that controls the order + # in which the values are displayed. + # profile_setting - If a user has chosen to use a value other than the + # global default for a given setting, it will be + # stored in this table. Note: even if a setting is + # later changed so is_enabled = false, the stored + # value will remain in case it is ever enabled again. + # + setting => { + FIELDS => [ + name => {TYPE => 'varchar(32)', NOTNULL => 1, PRIMARYKEY => 1}, + default_value => {TYPE => 'varchar(32)', NOTNULL => 1}, + is_enabled => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}, + subclass => {TYPE => 'varchar(32)'}, + ], + }, + + setting_value => { + FIELDS => [ + name => { + TYPE => 'varchar(32)', + NOTNULL => 1, + REFERENCES => {TABLE => 'setting', COLUMN => 'name', DELETE => 'CASCADE'} + }, + value => {TYPE => 'varchar(32)', NOTNULL => 1}, + sortindex => {TYPE => 'INT2', NOTNULL => 1}, + ], + INDEXES => [ + setting_value_nv_unique_idx => {FIELDS => [qw(name value)], TYPE => 'UNIQUE'}, + setting_value_ns_unique_idx => + {FIELDS => [qw(name sortindex)], TYPE => 'UNIQUE'}, + ], + }, + + profile_setting => { + FIELDS => [ + user_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'} + }, + setting_name => { + TYPE => 'varchar(32)', + NOTNULL => 1, + REFERENCES => {TABLE => 'setting', COLUMN => 'name', DELETE => 'CASCADE'} + }, + setting_value => {TYPE => 'varchar(32)', NOTNULL => 1}, + ], + INDEXES => [ + profile_setting_value_unique_idx => + {FIELDS => [qw(user_id setting_name)], TYPE => 'UNIQUE'}, + ], + }, + + # BUGMAIL + # ------- + + mail_staging => { + FIELDS => [ + id => {TYPE => 'INTSERIAL', PRIMARYKEY => 1, NOTNULL => 1}, + message => {TYPE => 'LONGBLOB', NOTNULL => 1}, + ], + }, + + # THESCHWARTZ TABLES + # ------------------ + # Note: In the standard TheSchwartz schema, most integers are unsigned, + # but we didn't implement unsigned ints for Bugzilla schemas, so we + # just create signed ints, which should be fine. + + ts_funcmap => { + FIELDS => [ + funcid => {TYPE => 'INTSERIAL', PRIMARYKEY => 1, NOTNULL => 1}, + funcname => {TYPE => 'varchar(255)', NOTNULL => 1}, + ], + INDEXES => + [ts_funcmap_funcname_idx => {FIELDS => ['funcname'], TYPE => 'UNIQUE'},], + }, + + ts_job => { + FIELDS => [ + + # In a standard TheSchwartz schema, this is a BIGINT, but we + # don't have those and I didn't want to add them just for this. + jobid => {TYPE => 'INTSERIAL', PRIMARYKEY => 1, NOTNULL => 1}, + funcid => {TYPE => 'INT4', NOTNULL => 1}, + + # In standard TheSchwartz, this is a MEDIUMBLOB. + arg => {TYPE => 'LONGBLOB'}, + uniqkey => {TYPE => 'varchar(255)'}, + insert_time => {TYPE => 'INT4'}, + run_after => {TYPE => 'INT4', NOTNULL => 1}, + grabbed_until => {TYPE => 'INT4', NOTNULL => 1}, + priority => {TYPE => 'INT2'}, + coalesce => {TYPE => 'varchar(255)'}, + ], + INDEXES => [ + ts_job_funcid_idx => {FIELDS => [qw(funcid uniqkey)], TYPE => 'UNIQUE'}, + + # In a standard TheSchewartz schema, these both go in the other + # direction, but there's no reason to have three indexes that + # all start with the same column, and our naming scheme doesn't + # allow it anyhow. + ts_job_run_after_idx => [qw(run_after funcid)], + ts_job_coalesce_idx => [qw(coalesce funcid)], + ], + }, + + ts_note => { + FIELDS => [ + + # This is a BIGINT in standard TheSchwartz schemas. + jobid => {TYPE => 'INT4', NOTNULL => 1}, + notekey => {TYPE => 'varchar(255)'}, + value => {TYPE => 'LONGBLOB'}, + ], + INDEXES => + [ts_note_jobid_idx => {FIELDS => [qw(jobid notekey)], TYPE => 'UNIQUE'},], + }, + + ts_error => { + FIELDS => [ + error_time => {TYPE => 'INT4', NOTNULL => 1}, + jobid => {TYPE => 'INT4', NOTNULL => 1}, + message => {TYPE => 'varchar(255)', NOTNULL => 1}, + funcid => {TYPE => 'INT4', NOTNULL => 1, DEFAULT => 0}, + ], + INDEXES => [ + ts_error_funcid_idx => [qw(funcid error_time)], + ts_error_error_time_idx => ['error_time'], + ts_error_jobid_idx => ['jobid'], + ], + }, + + ts_exitstatus => { + FIELDS => [ + jobid => {TYPE => 'INTSERIAL', PRIMARYKEY => 1, NOTNULL => 1}, + funcid => {TYPE => 'INT4', NOTNULL => 1, DEFAULT => 0}, + status => {TYPE => 'INT2'}, + completion_time => {TYPE => 'INT4'}, + delete_after => {TYPE => 'INT4'}, + ], + INDEXES => [ + ts_exitstatus_funcid_idx => ['funcid'], + ts_exitstatus_delete_after_idx => ['delete_after'], + ], + }, + + # SCHEMA STORAGE + # -------------- + + bz_schema => { + FIELDS => [ + schema_data => {TYPE => 'LONGBLOB', NOTNULL => 1}, + version => {TYPE => 'decimal(3,2)', NOTNULL => 1}, + ], + }, + + bug_user_last_visit => { + FIELDS => [ + id => {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + user_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'} + }, + bug_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'} + }, + last_visit_ts => {TYPE => 'DATETIME', NOTNULL => 1}, + ], + INDEXES => [ + bug_user_last_visit_idx => {FIELDS => ['user_id', 'bug_id'], TYPE => 'UNIQUE'}, + bug_user_last_visit_last_visit_ts_idx => ['last_visit_ts'], + ], + }, + + user_api_keys => { + FIELDS => [ + id => {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + user_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'} + }, + api_key => {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, + description => {TYPE => 'VARCHAR(255)', NOTNULL => 1}, + revoked => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + banned => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + last_used => {TYPE => 'DATETIME'}, ], INDEXES => [ - bug_id_idx => {FIELDS => [qw( bug_id value)], TYPE => 'UNIQUE'}, + user_api_keys_api_key_idx => {FIELDS => ['api_key'], TYPE => 'UNIQUE'}, + user_api_keys_user_id_idx => ['user_id'], ], + }, +}; + +# Foreign Keys are added in Bugzilla::DB::bz_add_field_tables +use constant MULTI_SELECT_VALUE_TABLE => { + FIELDS => [ + bug_id => {TYPE => 'INT3', NOTNULL => 1}, + value => {TYPE => 'varchar(64)', NOTNULL => 1}, + ], + INDEXES => [bug_id_idx => {FIELDS => [qw( bug_id value)], TYPE => 'UNIQUE'},], +}; + +use constant SELECT_ONE_VALUE_TABLE => { + FIELDS => [ + bug_id => {TYPE => 'INT3', NOTNULL => 1}, + value => {TYPE => 'varchar(64)', NOTNULL => 1}, + ], + INDEXES => [bug_id_idx => {FIELDS => [qw( bug_id )], TYPE => 'UNIQUE'},], }; #-------------------------------------------------------------------------- @@ -1821,27 +1802,28 @@ sub new { =cut - my $this = shift; - my $class = ref($this) || $this; - my $driver = shift; + my $this = shift; + my $class = ref($this) || $this; + my $driver = shift; - if ($driver) { - (my $subclass = $driver) =~ s/^(\S)/\U$1/; - $class .= '::' . $subclass; - eval "require $class;"; - die "The $class class could not be found ($subclass " . - "not supported?): $@" if ($@); - } - die "$class is an abstract base class. Instantiate a subclass instead." - if ($class eq __PACKAGE__); + if ($driver) { + (my $subclass = $driver) =~ s/^(\S)/\U$1/; + $class .= '::' . $subclass; + eval "require $class;"; + die "The $class class could not be found ($subclass " . "not supported?): $@" + if ($@); + } + die "$class is an abstract base class. Instantiate a subclass instead." + if ($class eq __PACKAGE__); - my $self = {}; - bless $self, $class; - $self = $self->_initialize(@_); + my $self = {}; + bless $self, $class; + $self = $self->_initialize(@_); - return($self); + return ($self); + +} #eosub--new -} #eosub--new #-------------------------------------------------------------------------- sub _initialize { @@ -1864,33 +1846,34 @@ sub _initialize { =cut - my $self = shift; - my $abstract_schema = shift; + my $self = shift; + my $abstract_schema = shift; - if (!$abstract_schema) { - # While ABSTRACT_SCHEMA cannot be modified, $abstract_schema can be. - # So, we dclone it to prevent anything from mucking with the constant. - $abstract_schema = dclone(ABSTRACT_SCHEMA); + if (!$abstract_schema) { - # Let extensions add tables, but make sure they can't modify existing - # tables. If we don't lock/unlock keys, lock_value complains. - lock_keys(%$abstract_schema); - foreach my $table (keys %{ABSTRACT_SCHEMA()}) { - lock_value(%$abstract_schema, $table) - if exists $abstract_schema->{$table}; - } - unlock_keys(%$abstract_schema); - Bugzilla::Hook::process('db_schema_abstract_schema', - { schema => $abstract_schema }); - unlock_hash(%$abstract_schema); + # While ABSTRACT_SCHEMA cannot be modified, $abstract_schema can be. + # So, we dclone it to prevent anything from mucking with the constant. + $abstract_schema = dclone(ABSTRACT_SCHEMA); + + # Let extensions add tables, but make sure they can't modify existing + # tables. If we don't lock/unlock keys, lock_value complains. + lock_keys(%$abstract_schema); + foreach my $table (keys %{ABSTRACT_SCHEMA()}) { + lock_value(%$abstract_schema, $table) if exists $abstract_schema->{$table}; } + unlock_keys(%$abstract_schema); + Bugzilla::Hook::process('db_schema_abstract_schema', + {schema => $abstract_schema}); + unlock_hash(%$abstract_schema); + } - $self->{schema} = dclone($abstract_schema); - $self->{abstract_schema} = $abstract_schema; + $self->{schema} = dclone($abstract_schema); + $self->{abstract_schema} = $abstract_schema; - return $self; + return $self; + +} #eosub--_initialize -} #eosub--_initialize #-------------------------------------------------------------------------- sub _adjust_schema { @@ -1906,36 +1889,41 @@ sub _adjust_schema { =cut - my $self = shift; - - # The _initialize method has already set up the db_specific hash with - # the information on how to implement the abstract data types for the - # instantiated DBMS-specific subclass. - my $db_specific = $self->{db_specific}; - - # Loop over each table in the abstract database schema. - foreach my $table (keys %{ $self->{schema} }) { - my %fields = (@{ $self->{schema}{$table}{FIELDS} }); - # Loop over the field definitions in each table. - foreach my $field_def (values %fields) { - # If the field type is an abstract data type defined in the - # $db_specific hash, replace it with the DBMS-specific data type - # that implements it. - if (exists($db_specific->{$field_def->{TYPE}})) { - $field_def->{TYPE} = $db_specific->{$field_def->{TYPE}}; - } - # Replace abstract default values (such as 'TRUE' and 'FALSE') - # with their database-specific implementations. - if (exists($field_def->{DEFAULT}) - && exists($db_specific->{$field_def->{DEFAULT}})) { - $field_def->{DEFAULT} = $db_specific->{$field_def->{DEFAULT}}; - } - } + my $self = shift; + + # The _initialize method has already set up the db_specific hash with + # the information on how to implement the abstract data types for the + # instantiated DBMS-specific subclass. + my $db_specific = $self->{db_specific}; + + # Loop over each table in the abstract database schema. + foreach my $table (keys %{$self->{schema}}) { + my %fields = (@{$self->{schema}{$table}{FIELDS}}); + + # Loop over the field definitions in each table. + foreach my $field_def (values %fields) { + + # If the field type is an abstract data type defined in the + # $db_specific hash, replace it with the DBMS-specific data type + # that implements it. + if (exists($db_specific->{$field_def->{TYPE}})) { + $field_def->{TYPE} = $db_specific->{$field_def->{TYPE}}; + } + + # Replace abstract default values (such as 'TRUE' and 'FALSE') + # with their database-specific implementations. + if ( exists($field_def->{DEFAULT}) + && exists($db_specific->{$field_def->{DEFAULT}})) + { + $field_def->{DEFAULT} = $db_specific->{$field_def->{DEFAULT}}; + } } + } - return $self; + return $self; + +} #eosub--_adjust_schema -} #eosub--_adjust_schema #-------------------------------------------------------------------------- sub get_type_ddl { @@ -1969,30 +1957,34 @@ C SQL statement =cut - my $self = shift; - my $finfo = (@_ == 1 && ref($_[0]) eq 'HASH') ? $_[0] : { @_ }; - my $type = $finfo->{TYPE}; - confess "A valid TYPE was not specified for this column (got " - . Dumper($finfo) . ")" unless ($type); - - my $default = $finfo->{DEFAULT}; - # Replace any abstract default value (such as 'TRUE' or 'FALSE') - # with its database-specific implementation. - if ( defined $default && exists($self->{db_specific}->{$default}) ) { - $default = $self->{db_specific}->{$default}; - } + my $self = shift; + my $finfo = (@_ == 1 && ref($_[0]) eq 'HASH') ? $_[0] : {@_}; + my $type = $finfo->{TYPE}; + confess "A valid TYPE was not specified for this column (got " + . Dumper($finfo) . ")" + unless ($type); + + my $default = $finfo->{DEFAULT}; + + # Replace any abstract default value (such as 'TRUE' or 'FALSE') + # with its database-specific implementation. + if (defined $default && exists($self->{db_specific}->{$default})) { + $default = $self->{db_specific}->{$default}; + } + + my $type_ddl = $self->convert_type($type); - my $type_ddl = $self->convert_type($type); - # DEFAULT attribute must appear before any column constraints - # (e.g., NOT NULL), for Oracle - $type_ddl .= " DEFAULT $default" if (defined($default)); - # PRIMARY KEY must appear before NOT NULL for SQLite. - $type_ddl .= " PRIMARY KEY" if ($finfo->{PRIMARYKEY}); - $type_ddl .= " NOT NULL" if ($finfo->{NOTNULL}); + # DEFAULT attribute must appear before any column constraints + # (e.g., NOT NULL), for Oracle + $type_ddl .= " DEFAULT $default" if (defined($default)); - return($type_ddl); + # PRIMARY KEY must appear before NOT NULL for SQLite. + $type_ddl .= " PRIMARY KEY" if ($finfo->{PRIMARYKEY}); + $type_ddl .= " NOT NULL" if ($finfo->{NOTNULL}); -} #eosub--get_type_ddl + return ($type_ddl); + +} #eosub--get_type_ddl sub get_fk_ddl { @@ -2026,78 +2018,80 @@ is undefined. =cut - my ($self, $table, $column, $references) = @_; - return "" if !$references; + my ($self, $table, $column, $references) = @_; + return "" if !$references; - my $update = $references->{UPDATE} || 'CASCADE'; - my $delete = $references->{DELETE} || 'RESTRICT'; - my $to_table = $references->{TABLE} || confess "No table in reference"; - my $to_column = $references->{COLUMN} || confess "No column in reference"; - my $fk_name = $self->_get_fk_name($table, $column, $references); + my $update = $references->{UPDATE} || 'CASCADE'; + my $delete = $references->{DELETE} || 'RESTRICT'; + my $to_table = $references->{TABLE} || confess "No table in reference"; + my $to_column = $references->{COLUMN} || confess "No column in reference"; + my $fk_name = $self->_get_fk_name($table, $column, $references); - return "\n CONSTRAINT $fk_name FOREIGN KEY ($column)\n" - . " REFERENCES $to_table($to_column)\n" - . " ON UPDATE $update ON DELETE $delete"; + return + "\n CONSTRAINT $fk_name FOREIGN KEY ($column)\n" + . " REFERENCES $to_table($to_column)\n" + . " ON UPDATE $update ON DELETE $delete"; } # Generates a name for a Foreign Key. It's separate from get_fk_ddl # so that certain databases can override it (for shorter identifiers or # other reasons). sub _get_fk_name { - my ($self, $table, $column, $references) = @_; - my $to_table = $references->{TABLE}; - my $to_column = $references->{COLUMN}; - my $name = "fk_${table}_${column}_${to_table}_${to_column}"; + my ($self, $table, $column, $references) = @_; + my $to_table = $references->{TABLE}; + my $to_column = $references->{COLUMN}; + my $name = "fk_${table}_${column}_${to_table}_${to_column}"; - if (length($name) > $self->MAX_IDENTIFIER_LEN) { - $name = 'fk_' . $self->_hash_identifier($name); - } + if (length($name) > $self->MAX_IDENTIFIER_LEN) { + $name = 'fk_' . $self->_hash_identifier($name); + } - return $name; + return $name; } sub _hash_identifier { - my ($invocant, $value) = @_; - # We do -7 to allow prefixes like "idx_" or "fk_", or perhaps something - # longer in the future. - return substr(md5_hex($value), 0, $invocant->MAX_IDENTIFIER_LEN - 7); + my ($invocant, $value) = @_; + + # We do -7 to allow prefixes like "idx_" or "fk_", or perhaps something + # longer in the future. + return substr(md5_hex($value), 0, $invocant->MAX_IDENTIFIER_LEN - 7); } sub get_add_fks_sql { - my ($self, $table, $column_fks) = @_; - - my @add = $self->_column_fks_to_ddl($table, $column_fks); - - my @sql; - if ($self->MULTIPLE_FKS_IN_ALTER) { - my $alter = "ALTER TABLE $table ADD " . join(', ADD ', @add); - push(@sql, $alter); - } - else { - foreach my $fk_string (@add) { - push(@sql, "ALTER TABLE $table ADD $fk_string"); - } + my ($self, $table, $column_fks) = @_; + + my @add = $self->_column_fks_to_ddl($table, $column_fks); + + my @sql; + if ($self->MULTIPLE_FKS_IN_ALTER) { + my $alter = "ALTER TABLE $table ADD " . join(', ADD ', @add); + push(@sql, $alter); + } + else { + foreach my $fk_string (@add) { + push(@sql, "ALTER TABLE $table ADD $fk_string"); } - return @sql; + } + return @sql; } sub _column_fks_to_ddl { - my ($self, $table, $column_fks) = @_; - my @ddl; - foreach my $column (keys %$column_fks) { - my $def = $column_fks->{$column}; - my $fk_string = $self->get_fk_ddl($table, $column, $def); - push(@ddl, $fk_string); - } - return @ddl; + my ($self, $table, $column_fks) = @_; + my @ddl; + foreach my $column (keys %$column_fks) { + my $def = $column_fks->{$column}; + my $fk_string = $self->get_fk_ddl($table, $column, $def); + push(@ddl, $fk_string); + } + return @ddl; } -sub get_drop_fk_sql { - my ($self, $table, $column, $references) = @_; - my $fk_name = $self->_get_fk_name($table, $column, $references); +sub get_drop_fk_sql { + my ($self, $table, $column, $references) = @_; + my $fk_name = $self->_get_fk_name($table, $column, $references); - return ("ALTER TABLE $table DROP CONSTRAINT $fk_name"); + return ("ALTER TABLE $table DROP CONSTRAINT $fk_name"); } sub convert_type { @@ -2108,8 +2102,8 @@ Converts a TYPE from the L format into the real SQL type. =cut - my ($self, $type) = @_; - return $self->{db_specific}->{$type} || $type; + my ($self, $type) = @_; + return ($self->{db_specific}->{$type} || $type); } sub get_column { @@ -2126,16 +2120,16 @@ sub get_column { =cut - my($self, $table, $column) = @_; + my ($self, $table, $column) = @_; - # Prevent a possible dereferencing of an undef hash, if the - # table doesn't exist. - if (exists $self->{schema}->{$table}) { - my %fields = (@{ $self->{schema}{$table}{FIELDS} }); - return $fields{$column}; - } - return undef; -} #eosub--get_column + # Prevent a possible dereferencing of an undef hash, if the + # table doesn't exist. + if (exists $self->{schema}->{$table}) { + my %fields = (@{$self->{schema}{$table}{FIELDS}}); + return $fields{$column}; + } + return undef; +} #eosub--get_column sub get_table_list { @@ -2150,8 +2144,8 @@ sub get_table_list { =cut - my $self = shift; - return sort keys %{$self->{schema}}; + my $self = shift; + return sort keys %{$self->{schema}}; } sub get_table_columns { @@ -2165,34 +2159,33 @@ sub get_table_columns { =cut - my($self, $table) = @_; - my @ddl = (); + my ($self, $table) = @_; + my @ddl = (); - my $thash = $self->{schema}{$table}; - die "Table $table does not exist in the database schema." - unless (ref($thash)); + my $thash = $self->{schema}{$table}; + die "Table $table does not exist in the database schema." unless (ref($thash)); - my @columns = (); - my @fields = @{ $thash->{FIELDS} }; - while (@fields) { - push(@columns, shift(@fields)); - shift(@fields); - } + my @columns = (); + my @fields = @{$thash->{FIELDS}}; + while (@fields) { + push(@columns, shift(@fields)); + shift(@fields); + } - return @columns; + return @columns; -} #eosub--get_table_columns +} #eosub--get_table_columns sub get_table_indexes_abstract { - my ($self, $table) = @_; - my $table_def = $self->get_table_abstract($table); - my %indexes = @{$table_def->{INDEXES} || []}; - return \%indexes; + my ($self, $table) = @_; + my $table_def = $self->get_table_abstract($table); + my %indexes = @{$table_def->{INDEXES} || []}; + return \%indexes; } sub get_create_database_sql { - my ($self, $name) = @_; - return ("CREATE DATABASE $name"); + my ($self, $name) = @_; + return ("CREATE DATABASE $name"); } sub get_table_ddl { @@ -2209,30 +2202,29 @@ sub get_table_ddl { =cut - my($self, $table) = @_; - my @ddl = (); + my ($self, $table) = @_; + my @ddl = (); - die "Table $table does not exist in the database schema." - unless (ref($self->{schema}{$table})); + die "Table $table does not exist in the database schema." + unless (ref($self->{schema}{$table})); - my $create_table = $self->_get_create_table_ddl($table); - push(@ddl, $create_table) if $create_table; + my $create_table = $self->_get_create_table_ddl($table); + push(@ddl, $create_table) if $create_table; - my @indexes = @{ $self->{schema}{$table}{INDEXES} || [] }; - while (@indexes) { - my $index_name = shift(@indexes); - my $index_info = shift(@indexes); - my $index_sql = $self->get_add_index_ddl($table, $index_name, - $index_info); - push(@ddl, $index_sql) if $index_sql; - } + my @indexes = @{$self->{schema}{$table}{INDEXES} || []}; + while (@indexes) { + my $index_name = shift(@indexes); + my $index_info = shift(@indexes); + my $index_sql = $self->get_add_index_ddl($table, $index_name, $index_info); + push(@ddl, $index_sql) if $index_sql; + } - push(@ddl, @{ $self->{schema}{$table}{DB_EXTRAS} }) - if (ref($self->{schema}{$table}{DB_EXTRAS})); + push(@ddl, @{$self->{schema}{$table}{DB_EXTRAS}}) + if (ref($self->{schema}{$table}{DB_EXTRAS})); - return @ddl; + return @ddl; -} #eosub--get_table_ddl +} #eosub--get_table_ddl sub _get_create_table_ddl { @@ -2245,30 +2237,29 @@ sub _get_create_table_ddl { =cut - my($self, $table) = @_; - - my $thash = $self->{schema}{$table}; - die "Table $table does not exist in the database schema." - unless ref $thash; - - my (@col_lines, @fk_lines); - my @fields = @{ $thash->{FIELDS} }; - while (@fields) { - my $field = shift(@fields); - my $finfo = shift(@fields); - push(@col_lines, "\t$field\t" . $self->get_type_ddl($finfo)); - if ($self->FK_ON_CREATE and $finfo->{REFERENCES}) { - my $fk = $finfo->{REFERENCES}; - my $fk_ddl = $self->get_fk_ddl($table, $field, $fk); - push(@fk_lines, $fk_ddl); - } + my ($self, $table) = @_; + + my $thash = $self->{schema}{$table}; + die "Table $table does not exist in the database schema." unless ref $thash; + + my (@col_lines, @fk_lines); + my @fields = @{$thash->{FIELDS}}; + while (@fields) { + my $field = shift(@fields); + my $finfo = shift(@fields); + push(@col_lines, "\t$field\t" . $self->get_type_ddl($finfo)); + if ($self->FK_ON_CREATE and $finfo->{REFERENCES}) { + my $fk = $finfo->{REFERENCES}; + my $fk_ddl = $self->get_fk_ddl($table, $field, $fk); + push(@fk_lines, $fk_ddl); } - - my $sql = "CREATE TABLE $table (\n" . join(",\n", @col_lines, @fk_lines) - . "\n)"; - return $sql + } + + my $sql + = "CREATE TABLE $table (\n" . join(",\n", @col_lines, @fk_lines) . "\n)"; + return $sql; -} +} sub _get_create_index_ddl { @@ -2284,16 +2275,17 @@ sub _get_create_index_ddl { =cut - my ($self, $table_name, $index_name, $index_fields, $index_type) = @_; + my ($self, $table_name, $index_name, $index_fields, $index_type) = @_; - my $sql = "CREATE "; - $sql .= "$index_type " if ($index_type && $index_type eq 'UNIQUE'); - $sql .= "INDEX $index_name ON $table_name \(" . - join(", ", @$index_fields) . "\)"; + my $sql = "CREATE "; + $sql .= "$index_type " if ($index_type && $index_type eq 'UNIQUE'); + $sql + .= "INDEX $index_name ON $table_name \(" . join(", ", @$index_fields) . "\)"; - return($sql); + return ($sql); + +} #eosub--_get_create_index_ddl -} #eosub--_get_create_index_ddl #-------------------------------------------------------------------------- sub get_add_column_ddl { @@ -2312,22 +2304,25 @@ sub get_add_column_ddl { =cut - my ($self, $table, $column, $definition, $init_value) = @_; - my @statements; - push(@statements, "ALTER TABLE $table ". $self->ADD_COLUMN ." $column " . - $self->get_type_ddl($definition)); - - # XXX - Note that although this works for MySQL, most databases will fail - # before this point, if we haven't set a default. - (push(@statements, "UPDATE $table SET $column = $init_value")) - if defined $init_value; - - if (defined $definition->{REFERENCES}) { - push(@statements, $self->get_add_fks_sql($table, { $column => - $definition->{REFERENCES} })); - } - - return (@statements); + my ($self, $table, $column, $definition, $init_value) = @_; + my @statements; + push(@statements, + "ALTER TABLE $table " + . $self->ADD_COLUMN + . " $column " + . $self->get_type_ddl($definition)); + + # XXX - Note that although this works for MySQL, most databases will fail + # before this point, if we haven't set a default. + (push(@statements, "UPDATE $table SET $column = $init_value")) + if defined $init_value; + + if (defined $definition->{REFERENCES}) { + push(@statements, + $self->get_add_fks_sql($table, {$column => $definition->{REFERENCES}})); + } + + return (@statements); } sub get_add_index_ddl { @@ -2348,20 +2343,21 @@ sub get_add_index_ddl { =cut - my ($self, $table, $name, $definition) = @_; + my ($self, $table, $name, $definition) = @_; - my ($index_fields, $index_type); - # Index defs can be arrays or hashes - if (ref($definition) eq 'HASH') { - $index_fields = $definition->{FIELDS}; - $index_type = $definition->{TYPE}; - } else { - $index_fields = $definition; - $index_type = ''; - } - - return $self->_get_create_index_ddl($table, $name, $index_fields, - $index_type); + my ($index_fields, $index_type); + + # Index defs can be arrays or hashes + if (ref($definition) eq 'HASH') { + $index_fields = $definition->{FIELDS}; + $index_type = $definition->{TYPE}; + } + else { + $index_fields = $definition; + $index_type = ''; + } + + return $self->_get_create_index_ddl($table, $name, $index_fields, $index_type); } sub get_alter_column_ddl { @@ -2384,85 +2380,88 @@ sub get_alter_column_ddl { =cut - my $self = shift; - my ($table, $column, $new_def, $set_nulls_to) = @_; - - my @statements; - my $old_def = $self->get_column_abstract($table, $column); - my $specific = $self->{db_specific}; - - # If the types have changed, we have to deal with that. - if (uc(trim($old_def->{TYPE})) ne uc(trim($new_def->{TYPE}))) { - push(@statements, $self->_get_alter_type_sql($table, $column, - $new_def, $old_def)); - } - - my $default = $new_def->{DEFAULT}; - my $default_old = $old_def->{DEFAULT}; - - if (defined $default) { - $default = $specific->{$default} if exists $specific->{$default}; - } - # This first condition prevents "uninitialized value" errors. - if (!defined $default && !defined $default_old) { - # Do Nothing - } - # If we went from having a default to not having one - elsif (!defined $default && defined $default_old) { - push(@statements, "ALTER TABLE $table ALTER COLUMN $column" - . " DROP DEFAULT"); - } - # If we went from no default to a default, or we changed the default. - elsif ( (defined $default && !defined $default_old) || - ($default ne $default_old) ) - { - push(@statements, "ALTER TABLE $table ALTER COLUMN $column " - . " SET DEFAULT $default"); - } - - # If we went from NULL to NOT NULL. - if (!$old_def->{NOTNULL} && $new_def->{NOTNULL}) { - push(@statements, $self->_set_nulls_sql(@_)); - push(@statements, "ALTER TABLE $table ALTER COLUMN $column" - . " SET NOT NULL"); - } - # If we went from NOT NULL to NULL - elsif ($old_def->{NOTNULL} && !$new_def->{NOTNULL}) { - push(@statements, "ALTER TABLE $table ALTER COLUMN $column" - . " DROP NOT NULL"); - } - - # If we went from not being a PRIMARY KEY to being a PRIMARY KEY. - if (!$old_def->{PRIMARYKEY} && $new_def->{PRIMARYKEY}) { - push(@statements, "ALTER TABLE $table ADD PRIMARY KEY ($column)"); - } - # If we went from being a PK to not being a PK - elsif ( $old_def->{PRIMARYKEY} && !$new_def->{PRIMARYKEY} ) { - push(@statements, "ALTER TABLE $table DROP PRIMARY KEY"); - } - - return @statements; + my $self = shift; + my ($table, $column, $new_def, $set_nulls_to) = @_; + + my @statements; + my $old_def = $self->get_column_abstract($table, $column); + my $specific = $self->{db_specific}; + + # If the types have changed, we have to deal with that. + if (uc(trim($old_def->{TYPE})) ne uc(trim($new_def->{TYPE}))) { + push(@statements, + $self->_get_alter_type_sql($table, $column, $new_def, $old_def)); + } + + my $default = $new_def->{DEFAULT}; + my $default_old = $old_def->{DEFAULT}; + + if (defined $default) { + $default = $specific->{$default} if exists $specific->{$default}; + } + + # This first condition prevents "uninitialized value" errors. + if (!defined $default && !defined $default_old) { + + # Do Nothing + } + + # If we went from having a default to not having one + elsif (!defined $default && defined $default_old) { + push(@statements, "ALTER TABLE $table ALTER COLUMN $column" . " DROP DEFAULT"); + } + + # If we went from no default to a default, or we changed the default. + elsif ((defined $default && !defined $default_old) + || ($default ne $default_old)) + { + push(@statements, + "ALTER TABLE $table ALTER COLUMN $column " . " SET DEFAULT $default"); + } + + # If we went from NULL to NOT NULL. + if (!$old_def->{NOTNULL} && $new_def->{NOTNULL}) { + push(@statements, $self->_set_nulls_sql(@_)); + push(@statements, "ALTER TABLE $table ALTER COLUMN $column" . " SET NOT NULL"); + } + + # If we went from NOT NULL to NULL + elsif ($old_def->{NOTNULL} && !$new_def->{NOTNULL}) { + push(@statements, "ALTER TABLE $table ALTER COLUMN $column" . " DROP NOT NULL"); + } + + # If we went from not being a PRIMARY KEY to being a PRIMARY KEY. + if (!$old_def->{PRIMARYKEY} && $new_def->{PRIMARYKEY}) { + push(@statements, "ALTER TABLE $table ADD PRIMARY KEY ($column)"); + } + + # If we went from being a PK to not being a PK + elsif ($old_def->{PRIMARYKEY} && !$new_def->{PRIMARYKEY}) { + push(@statements, "ALTER TABLE $table DROP PRIMARY KEY"); + } + + return @statements; } # Helps handle any fields that were NULL before, if we have a default, # when doing an ALTER COLUMN. sub _set_nulls_sql { - my ($self, $table, $column, $new_def, $set_nulls_to) = @_; - my $default = $new_def->{DEFAULT}; - # If we have a set_nulls_to, that overrides the DEFAULT - # (although nobody would usually specify both a default and - # a set_nulls_to.) - $default = $set_nulls_to if defined $set_nulls_to; - if (defined $default) { - my $specific = $self->{db_specific}; - $default = $specific->{$default} if exists $specific->{$default}; - } - my @sql; - if (defined $default) { - push(@sql, "UPDATE $table SET $column = $default" - . " WHERE $column IS NULL"); - } - return @sql; + my ($self, $table, $column, $new_def, $set_nulls_to) = @_; + my $default = $new_def->{DEFAULT}; + + # If we have a set_nulls_to, that overrides the DEFAULT + # (although nobody would usually specify both a default and + # a set_nulls_to.) + $default = $set_nulls_to if defined $set_nulls_to; + if (defined $default) { + my $specific = $self->{db_specific}; + $default = $specific->{$default} if exists $specific->{$default}; + } + my @sql; + if (defined $default) { + push(@sql, "UPDATE $table SET $column = $default" . " WHERE $column IS NULL"); + } + return @sql; } sub get_drop_index_ddl { @@ -2476,11 +2475,11 @@ sub get_drop_index_ddl { =cut - my ($self, $table, $name) = @_; + my ($self, $table, $name) = @_; - # Although ANSI SQL-92 doesn't specify a method of dropping an index, - # many DBs support this syntax. - return ("DROP INDEX $name"); + # Although ANSI SQL-92 doesn't specify a method of dropping an index, + # many DBs support this syntax. + return ("DROP INDEX $name"); } sub get_drop_column_ddl { @@ -2494,8 +2493,8 @@ sub get_drop_column_ddl { =cut - my ($self, $table, $column) = @_; - return ("ALTER TABLE $table DROP COLUMN $column"); + my ($self, $table, $column) = @_; + return ("ALTER TABLE $table DROP COLUMN $column"); } =item C @@ -2507,8 +2506,8 @@ sub get_drop_column_ddl { =cut sub get_drop_table_ddl { - my ($self, $table) = @_; - return ("DROP TABLE $table"); + my ($self, $table) = @_; + return ("DROP TABLE $table"); } sub get_rename_column_ddl { @@ -2526,8 +2525,8 @@ sub get_rename_column_ddl { =cut - die "ANSI SQL has no way to rename a column, and your database driver\n" - . " has not implemented a method."; + die "ANSI SQL has no way to rename a column, and your database driver\n" + . " has not implemented a method."; } @@ -2557,8 +2556,8 @@ Gets SQL to rename a table in the database. =cut - my ($self, $old_name, $new_name) = @_; - return ("ALTER TABLE $old_name RENAME TO $new_name"); + my ($self, $old_name, $new_name) = @_; + return ("ALTER TABLE $old_name RENAME TO $new_name"); } =item C @@ -2571,13 +2570,13 @@ Gets SQL to rename a table in the database. =cut sub delete_table { - my ($self, $name) = @_; + my ($self, $name) = @_; - die "Attempted to delete nonexistent table '$name'." unless - $self->get_table_abstract($name); + die "Attempted to delete nonexistent table '$name'." + unless $self->get_table_abstract($name); - delete $self->{abstract_schema}->{$name}; - delete $self->{schema}->{$name}; + delete $self->{abstract_schema}->{$name}; + delete $self->{schema}->{$name}; } sub get_column_abstract { @@ -2594,15 +2593,15 @@ sub get_column_abstract { =cut - my ($self, $table, $column) = @_; + my ($self, $table, $column) = @_; - # Prevent a possible dereferencing of an undef hash, if the - # table doesn't exist. - if ($self->get_table_abstract($table)) { - my %fields = (@{ $self->{abstract_schema}{$table}{FIELDS} }); - return $fields{$column}; - } - return undef; + # Prevent a possible dereferencing of an undef hash, if the + # table doesn't exist. + if ($self->get_table_abstract($table)) { + my %fields = (@{$self->{abstract_schema}{$table}{FIELDS}}); + return $fields{$column}; + } + return undef; } =item C @@ -2620,29 +2619,31 @@ sub get_column_abstract { =cut sub get_indexes_on_column_abstract { - my ($self, $table, $column) = @_; - my %ret_hash; - - my $table_def = $self->get_table_abstract($table); - if ($table_def && exists $table_def->{INDEXES}) { - my %indexes = (@{ $table_def->{INDEXES} }); - foreach my $index_name (keys %indexes) { - my $col_list; - # Get the column list, depending on whether the index - # is in hashref or arrayref format. - if (ref($indexes{$index_name}) eq 'HASH') { - $col_list = $indexes{$index_name}->{FIELDS}; - } else { - $col_list = $indexes{$index_name}; - } - - if(grep($_ eq $column, @$col_list)) { - $ret_hash{$index_name} = dclone($indexes{$index_name}); - } - } + my ($self, $table, $column) = @_; + my %ret_hash; + + my $table_def = $self->get_table_abstract($table); + if ($table_def && exists $table_def->{INDEXES}) { + my %indexes = (@{$table_def->{INDEXES}}); + foreach my $index_name (keys %indexes) { + my $col_list; + + # Get the column list, depending on whether the index + # is in hashref or arrayref format. + if (ref($indexes{$index_name}) eq 'HASH') { + $col_list = $indexes{$index_name}->{FIELDS}; + } + else { + $col_list = $indexes{$index_name}; + } + + if (grep($_ eq $column, @$col_list)) { + $ret_hash{$index_name} = dclone($indexes{$index_name}); + } } + } - return %ret_hash; + return %ret_hash; } sub get_index_abstract { @@ -2658,16 +2659,16 @@ sub get_index_abstract { =cut - my ($self, $table, $index) = @_; + my ($self, $table, $index) = @_; - # Prevent a possible dereferencing of an undef hash, if the - # table doesn't exist. - my $index_table = $self->get_table_abstract($table); - if ($index_table && exists $index_table->{INDEXES}) { - my %indexes = (@{ $index_table->{INDEXES} }); - return $indexes{$index}; - } - return undef; + # Prevent a possible dereferencing of an undef hash, if the + # table doesn't exist. + my $index_table = $self->get_table_abstract($table); + if ($index_table && exists $index_table->{INDEXES}) { + my %indexes = (@{$index_table->{INDEXES}}); + return $indexes{$index}; + } + return undef; } =item C @@ -2681,8 +2682,8 @@ sub get_index_abstract { =cut sub get_table_abstract { - my ($self, $table) = @_; - return $self->{abstract_schema}->{$table}; + my ($self, $table) = @_; + return $self->{abstract_schema}->{$table}; } =item C @@ -2698,22 +2699,20 @@ sub get_table_abstract { =cut sub add_table { - my ($self, $name, $definition) = @_; - (die "Table already exists: $name") - if exists $self->{abstract_schema}->{$name}; - if ($definition) { - $self->{abstract_schema}->{$name} = dclone($definition); - $self->{schema} = dclone($self->{abstract_schema}); - $self->_adjust_schema(); - } - else { - $self->{abstract_schema}->{$name} = {FIELDS => []}; - $self->{schema}->{$name} = {FIELDS => []}; - } + my ($self, $name, $definition) = @_; + (die "Table already exists: $name") if exists $self->{abstract_schema}->{$name}; + if ($definition) { + $self->{abstract_schema}->{$name} = dclone($definition); + $self->{schema} = dclone($self->{abstract_schema}); + $self->_adjust_schema(); + } + else { + $self->{abstract_schema}->{$name} = {FIELDS => []}; + $self->{schema}->{$name} = {FIELDS => []}; + } } - sub rename_table { =item C @@ -2723,10 +2722,10 @@ Renames a table from C<$old_name> to C<$new_name> in this Schema object. =cut - my ($self, $old_name, $new_name) = @_; - my $table = $self->get_table_abstract($old_name); - $self->delete_table($old_name); - $self->add_table($new_name, $table); + my ($self, $old_name, $new_name) = @_; + my $table = $self->get_table_abstract($old_name); + $self->delete_table($old_name); + $self->add_table($new_name, $table); } sub delete_column { @@ -2741,17 +2740,18 @@ sub delete_column { =cut - my ($self, $table, $column) = @_; + my ($self, $table, $column) = @_; - my $abstract_fields = $self->{abstract_schema}{$table}{FIELDS}; - my $name_position = firstidx { $_ eq $column } @$abstract_fields; - die "Attempted to delete nonexistent column ${table}.${column}" - if $name_position == -1; - # Delete the key/value pair from the array. - splice(@$abstract_fields, $name_position, 2); + my $abstract_fields = $self->{abstract_schema}{$table}{FIELDS}; + my $name_position = firstidx { $_ eq $column } @$abstract_fields; + die "Attempted to delete nonexistent column ${table}.${column}" + if $name_position == -1; - $self->{schema} = dclone($self->{abstract_schema}); - $self->_adjust_schema(); + # Delete the key/value pair from the array. + splice(@$abstract_fields, $name_position, 2); + + $self->{schema} = dclone($self->{abstract_schema}); + $self->_adjust_schema(); } sub rename_column { @@ -2767,11 +2767,11 @@ sub rename_column { =cut - my ($self, $table, $old_name, $new_name) = @_; - my $def = $self->get_column_abstract($table, $old_name); - die "Renaming a column that doesn't exist" if !$def; - $self->delete_column($table, $old_name); - $self->set_column($table, $new_name, $def); + my ($self, $table, $old_name, $new_name) = @_; + my $def = $self->get_column_abstract($table, $old_name); + die "Renaming a column that doesn't exist" if !$def; + $self->delete_column($table, $old_name); + $self->set_column($table, $new_name, $def); } sub set_column { @@ -2792,10 +2792,10 @@ sub set_column { =cut - my ($self, $table, $column, $new_def) = @_; + my ($self, $table, $column, $new_def) = @_; - my $fields = $self->{abstract_schema}{$table}{FIELDS}; - $self->_set_object($table, $column, $new_def, $fields); + my $fields = $self->{abstract_schema}{$table}{FIELDS}; + $self->_set_object($table, $column, $new_def, $fields); } =item C @@ -2805,19 +2805,20 @@ Sets the C item on the specified column. =cut sub set_fk { - my ($self, $table, $column, $fk_def) = @_; - # Don't want to modify the source def before we explicitly set it below. - # This is just us being extra-cautious. - my $column_def = dclone($self->get_column_abstract($table, $column)); - die "Tried to set an fk on $table.$column, but that column doesn't exist" - if !$column_def; - if ($fk_def) { - $column_def->{REFERENCES} = $fk_def; - } - else { - delete $column_def->{REFERENCES}; - } - $self->set_column($table, $column, $column_def); + my ($self, $table, $column, $fk_def) = @_; + + # Don't want to modify the source def before we explicitly set it below. + # This is just us being extra-cautious. + my $column_def = dclone($self->get_column_abstract($table, $column)); + die "Tried to set an fk on $table.$column, but that column doesn't exist" + if !$column_def; + if ($fk_def) { + $column_def->{REFERENCES} = $fk_def; + } + else { + delete $column_def->{REFERENCES}; + } + $self->set_column($table, $column, $column_def); } sub set_index { @@ -2838,36 +2839,39 @@ sub set_index { =cut - my ($self, $table, $name, $definition) = @_; + my ($self, $table, $name, $definition) = @_; - if ( exists $self->{abstract_schema}{$table} - && !exists $self->{abstract_schema}{$table}{INDEXES} ) { - $self->{abstract_schema}{$table}{INDEXES} = []; - } + if (exists $self->{abstract_schema}{$table} + && !exists $self->{abstract_schema}{$table}{INDEXES}) + { + $self->{abstract_schema}{$table}{INDEXES} = []; + } - my $indexes = $self->{abstract_schema}{$table}{INDEXES}; - $self->_set_object($table, $name, $definition, $indexes); + my $indexes = $self->{abstract_schema}{$table}{INDEXES}; + $self->_set_object($table, $name, $definition, $indexes); } # A private helper for set_index and set_column. # This does the actual "work" of those two functions. # $array_to_change is an arrayref. sub _set_object { - my ($self, $table, $name, $definition, $array_to_change) = @_; + my ($self, $table, $name, $definition, $array_to_change) = @_; - my $obj_position = (firstidx { $_ eq $name } @$array_to_change) + 1; - # If the object doesn't exist, then add it. - if (!$obj_position) { - push(@$array_to_change, $name); - push(@$array_to_change, $definition); - } - # We're modifying an existing object in the Schema. - else { - splice(@$array_to_change, $obj_position, 1, $definition); - } + my $obj_position = (firstidx { $_ eq $name } @$array_to_change) + 1; - $self->{schema} = dclone($self->{abstract_schema}); - $self->_adjust_schema(); + # If the object doesn't exist, then add it. + if (!$obj_position) { + push(@$array_to_change, $name); + push(@$array_to_change, $definition); + } + + # We're modifying an existing object in the Schema. + else { + splice(@$array_to_change, $obj_position, 1, $definition); + } + + $self->{schema} = dclone($self->{abstract_schema}); + $self->_adjust_schema(); } =item C @@ -2885,16 +2889,17 @@ sub _set_object { =cut sub delete_index { - my ($self, $table, $name) = @_; - - my $indexes = $self->{abstract_schema}{$table}{INDEXES}; - my $name_position = firstidx { $_ eq $name } @$indexes; - die "Attempted to delete nonexistent index $name on the $table table" - if $name_position == -1; - # Delete the key/value pair from the array. - splice(@$indexes, $name_position, 2); - $self->{schema} = dclone($self->{abstract_schema}); - $self->_adjust_schema(); + my ($self, $table, $name) = @_; + + my $indexes = $self->{abstract_schema}{$table}{INDEXES}; + my $name_position = firstidx { $_ eq $name } @$indexes; + die "Attempted to delete nonexistent index $name on the $table table" + if $name_position == -1; + + # Delete the key/value pair from the array. + splice(@$indexes, $name_position, 2); + $self->{schema} = dclone($self->{abstract_schema}); + $self->_adjust_schema(); } sub columns_equal { @@ -2912,24 +2917,24 @@ sub columns_equal { =cut - my $self = shift; - my $col_one = dclone(shift); - my $col_two = dclone(shift); + my $self = shift; + my $col_one = dclone(shift); + my $col_two = dclone(shift); - $col_one->{TYPE} = uc($col_one->{TYPE}); - $col_two->{TYPE} = uc($col_two->{TYPE}); + $col_one->{TYPE} = uc($col_one->{TYPE}); + $col_two->{TYPE} = uc($col_two->{TYPE}); - # We don't care about foreign keys when comparing column definitions. - delete $col_one->{REFERENCES}; - delete $col_two->{REFERENCES}; + # We don't care about foreign keys when comparing column definitions. + delete $col_one->{REFERENCES}; + delete $col_two->{REFERENCES}; - my @col_one_array = %$col_one; - my @col_two_array = %$col_two; + my @col_one_array = %$col_one; + my @col_two_array = %$col_two; - my ($removed, $added) = diff_arrays(\@col_one_array, \@col_two_array); + my ($removed, $added) = diff_arrays(\@col_one_array, \@col_two_array); - # If there are no differences between the arrays, then they are equal. - return !scalar(@$removed) && !scalar(@$added) ? 1 : 0; + # If there are no differences between the arrays, then they are equal. + return !scalar(@$removed) && !scalar(@$added) ? 1 : 0; } @@ -2953,18 +2958,18 @@ sub columns_equal { =cut sub serialize_abstract { - my ($self) = @_; - - # Make it ok to eval - local $Data::Dumper::Purity = 1; - - # Avoid cross-refs - local $Data::Dumper::Deepcopy = 1; - - # Always sort keys to allow textual compare - local $Data::Dumper::Sortkeys = 1; - - return Dumper($self->{abstract_schema}); + my ($self) = @_; + + # Make it ok to eval + local $Data::Dumper::Purity = 1; + + # Avoid cross-refs + local $Data::Dumper::Deepcopy = 1; + + # Always sort keys to allow textual compare + local $Data::Dumper::Sortkeys = 1; + + return Dumper($self->{abstract_schema}); } =item C @@ -2983,36 +2988,34 @@ sub serialize_abstract { =cut sub deserialize_abstract { - my ($class, $serialized, $version) = @_; - - my $thawed_hash; - if ($version < 2) { - $thawed_hash = thaw($serialized); - } - else { - my $cpt = new Safe; - $cpt->reval($serialized) || - die "Unable to restore cached schema: " . $@; - $thawed_hash = ${$cpt->varglob('VAR1')}; - } - - # Version 2 didn't have the "created" key for REFERENCES items. - if ($version < 3) { - my $standard = $class->new()->{abstract_schema}; - foreach my $table_name (keys %$thawed_hash) { - my %standard_fields = - @{ $standard->{$table_name}->{FIELDS} || [] }; - my $table = $thawed_hash->{$table_name}; - my %fields = @{ $table->{FIELDS} || [] }; - while (my ($field, $def) = each %fields) { - if (exists $def->{REFERENCES}) { - $def->{REFERENCES}->{created} = 1; - } - } + my ($class, $serialized, $version) = @_; + + my $thawed_hash; + if ($version < 2) { + $thawed_hash = thaw($serialized); + } + else { + my $cpt = new Safe; + $cpt->reval($serialized) || die "Unable to restore cached schema: " . $@; + $thawed_hash = ${$cpt->varglob('VAR1')}; + } + + # Version 2 didn't have the "created" key for REFERENCES items. + if ($version < 3) { + my $standard = $class->new()->{abstract_schema}; + foreach my $table_name (keys %$thawed_hash) { + my %standard_fields = @{$standard->{$table_name}->{FIELDS} || []}; + my $table = $thawed_hash->{$table_name}; + my %fields = @{$table->{FIELDS} || []}; + while (my ($field, $def) = each %fields) { + if (exists $def->{REFERENCES}) { + $def->{REFERENCES}->{created} = 1; } + } } + } - return $class->new(undef, $thawed_hash); + return $class->new(undef, $thawed_hash); } ##################################################################### @@ -3040,8 +3043,8 @@ object. =cut sub get_empty_schema { - my ($class) = @_; - return $class->deserialize_abstract(Dumper({}), SCHEMA_VERSION); + my ($class) = @_; + return $class->deserialize_abstract(Dumper({}), SCHEMA_VERSION); } 1; diff --git a/Bugzilla/DB/Schema/Mysql.pm b/Bugzilla/DB/Schema/Mysql.pm index 7ff8ade9f..f552be115 100644 --- a/Bugzilla/DB/Schema/Mysql.pm +++ b/Bugzilla/DB/Schema/Mysql.pm @@ -21,7 +21,7 @@ use Bugzilla::Error; use parent qw(Bugzilla::DB::Schema); -# This is for column_info_to_column, to know when a tinyint is a +# This is for column_info_to_column, to know when a tinyint is a # boolean and when it's really a tinyint. This only has to be accurate # up to and through 2.19.3, because that's the only time we need # column_info_to_column. @@ -30,50 +30,59 @@ use parent qw(Bugzilla::DB::Schema); # that should be interpreted as a BOOLEAN instead of as an INT1 when # reading in the Schema from the disk. The values are discarded; I just # used "1" for simplicity. -# +# # THIS CONSTANT IS ONLY USED FOR UPGRADES FROM 2.18 OR EARLIER. DON'T # UPDATE IT TO MODERN COLUMN NAMES OR DEFINITIONS. use constant BOOLEAN_MAP => { - bugs => {everconfirmed => 1, reporter_accessible => 1, - cclist_accessible => 1, qacontact_accessible => 1, - assignee_accessible => 1}, - longdescs => {isprivate => 1, already_wrapped => 1}, - attachments => {ispatch => 1, isobsolete => 1, isprivate => 1}, - flags => {is_active => 1}, - flagtypes => {is_active => 1, is_requestable => 1, - is_requesteeble => 1, is_multiplicable => 1}, - fielddefs => {mailhead => 1, obsolete => 1}, - bug_status => {isactive => 1}, - resolution => {isactive => 1}, - bug_severity => {isactive => 1}, - priority => {isactive => 1}, - rep_platform => {isactive => 1}, - op_sys => {isactive => 1}, - profiles => {mybugslink => 1, newemailtech => 1}, - namedqueries => {linkinfooter => 1, watchfordiffs => 1}, - groups => {isbuggroup => 1, isactive => 1}, - group_control_map => {entry => 1, membercontrol => 1, othercontrol => 1, - canedit => 1}, - group_group_map => {isbless => 1}, - user_group_map => {isbless => 1, isderived => 1}, - products => {disallownew => 1}, - series => {public => 1}, - whine_queries => {onemailperbug => 1}, - quips => {approved => 1}, - setting => {is_enabled => 1} + bugs => { + everconfirmed => 1, + reporter_accessible => 1, + cclist_accessible => 1, + qacontact_accessible => 1, + assignee_accessible => 1 + }, + longdescs => {isprivate => 1, already_wrapped => 1}, + attachments => {ispatch => 1, isobsolete => 1, isprivate => 1}, + flags => {is_active => 1}, + flagtypes => { + is_active => 1, + is_requestable => 1, + is_requesteeble => 1, + is_multiplicable => 1 + }, + fielddefs => {mailhead => 1, obsolete => 1}, + bug_status => {isactive => 1}, + resolution => {isactive => 1}, + bug_severity => {isactive => 1}, + priority => {isactive => 1}, + rep_platform => {isactive => 1}, + op_sys => {isactive => 1}, + profiles => {mybugslink => 1, newemailtech => 1}, + namedqueries => {linkinfooter => 1, watchfordiffs => 1}, + groups => {isbuggroup => 1, isactive => 1}, + group_control_map => + {entry => 1, membercontrol => 1, othercontrol => 1, canedit => 1}, + group_group_map => {isbless => 1}, + user_group_map => {isbless => 1, isderived => 1}, + products => {disallownew => 1}, + series => {public => 1}, + whine_queries => {onemailperbug => 1}, + quips => {approved => 1}, + setting => {is_enabled => 1} }; # Maps the db_specific hash backwards, for use in column_info_to_column. use constant REVERSE_MAPPING => { - # Boolean and the SERIAL fields are handled in column_info_to_column, - # and so don't have an entry here. - TINYINT => 'INT1', - SMALLINT => 'INT2', - MEDIUMINT => 'INT3', - INTEGER => 'INT4', - - # All the other types have the same name in their abstract version - # as in their db-specific version, so no reverse mapping is needed. + + # Boolean and the SERIAL fields are handled in column_info_to_column, + # and so don't have an entry here. + TINYINT => 'INT1', + SMALLINT => 'INT2', + MEDIUMINT => 'INT3', + INTEGER => 'INT4', + + # All the other types have the same name in their abstract version + # as in their db-specific version, so no reverse mapping is needed. }; use constant MYISAM_TABLES => qw(bugs_fulltext); @@ -81,181 +90,196 @@ use constant MYISAM_TABLES => qw(bugs_fulltext); #------------------------------------------------------------------------------ sub _initialize { - my $self = shift; + my $self = shift; + + $self = $self->SUPER::_initialize(@_); - $self = $self->SUPER::_initialize(@_); + $self->{db_specific} = { - $self->{db_specific} = { + BOOLEAN => 'tinyint', + FALSE => '0', + TRUE => '1', - BOOLEAN => 'tinyint', - FALSE => '0', - TRUE => '1', + INT1 => 'tinyint', + INT2 => 'smallint', + INT3 => 'mediumint', + INT4 => 'integer', - INT1 => 'tinyint', - INT2 => 'smallint', - INT3 => 'mediumint', - INT4 => 'integer', + SMALLSERIAL => 'smallint auto_increment', + MEDIUMSERIAL => 'mediumint auto_increment', + INTSERIAL => 'integer auto_increment', - SMALLSERIAL => 'smallint auto_increment', - MEDIUMSERIAL => 'mediumint auto_increment', - INTSERIAL => 'integer auto_increment', + TINYTEXT => 'tinytext', + MEDIUMTEXT => 'mediumtext', + LONGTEXT => 'mediumtext', - TINYTEXT => 'tinytext', - MEDIUMTEXT => 'mediumtext', - LONGTEXT => 'mediumtext', + LONGBLOB => 'longblob', - LONGBLOB => 'longblob', + DATETIME => 'datetime', + DATE => 'date', + }; - DATETIME => 'datetime', - DATE => 'date', - }; + $self->_adjust_schema; - $self->_adjust_schema; + return $self; - return $self; +} #eosub--_initialize -} #eosub--_initialize #------------------------------------------------------------------------------ sub _get_create_table_ddl { - # Extend superclass method to specify the MYISAM storage engine. - # Returns a "create table" SQL statement. - my($self, $table) = @_; + # Extend superclass method to specify the MYISAM storage engine. + # Returns a "create table" SQL statement. - my $charset = Bugzilla->dbh->bz_db_is_utf8 ? "CHARACTER SET utf8" : ''; - my $type = grep($_ eq $table, MYISAM_TABLES) ? 'MYISAM' : 'InnoDB'; - return($self->SUPER::_get_create_table_ddl($table) - . " ENGINE = $type $charset"); + my ($self, $table) = @_; + + my $charset = Bugzilla->dbh->bz_db_is_utf8 ? "CHARACTER SET utf8" : ''; + my $type = grep($_ eq $table, MYISAM_TABLES) ? 'MYISAM' : 'InnoDB'; + return ( + $self->SUPER::_get_create_table_ddl($table) . " ENGINE = $type $charset"); + +} #eosub--_get_create_table_ddl -} #eosub--_get_create_table_ddl #------------------------------------------------------------------------------ sub _get_create_index_ddl { - # Extend superclass method to create FULLTEXT indexes on text fields. - # Returns a "create index" SQL statement. - my($self, $table_name, $index_name, $index_fields, $index_type) = @_; + # Extend superclass method to create FULLTEXT indexes on text fields. + # Returns a "create index" SQL statement. + + my ($self, $table_name, $index_name, $index_fields, $index_type) = @_; + + my $sql = "CREATE "; + $sql .= "$index_type " + if ($index_type eq 'UNIQUE' || $index_type eq 'FULLTEXT'); + $sql .= "INDEX \`$index_name\` ON $table_name \(" + . join(", ", @$index_fields) . "\)"; - my $sql = "CREATE "; - $sql .= "$index_type " if ($index_type eq 'UNIQUE' - || $index_type eq 'FULLTEXT'); - $sql .= "INDEX \`$index_name\` ON $table_name \(" . - join(", ", @$index_fields) . "\)"; + return ($sql); - return($sql); +} #eosub--_get_create_index_ddl -} #eosub--_get_create_index_ddl #-------------------------------------------------------------------- sub get_create_database_sql { - my ($self, $name) = @_; - # We only create as utf8 if we have no params (meaning we're doing - # a new installation) or if the utf8 param is on. - my $create_utf8 = Bugzilla->params->{'utf8'} - || !defined Bugzilla->params->{'utf8'}; - my $charset = $create_utf8 ? "CHARACTER SET utf8" : ''; - return ("CREATE DATABASE $name $charset"); + my ($self, $name) = @_; + + # We only create as utf8 if we have no params (meaning we're doing + # a new installation) or if the utf8 param is on. + my $create_utf8 + = Bugzilla->params->{'utf8'} || !defined Bugzilla->params->{'utf8'}; + my $charset = $create_utf8 ? "CHARACTER SET utf8" : ''; + return ("CREATE DATABASE $name $charset"); } # MySQL has a simpler ALTER TABLE syntax than ANSI. sub get_alter_column_ddl { - my ($self, $table, $column, $new_def, $set_nulls_to) = @_; - my $old_def = $self->get_column($table, $column); - my %new_def_copy = %$new_def; - if ($old_def->{PRIMARYKEY} && $new_def->{PRIMARYKEY}) { - # If a column stays a primary key do NOT specify PRIMARY KEY in the - # ALTER TABLE statement. This avoids a MySQL error that two primary - # keys are not allowed. - delete $new_def_copy{PRIMARYKEY}; - } - - my @statements; - - push(@statements, "UPDATE $table SET $column = $set_nulls_to - WHERE $column IS NULL") if defined $set_nulls_to; - - # Calling SET DEFAULT or DROP DEFAULT is *way* faster than calling - # CHANGE COLUMN, so just do that if we're just changing the default. - my %old_defaultless = %$old_def; - my %new_defaultless = %$new_def; - delete $old_defaultless{DEFAULT}; - delete $new_defaultless{DEFAULT}; - if (!$self->columns_equal($old_def, $new_def) - && $self->columns_equal(\%new_defaultless, \%old_defaultless)) - { - if (!defined $new_def->{DEFAULT}) { - push(@statements, - "ALTER TABLE $table ALTER COLUMN $column DROP DEFAULT"); - } - else { - push(@statements, "ALTER TABLE $table ALTER COLUMN $column - SET DEFAULT " . $new_def->{DEFAULT}); - } + my ($self, $table, $column, $new_def, $set_nulls_to) = @_; + my $old_def = $self->get_column($table, $column); + my %new_def_copy = %$new_def; + if ($old_def->{PRIMARYKEY} && $new_def->{PRIMARYKEY}) { + + # If a column stays a primary key do NOT specify PRIMARY KEY in the + # ALTER TABLE statement. This avoids a MySQL error that two primary + # keys are not allowed. + delete $new_def_copy{PRIMARYKEY}; + } + + my @statements; + + push( + @statements, "UPDATE $table SET $column = $set_nulls_to + WHERE $column IS NULL" + ) if defined $set_nulls_to; + + # Calling SET DEFAULT or DROP DEFAULT is *way* faster than calling + # CHANGE COLUMN, so just do that if we're just changing the default. + my %old_defaultless = %$old_def; + my %new_defaultless = %$new_def; + delete $old_defaultless{DEFAULT}; + delete $new_defaultless{DEFAULT}; + if (!$self->columns_equal($old_def, $new_def) + && $self->columns_equal(\%new_defaultless, \%old_defaultless)) + { + if (!defined $new_def->{DEFAULT}) { + push(@statements, "ALTER TABLE $table ALTER COLUMN $column DROP DEFAULT"); } else { - my $new_ddl = $self->get_type_ddl(\%new_def_copy); - push(@statements, "ALTER TABLE $table CHANGE COLUMN - $column $column $new_ddl"); - } - - if ($old_def->{PRIMARYKEY} && !$new_def->{PRIMARYKEY}) { - # Dropping a PRIMARY KEY needs an explicit DROP PRIMARY KEY - push(@statements, "ALTER TABLE $table DROP PRIMARY KEY"); + push( + @statements, "ALTER TABLE $table ALTER COLUMN $column + SET DEFAULT " . $new_def->{DEFAULT} + ); } - - return @statements; + } + else { + my $new_ddl = $self->get_type_ddl(\%new_def_copy); + push( + @statements, "ALTER TABLE $table CHANGE COLUMN + $column $column $new_ddl" + ); + } + + if ($old_def->{PRIMARYKEY} && !$new_def->{PRIMARYKEY}) { + + # Dropping a PRIMARY KEY needs an explicit DROP PRIMARY KEY + push(@statements, "ALTER TABLE $table DROP PRIMARY KEY"); + } + + return @statements; } sub get_drop_fk_sql { - my ($self, $table, $column, $references) = @_; - my $fk_name = $self->_get_fk_name($table, $column, $references); - my @sql = ("ALTER TABLE $table DROP FOREIGN KEY $fk_name"); - my $dbh = Bugzilla->dbh; - - # MySQL requires, and will create, an index on any column with - # an FK. It will name it after the fk, which we never do. - # So if there's an index named after the fk, we also have to delete it. - if ($dbh->bz_index_info_real($table, $fk_name)) { - push(@sql, $self->get_drop_index_ddl($table, $fk_name)); - } - - return @sql; + my ($self, $table, $column, $references) = @_; + my $fk_name = $self->_get_fk_name($table, $column, $references); + my @sql = ("ALTER TABLE $table DROP FOREIGN KEY $fk_name"); + my $dbh = Bugzilla->dbh; + + # MySQL requires, and will create, an index on any column with + # an FK. It will name it after the fk, which we never do. + # So if there's an index named after the fk, we also have to delete it. + if ($dbh->bz_index_info_real($table, $fk_name)) { + push(@sql, $self->get_drop_index_ddl($table, $fk_name)); + } + + return @sql; } sub get_drop_index_ddl { - my ($self, $table, $name) = @_; - return ("DROP INDEX \`$name\` ON $table"); + my ($self, $table, $name) = @_; + return ("DROP INDEX \`$name\` ON $table"); } # A special function for MySQL, for renaming a lot of indexes. -# Index renames is a hash, where the key is a string - the +# Index renames is a hash, where the key is a string - the # old names of the index, and the value is a hash - the index # definition that we're renaming to, with an extra key of "NAME" # that contains the new index name. # The indexes in %indexes must be in hashref format. sub get_rename_indexes_ddl { - my ($self, $table, %indexes) = @_; - my @keys = keys %indexes or return (); - - my $sql = "ALTER TABLE $table "; - - foreach my $old_name (@keys) { - my $name = $indexes{$old_name}->{NAME}; - my $type = $indexes{$old_name}->{TYPE}; - $type ||= 'INDEX'; - my $fields = join(',', @{$indexes{$old_name}->{FIELDS}}); - # $old_name needs to be escaped, sometimes, because it was - # a reserved word. - $old_name = '`' . $old_name . '`'; - $sql .= " ADD $type $name ($fields), DROP INDEX $old_name,"; - } - # Remove the last comma. - chop($sql); - return ($sql); + my ($self, $table, %indexes) = @_; + my @keys = keys %indexes or return (); + + my $sql = "ALTER TABLE $table "; + + foreach my $old_name (@keys) { + my $name = $indexes{$old_name}->{NAME}; + my $type = $indexes{$old_name}->{TYPE}; + $type ||= 'INDEX'; + my $fields = join(',', @{$indexes{$old_name}->{FIELDS}}); + + # $old_name needs to be escaped, sometimes, because it was + # a reserved word. + $old_name = '`' . $old_name . '`'; + $sql .= " ADD $type $name ($fields), DROP INDEX $old_name,"; + } + + # Remove the last comma. + chop($sql); + return ($sql); } sub get_set_serial_sql { - my ($self, $table, $column, $value) = @_; - return ("ALTER TABLE $table AUTO_INCREMENT = $value"); + my ($self, $table, $column, $value) = @_; + return ("ALTER TABLE $table AUTO_INCREMENT = $value"); } # Converts a DBI column_info output to an abstract column definition. @@ -263,124 +287,137 @@ sub get_set_serial_sql { # although there's a chance that it will also work properly if called # elsewhere. sub column_info_to_column { - my ($self, $column_info) = @_; - - # Unfortunately, we have to break Schema's normal "no database" - # barrier a few times in this function. - my $dbh = Bugzilla->dbh; - - my $table = $column_info->{TABLE_NAME}; - my $col_name = $column_info->{COLUMN_NAME}; - - my $column = {}; - - ($column->{NOTNULL} = 1) if $column_info->{NULLABLE} == 0; - - if ($column_info->{mysql_is_pri_key}) { - # In MySQL, if a table has no PK, but it has a UNIQUE index, - # that index will show up as the PK. So we have to eliminate - # that possibility. - # Unfortunately, the only way to definitely solve this is - # to break Schema's standard of not touching the live database - # and check if the index called PRIMARY is on that field. - my $pri_index = $dbh->bz_index_info_real($table, 'PRIMARY'); - if ( $pri_index && grep($_ eq $col_name, @{$pri_index->{FIELDS}}) ) { - $column->{PRIMARYKEY} = 1; - } - } + my ($self, $column_info) = @_; - # MySQL frequently defines a default for a field even when we - # didn't explicitly set one. So we have to have some special - # hacks to determine whether or not we should actually put - # a default in the abstract schema for this field. - if (defined $column_info->{COLUMN_DEF}) { - # The defaults that MySQL inputs automatically are usually - # something that would be considered "false" by perl, either - # a 0 or an empty string. (Except for datetime and decimal - # fields, which have their own special auto-defaults.) - # - # Here's how we handle this: If it exists in the schema - # without a default, then we don't use the default. If it - # doesn't exist in the schema, then we're either going to - # be dropping it soon, or it's a custom end-user column, in which - # case having a bogus default won't harm anything. - my $schema_column = $self->get_column($table, $col_name); - unless ( (!$column_info->{COLUMN_DEF} - || $column_info->{COLUMN_DEF} eq '0000-00-00 00:00:00' - || $column_info->{COLUMN_DEF} eq '0.00') - && $schema_column - && !exists $schema_column->{DEFAULT}) { - - my $default = $column_info->{COLUMN_DEF}; - # Schema uses '0' for the defaults for decimal fields. - $default = 0 if $default =~ /^0\.0+$/; - # If we're not a number, we're a string and need to be - # quoted. - $default = $dbh->quote($default) if !($default =~ /^(-)?([0-9]+)(\.[0-9]+)?$/); - $column->{DEFAULT} = $default; - } - } + # Unfortunately, we have to break Schema's normal "no database" + # barrier a few times in this function. + my $dbh = Bugzilla->dbh; - my $type = $column_info->{TYPE_NAME}; + my $table = $column_info->{TABLE_NAME}; + my $col_name = $column_info->{COLUMN_NAME}; - # Certain types of columns need the size/precision appended. - if ($type =~ /CHAR$/ || $type eq 'DECIMAL') { - # This is nicely lowercase and has the size/precision appended. - $type = $column_info->{mysql_type_name}; - } + my $column = {}; - # If we're a tinyint, we could be either a BOOLEAN or an INT1. - # Only the BOOLEAN_MAP knows the difference. - elsif ($type eq 'TINYINT' && exists BOOLEAN_MAP->{$table} - && exists BOOLEAN_MAP->{$table}->{$col_name}) { - $type = 'BOOLEAN'; - if (exists $column->{DEFAULT}) { - $column->{DEFAULT} = $column->{DEFAULT} ? 'TRUE' : 'FALSE'; - } - } + ($column->{NOTNULL} = 1) if $column_info->{NULLABLE} == 0; - # We also need to check if we're an auto_increment field. - elsif ($type =~ /INT/) { - # Unfortunately, the only way to do this in DBI is to query the - # database, so we have to break the rule here that Schema normally - # doesn't touch the live DB. - my $ref_sth = $dbh->prepare( - "SELECT $col_name FROM $table LIMIT 1"); - $ref_sth->execute; - if ($ref_sth->{mysql_is_auto_increment}->[0]) { - if ($type eq 'MEDIUMINT') { - $type = 'MEDIUMSERIAL'; - } - elsif ($type eq 'SMALLINT') { - $type = 'SMALLSERIAL'; - } - else { - $type = 'INTSERIAL'; - } - } - $ref_sth->finish; + if ($column_info->{mysql_is_pri_key}) { + # In MySQL, if a table has no PK, but it has a UNIQUE index, + # that index will show up as the PK. So we have to eliminate + # that possibility. + # Unfortunately, the only way to definitely solve this is + # to break Schema's standard of not touching the live database + # and check if the index called PRIMARY is on that field. + my $pri_index = $dbh->bz_index_info_real($table, 'PRIMARY'); + if ($pri_index && grep($_ eq $col_name, @{$pri_index->{FIELDS}})) { + $column->{PRIMARYKEY} = 1; } + } + + # MySQL frequently defines a default for a field even when we + # didn't explicitly set one. So we have to have some special + # hacks to determine whether or not we should actually put + # a default in the abstract schema for this field. + if (defined $column_info->{COLUMN_DEF}) { + + # The defaults that MySQL inputs automatically are usually + # something that would be considered "false" by perl, either + # a 0 or an empty string. (Except for datetime and decimal + # fields, which have their own special auto-defaults.) + # + # Here's how we handle this: If it exists in the schema + # without a default, then we don't use the default. If it + # doesn't exist in the schema, then we're either going to + # be dropping it soon, or it's a custom end-user column, in which + # case having a bogus default won't harm anything. + my $schema_column = $self->get_column($table, $col_name); + unless ( + ( + !$column_info->{COLUMN_DEF} + || $column_info->{COLUMN_DEF} eq '0000-00-00 00:00:00' + || $column_info->{COLUMN_DEF} eq '0.00' + ) + && $schema_column + && !exists $schema_column->{DEFAULT} + ) + { - # For all other db-specific types, check if they exist in - # REVERSE_MAPPING and use the type found there. - if (exists REVERSE_MAPPING->{$type}) { - $type = REVERSE_MAPPING->{$type}; + my $default = $column_info->{COLUMN_DEF}; + + # Schema uses '0' for the defaults for decimal fields. + $default = 0 if $default =~ /^0\.0+$/; + + # If we're not a number, we're a string and need to be + # quoted. + $default = $dbh->quote($default) if !($default =~ /^(-)?([0-9]+)(\.[0-9]+)?$/); + $column->{DEFAULT} = $default; + } + } + + my $type = $column_info->{TYPE_NAME}; + + # Certain types of columns need the size/precision appended. + if ($type =~ /CHAR$/ || $type eq 'DECIMAL') { + + # This is nicely lowercase and has the size/precision appended. + $type = $column_info->{mysql_type_name}; + } + + # If we're a tinyint, we could be either a BOOLEAN or an INT1. + # Only the BOOLEAN_MAP knows the difference. + elsif ($type eq 'TINYINT' + && exists BOOLEAN_MAP->{$table} + && exists BOOLEAN_MAP->{$table}->{$col_name}) + { + $type = 'BOOLEAN'; + if (exists $column->{DEFAULT}) { + $column->{DEFAULT} = $column->{DEFAULT} ? 'TRUE' : 'FALSE'; + } + } + + # We also need to check if we're an auto_increment field. + elsif ($type =~ /INT/) { + + # Unfortunately, the only way to do this in DBI is to query the + # database, so we have to break the rule here that Schema normally + # doesn't touch the live DB. + my $ref_sth = $dbh->prepare("SELECT $col_name FROM $table LIMIT 1"); + $ref_sth->execute; + if ($ref_sth->{mysql_is_auto_increment}->[0]) { + if ($type eq 'MEDIUMINT') { + $type = 'MEDIUMSERIAL'; + } + elsif ($type eq 'SMALLINT') { + $type = 'SMALLSERIAL'; + } + else { + $type = 'INTSERIAL'; + } } + $ref_sth->finish; - $column->{TYPE} = $type; + } - #print "$table.$col_name: " . Data::Dumper->Dump([$column]) . "\n"; + # For all other db-specific types, check if they exist in + # REVERSE_MAPPING and use the type found there. + if (exists REVERSE_MAPPING->{$type}) { + $type = REVERSE_MAPPING->{$type}; + } - return $column; + $column->{TYPE} = $type; + + #print "$table.$col_name: " . Data::Dumper->Dump([$column]) . "\n"; + + return $column; } sub get_rename_column_ddl { - my ($self, $table, $old_name, $new_name) = @_; - my $def = $self->get_type_ddl($self->get_column($table, $old_name)); - # MySQL doesn't like having the PRIMARY KEY statement in a rename. - $def =~ s/PRIMARY KEY//i; - return ("ALTER TABLE $table CHANGE COLUMN $old_name $new_name $def"); + my ($self, $table, $old_name, $new_name) = @_; + my $def = $self->get_type_ddl($self->get_column($table, $old_name)); + + # MySQL doesn't like having the PRIMARY KEY statement in a rename. + $def =~ s/PRIMARY KEY//i; + return ("ALTER TABLE $table CHANGE COLUMN $old_name $new_name $def"); } 1; diff --git a/Bugzilla/DB/Schema/Oracle.pm b/Bugzilla/DB/Schema/Oracle.pm index 8fb5479b1..416e9204b 100644 --- a/Bugzilla/DB/Schema/Oracle.pm +++ b/Bugzilla/DB/Schema/Oracle.pm @@ -21,8 +21,9 @@ use parent qw(Bugzilla::DB::Schema); use Carp qw(confess); use Bugzilla::Util; -use constant ADD_COLUMN => 'ADD'; +use constant ADD_COLUMN => 'ADD'; use constant MULTIPLE_FKS_IN_ALTER => 0; + # Whether this is true or not, this is what it needs to be in order for # hash_identifier to maintain backwards compatibility with versions before # 3.2rc2. @@ -31,123 +32,128 @@ use constant MAX_IDENTIFIER_LEN => 27; #------------------------------------------------------------------------------ sub _initialize { - my $self = shift; + my $self = shift; + + $self = $self->SUPER::_initialize(@_); - $self = $self->SUPER::_initialize(@_); + $self->{db_specific} = { - $self->{db_specific} = { + BOOLEAN => 'integer', + FALSE => '0', + TRUE => '1', - BOOLEAN => 'integer', - FALSE => '0', - TRUE => '1', + INT1 => 'integer', + INT2 => 'integer', + INT3 => 'integer', + INT4 => 'integer', - INT1 => 'integer', - INT2 => 'integer', - INT3 => 'integer', - INT4 => 'integer', + SMALLSERIAL => 'integer', + MEDIUMSERIAL => 'integer', + INTSERIAL => 'integer', - SMALLSERIAL => 'integer', - MEDIUMSERIAL => 'integer', - INTSERIAL => 'integer', + TINYTEXT => 'varchar(255)', + MEDIUMTEXT => 'varchar(4000)', + LONGTEXT => 'clob', - TINYTEXT => 'varchar(255)', - MEDIUMTEXT => 'varchar(4000)', - LONGTEXT => 'clob', + LONGBLOB => 'blob', - LONGBLOB => 'blob', + DATETIME => 'date', + DATE => 'date', + }; - DATETIME => 'date', - DATE => 'date', - }; + $self->_adjust_schema; - $self->_adjust_schema; + return $self; - return $self; +} #eosub--_initialize -} #eosub--_initialize #-------------------------------------------------------------------- sub get_table_ddl { - my $self = shift; - my $table = shift; - unshift @_, $table; - my @ddl = $self->SUPER::get_table_ddl(@_); - - my @fields = @{ $self->{abstract_schema}{$table}{FIELDS} || [] }; - while (@fields) { - my $field_name = shift @fields; - my $field_info = shift @fields; - # Create triggers to deal with empty string. - if ( $field_info->{TYPE} =~ /varchar|TEXT/i - && $field_info->{NOTNULL} ) { - push (@ddl, _get_notnull_trigger_ddl($table, $field_name)); - } - # Create sequences and triggers to emulate SERIAL datatypes. - if ( $field_info->{TYPE} =~ /SERIAL/i ) { - push (@ddl, $self->_get_create_seq_ddl($table, $field_name)); - } + my $self = shift; + my $table = shift; + unshift @_, $table; + my @ddl = $self->SUPER::get_table_ddl(@_); + + my @fields = @{$self->{abstract_schema}{$table}{FIELDS} || []}; + while (@fields) { + my $field_name = shift @fields; + my $field_info = shift @fields; + + # Create triggers to deal with empty string. + if ($field_info->{TYPE} =~ /varchar|TEXT/i && $field_info->{NOTNULL}) { + push(@ddl, _get_notnull_trigger_ddl($table, $field_name)); } - return @ddl; -} #eosub--get_table_ddl + # Create sequences and triggers to emulate SERIAL datatypes. + if ($field_info->{TYPE} =~ /SERIAL/i) { + push(@ddl, $self->_get_create_seq_ddl($table, $field_name)); + } + } + return @ddl; -# Extend superclass method to create Oracle Text indexes if index type +} #eosub--get_table_ddl + +# Extend superclass method to create Oracle Text indexes if index type # is FULLTEXT from schema. Returns a "create index" SQL statement. sub _get_create_index_ddl { - my ($self, $table_name, $index_name, $index_fields, $index_type) = @_; - $index_name = "idx_" . $self->_hash_identifier($index_name); - if ($index_type eq 'FULLTEXT') { - my $sql = "CREATE INDEX $index_name ON $table_name (" - . join(',',@$index_fields) - . ") INDEXTYPE IS CTXSYS.CONTEXT " - . " PARAMETERS('LEXER BZ_LEX SYNC(ON COMMIT)')" ; - return $sql; - } - - return($self->SUPER::_get_create_index_ddl($table_name, $index_name, - $index_fields, $index_type)); + my ($self, $table_name, $index_name, $index_fields, $index_type) = @_; + $index_name = "idx_" . $self->_hash_identifier($index_name); + if ($index_type eq 'FULLTEXT') { + my $sql + = "CREATE INDEX $index_name ON $table_name (" + . join(',', @$index_fields) + . ") INDEXTYPE IS CTXSYS.CONTEXT " + . " PARAMETERS('LEXER BZ_LEX SYNC(ON COMMIT)')"; + return $sql; + } + + return ($self->SUPER::_get_create_index_ddl( + $table_name, $index_name, $index_fields, $index_type + )); } sub get_drop_index_ddl { - my $self = shift; - my ($table, $name) = @_; + my $self = shift; + my ($table, $name) = @_; - $name = 'idx_' . $self->_hash_identifier($name); - return $self->SUPER::get_drop_index_ddl($table, $name); + $name = 'idx_' . $self->_hash_identifier($name); + return $self->SUPER::get_drop_index_ddl($table, $name); } -# Oracle supports the use of FOREIGN KEY integrity constraints +# Oracle supports the use of FOREIGN KEY integrity constraints # to define the referential integrity actions, including: # - Update and delete No Action (default) # - Delete CASCADE # - Delete SET NULL sub get_fk_ddl { - my $self = shift; - my $ddl = $self->SUPER::get_fk_ddl(@_); + my $self = shift; + my $ddl = $self->SUPER::get_fk_ddl(@_); - # iThe Bugzilla Oracle driver implements UPDATE via a trigger. - $ddl =~ s/ON UPDATE \S+//i; - # RESTRICT is the default for DELETE on Oracle and may not be specified. - $ddl =~ s/ON DELETE RESTRICT//i; + # iThe Bugzilla Oracle driver implements UPDATE via a trigger. + $ddl =~ s/ON UPDATE \S+//i; - return $ddl; + # RESTRICT is the default for DELETE on Oracle and may not be specified. + $ddl =~ s/ON DELETE RESTRICT//i; + + return $ddl; } sub get_add_fks_sql { - my $self = shift; - my ($table, $column_fks) = @_; - my @sql = $self->SUPER::get_add_fks_sql(@_); - - foreach my $column (keys %$column_fks) { - my $fk = $column_fks->{$column}; - next if $fk->{UPDATE} && uc($fk->{UPDATE}) ne 'CASCADE'; - my $fk_name = $self->_get_fk_name($table, $column, $fk); - my $to_column = $fk->{COLUMN}; - my $to_table = $fk->{TABLE}; - - my $trigger = <SUPER::get_add_fks_sql(@_); + + foreach my $column (keys %$column_fks) { + my $fk = $column_fks->{$column}; + next if $fk->{UPDATE} && uc($fk->{UPDATE}) ne 'CASCADE'; + my $fk_name = $self->_get_fk_name($table, $column, $fk); + my $to_column = $fk->{COLUMN}; + my $to_table = $fk->{TABLE}; + + my $trigger = <_get_fk_name(@_); - my @sql; - if (!$references->{UPDATE} || $references->{UPDATE} =~ /CASCADE/i) { - push(@sql, "DROP TRIGGER ${fk_name}_uc"); - } - push(@sql, $self->SUPER::get_drop_fk_sql(@_)); - return @sql; + my $self = shift; + my ($table, $column, $references) = @_; + my $fk_name = $self->_get_fk_name(@_); + my @sql; + if (!$references->{UPDATE} || $references->{UPDATE} =~ /CASCADE/i) { + push(@sql, "DROP TRIGGER ${fk_name}_uc"); + } + push(@sql, $self->SUPER::get_drop_fk_sql(@_)); + return @sql; } sub _get_fk_name { - my ($self, $table, $column, $references) = @_; - my $to_table = $references->{TABLE}; - my $to_column = $references->{COLUMN}; - my $fk_name = "${table}_${column}_${to_table}_${to_column}"; - $fk_name = "fk_" . $self->_hash_identifier($fk_name); - - return $fk_name; + my ($self, $table, $column, $references) = @_; + my $to_table = $references->{TABLE}; + my $to_column = $references->{COLUMN}; + my $fk_name = "${table}_${column}_${to_table}_${to_column}"; + $fk_name = "fk_" . $self->_hash_identifier($fk_name); + + return $fk_name; } sub get_add_column_ddl { - my $self = shift; - my ($table, $column, $definition, $init_value) = @_; - my @sql; - - # Create sequences and triggers to emulate SERIAL datatypes. - if ($definition->{TYPE} =~ /SERIAL/i) { - # Clone the definition to not alter the original one. - my %def = %$definition; - # Oracle requires to define the column is several steps. - my $pk = delete $def{PRIMARYKEY}; - my $notnull = delete $def{NOTNULL}; - @sql = $self->SUPER::get_add_column_ddl($table, $column, \%def, $init_value); - push(@sql, $self->_get_create_seq_ddl($table, $column)); - push(@sql, "UPDATE $table SET $column = ${table}_${column}_SEQ.NEXTVAL"); - push(@sql, "ALTER TABLE $table MODIFY $column NOT NULL") if $notnull; - push(@sql, "ALTER TABLE $table ADD PRIMARY KEY ($column)") if $pk; - } - else { - @sql = $self->SUPER::get_add_column_ddl(@_); - # Create triggers to deal with empty string. - if ($definition->{TYPE} =~ /varchar|TEXT/i && $definition->{NOTNULL}) { - push(@sql, _get_notnull_trigger_ddl($table, $column)); - } + my $self = shift; + my ($table, $column, $definition, $init_value) = @_; + my @sql; + + # Create sequences and triggers to emulate SERIAL datatypes. + if ($definition->{TYPE} =~ /SERIAL/i) { + + # Clone the definition to not alter the original one. + my %def = %$definition; + + # Oracle requires to define the column is several steps. + my $pk = delete $def{PRIMARYKEY}; + my $notnull = delete $def{NOTNULL}; + @sql = $self->SUPER::get_add_column_ddl($table, $column, \%def, $init_value); + push(@sql, $self->_get_create_seq_ddl($table, $column)); + push(@sql, "UPDATE $table SET $column = ${table}_${column}_SEQ.NEXTVAL"); + push(@sql, "ALTER TABLE $table MODIFY $column NOT NULL") if $notnull; + push(@sql, "ALTER TABLE $table ADD PRIMARY KEY ($column)") if $pk; + } + else { + @sql = $self->SUPER::get_add_column_ddl(@_); + + # Create triggers to deal with empty string. + if ($definition->{TYPE} =~ /varchar|TEXT/i && $definition->{NOTNULL}) { + push(@sql, _get_notnull_trigger_ddl($table, $column)); } + } - return @sql; + return @sql; } sub get_alter_column_ddl { - my ($self, $table, $column, $new_def, $set_nulls_to) = @_; - - my @statements; - my $old_def = $self->get_column_abstract($table, $column); - my $specific = $self->{db_specific}; - - # If the types have changed, we have to deal with that. - if (uc(trim($old_def->{TYPE})) ne uc(trim($new_def->{TYPE}))) { - push(@statements, $self->_get_alter_type_sql($table, $column, - $new_def, $old_def)); - } - - my $default = $new_def->{DEFAULT}; - my $default_old = $old_def->{DEFAULT}; - - if (defined $default) { - $default = $specific->{$default} if exists $specific->{$default}; - } - # This first condition prevents "uninitialized value" errors. - if (!defined $default && !defined $default_old) { - # Do Nothing - } - # If we went from having a default to not having one - elsif (!defined $default && defined $default_old) { - push(@statements, "ALTER TABLE $table MODIFY $column" - . " DEFAULT NULL"); - } - # If we went from no default to a default, or we changed the default. - elsif ( (defined $default && !defined $default_old) || - ($default ne $default_old) ) - { - push(@statements, "ALTER TABLE $table MODIFY $column " - . " DEFAULT $default"); - } - - # If we went from NULL to NOT NULL. - if (!$old_def->{NOTNULL} && $new_def->{NOTNULL}) { - my $setdefault; - # Handle any fields that were NULL before, if we have a default, - $setdefault = $default if defined $default; - # But if we have a set_nulls_to, that overrides the DEFAULT - # (although nobody would usually specify both a default and - # a set_nulls_to.) - $setdefault = $set_nulls_to if defined $set_nulls_to; - if (defined $setdefault) { - push(@statements, "UPDATE $table SET $column = $setdefault" - . " WHERE $column IS NULL"); - } - push(@statements, "ALTER TABLE $table MODIFY $column" - . " NOT NULL"); - push (@statements, _get_notnull_trigger_ddl($table, $column)) - if $old_def->{TYPE} =~ /varchar|text/i - && $new_def->{TYPE} =~ /varchar|text/i; - } - # If we went from NOT NULL to NULL - elsif ($old_def->{NOTNULL} && !$new_def->{NOTNULL}) { - push(@statements, "ALTER TABLE $table MODIFY $column" - . " NULL"); - push(@statements, "DROP TRIGGER ${table}_${column}") - if $new_def->{TYPE} =~ /varchar|text/i - && $old_def->{TYPE} =~ /varchar|text/i; - } - - # If we went from not being a PRIMARY KEY to being a PRIMARY KEY. - if (!$old_def->{PRIMARYKEY} && $new_def->{PRIMARYKEY}) { - push(@statements, "ALTER TABLE $table ADD PRIMARY KEY ($column)"); + my ($self, $table, $column, $new_def, $set_nulls_to) = @_; + + my @statements; + my $old_def = $self->get_column_abstract($table, $column); + my $specific = $self->{db_specific}; + + # If the types have changed, we have to deal with that. + if (uc(trim($old_def->{TYPE})) ne uc(trim($new_def->{TYPE}))) { + push(@statements, + $self->_get_alter_type_sql($table, $column, $new_def, $old_def)); + } + + my $default = $new_def->{DEFAULT}; + my $default_old = $old_def->{DEFAULT}; + + if (defined $default) { + $default = $specific->{$default} if exists $specific->{$default}; + } + + # This first condition prevents "uninitialized value" errors. + if (!defined $default && !defined $default_old) { + + # Do Nothing + } + + # If we went from having a default to not having one + elsif (!defined $default && defined $default_old) { + push(@statements, "ALTER TABLE $table MODIFY $column" . " DEFAULT NULL"); + } + + # If we went from no default to a default, or we changed the default. + elsif ((defined $default && !defined $default_old) + || ($default ne $default_old)) + { + push(@statements, "ALTER TABLE $table MODIFY $column " . " DEFAULT $default"); + } + + # If we went from NULL to NOT NULL. + if (!$old_def->{NOTNULL} && $new_def->{NOTNULL}) { + my $setdefault; + + # Handle any fields that were NULL before, if we have a default, + $setdefault = $default if defined $default; + + # But if we have a set_nulls_to, that overrides the DEFAULT + # (although nobody would usually specify both a default and + # a set_nulls_to.) + $setdefault = $set_nulls_to if defined $set_nulls_to; + if (defined $setdefault) { + push(@statements, + "UPDATE $table SET $column = $setdefault" . " WHERE $column IS NULL"); } - # If we went from being a PK to not being a PK - elsif ( $old_def->{PRIMARYKEY} && !$new_def->{PRIMARYKEY} ) { - push(@statements, "ALTER TABLE $table DROP PRIMARY KEY"); - } - - return @statements; + push(@statements, "ALTER TABLE $table MODIFY $column" . " NOT NULL"); + push(@statements, _get_notnull_trigger_ddl($table, $column)) + if $old_def->{TYPE} =~ /varchar|text/i && $new_def->{TYPE} =~ /varchar|text/i; + } + + # If we went from NOT NULL to NULL + elsif ($old_def->{NOTNULL} && !$new_def->{NOTNULL}) { + push(@statements, "ALTER TABLE $table MODIFY $column" . " NULL"); + push(@statements, "DROP TRIGGER ${table}_${column}") + if $new_def->{TYPE} =~ /varchar|text/i && $old_def->{TYPE} =~ /varchar|text/i; + } + + # If we went from not being a PRIMARY KEY to being a PRIMARY KEY. + if (!$old_def->{PRIMARYKEY} && $new_def->{PRIMARYKEY}) { + push(@statements, "ALTER TABLE $table ADD PRIMARY KEY ($column)"); + } + + # If we went from being a PK to not being a PK + elsif ($old_def->{PRIMARYKEY} && !$new_def->{PRIMARYKEY}) { + push(@statements, "ALTER TABLE $table DROP PRIMARY KEY"); + } + + return @statements; } sub _get_alter_type_sql { - my ($self, $table, $column, $new_def, $old_def) = @_; - my @statements; - - my $type = $new_def->{TYPE}; - $type = $self->{db_specific}->{$type} - if exists $self->{db_specific}->{$type}; - - if ($type =~ /serial/i && $old_def->{TYPE} !~ /serial/i) { - die("You cannot specify a DEFAULT on a SERIAL-type column.") - if $new_def->{DEFAULT}; + my ($self, $table, $column, $new_def, $old_def) = @_; + my @statements; + + my $type = $new_def->{TYPE}; + $type = $self->{db_specific}->{$type} if exists $self->{db_specific}->{$type}; + + if ($type =~ /serial/i && $old_def->{TYPE} !~ /serial/i) { + die("You cannot specify a DEFAULT on a SERIAL-type column.") + if $new_def->{DEFAULT}; + } + + if ( ($old_def->{TYPE} =~ /LONGTEXT/i && $new_def->{TYPE} !~ /LONGTEXT/i) + || ($old_def->{TYPE} !~ /LONGTEXT/i && $new_def->{TYPE} =~ /LONGTEXT/i)) + { + # LONG to VARCHAR or VARCHAR to LONG is not allowed in Oracle, + # just a way to work around. + # Determine whether column_temp is already exist. + my $dbh = Bugzilla->dbh; + my $column_exist = $dbh->selectcol_arrayref( + "SELECT CNAME FROM COL WHERE TNAME = UPPER(?) AND + CNAME = UPPER(?)", undef, $table, $column . "_temp" + ); + if (!@$column_exist) { + push(@statements, "ALTER TABLE $table ADD ${column}_temp $type"); } - - if ( ($old_def->{TYPE} =~ /LONGTEXT/i && $new_def->{TYPE} !~ /LONGTEXT/i) - || ($old_def->{TYPE} !~ /LONGTEXT/i && $new_def->{TYPE} =~ /LONGTEXT/i) - ) { - # LONG to VARCHAR or VARCHAR to LONG is not allowed in Oracle, - # just a way to work around. - # Determine whether column_temp is already exist. - my $dbh=Bugzilla->dbh; - my $column_exist = $dbh->selectcol_arrayref( - "SELECT CNAME FROM COL WHERE TNAME = UPPER(?) AND - CNAME = UPPER(?)", undef,$table,$column . "_temp"); - if(!@$column_exist) { - push(@statements, - "ALTER TABLE $table ADD ${column}_temp $type"); - } - push(@statements, "UPDATE $table SET ${column}_temp = $column"); - push(@statements, "COMMIT"); - push(@statements, "ALTER TABLE $table DROP COLUMN $column"); - push(@statements, - "ALTER TABLE $table RENAME COLUMN ${column}_temp TO $column"); - } else { - push(@statements, "ALTER TABLE $table MODIFY $column $type"); - } - - if ($new_def->{TYPE} =~ /serial/i && $old_def->{TYPE} !~ /serial/i) { - push(@statements, _get_create_seq_ddl($table, $column)); - } - - # If this column is no longer SERIAL, we need to drop the sequence - # that went along with it. - if ($old_def->{TYPE} =~ /serial/i && $new_def->{TYPE} !~ /serial/i) { - push(@statements, "DROP SEQUENCE ${table}_${column}_SEQ"); - push(@statements, "DROP TRIGGER ${table}_${column}_TR"); - } - - # If this column is changed to type TEXT/VARCHAR, we need to deal with - # empty string. - if ( $old_def->{TYPE} !~ /varchar|text/i - && $new_def->{TYPE} =~ /varchar|text/i - && $new_def->{NOTNULL} ) - { - push (@statements, _get_notnull_trigger_ddl($table, $column)); - } - # If this column is no longer TEXT/VARCHAR, we need to drop the trigger - # that went along with it. - if ( $old_def->{TYPE} =~ /varchar|text/i - && $old_def->{NOTNULL} - && $new_def->{TYPE} !~ /varchar|text/i ) - { - push(@statements, "DROP TRIGGER ${table}_${column}"); - } - return @statements; + push(@statements, "UPDATE $table SET ${column}_temp = $column"); + push(@statements, "COMMIT"); + push(@statements, "ALTER TABLE $table DROP COLUMN $column"); + push(@statements, "ALTER TABLE $table RENAME COLUMN ${column}_temp TO $column"); + } + else { + push(@statements, "ALTER TABLE $table MODIFY $column $type"); + } + + if ($new_def->{TYPE} =~ /serial/i && $old_def->{TYPE} !~ /serial/i) { + push(@statements, _get_create_seq_ddl($table, $column)); + } + + # If this column is no longer SERIAL, we need to drop the sequence + # that went along with it. + if ($old_def->{TYPE} =~ /serial/i && $new_def->{TYPE} !~ /serial/i) { + push(@statements, "DROP SEQUENCE ${table}_${column}_SEQ"); + push(@statements, "DROP TRIGGER ${table}_${column}_TR"); + } + + # If this column is changed to type TEXT/VARCHAR, we need to deal with + # empty string. + if ( $old_def->{TYPE} !~ /varchar|text/i + && $new_def->{TYPE} =~ /varchar|text/i + && $new_def->{NOTNULL}) + { + push(@statements, _get_notnull_trigger_ddl($table, $column)); + } + + # If this column is no longer TEXT/VARCHAR, we need to drop the trigger + # that went along with it. + if ( $old_def->{TYPE} =~ /varchar|text/i + && $old_def->{NOTNULL} + && $new_def->{TYPE} !~ /varchar|text/i) + { + push(@statements, "DROP TRIGGER ${table}_${column}"); + } + return @statements; } sub get_rename_column_ddl { - my ($self, $table, $old_name, $new_name) = @_; - if (lc($old_name) eq lc($new_name)) { - # if the only change is a case change, return an empty list. - return (); - } - my @sql = ("ALTER TABLE $table RENAME COLUMN $old_name TO $new_name"); - my $def = $self->get_column_abstract($table, $old_name); - if ($def->{TYPE} =~ /SERIAL/i) { - # We have to rename the series also, and fix the default of the series. - my $old_seq = "${table}_${old_name}_SEQ"; - my $new_seq = "${table}_${new_name}_SEQ"; - push(@sql, "RENAME $old_seq TO $new_seq"); - push(@sql, $self->_get_create_trigger_ddl($table, $new_name, $new_seq)); - push(@sql, "DROP TRIGGER ${table}_${old_name}_TR"); - } - if ($def->{TYPE} =~ /varchar|text/i && $def->{NOTNULL} ) { - push(@sql, _get_notnull_trigger_ddl($table,$new_name)); - push(@sql, "DROP TRIGGER ${table}_${old_name}"); - } - return @sql; + my ($self, $table, $old_name, $new_name) = @_; + if (lc($old_name) eq lc($new_name)) { + + # if the only change is a case change, return an empty list. + return (); + } + my @sql = ("ALTER TABLE $table RENAME COLUMN $old_name TO $new_name"); + my $def = $self->get_column_abstract($table, $old_name); + if ($def->{TYPE} =~ /SERIAL/i) { + + # We have to rename the series also, and fix the default of the series. + my $old_seq = "${table}_${old_name}_SEQ"; + my $new_seq = "${table}_${new_name}_SEQ"; + push(@sql, "RENAME $old_seq TO $new_seq"); + push(@sql, $self->_get_create_trigger_ddl($table, $new_name, $new_seq)); + push(@sql, "DROP TRIGGER ${table}_${old_name}_TR"); + } + if ($def->{TYPE} =~ /varchar|text/i && $def->{NOTNULL}) { + push(@sql, _get_notnull_trigger_ddl($table, $new_name)); + push(@sql, "DROP TRIGGER ${table}_${old_name}"); + } + return @sql; } sub get_drop_column_ddl { - my $self = shift; - my ($table, $column) = @_; - my @sql; - push(@sql, $self->SUPER::get_drop_column_ddl(@_)); - my $dbh=Bugzilla->dbh; - my $trigger_name = uc($table . "_" . $column); - my $exist_trigger = $dbh->selectcol_arrayref( - "SELECT OBJECT_NAME FROM USER_OBJECTS - WHERE OBJECT_NAME = ?", undef, $trigger_name); - if(@$exist_trigger) { - push(@sql, "DROP TRIGGER $trigger_name"); - } - # If this column is of type SERIAL, we need to drop the sequence - # and trigger that went along with it. - my $def = $self->get_column_abstract($table, $column); - if ($def->{TYPE} =~ /SERIAL/i) { - push(@sql, "DROP SEQUENCE ${table}_${column}_SEQ"); - push(@sql, "DROP TRIGGER ${table}_${column}_TR"); - } - return @sql; + my $self = shift; + my ($table, $column) = @_; + my @sql; + push(@sql, $self->SUPER::get_drop_column_ddl(@_)); + my $dbh = Bugzilla->dbh; + my $trigger_name = uc($table . "_" . $column); + my $exist_trigger = $dbh->selectcol_arrayref( + "SELECT OBJECT_NAME FROM USER_OBJECTS + WHERE OBJECT_NAME = ?", undef, $trigger_name + ); + if (@$exist_trigger) { + push(@sql, "DROP TRIGGER $trigger_name"); + } + + # If this column is of type SERIAL, we need to drop the sequence + # and trigger that went along with it. + my $def = $self->get_column_abstract($table, $column); + if ($def->{TYPE} =~ /SERIAL/i) { + push(@sql, "DROP SEQUENCE ${table}_${column}_SEQ"); + push(@sql, "DROP TRIGGER ${table}_${column}_TR"); + } + return @sql; } sub get_rename_table_sql { - my ($self, $old_name, $new_name) = @_; - if (lc($old_name) eq lc($new_name)) { - # if the only change is a case change, return an empty list. - return (); - } + my ($self, $old_name, $new_name) = @_; + if (lc($old_name) eq lc($new_name)) { + + # if the only change is a case change, return an empty list. + return (); + } - my @sql = ("ALTER TABLE $old_name RENAME TO $new_name"); - my @columns = $self->get_table_columns($old_name); - foreach my $column (@columns) { - my $def = $self->get_column_abstract($old_name, $column); - if ($def->{TYPE} =~ /SERIAL/i) { - # If there's a SERIAL column on this table, we also need - # to rename the sequence. - my $old_seq = "${old_name}_${column}_SEQ"; - my $new_seq = "${new_name}_${column}_SEQ"; - push(@sql, "RENAME $old_seq TO $new_seq"); - push(@sql, $self->_get_create_trigger_ddl($new_name, $column, $new_seq)); - push(@sql, "DROP TRIGGER ${old_name}_${column}_TR"); - } - if ($def->{TYPE} =~ /varchar|text/i && $def->{NOTNULL}) { - push(@sql, _get_notnull_trigger_ddl($new_name, $column)); - push(@sql, "DROP TRIGGER ${old_name}_${column}"); - } + my @sql = ("ALTER TABLE $old_name RENAME TO $new_name"); + my @columns = $self->get_table_columns($old_name); + foreach my $column (@columns) { + my $def = $self->get_column_abstract($old_name, $column); + if ($def->{TYPE} =~ /SERIAL/i) { + + # If there's a SERIAL column on this table, we also need + # to rename the sequence. + my $old_seq = "${old_name}_${column}_SEQ"; + my $new_seq = "${new_name}_${column}_SEQ"; + push(@sql, "RENAME $old_seq TO $new_seq"); + push(@sql, $self->_get_create_trigger_ddl($new_name, $column, $new_seq)); + push(@sql, "DROP TRIGGER ${old_name}_${column}_TR"); + } + if ($def->{TYPE} =~ /varchar|text/i && $def->{NOTNULL}) { + push(@sql, _get_notnull_trigger_ddl($new_name, $column)); + push(@sql, "DROP TRIGGER ${old_name}_${column}"); } + } - return @sql; + return @sql; } sub get_drop_table_ddl { - my ($self, $name) = @_; - my @sql; - - my @columns = $self->get_table_columns($name); - foreach my $column (@columns) { - my $def = $self->get_column_abstract($name, $column); - if ($def->{TYPE} =~ /SERIAL/i) { - # If there's a SERIAL column on this table, we also need - # to remove the sequence. - push(@sql, "DROP SEQUENCE ${name}_${column}_SEQ"); - } + my ($self, $name) = @_; + my @sql; + + my @columns = $self->get_table_columns($name); + foreach my $column (@columns) { + my $def = $self->get_column_abstract($name, $column); + if ($def->{TYPE} =~ /SERIAL/i) { + + # If there's a SERIAL column on this table, we also need + # to remove the sequence. + push(@sql, "DROP SEQUENCE ${name}_${column}_SEQ"); } - push(@sql, "DROP TABLE $name CASCADE CONSTRAINTS PURGE"); + } + push(@sql, "DROP TABLE $name CASCADE CONSTRAINTS PURGE"); - return @sql; + return @sql; } sub _get_notnull_trigger_ddl { - my ($table, $column) = @_; - - my $notnull_sql = "CREATE OR REPLACE TRIGGER " - . " ${table}_${column}" - . " BEFORE INSERT OR UPDATE ON ". $table - . " FOR EACH ROW" - . " BEGIN " - . " IF :NEW.". $column ." IS NULL THEN " - . " SELECT '" . Bugzilla::DB::Oracle->EMPTY_STRING - . "' INTO :NEW.". $column ." FROM DUAL; " - . " END IF; " - . " END ".$table.";"; - return $notnull_sql; + my ($table, $column) = @_; + + my $notnull_sql + = "CREATE OR REPLACE TRIGGER " + . " ${table}_${column}" + . " BEFORE INSERT OR UPDATE ON " + . $table + . " FOR EACH ROW" + . " BEGIN " + . " IF :NEW." + . $column + . " IS NULL THEN " + . " SELECT '" + . Bugzilla::DB::Oracle->EMPTY_STRING + . "' INTO :NEW." + . $column + . " FROM DUAL; " + . " END IF; " . " END " + . $table . ";"; + return $notnull_sql; } sub _get_create_seq_ddl { - my ($self, $table, $column, $start_with) = @_; - $start_with ||= 1; - my @ddl; - my $seq_name = "${table}_${column}_SEQ"; - my $seq_sql = "CREATE SEQUENCE $seq_name " - . " INCREMENT BY 1 " - . " START WITH $start_with " - . " NOMAXVALUE " - . " NOCYCLE " - . " NOCACHE"; - push (@ddl, $seq_sql); - push(@ddl, $self->_get_create_trigger_ddl($table, $column, $seq_name)); - - return @ddl; + my ($self, $table, $column, $start_with) = @_; + $start_with ||= 1; + my @ddl; + my $seq_name = "${table}_${column}_SEQ"; + my $seq_sql + = "CREATE SEQUENCE $seq_name " + . " INCREMENT BY 1 " + . " START WITH $start_with " + . " NOMAXVALUE " + . " NOCYCLE " + . " NOCACHE"; + push(@ddl, $seq_sql); + push(@ddl, $self->_get_create_trigger_ddl($table, $column, $seq_name)); + + return @ddl; } sub _get_create_trigger_ddl { - my ($self, $table, $column, $seq_name) = @_; - my $serial_sql = "CREATE OR REPLACE TRIGGER ${table}_${column}_TR " - . " BEFORE INSERT ON $table " - . " FOR EACH ROW " - . " BEGIN " - . " SELECT ${seq_name}.NEXTVAL " - . " INTO :NEW.$column FROM DUAL; " - . " END;"; - return $serial_sql; + my ($self, $table, $column, $seq_name) = @_; + my $serial_sql + = "CREATE OR REPLACE TRIGGER ${table}_${column}_TR " + . " BEFORE INSERT ON $table " + . " FOR EACH ROW " + . " BEGIN " + . " SELECT ${seq_name}.NEXTVAL " + . " INTO :NEW.$column FROM DUAL; " . " END;"; + return $serial_sql; } -sub get_set_serial_sql { - my ($self, $table, $column, $value) = @_; - my @sql; - my $seq_name = "${table}_${column}_SEQ"; - push(@sql, "DROP SEQUENCE ${seq_name}"); - push(@sql, $self->_get_create_seq_ddl($table, $column, $value)); - return @sql; -} +sub get_set_serial_sql { + my ($self, $table, $column, $value) = @_; + my @sql; + my $seq_name = "${table}_${column}_SEQ"; + push(@sql, "DROP SEQUENCE ${seq_name}"); + push(@sql, $self->_get_create_seq_ddl($table, $column, $value)); + return @sql; +} 1; diff --git a/Bugzilla/DB/Schema/Pg.pm b/Bugzilla/DB/Schema/Pg.pm index 55a932272..e8f75f1e6 100644 --- a/Bugzilla/DB/Schema/Pg.pm +++ b/Bugzilla/DB/Schema/Pg.pm @@ -23,169 +23,196 @@ use Storable qw(dclone); #------------------------------------------------------------------------------ sub _initialize { - my $self = shift; - - $self = $self->SUPER::_initialize(@_); - - # Remove FULLTEXT index types from the schemas. - foreach my $table (keys %{ $self->{schema} }) { - if ($self->{schema}{$table}{INDEXES}) { - foreach my $index (@{ $self->{schema}{$table}{INDEXES} }) { - if (ref($index) eq 'HASH') { - delete($index->{TYPE}) if (exists $index->{TYPE} - && $index->{TYPE} eq 'FULLTEXT'); - } - } - foreach my $index (@{ $self->{abstract_schema}{$table}{INDEXES} }) { - if (ref($index) eq 'HASH') { - delete($index->{TYPE}) if (exists $index->{TYPE} - && $index->{TYPE} eq 'FULLTEXT'); - } - } + my $self = shift; + + $self = $self->SUPER::_initialize(@_); + + # Remove FULLTEXT index types from the schemas. + foreach my $table (keys %{$self->{schema}}) { + if ($self->{schema}{$table}{INDEXES}) { + foreach my $index (@{$self->{schema}{$table}{INDEXES}}) { + if (ref($index) eq 'HASH') { + delete($index->{TYPE}) + if (exists $index->{TYPE} && $index->{TYPE} eq 'FULLTEXT'); + } + } + foreach my $index (@{$self->{abstract_schema}{$table}{INDEXES}}) { + if (ref($index) eq 'HASH') { + delete($index->{TYPE}) + if (exists $index->{TYPE} && $index->{TYPE} eq 'FULLTEXT'); } + } } + } - $self->{db_specific} = { + $self->{db_specific} = { - BOOLEAN => 'smallint', - FALSE => '0', - TRUE => '1', + BOOLEAN => 'smallint', + FALSE => '0', + TRUE => '1', - INT1 => 'integer', - INT2 => 'integer', - INT3 => 'integer', - INT4 => 'integer', + INT1 => 'integer', + INT2 => 'integer', + INT3 => 'integer', + INT4 => 'integer', - SMALLSERIAL => 'serial unique', - MEDIUMSERIAL => 'serial unique', - INTSERIAL => 'serial unique', + SMALLSERIAL => 'serial unique', + MEDIUMSERIAL => 'serial unique', + INTSERIAL => 'serial unique', - TINYTEXT => 'varchar(255)', - MEDIUMTEXT => 'text', - LONGTEXT => 'text', + TINYTEXT => 'varchar(255)', + MEDIUMTEXT => 'text', + LONGTEXT => 'text', - LONGBLOB => 'bytea', + LONGBLOB => 'bytea', - DATETIME => 'timestamp(0) without time zone', - DATE => 'date', - }; + DATETIME => 'timestamp(0) without time zone', + DATE => 'date', + }; - $self->_adjust_schema; + $self->_adjust_schema; - return $self; + return $self; + +} #eosub--_initialize -} #eosub--_initialize #-------------------------------------------------------------------- sub get_create_database_sql { - my ($self, $name) = @_; - # We only create as utf8 if we have no params (meaning we're doing - # a new installation) or if the utf8 param is on. - my $create_utf8 = Bugzilla->params->{'utf8'} - || !defined Bugzilla->params->{'utf8'}; - my $charset = $create_utf8 ? "ENCODING 'UTF8' TEMPLATE template0" : ''; - return ("CREATE DATABASE $name $charset"); + my ($self, $name) = @_; + + # We only create as utf8 if we have no params (meaning we're doing + # a new installation) or if the utf8 param is on. + my $create_utf8 + = Bugzilla->params->{'utf8'} || !defined Bugzilla->params->{'utf8'}; + my $charset = $create_utf8 ? "ENCODING 'UTF8' TEMPLATE template0" : ''; + return ("CREATE DATABASE $name $charset"); } sub get_rename_column_ddl { - my ($self, $table, $old_name, $new_name) = @_; - if (lc($old_name) eq lc($new_name)) { - # if the only change is a case change, return an empty list, since Pg - # is case-insensitive and will return an error about a duplicate name - return (); - } - my @sql = ("ALTER TABLE $table RENAME COLUMN $old_name TO $new_name"); - my $def = $self->get_column_abstract($table, $old_name); - if ($def->{TYPE} =~ /SERIAL/i) { - # We have to rename the series also. - push(@sql, "ALTER SEQUENCE ${table}_${old_name}_seq - RENAME TO ${table}_${new_name}_seq"); - } - return @sql; + my ($self, $table, $old_name, $new_name) = @_; + if (lc($old_name) eq lc($new_name)) { + + # if the only change is a case change, return an empty list, since Pg + # is case-insensitive and will return an error about a duplicate name + return (); + } + my @sql = ("ALTER TABLE $table RENAME COLUMN $old_name TO $new_name"); + my $def = $self->get_column_abstract($table, $old_name); + if ($def->{TYPE} =~ /SERIAL/i) { + + # We have to rename the series also. + push( + @sql, "ALTER SEQUENCE ${table}_${old_name}_seq + RENAME TO ${table}_${new_name}_seq" + ); + } + return @sql; } sub get_rename_table_sql { - my ($self, $old_name, $new_name) = @_; - if (lc($old_name) eq lc($new_name)) { - # if the only change is a case change, return an empty list, since Pg - # is case-insensitive and will return an error about a duplicate name - return (); + my ($self, $old_name, $new_name) = @_; + if (lc($old_name) eq lc($new_name)) { + + # if the only change is a case change, return an empty list, since Pg + # is case-insensitive and will return an error about a duplicate name + return (); + } + + my @sql = ("ALTER TABLE $old_name RENAME TO $new_name"); + + # If there's a SERIAL column on this table, we also need to rename the + # sequence. + # If there is a PRIMARY KEY, we need to rename it too. + my @columns = $self->get_table_columns($old_name); + foreach my $column (@columns) { + my $def = $self->get_column_abstract($old_name, $column); + if ($def->{TYPE} =~ /SERIAL/i) { + my $old_seq = "${old_name}_${column}_seq"; + my $new_seq = "${new_name}_${column}_seq"; + push(@sql, "ALTER SEQUENCE $old_seq RENAME TO $new_seq"); + push( + @sql, "ALTER TABLE $new_name ALTER COLUMN $column + SET DEFAULT NEXTVAL('$new_seq')" + ); } - - my @sql = ("ALTER TABLE $old_name RENAME TO $new_name"); - - # If there's a SERIAL column on this table, we also need to rename the - # sequence. - # If there is a PRIMARY KEY, we need to rename it too. - my @columns = $self->get_table_columns($old_name); - foreach my $column (@columns) { - my $def = $self->get_column_abstract($old_name, $column); - if ($def->{TYPE} =~ /SERIAL/i) { - my $old_seq = "${old_name}_${column}_seq"; - my $new_seq = "${new_name}_${column}_seq"; - push(@sql, "ALTER SEQUENCE $old_seq RENAME TO $new_seq"); - push(@sql, "ALTER TABLE $new_name ALTER COLUMN $column - SET DEFAULT NEXTVAL('$new_seq')"); - } - if ($def->{PRIMARYKEY}) { - my $old_pk = "${old_name}_pkey"; - my $new_pk = "${new_name}_pkey"; - push(@sql, "ALTER INDEX $old_pk RENAME to $new_pk"); - } + if ($def->{PRIMARYKEY}) { + my $old_pk = "${old_name}_pkey"; + my $new_pk = "${new_name}_pkey"; + push(@sql, "ALTER INDEX $old_pk RENAME to $new_pk"); } + } - return @sql; + return @sql; } sub get_set_serial_sql { - my ($self, $table, $column, $value) = @_; - return ("SELECT setval('${table}_${column}_seq', $value, false) - FROM $table"); + my ($self, $table, $column, $value) = @_; + return ( + "SELECT setval('${table}_${column}_seq', $value, false) + FROM $table" + ); } sub _get_alter_type_sql { - my ($self, $table, $column, $new_def, $old_def) = @_; - my @statements; - - my $type = $new_def->{TYPE}; - $type = $self->{db_specific}->{$type} - if exists $self->{db_specific}->{$type}; - - if ($type =~ /serial/i && $old_def->{TYPE} !~ /serial/i) { - die("You cannot specify a DEFAULT on a SERIAL-type column.") - if $new_def->{DEFAULT}; - } - - $type =~ s/\bserial\b/integer/i; - - # On Pg, you don't need UNIQUE if you're a PK--it creates - # two identical indexes otherwise. - $type =~ s/unique//i if $new_def->{PRIMARYKEY}; - - push(@statements, "ALTER TABLE $table ALTER COLUMN $column - TYPE $type"); - - if ($new_def->{TYPE} =~ /serial/i && $old_def->{TYPE} !~ /serial/i) { - push(@statements, "CREATE SEQUENCE ${table}_${column}_seq - OWNED BY $table.$column"); - push(@statements, "SELECT setval('${table}_${column}_seq', + my ($self, $table, $column, $new_def, $old_def) = @_; + my @statements; + + my $type = $new_def->{TYPE}; + $type = $self->{db_specific}->{$type} if exists $self->{db_specific}->{$type}; + + if ($type =~ /serial/i && $old_def->{TYPE} !~ /serial/i) { + die("You cannot specify a DEFAULT on a SERIAL-type column.") + if $new_def->{DEFAULT}; + } + + $type =~ s/\bserial\b/integer/i; + + # On Pg, you don't need UNIQUE if you're a PK--it creates + # two identical indexes otherwise. + $type =~ s/unique//i if $new_def->{PRIMARYKEY}; + + push( + @statements, "ALTER TABLE $table ALTER COLUMN $column + TYPE $type" + ); + + if ($new_def->{TYPE} =~ /serial/i && $old_def->{TYPE} !~ /serial/i) { + push( + @statements, "CREATE SEQUENCE ${table}_${column}_seq + OWNED BY $table.$column" + ); + push( + @statements, "SELECT setval('${table}_${column}_seq', MAX($table.$column)) - FROM $table"); - push(@statements, "ALTER TABLE $table ALTER COLUMN $column - SET DEFAULT nextval('${table}_${column}_seq')"); - } - - # If this column is no longer SERIAL, we need to drop the sequence - # that went along with it. - if ($old_def->{TYPE} =~ /serial/i && $new_def->{TYPE} !~ /serial/i) { - push(@statements, "ALTER TABLE $table ALTER COLUMN $column - DROP DEFAULT"); - push(@statements, "ALTER SEQUENCE ${table}_${column}_seq - OWNED BY NONE"); - push(@statements, "DROP SEQUENCE ${table}_${column}_seq"); - } + FROM $table" + ); + push( + @statements, "ALTER TABLE $table ALTER COLUMN $column + SET DEFAULT nextval('${table}_${column}_seq')" + ); + } + + # If this column is no longer SERIAL, we need to drop the sequence + # that went along with it. + if ($old_def->{TYPE} =~ /serial/i && $new_def->{TYPE} !~ /serial/i) { + push( + @statements, "ALTER TABLE $table ALTER COLUMN $column + DROP DEFAULT" + ); + push( + @statements, "ALTER SEQUENCE ${table}_${column}_seq + OWNED BY NONE" + ); + push(@statements, "DROP SEQUENCE ${table}_${column}_seq"); + } + + return @statements; +} - return @statements; +sub get_drop_table_ddl { + my ($self, $table) = @_; + return ("DROP TABLE $table CASCADE"); } 1; @@ -202,4 +229,6 @@ sub _get_alter_type_sql { =item get_set_serial_sql +=item get_drop_table_ddl + =back diff --git a/Bugzilla/DB/Schema/Sqlite.pm b/Bugzilla/DB/Schema/Sqlite.pm index ccdbfd8aa..b00a4f395 100644 --- a/Bugzilla/DB/Schema/Sqlite.pm +++ b/Bugzilla/DB/Schema/Sqlite.pm @@ -22,37 +22,37 @@ use constant FK_ON_CREATE => 1; sub _initialize { - my $self = shift; + my $self = shift; - $self = $self->SUPER::_initialize(@_); + $self = $self->SUPER::_initialize(@_); - $self->{db_specific} = { - BOOLEAN => 'integer', - FALSE => '0', - TRUE => '1', + $self->{db_specific} = { + BOOLEAN => 'integer', + FALSE => '0', + TRUE => '1', - INT1 => 'integer', - INT2 => 'integer', - INT3 => 'integer', - INT4 => 'integer', + INT1 => 'integer', + INT2 => 'integer', + INT3 => 'integer', + INT4 => 'integer', - SMALLSERIAL => 'SERIAL', - MEDIUMSERIAL => 'SERIAL', - INTSERIAL => 'SERIAL', + SMALLSERIAL => 'SERIAL', + MEDIUMSERIAL => 'SERIAL', + INTSERIAL => 'SERIAL', - TINYTEXT => 'text', - MEDIUMTEXT => 'text', - LONGTEXT => 'text', + TINYTEXT => 'text', + MEDIUMTEXT => 'text', + LONGTEXT => 'text', - LONGBLOB => 'blob', + LONGBLOB => 'blob', - DATETIME => 'DATETIME', - DATE => 'DATETIME', - }; + DATETIME => 'DATETIME', + DATE => 'DATETIME', + }; - $self->_adjust_schema; + $self->_adjust_schema; - return $self; + return $self; } @@ -61,83 +61,86 @@ sub _initialize { ################################# sub _sqlite_create_table { - my ($self, $table) = @_; - return scalar Bugzilla->dbh->selectrow_array( - "SELECT sql FROM sqlite_master WHERE name = ? AND type = 'table'", - undef, $table); + my ($self, $table) = @_; + return + scalar Bugzilla->dbh->selectrow_array( + "SELECT sql FROM sqlite_master WHERE name = ? AND type = 'table'", + undef, $table); } sub _sqlite_table_lines { - my $self = shift; - my $table_sql = $self->_sqlite_create_table(@_); - $table_sql =~ s/\n*\)$//s; - # The $ makes this work even if people some day add crazy stuff to their - # schema like multi-column foreign keys. - return split(/,\s*$/m, $table_sql); + my $self = shift; + my $table_sql = $self->_sqlite_create_table(@_); + $table_sql =~ s/\n*\)$//s; + + # The $ makes this work even if people some day add crazy stuff to their + # schema like multi-column foreign keys. + return split(/,\s*$/m, $table_sql); } # This does most of the "heavy lifting" of the schema-altering functions. sub _sqlite_alter_schema { - my ($self, $table, $create_table, $options) = @_; - - # $create_table is sometimes an array in the form that _sqlite_table_lines - # returns. - if (ref $create_table) { - $create_table = join(',', @$create_table) . "\n)"; - } - - my $dbh = Bugzilla->dbh; - - my $random = generate_random_password(5); - my $rename_to = "${table}_$random"; - - my @columns = $dbh->bz_table_columns_real($table); - push(@columns, $options->{extra_column}) if $options->{extra_column}; - if (my $exclude = $options->{exclude_column}) { - @columns = grep { $_ ne $exclude } @columns; - } - my @insert_cols = @columns; - my @select_cols = @columns; - if (my $rename = $options->{rename}) { - foreach my $from (keys %$rename) { - my $to = $rename->{$from}; - @insert_cols = map { $_ eq $from ? $to : $_ } @insert_cols; - } + my ($self, $table, $create_table, $options) = @_; + + # $create_table is sometimes an array in the form that _sqlite_table_lines + # returns. + if (ref $create_table) { + $create_table = join(',', @$create_table) . "\n)"; + } + + my $dbh = Bugzilla->dbh; + + my $random = generate_random_password(5); + my $rename_to = "${table}_$random"; + + my @columns = $dbh->bz_table_columns_real($table); + push(@columns, $options->{extra_column}) if $options->{extra_column}; + if (my $exclude = $options->{exclude_column}) { + @columns = grep { $_ ne $exclude } @columns; + } + my @insert_cols = @columns; + my @select_cols = @columns; + if (my $rename = $options->{rename}) { + foreach my $from (keys %$rename) { + my $to = $rename->{$from}; + @insert_cols = map { $_ eq $from ? $to : $_ } @insert_cols; } - - my $insert_str = join(',', @insert_cols); - my $select_str = join(',', @select_cols); - my $copy_sql = "INSERT INTO $table ($insert_str)" - . " SELECT $select_str FROM $rename_to"; - - # We have to turn FKs off before doing this. Otherwise, when we rename - # the table, all of the FKs in the other tables will be automatically - # updated to point to the renamed table. Note that PRAGMA foreign_keys - # can only be set outside of a transaction--otherwise it is a no-op. - if ($dbh->bz_in_transaction) { - die "can't alter the schema inside of a transaction"; - } - my @sql = ( - 'PRAGMA foreign_keys = OFF', - 'BEGIN EXCLUSIVE TRANSACTION', - @{ $options->{pre_sql} || [] }, - "ALTER TABLE $table RENAME TO $rename_to", - $create_table, - $copy_sql, - "DROP TABLE $rename_to", - 'COMMIT TRANSACTION', - 'PRAGMA foreign_keys = ON', - ); + } + + my $insert_str = join(',', @insert_cols); + my $select_str = join(',', @select_cols); + my $copy_sql + = "INSERT INTO $table ($insert_str)" . " SELECT $select_str FROM $rename_to"; + + # We have to turn FKs off before doing this. Otherwise, when we rename + # the table, all of the FKs in the other tables will be automatically + # updated to point to the renamed table. Note that PRAGMA foreign_keys + # can only be set outside of a transaction--otherwise it is a no-op. + if ($dbh->bz_in_transaction) { + die "can't alter the schema inside of a transaction"; + } + my @sql = ( + 'PRAGMA foreign_keys = OFF', + 'BEGIN EXCLUSIVE TRANSACTION', + @{$options->{pre_sql} || []}, + "ALTER TABLE $table RENAME TO $rename_to", + $create_table, + $copy_sql, + "DROP TABLE $rename_to", + 'COMMIT TRANSACTION', + 'PRAGMA foreign_keys = ON', + ); } # For finding a particular column's definition in a CREATE TABLE statement. sub _sqlite_column_regex { - my ($column) = @_; - # 1 = Comma at start - # 2 = Column name + Space - # 3 = Definition - # 4 = Ending comma - return qr/(^|,)(\s\Q$column\E\s+)(.*?)(,|$)/m; + my ($column) = @_; + + # 1 = Comma at start + # 2 = Column name + Space + # 3 = Definition + # 4 = Ending comma + return qr/(^|,)(\s\Q$column\E\s+)(.*?)(,|$)/m; } ############################# @@ -145,133 +148,140 @@ sub _sqlite_column_regex { ############################# sub get_create_database_sql { - # If we get here, it means there was some error creating the - # database file during bz_create_database in Bugzilla::DB, - # and we just want to display that error instead of doing - # anything else. - Bugzilla->dbh; - die "Reached an unreachable point"; + + # If we get here, it means there was some error creating the + # database file during bz_create_database in Bugzilla::DB, + # and we just want to display that error instead of doing + # anything else. + Bugzilla->dbh; + die "Reached an unreachable point"; } sub _get_create_table_ddl { - my $self = shift; - my ($table) = @_; - my $ddl = $self->SUPER::_get_create_table_ddl(@_); - - # TheSchwartz uses its own driver to access its tables, meaning - # that it doesn't understand "COLLATE bugzilla" and in fact - # SQLite throws an error when TheSchwartz tries to access its - # own tables, if COLLATE bugzilla is on them. We don't have - # to fix this elsewhere currently, because we only create - # TheSchwartz's tables, we never modify them. - if ($table =~ /^ts_/) { - $ddl =~ s/ COLLATE bugzilla//g; - } - return $ddl; + my $self = shift; + my ($table) = @_; + my $ddl = $self->SUPER::_get_create_table_ddl(@_); + + # TheSchwartz uses its own driver to access its tables, meaning + # that it doesn't understand "COLLATE bugzilla" and in fact + # SQLite throws an error when TheSchwartz tries to access its + # own tables, if COLLATE bugzilla is on them. We don't have + # to fix this elsewhere currently, because we only create + # TheSchwartz's tables, we never modify them. + if ($table =~ /^ts_/) { + $ddl =~ s/ COLLATE bugzilla//g; + } + + $ddl =~ s/NOW\(\)/CURRENT_TIMESTAMP/g; + return $ddl; } sub get_type_ddl { - my $self = shift; - my $def = dclone($_[0]); - - my $ddl = $self->SUPER::get_type_ddl(@_); - if ($def->{PRIMARYKEY} and $def->{TYPE} =~ /SERIAL/i) { - $ddl =~ s/\bSERIAL\b/integer/; - $ddl =~ s/\bPRIMARY KEY\b/PRIMARY KEY AUTOINCREMENT/; - } - if ($def->{TYPE} =~ /text/i or $def->{TYPE} =~ /char/i) { - $ddl .= " COLLATE bugzilla"; - } - # Don't collate DATETIME fields. - if ($def->{TYPE} eq 'DATETIME') { - $ddl =~ s/\bDATETIME\b/text COLLATE BINARY/; - } - return $ddl; + my $self = shift; + my $def = dclone($_[0]); + + my $ddl = $self->SUPER::get_type_ddl(@_); + if ($def->{PRIMARYKEY} and $def->{TYPE} =~ /SERIAL/i) { + $ddl =~ s/\bSERIAL\b/integer/; + $ddl =~ s/\bPRIMARY KEY\b/PRIMARY KEY AUTOINCREMENT/; + } + if ($def->{TYPE} =~ /text/i or $def->{TYPE} =~ /char/i) { + $ddl .= " COLLATE bugzilla"; + } + + # Don't collate DATETIME fields. + if ($def->{TYPE} eq 'DATETIME') { + $ddl =~ s/\bDATETIME\b/text COLLATE BINARY/; + $ddl =~ s/NOW\(\)/now/g; + } + return $ddl; } sub get_alter_column_ddl { - my $self = shift; - my ($table, $column, $new_def, $set_nulls_to) = @_; - my $dbh = Bugzilla->dbh; - - my $table_sql = $self->_sqlite_create_table($table); - my $new_ddl = $self->get_type_ddl($new_def); - # When we do ADD COLUMN, columns can show up all on one line separated - # by commas, so we have to account for that. - my $column_regex = _sqlite_column_regex($column); - $table_sql =~ s/$column_regex/$1$2$new_ddl$4/ - || die "couldn't find $column in $table:\n$table_sql"; - my @pre_sql = $self->_set_nulls_sql(@_); - return $self->_sqlite_alter_schema($table, $table_sql, - { pre_sql => \@pre_sql }); + my $self = shift; + my ($table, $column, $new_def, $set_nulls_to) = @_; + my $dbh = Bugzilla->dbh; + + my $table_sql = $self->_sqlite_create_table($table); + my $new_ddl = $self->get_type_ddl($new_def); + + # When we do ADD COLUMN, columns can show up all on one line separated + # by commas, so we have to account for that. + my $column_regex = _sqlite_column_regex($column); + $table_sql =~ s/$column_regex/$1$2$new_ddl$4/ + || die "couldn't find $column in $table:\n$table_sql"; + my @pre_sql = $self->_set_nulls_sql(@_); + return $self->_sqlite_alter_schema($table, $table_sql, {pre_sql => \@pre_sql}); } sub get_add_column_ddl { - my $self = shift; - my ($table, $column, $definition, $init_value) = @_; - # SQLite can use the normal ADD COLUMN when: - # * The column isn't a PK - if ($definition->{PRIMARYKEY}) { - if ($definition->{NOTNULL} and $definition->{TYPE} !~ /SERIAL/i) { - die "You can only add new SERIAL type PKs with SQLite"; - } - my $table_sql = $self->_sqlite_new_column_sql(@_); - # This works because _sqlite_alter_schema will exclude the new column - # in its INSERT ... SELECT statement, meaning that when the "new" - # table is populated, it will have AUTOINCREMENT values generated - # for it. - return $self->_sqlite_alter_schema($table, $table_sql); - } - # * The column has a default one way or another. Either it - # defaults to NULL (it lacks NOT NULL) or it has a DEFAULT - # clause. Since we also require this when doing bz_add_column (in - # the way of forcing an init_value for NOT NULL columns with no - # default), we first set the init_value as the default and then - # alter the column. - if ($definition->{NOTNULL} and !defined $definition->{DEFAULT}) { - my %with_default = %$definition; - $with_default{DEFAULT} = $init_value; - my @pre_sql = - $self->SUPER::get_add_column_ddl($table, $column, \%with_default); - my $table_sql = $self->_sqlite_new_column_sql(@_); - return $self->_sqlite_alter_schema($table, $table_sql, - { pre_sql => \@pre_sql, extra_column => $column }); + my $self = shift; + my ($table, $column, $definition, $init_value) = @_; + + # SQLite can use the normal ADD COLUMN when: + # * The column isn't a PK + if ($definition->{PRIMARYKEY}) { + if ($definition->{NOTNULL} and $definition->{TYPE} !~ /SERIAL/i) { + die "You can only add new SERIAL type PKs with SQLite"; } - - return $self->SUPER::get_add_column_ddl(@_); + my $table_sql = $self->_sqlite_new_column_sql(@_); + + # This works because _sqlite_alter_schema will exclude the new column + # in its INSERT ... SELECT statement, meaning that when the "new" + # table is populated, it will have AUTOINCREMENT values generated + # for it. + return $self->_sqlite_alter_schema($table, $table_sql); + } + + # * The column has a default one way or another. Either it + # defaults to NULL (it lacks NOT NULL) or it has a DEFAULT + # clause. Since we also require this when doing bz_add_column (in + # the way of forcing an init_value for NOT NULL columns with no + # default), we first set the init_value as the default and then + # alter the column. + if ($definition->{NOTNULL} and !defined $definition->{DEFAULT}) { + my %with_default = %$definition; + $with_default{DEFAULT} = $init_value; + my @pre_sql = $self->SUPER::get_add_column_ddl($table, $column, \%with_default); + my $table_sql = $self->_sqlite_new_column_sql(@_); + return $self->_sqlite_alter_schema($table, $table_sql, + {pre_sql => \@pre_sql, extra_column => $column}); + } + + return $self->SUPER::get_add_column_ddl(@_); } sub _sqlite_new_column_sql { - my ($self, $table, $column, $def) = @_; - my $table_sql = $self->_sqlite_create_table($table); - my $new_ddl = $self->get_type_ddl($def); - my $new_line = "\t$column\t$new_ddl"; - $table_sql =~ s/^(CREATE TABLE \w+ \()/$1\n$new_line,/s - || die "Can't find start of CREATE TABLE:\n$table_sql"; - return $table_sql; + my ($self, $table, $column, $def) = @_; + my $table_sql = $self->_sqlite_create_table($table); + my $new_ddl = $self->get_type_ddl($def); + my $new_line = "\t$column\t$new_ddl"; + $table_sql =~ s/^(CREATE TABLE \w+ \()/$1\n$new_line,/s + || die "Can't find start of CREATE TABLE:\n$table_sql"; + return $table_sql; } sub get_drop_column_ddl { - my ($self, $table, $column) = @_; - my $table_sql = $self->_sqlite_create_table($table); - my $column_regex = _sqlite_column_regex($column); - $table_sql =~ s/$column_regex/$1/ - || die "Can't find column $column: $table_sql"; - # Make sure we don't end up with a comma at the end of the definition. - $table_sql =~ s/,\s+\)$/\n)/s; - return $self->_sqlite_alter_schema($table, $table_sql, - { exclude_column => $column }); + my ($self, $table, $column) = @_; + my $table_sql = $self->_sqlite_create_table($table); + my $column_regex = _sqlite_column_regex($column); + $table_sql =~ s/$column_regex/$1/ + || die "Can't find column $column: $table_sql"; + + # Make sure we don't end up with a comma at the end of the definition. + $table_sql =~ s/,\s+\)$/\n)/s; + return $self->_sqlite_alter_schema($table, $table_sql, + {exclude_column => $column}); } sub get_rename_column_ddl { - my ($self, $table, $old_name, $new_name) = @_; - my $table_sql = $self->_sqlite_create_table($table); - my $column_regex = _sqlite_column_regex($old_name); - $table_sql =~ s/$column_regex/$1\t$new_name\t$3$4/ - || die "Can't find $old_name: $table_sql"; - my %rename = ($old_name => $new_name); - return $self->_sqlite_alter_schema($table, $table_sql, - { rename => \%rename }); + my ($self, $table, $old_name, $new_name) = @_; + my $table_sql = $self->_sqlite_create_table($table); + my $column_regex = _sqlite_column_regex($old_name); + $table_sql =~ s/$column_regex/$1\t$new_name\t$3$4/ + || die "Can't find $old_name: $table_sql"; + my %rename = ($old_name => $new_name); + return $self->_sqlite_alter_schema($table, $table_sql, {rename => \%rename}); } ################ @@ -279,24 +289,23 @@ sub get_rename_column_ddl { ################ sub get_add_fks_sql { - my ($self, $table, $column_fks) = @_; - my @clauses = $self->_sqlite_table_lines($table); - my @add = $self->_column_fks_to_ddl($table, $column_fks); - push(@clauses, @add); - return $self->_sqlite_alter_schema($table, \@clauses); + my ($self, $table, $column_fks) = @_; + my @clauses = $self->_sqlite_table_lines($table); + my @add = $self->_column_fks_to_ddl($table, $column_fks); + push(@clauses, @add); + return $self->_sqlite_alter_schema($table, \@clauses); } sub get_drop_fk_sql { - my ($self, $table, $column, $references) = @_; - my @clauses = $self->_sqlite_table_lines($table); - my $fk_name = $self->_get_fk_name($table, $column, $references); - - my $line_re = qr/^\s+CONSTRAINT $fk_name /s; - grep { $line_re } @clauses - or die "Can't find $fk_name: " . join(',', @clauses); - @clauses = grep { $_ !~ $line_re } @clauses; - - return $self->_sqlite_alter_schema($table, \@clauses); + my ($self, $table, $column, $references) = @_; + my @clauses = $self->_sqlite_table_lines($table); + my $fk_name = $self->_get_fk_name($table, $column, $references); + + my $line_re = qr/^\s+CONSTRAINT $fk_name /s; + grep {$line_re} @clauses or die "Can't find $fk_name: " . join(',', @clauses); + @clauses = grep { $_ !~ $line_re } @clauses; + + return $self->_sqlite_alter_schema($table, \@clauses); } diff --git a/Bugzilla/DB/Sqlite.pm b/Bugzilla/DB/Sqlite.pm index a56ed31ad..55c544e97 100644 --- a/Bugzilla/DB/Sqlite.pm +++ b/Bugzilla/DB/Sqlite.pm @@ -46,23 +46,23 @@ sub _sqlite_collate_ci { lc($_[0]) cmp lc($_[1]) } sub _sqlite_mod { $_[0] % $_[1] } sub _sqlite_now { - my $now = DateTime->now(time_zone => Bugzilla->local_timezone); - return $now->ymd . ' ' . $now->hms; + my $now = DateTime->now(time_zone => Bugzilla->local_timezone); + return $now->ymd . ' ' . $now->hms; } # SQL's POSITION starts its values from 1 instead of 0 (so we add 1). sub _sqlite_position { - my ($text, $fragment) = @_; - if (!defined $text or !defined $fragment) { - return undef; - } - my $pos = index $text, $fragment; - return $pos + 1; + my ($text, $fragment) = @_; + if (!defined $text or !defined $fragment) { + return undef; + } + my $pos = index $text, $fragment; + return $pos + 1; } sub _sqlite_position_ci { - my ($text, $fragment) = @_; - return _sqlite_position(lc($text), lc($fragment)); + my ($text, $fragment) = @_; + return _sqlite_position(lc($text), lc($fragment)); } ############### @@ -70,76 +70,88 @@ sub _sqlite_position_ci { ############### sub new { - my ($class, $params) = @_; - my $db_name = $params->{db_name}; - - # Let people specify paths intead of data/ for the DB. - if ($db_name and $db_name !~ m{[\\/]}) { - # When the DB is first created, there's a chance that the - # data directory doesn't exist at all, because the Install::Filesystem - # code happens after DB creation. So we create the directory ourselves - # if it doesn't exist. - my $datadir = bz_locations()->{datadir}; - if (!-d $datadir) { - mkdir $datadir or warn "$datadir: $!"; - } - if (!-d "$datadir/db/") { - mkdir "$datadir/db/" or warn "$datadir/db: $!"; - } - $db_name = bz_locations()->{datadir} . "/db/$db_name"; + my ($class, $params) = @_; + my $db_name = $params->{db_name}; + + # Let people specify paths intead of data/ for the DB. + if ($db_name and $db_name !~ m{[\\/]}) { + + # When the DB is first created, there's a chance that the + # data directory doesn't exist at all, because the Install::Filesystem + # code happens after DB creation. So we create the directory ourselves + # if it doesn't exist. + my $datadir = bz_locations()->{datadir}; + if (!-d $datadir) { + mkdir $datadir or warn "$datadir: $!"; } - - # construct the DSN from the parameters we got - my $dsn = "dbi:SQLite:dbname=$db_name"; - - my $attrs = { - # XXX Should we just enforce this to be always on? - sqlite_unicode => Bugzilla->params->{'utf8'}, - }; - - my $self = $class->db_new({ dsn => $dsn, user => '', - pass => '', attrs => $attrs }); - # Needed by TheSchwartz - $self->{private_bz_dsn} = $dsn; - - my %pragmas = ( - # Make sure that the sqlite file doesn't grow without bound. - auto_vacuum => 1, - encoding => "'UTF-8'", - foreign_keys => 'ON', - # We want the latest file format. - legacy_file_format => 'OFF', - # This guarantees that we get column names like "foo" - # instead of "table.foo" in selectrow_hashref. - short_column_names => 'ON', - # The write-ahead log mode in SQLite 3.7 gets us better concurrency, - # but breaks backwards-compatibility with older versions of - # SQLite. (Which is important because people may also want to use - # command-line clients to access and back up their DB.) If you need - # better concurrency and don't need 3.6 compatibility, then you can - # uncomment this line. - #journal_mode => "'WAL'", - ); - - while (my ($name, $value) = each %pragmas) { - $self->do("PRAGMA $name = $value"); + if (!-d "$datadir/db/") { + mkdir "$datadir/db/" or warn "$datadir/db: $!"; } - - $self->sqlite_create_collation('bugzilla', \&_sqlite_collate_ci); - $self->sqlite_create_function('position', 2, \&_sqlite_position); - $self->sqlite_create_function('iposition', 2, \&_sqlite_position_ci); - # SQLite has a "substr" function, but other DBs call it "SUBSTRING" - # so that's what we use, and I don't know of any way in SQLite to - # alias the SQL "substr" function to be called "SUBSTRING". - $self->sqlite_create_function('substring', 3, \&CORE::substr); - $self->sqlite_create_function('char_length', 1, sub { length($_[0]) }); - $self->sqlite_create_function('mod', 2, \&_sqlite_mod); - $self->sqlite_create_function('now', 0, \&_sqlite_now); - $self->sqlite_create_function('localtimestamp', 1, \&_sqlite_now); - $self->sqlite_create_function('floor', 1, \&POSIX::floor); - - bless ($self, $class); - return $self; + $db_name = bz_locations()->{datadir} . "/db/$db_name"; + } + + # construct the DSN from the parameters we got + my $dsn = "dbi:SQLite:dbname=$db_name"; + + my $attrs = { + + # XXX Should we just enforce this to be always on? + sqlite_unicode => Bugzilla->params->{'utf8'}, + }; + + my $self + = $class->db_new({dsn => $dsn, user => '', pass => '', attrs => $attrs}); + + # Needed by TheSchwartz + $self->{private_bz_dsn} = $dsn; + + my %pragmas = ( + + # Make sure that the sqlite file doesn't grow without bound. + auto_vacuum => 1, + encoding => "'UTF-8'", + foreign_keys => 'ON', + + # We want the latest file format. + legacy_file_format => 'OFF', + + # This guarantees that we get column names like "foo" + # instead of "table.foo" in selectrow_hashref. + short_column_names => 'ON', + + # The write-ahead log mode in SQLite 3.7 gets us better concurrency, + # but breaks backwards-compatibility with older versions of + # SQLite. (Which is important because people may also want to use + # command-line clients to access and back up their DB.) If you need + # better concurrency and don't need 3.6 compatibility, then you can + # uncomment this line. + #journal_mode => "'WAL'", + + ## RED HAT EXTENSION - something in our custom schema breaks sqlite-3.26 + ## when creating the DB + legacy_alter_table => 'ON', + ); + + while (my ($name, $value) = each %pragmas) { + $self->do("PRAGMA $name = $value"); + } + + $self->sqlite_create_collation('bugzilla', \&_sqlite_collate_ci); + $self->sqlite_create_function('position', 2, \&_sqlite_position); + $self->sqlite_create_function('iposition', 2, \&_sqlite_position_ci); + + # SQLite has a "substr" function, but other DBs call it "SUBSTRING" + # so that's what we use, and I don't know of any way in SQLite to + # alias the SQL "substr" function to be called "SUBSTRING". + $self->sqlite_create_function('substring', 3, \&CORE::substr); + $self->sqlite_create_function('char_length', 1, sub { length($_[0]) }); + $self->sqlite_create_function('mod', 2, \&_sqlite_mod); + $self->sqlite_create_function('now', 0, \&_sqlite_now); + $self->sqlite_create_function('localtimestamp', 1, \&_sqlite_now); + $self->sqlite_create_function('floor', 1, \&POSIX::floor); + + bless($self, $class); + return $self; } ############### @@ -147,86 +159,89 @@ sub new { ############### sub sql_position { - my ($self, $fragment, $text) = @_; - return "POSITION($text, $fragment)"; + my ($self, $fragment, $text) = @_; + return "POSITION($text, $fragment)"; } sub sql_iposition { - my ($self, $fragment, $text) = @_; - return "IPOSITION($text, $fragment)"; + my ($self, $fragment, $text) = @_; + return "IPOSITION($text, $fragment)"; } # SQLite does not have to GROUP BY the optional columns. sub sql_group_by { - my ($self, $needed_columns, $optional_columns) = @_; - my $expression = "GROUP BY $needed_columns"; - return $expression; + my ($self, $needed_columns, $optional_columns) = @_; + my $expression = "GROUP BY $needed_columns"; + return $expression; } # XXX SQLite does not support sorting a GROUP_CONCAT, so $sort is unimplemented. sub sql_group_concat { - my ($self, $column, $separator, $sort) = @_; - $separator = $self->quote(', ') if !defined $separator; - # In SQLite, a GROUP_CONCAT call with a DISTINCT argument can't - # specify its separator, and has to accept the default of ",". - if ($column =~ /^DISTINCT/) { - return "GROUP_CONCAT($column)"; - } - return "GROUP_CONCAT($column, $separator)"; + my ($self, $column, $separator, $sort) = @_; + $separator = $self->quote(', ') if !defined $separator; + + # In SQLite, a GROUP_CONCAT call with a DISTINCT argument can't + # specify its separator, and has to accept the default of ",". + if ($column =~ /^DISTINCT/) { + return "GROUP_CONCAT($column)"; + } + return "GROUP_CONCAT($column, $separator)"; } sub sql_istring { - my ($self, $string) = @_; - return $string; + my ($self, $string) = @_; + return $string; } sub sql_regexp { - my ($self, $expr, $pattern, $nocheck, $real_pattern) = @_; - $real_pattern ||= $pattern; + my ($self, $expr, $pattern, $nocheck, $real_pattern) = @_; + $real_pattern ||= $pattern; - $self->bz_check_regexp($real_pattern) if !$nocheck; + $self->bz_check_regexp($real_pattern) if !$nocheck; - return "$expr REGEXP $pattern"; + return "$expr REGEXP $pattern"; } sub sql_not_regexp { - my $self = shift; - my $re_expression = $self->sql_regexp(@_); - return "NOT($re_expression)"; + my $self = shift; + my $re_expression = $self->sql_regexp(@_); + return "NOT($re_expression)"; } sub sql_limit { - my ($self, $limit, $offset) = @_; - - if (defined($offset)) { - return "LIMIT $limit OFFSET $offset"; - } else { - return "LIMIT $limit"; - } + my ($self, $limit, $offset) = @_; + + if (defined($offset)) { + return "LIMIT $limit OFFSET $offset"; + } + else { + return "LIMIT $limit"; + } } sub sql_from_days { - my ($self, $days) = @_; - return "DATETIME($days)"; + my ($self, $days) = @_; + return "DATETIME($days)"; } sub sql_to_days { - my ($self, $date) = @_; - return "JULIANDAY($date)"; + my ($self, $date) = @_; + return "JULIANDAY($date)"; } sub sql_date_format { - my ($self, $date, $format) = @_; - $format = "%Y.%m.%d %H:%M:%S" if !$format; - $format =~ s/\%i/\%M/g; - $format =~ s/\%s/\%S/g; - return "STRFTIME(" . $self->quote($format) . ", $date)"; + my ($self, $date, $format) = @_; + $format = "%Y.%m.%d %H:%M:%S" if !$format; + $format =~ s/\%i/\%M/g; + $format =~ s/\%s/\%S/g; + return "STRFTIME(" . $self->quote($format) . ", $date)"; } sub sql_date_math { - my ($self, $date, $operator, $interval, $units) = @_; - # We do the || thing (concatenation) so that placeholders work properly. - return "DATETIME($date, '$operator' || $interval || ' $units')"; + my ($self, $date, $operator, $interval, $units) = @_; + + # We do the || thing (concatenation) so that placeholders work properly. + return "DATETIME($date, '$operator' || $interval || ' $units')"; } ############### @@ -234,56 +249,57 @@ sub sql_date_math { ############### sub bz_setup_database { - my $self = shift; - $self->SUPER::bz_setup_database(@_); - - # If we created TheSchwartz tables with COLLATE bugzilla (during the - # 4.1.x development series) re-create them without it. - my @tables = $self->bz_table_list(); - my @ts_tables = grep { /^ts_/ } @tables; - my $drop_ok; - foreach my $table (@ts_tables) { - my $create_table = - $self->_bz_real_schema->_sqlite_create_table($table); - if ($create_table =~ /COLLATE bugzilla/) { - if (!$drop_ok) { - _sqlite_jobqueue_drop_message(); - $drop_ok = 1; - } - $self->bz_drop_table($table); - $self->bz_add_table($table); - } + my $self = shift; + $self->SUPER::bz_setup_database(@_); + + # If we created TheSchwartz tables with COLLATE bugzilla (during the + # 4.1.x development series) re-create them without it. + my @tables = $self->bz_table_list(); + my @ts_tables = grep {/^ts_/} @tables; + my $drop_ok; + foreach my $table (@ts_tables) { + my $create_table = $self->_bz_real_schema->_sqlite_create_table($table); + if ($create_table =~ /COLLATE bugzilla/) { + if (!$drop_ok) { + _sqlite_jobqueue_drop_message(); + $drop_ok = 1; + } + $self->bz_drop_table($table); + $self->bz_add_table($table); } + } } sub _sqlite_jobqueue_drop_message { - # This is not translated because this situation will only happen if - # you are updating from a 4.1.x development version of Bugzilla using - # SQLite, and we don't want to maintain this string in strings.txt.pl - # forever for just this one uncommon circumstance. - print <installation_answers->{NO_PAUSE}) { - print install_string('enter_or_ctrl_c'); - getc; - } + unless (Bugzilla->installation_answers->{NO_PAUSE}) { + print install_string('enter_or_ctrl_c'); + getc; + } } # XXX This needs to be implemented. sub bz_explain { } sub bz_table_list_real { - my $self = shift; - my @tables = $self->SUPER::bz_table_list_real(@_); - # SQLite includes a sqlite_sequence table in every database that isn't - # one of our real tables. We exclude any table that starts with sqlite_, - # just to be safe. - @tables = grep { $_ !~ /^sqlite_/ } @tables; - return @tables; + my $self = shift; + my @tables = $self->SUPER::bz_table_list_real(@_); + + # SQLite includes a sqlite_sequence table in every database that isn't + # one of our real tables. We exclude any table that starts with sqlite_, + # just to be safe. + @tables = grep { $_ !~ /^sqlite_/ } @tables; + return @tables; } 1; diff --git a/Bugzilla/Error.pm b/Bugzilla/Error.pm index ef6320d15..6ca21a6b6 100644 --- a/Bugzilla/Error.pm +++ b/Bugzilla/Error.pm @@ -22,180 +22,241 @@ use Bugzilla::Hook; use Carp; use Data::Dumper; use Date::Format; +use Scalar::Util qw(blessed); # We cannot use $^S to detect if we are in an eval(), because mod_perl # already eval'uates everything, so $^S = 1 in all cases under mod_perl! sub _in_eval { - my $in_eval = 0; - for (my $stack = 1; my $sub = (caller($stack))[3]; $stack++) { - last if $sub =~ /^ModPerl/; - $in_eval = 1 if $sub =~ /^\(eval\)/; - } - return $in_eval; + my $in_eval = 0; + for (my $stack = 1; my $sub = (caller($stack))[3]; $stack++) { + last if $sub =~ /^ModPerl/; + $in_eval = 1 if $sub =~ /^\(eval\)/; + } + return $in_eval; } sub _throw_error { - my ($name, $error, $vars) = @_; - my $dbh = Bugzilla->dbh; - $vars ||= {}; - - $vars->{error} = $error; - - # Make sure any transaction is rolled back (if supported). - # If we are within an eval(), do not roll back transactions as we are - # eval'uating some test on purpose. - $dbh->bz_rollback_transaction() if ($dbh->bz_in_transaction() && !_in_eval()); - - my $datadir = bz_locations()->{'datadir'}; - # If a writable $datadir/errorlog exists, log error details there. - if (-w "$datadir/errorlog") { - require Bugzilla::Util; - require Data::Dumper; - my $mesg = ""; - for (1..75) { $mesg .= "-"; }; - $mesg .= "\n[$$] " . time2str("%D %H:%M:%S ", time()); - $mesg .= "$name $error "; - $mesg .= Bugzilla::Util::remote_ip(); - $mesg .= Bugzilla->user->login; - $mesg .= (' actually ' . Bugzilla->sudoer->login) if Bugzilla->sudoer; - $mesg .= "\n"; - my %params = Bugzilla->cgi->Vars; - $Data::Dumper::Useqq = 1; - for my $param (sort keys %params) { - my $val = $params{$param}; - # obscure passwords - $val = "*****" if $param =~ /password/i; - # limit line length - $val =~ s/^(.{512}).*$/$1\[CHOP\]/; - $mesg .= "[$$] " . Data::Dumper->Dump([$val],["param($param)"]); - } - for my $var (sort keys %ENV) { - my $val = $ENV{$var}; - $val = "*****" if $val =~ /password|http_pass/i; - $mesg .= "[$$] " . Data::Dumper->Dump([$val],["env($var)"]); - } - open(ERRORLOGFID, ">>", "$datadir/errorlog"); - print ERRORLOGFID "$mesg\n"; - close ERRORLOGFID; + my ($name, $error, $vars) = @_; + my $dbh = Bugzilla->dbh; + $vars ||= {}; + + $vars->{error} = $error; + + # Make sure any transaction is rolled back (if supported). + # If we are within an eval(), do not roll back transactions as we are + # eval'uating some test on purpose. + $dbh->bz_rollback_transaction() if ($dbh->bz_in_transaction() && !_in_eval()); + + my $datadir = bz_locations()->{'datadir'}; + + # If a writable $datadir/errorlog exists, log error details there. + if (-w "$datadir/errorlog") { + require Bugzilla::Util; + require Data::Dumper; + my $mesg = ""; + for (1 .. 75) { $mesg .= "-"; } + $mesg .= "\n[$$] " . time2str("%D %H:%M:%S ", time()); + $mesg .= "$name $error "; + $mesg .= Bugzilla::Util::remote_ip(); + $mesg .= Bugzilla->user->login; + $mesg .= (' actually ' . Bugzilla->sudoer->login) if Bugzilla->sudoer; + $mesg .= "\n"; + my %params = Bugzilla->cgi->Vars; + $Data::Dumper::Useqq = 1; + + for my $param (sort keys %params) { + my $val = $params{$param}; + + # obscure passwords + $val = "*****" if $param =~ /password/i; + + # limit line length + $val =~ s/^(.{512}).*$/$1\[CHOP\]/; + $mesg .= "[$$] " . Data::Dumper->Dump([$val], ["param($param)"]); } - - 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. - if (Bugzilla->error_mode != ERROR_MODE_TEST) { - $template->process($name, $vars, \$message) - || ThrowTemplateError($template->error()); + for my $var (sort keys %ENV) { + my $val = $ENV{$var}; + $val = "*****" if $val =~ /password|http_pass/i; + $mesg .= "[$$] " . Data::Dumper->Dump([$val], ["env($var)"]); } - - # Let's call the hook first, so that extensions can override - # or extend the default behavior, or add their own error codes. - Bugzilla::Hook::process('error_catch', { error => $error, vars => $vars, - message => \$message }); - - if (Bugzilla->error_mode == ERROR_MODE_WEBPAGE) { - my $cgi = Bugzilla->cgi; - $cgi->close_standby_message('text/html', 'inline', 'error', 'html'); - print $message; - print $cgi->multipart_final() if $cgi->{_multipart_in_progress}; + ## REDHAT EXTENSION BEGIN + # It's useful to get the traceback in the log file + my $i = 0; + while (my @x = caller($i++)) { + $mesg .= "PACK: $x[0]\nFILE: $x[1]\nLINE: $x[2]\nSUB: $x[3]\n\n"; } - elsif (Bugzilla->error_mode == ERROR_MODE_TEST) { - die Dumper($vars); + ## REDHAT EXTENSION END + open(ERRORLOGFID, ">>", "$datadir/errorlog"); + print ERRORLOGFID "$mesg\n"; + close ERRORLOGFID; + } + + 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. + if (Bugzilla->error_mode != ERROR_MODE_TEST + && !$Bugzilla::Template::is_processing) + { + $template->process($name, $vars, \$message) + || ThrowTemplateError($template->error()); + } + + # Let's call the hook first, so that extensions can override + # or extend the default behavior, or add their own error codes. + Bugzilla::Hook::process('error_catch', + {error => $error, vars => $vars, message => \$message}); + + if ($Bugzilla::Template::is_processing) { + $name =~ /^global\/(user|code)-error/; + my $type = $1 // 'unknown'; + die Template::Exception->new("bugzilla.$type.$error", $vars); + } + + if (Bugzilla->error_mode == ERROR_MODE_WEBPAGE) { + my $cgi = Bugzilla->cgi; + $cgi->close_standby_message('text/html', 'inline', 'error', 'html'); + print $message; + print $cgi->multipart_final() if $cgi->{_multipart_in_progress}; + } + elsif (Bugzilla->error_mode == ERROR_MODE_TEST) { + die Dumper($vars); + } + elsif (Bugzilla->error_mode == ERROR_MODE_DIE) { + die("$message\n"); + } + elsif (Bugzilla->error_mode == ERROR_MODE_DIE_SOAP_FAULT + || Bugzilla->error_mode == ERROR_MODE_JSON_RPC + || Bugzilla->error_mode == ERROR_MODE_REST) + { + # Clone the hash so we aren't modifying the constant. + my %error_map = %{WS_ERROR_CODE()}; + Bugzilla::Hook::process('webservice_error_codes', {error_map => \%error_map}); + my $code = $error_map{$error}; + if (!$code) { + $code = ERROR_UNKNOWN_FATAL if $name =~ /code/i; + $code = ERROR_UNKNOWN_TRANSIENT if $name =~ /user/i; } - elsif (Bugzilla->error_mode == ERROR_MODE_DIE) { - die("$message\n"); + + if (Bugzilla->error_mode == ERROR_MODE_DIE_SOAP_FAULT) { + die SOAP::Fault->faultcode($code)->faultstring($message); } - elsif (Bugzilla->error_mode == ERROR_MODE_DIE_SOAP_FAULT - || Bugzilla->error_mode == ERROR_MODE_JSON_RPC - || Bugzilla->error_mode == ERROR_MODE_REST) - { - # Clone the hash so we aren't modifying the constant. - my %error_map = %{ WS_ERROR_CODE() }; - Bugzilla::Hook::process('webservice_error_codes', - { error_map => \%error_map }); - my $code = $error_map{$error}; - if (!$code) { - $code = ERROR_UNKNOWN_FATAL if $name =~ /code/i; - $code = ERROR_UNKNOWN_TRANSIENT if $name =~ /user/i; - } - - if (Bugzilla->error_mode == ERROR_MODE_DIE_SOAP_FAULT) { - die SOAP::Fault->faultcode($code)->faultstring($message); - } - else { - my $server = Bugzilla->_json_server; - - my $status_code = 0; - if (Bugzilla->error_mode == ERROR_MODE_REST) { - my %status_code_map = %{ REST_STATUS_CODE_MAP() }; - $status_code = $status_code_map{$code} || $status_code_map{'_default'}; - } - # Technically JSON-RPC isn't allowed to have error numbers - # higher than 999, but we do this to avoid conflicts with - # the internal JSON::RPC error codes. - $server->raise_error(code => 100000 + $code, - status_code => $status_code, - message => $message, - id => $server->{_bz_request_id}, - version => $server->version); - # Most JSON-RPC Throw*Error calls happen within an eval inside - # of JSON::RPC. So, in that circumstance, instead of exiting, - # we die with no message. JSON::RPC checks raise_error before - # it checks $@, so it returns the proper error. - die if _in_eval(); - $server->response($server->error_response_header); - } + else { + my $server = Bugzilla->_json_server; + + my $status_code = 0; + if (Bugzilla->error_mode == ERROR_MODE_REST) { + my %status_code_map = %{REST_STATUS_CODE_MAP()}; + $status_code = $status_code_map{$code} || $status_code_map{'_default'}; + } + + # Technically JSON-RPC isn't allowed to have error numbers + # higher than 999, but we do this to avoid conflicts with + # the internal JSON::RPC error codes. + $server->raise_error( + code => 100000 + $code, + status_code => $status_code, + message => $message, + id => $server->{_bz_request_id}, + version => $server->version + ); + + # Most JSON-RPC Throw*Error calls happen within an eval inside + # of JSON::RPC. So, in that circumstance, instead of exiting, + # we die with no message. JSON::RPC checks raise_error before + # it checks $@, so it returns the proper error. + die if _in_eval(); + $server->response($server->error_response_header); } - exit; + } + + exit; } sub ThrowUserError { - _throw_error("global/user-error.html.tmpl", @_); + ## REDHAT EXTENSION BEGIN 406301 + eval { + # eval so that the error logging doesn't + # impede the error reporting to UI + local $" = ', '; + local $Data::Dumper::Indent = 0; + Bugzilla->logger()->error(Carp::longmess("ThrowUserError: " . Dumper(@_))); + }; + ## REDHAT EXTENSION END 406301 + _throw_error("global/user-error.html.tmpl", @_); } sub ThrowCodeError { - my (undef, $vars) = @_; - - # Don't show function arguments, in case they contain - # confidential data. - local $Carp::MaxArgNums = -1; - # Don't show the error as coming from Bugzilla::Error, show it - # as coming from the caller. - local $Carp::CarpInternal{'Bugzilla::Error'} = 1; - $vars->{traceback} = Carp::longmess(); - - _throw_error("global/code-error.html.tmpl", @_); + my (undef, $vars) = @_; + + # Don't show function arguments, in case they contain + # confidential data. + local $Carp::MaxArgNums = -1; + + # Don't show the error as coming from Bugzilla::Error, show it + # as coming from the caller. + local $Carp::CarpInternal{'Bugzilla::Error'} = 1; + $vars->{traceback} = Carp::longmess(); + + ## REDHAT EXTENSION BEGIN 406301 + eval { + # eval so that the error logging doesn't + # impede the error reporting to UI + local $" = ', '; + local $Data::Dumper::Indent = 0; + Bugzilla->logger()->error("ThrowCodeError: " . Dumper(@_)); + }; + ## REDHAT EXTENSION END 406301 + _throw_error("global/code-error.html.tmpl", @_); } sub ThrowTemplateError { - my ($template_err) = @_; - my $dbh = Bugzilla->dbh; - - # Make sure the transaction is rolled back (if supported). - $dbh->bz_rollback_transaction() if $dbh->bz_in_transaction(); - - my $vars = {}; - if (Bugzilla->error_mode == ERROR_MODE_DIE) { - die("error: template error: $template_err"); + my ($template_err) = @_; + my $dbh = Bugzilla->dbh; + + ## REDHAT EXTENSION BEGIN 406301 + eval { + # eval so that the error logging doesn't + # impede the error reporting to UI + local $" = ', '; + Bugzilla->logger()->error("ThrowTemplateError: " . Dumper(@_)); + }; + ## REDHAT EXTENSION END 406301 + + # Make sure the transaction is rolled back (if supported). + $dbh->bz_rollback_transaction() if $dbh->bz_in_transaction(); + + if (blessed($template_err) && $template_err->isa('Template::Exception')) { + my $type = $template_err->type; + if ($type =~ /^bugzilla\.(code|user)\.(.+)/) { + _throw_error("global/$1-error.html.tmpl", $2, $template_err->info); + return; } + } + + my $vars = {}; + if (Bugzilla->error_mode == ERROR_MODE_DIE) { + die("error: template error: $template_err"); + } - $vars->{'template_error_msg'} = $template_err; - $vars->{'error'} = "template_error"; + $vars->{'template_error_msg'} = $template_err; + $vars->{'error'} = "template_error"; - my $template = Bugzilla->template; + 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)) { - require Bugzilla::Util; - import Bugzilla::Util qw(html_quote); - my $maintainer = Bugzilla->params->{'maintainer'}; - my $error = html_quote($vars->{'template_error_msg'}); - my $error2 = html_quote($template->error()); - my $url = html_quote(Bugzilla->cgi->self_url); + # 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)) { + require Bugzilla::Util; + import Bugzilla::Util qw(html_quote); + my $maintainer = Bugzilla->params->{'maintainer'}; + my $error = html_quote($vars->{'template_error_msg'}); + my $error2 = html_quote($template->error()); + my $url = html_quote(Bugzilla->cgi->self_url); - print < Bugzilla has suffered an internal error. Please save this page and send it to $maintainer with details of what you were doing at the @@ -206,8 +267,8 @@ sub ThrowTemplateError { First error: $error
Second error: $error2

END - } - exit; + } + exit; } 1; diff --git a/Bugzilla/Extension.pm b/Bugzilla/Extension.pm index e24ceb9eb..a5522583e 100644 --- a/Bugzilla/Extension.pm +++ b/Bugzilla/Extension.pm @@ -14,8 +14,8 @@ use warnings; use Bugzilla::Constants; use Bugzilla::Error; use Bugzilla::Install::Util qw( - extension_code_files extension_template_directory - extension_package_directory extension_web_directory); + extension_code_files extension_template_directory + extension_package_directory extension_web_directory); use File::Basename; use File::Spec; @@ -25,10 +25,10 @@ use File::Spec; #################### sub new { - my ($class, $params) = @_; - $params ||= {}; - bless $params, $class; - return $params; + my ($class, $params) = @_; + $params ||= {}; + bless $params, $class; + return $params; } ####################################### @@ -36,148 +36,151 @@ sub new { ####################################### sub load { - my ($class, $extension_file, $config_file) = @_; - my $package; - - # This is needed during checksetup.pl, because Extension packages can - # only be loaded once (they return "1" the second time they're loaded, - # instead of their name). During checksetup.pl, extensions are loaded - # once by Bugzilla::Install::Requirements, and then later again via - # Bugzilla->extensions (because of hooks). - my $map = Bugzilla->request_cache->{extension_requirement_package_map}; - - if ($config_file) { - if ($map and defined $map->{$config_file}) { - $package = $map->{$config_file}; - } - else { - my $name = require $config_file; - if ($name =~ /^\d+$/) { - ThrowCodeError('extension_must_return_name', - { extension => $config_file, - returned => $name }); - } - $package = "${class}::$name"; - } - - __do_call($package, 'modify_inc', $config_file); - } - - if ($map and defined $map->{$extension_file}) { - $package = $map->{$extension_file}; - $package->modify_inc($extension_file) if !$config_file; + my ($class, $extension_file, $config_file) = @_; + my $package; + + # This is needed during checksetup.pl, because Extension packages can + # only be loaded once (they return "1" the second time they're loaded, + # instead of their name). During checksetup.pl, extensions are loaded + # once by Bugzilla::Install::Requirements, and then later again via + # Bugzilla->extensions (because of hooks). + my $map = Bugzilla->request_cache->{extension_requirement_package_map}; + + if ($config_file) { + if ($map and defined $map->{$config_file}) { + $package = $map->{$config_file}; } else { - my $name = require $extension_file; - if ($name =~ /^\d+$/) { - ThrowCodeError('extension_must_return_name', - { extension => $extension_file, returned => $name }); - } - $package = "${class}::$name"; - $package->modify_inc($extension_file) if !$config_file; + my $name = require $config_file; + if ($name =~ /^\d+$/) { + ThrowCodeError('extension_must_return_name', + {extension => $config_file, returned => $name}); + } + $package = "${class}::$name"; } - $class->_validate_package($package, $extension_file); - return $package; -} - -sub _validate_package { - my ($class, $package, $extension_file) = @_; - - # For extensions from data/extensions/additional, we don't have a file - # name, so we fake it. - if (!$extension_file) { - $extension_file = $package; - $extension_file =~ s/::/\//g; - $extension_file .= '.pm'; + __do_call($package, 'modify_inc', $config_file); + } + + if ($map and defined $map->{$extension_file}) { + $package = $map->{$extension_file}; + $package->modify_inc($extension_file) if !$config_file; + } + else { + my $name = require $extension_file; + if ($name =~ /^\d+$/) { + ThrowCodeError('extension_must_return_name', + {extension => $extension_file, returned => $name}); } + $package = "${class}::$name"; + $package->modify_inc($extension_file) if !$config_file; + } - if (!eval { $package->NAME }) { - ThrowCodeError('extension_no_name', - { filename => $extension_file, package => $package }); - } + $class->_validate_package($package, $extension_file); + return $package; +} - if (!$package->isa($class)) { - ThrowCodeError('extension_must_be_subclass', - { filename => $extension_file, - package => $package, - class => $class }); - } +sub _validate_package { + my ($class, $package, $extension_file) = @_; + + # For extensions from data/extensions/additional, we don't have a file + # name, so we fake it. + if (!$extension_file) { + $extension_file = $package; + $extension_file =~ s/::/\//g; + $extension_file .= '.pm'; + } + + if (!eval { $package->NAME }) { + ThrowCodeError('extension_no_name', + {filename => $extension_file, package => $package}); + } + + if (!$package->isa($class)) { + ThrowCodeError('extension_must_be_subclass', + {filename => $extension_file, package => $package, class => $class}); + } } sub load_all { - my $class = shift; - my ($file_sets, $extra_packages) = extension_code_files(); - my @packages; - foreach my $file_set (@$file_sets) { - my $package = $class->load(@$file_set); - push(@packages, $package); - } - - # Extensions from data/extensions/additional - foreach my $package (@$extra_packages) { - # Don't load an "additional" extension if we already have an extension - # loaded with that name. - next if grep($_ eq $package, @packages); - # Untaint the package name - $package =~ /([\w:]+)/; - $package = $1; - eval("require $package") || die $@; - $package->_validate_package($package); - push(@packages, $package); - } - - return \@packages; + my $class = shift; + my ($file_sets, $extra_packages) = extension_code_files(); + my @packages; + foreach my $file_set (@$file_sets) { + my $package = $class->load(@$file_set); + push(@packages, $package); + } + + # Extensions from data/extensions/additional + foreach my $package (@$extra_packages) { + + # Don't load an "additional" extension if we already have an extension + # loaded with that name. + next if grep($_ eq $package, @packages); + + # Untaint the package name + $package =~ /([\w:]+)/; + $package = $1; + eval("require $package") || die $@; + $package->_validate_package($package); + push(@packages, $package); + } + + return \@packages; } # Modifies @INC so that extensions can use modules like # "use Bugzilla::Extension::Foo::Bar", when Bar.pm is in the lib/ # directory of the extension. sub modify_inc { - my ($class, $file) = @_; - - # Note that this package_dir call is necessary to set things up - # for my_inc, even if we didn't take its return value. - my $package_dir = __do_call($class, 'package_dir', $file); - # Don't modify @INC for extensions that are just files in the extensions/ - # directory. We don't want Bugzilla's base lib/CGI.pm being loaded as - # Bugzilla::Extension::Foo::CGI or any other confusing thing like that. - return if $package_dir eq bz_locations->{'extensionsdir'}; - unshift(@INC, sub { __do_call($class, 'my_inc', @_) }); + my ($class, $file) = @_; + + # Note that this package_dir call is necessary to set things up + # for my_inc, even if we didn't take its return value. + my $package_dir = __do_call($class, 'package_dir', $file); + + # Don't modify @INC for extensions that are just files in the extensions/ + # directory. We don't want Bugzilla's base lib/CGI.pm being loaded as + # Bugzilla::Extension::Foo::CGI or any other confusing thing like that. + return if $package_dir eq bz_locations->{'extensionsdir'}; + unshift(@INC, sub { __do_call($class, 'my_inc', @_) }); } # This is what gets put into @INC by modify_inc. sub my_inc { - my ($class, undef, $file) = @_; - - # This avoids infinite recursion in case anything inside of this function - # does a "require". (I know for sure that File::Spec->case_tolerant does - # a "require" on Windows, for example.) - return if $file !~ /^Bugzilla/; - - my $lib_dir = __do_call($class, 'lib_dir'); - my @class_parts = split('::', $class); - my ($vol, $dir, $file_name) = File::Spec->splitpath($file); - my @dir_parts = File::Spec->splitdir($dir); - # File::Spec::Win32 (any maybe other OSes) add an empty directory at the - # end of @dir_parts. - @dir_parts = grep { $_ ne '' } @dir_parts; - # Validate that this is a sub-package of Bugzilla::Extension::Foo ($class). - for (my $i = 0; $i < scalar(@class_parts); $i++) { - return if !@dir_parts; - if (File::Spec->case_tolerant) { - return if lc($class_parts[$i]) ne lc($dir_parts[0]); - } - else { - return if $class_parts[$i] ne $dir_parts[0]; - } - shift(@dir_parts); + my ($class, undef, $file) = @_; + + # This avoids infinite recursion in case anything inside of this function + # does a "require". (I know for sure that File::Spec->case_tolerant does + # a "require" on Windows, for example.) + return if $file !~ /^Bugzilla/; + + my $lib_dir = __do_call($class, 'lib_dir'); + my @class_parts = split('::', $class); + my ($vol, $dir, $file_name) = File::Spec->splitpath($file); + my @dir_parts = File::Spec->splitdir($dir); + + # File::Spec::Win32 (any maybe other OSes) add an empty directory at the + # end of @dir_parts. + @dir_parts = grep { $_ ne '' } @dir_parts; + + # Validate that this is a sub-package of Bugzilla::Extension::Foo ($class). + for (my $i = 0; $i < scalar(@class_parts); $i++) { + return if !@dir_parts; + if (File::Spec->case_tolerant) { + return if lc($class_parts[$i]) ne lc($dir_parts[0]); } - # For Bugzilla::Extension::Foo::Bar, this would look something like - # extensions/Example/lib/Bar.pm - my $resolved_path = File::Spec->catfile($lib_dir, @dir_parts, $file_name); - open(my $fh, '<', $resolved_path); - return $fh; + else { + return if $class_parts[$i] ne $dir_parts[0]; + } + shift(@dir_parts); + } + + # For Bugzilla::Extension::Foo::Bar, this would look something like + # extensions/Example/lib/Bar.pm + my $resolved_path = File::Spec->catfile($lib_dir, @dir_parts, $file_name); + open(my $fh, '<', $resolved_path); + return $fh; } #################### @@ -187,23 +190,24 @@ sub my_inc { use constant enabled => 1; sub lib_dir { - my $invocant = shift; - my $package_dir = __do_call($invocant, 'package_dir'); - # For extensions that are just files in the extensions/ directory, - # use the base lib/ dir as our "lib_dir". Note that Bugzilla never - # uses lib_dir in this case, though, because modify_inc is prevented - # from modifying @INC when we're just a file in the extensions/ directory. - # So this particular code block exists just to make lib_dir return - # something right in case an extension needs it for some odd reason. - if ($package_dir eq bz_locations()->{'extensionsdir'}) { - return bz_locations->{'ext_libpath'}; - } - return File::Spec->catdir($package_dir, 'lib'); + my $invocant = shift; + my $package_dir = __do_call($invocant, 'package_dir'); + + # For extensions that are just files in the extensions/ directory, + # use the base lib/ dir as our "lib_dir". Note that Bugzilla never + # uses lib_dir in this case, though, because modify_inc is prevented + # from modifying @INC when we're just a file in the extensions/ directory. + # So this particular code block exists just to make lib_dir return + # something right in case an extension needs it for some odd reason. + if ($package_dir eq bz_locations()->{'extensionsdir'}) { + return bz_locations->{'ext_libpath'}; + } + return File::Spec->catdir($package_dir, 'lib'); } sub template_dir { return extension_template_directory(@_); } -sub package_dir { return extension_package_directory(@_); } -sub web_dir { return extension_web_directory(@_); } +sub package_dir { return extension_package_directory(@_); } +sub web_dir { return extension_web_directory(@_); } ###################### # Helper Subroutines # @@ -217,13 +221,13 @@ sub web_dir { return extension_web_directory(@_); } # the method. This is necessary because Config.pm is not a subclass of # Bugzilla::Extension. sub __do_call { - my ($class, $method, @args) = @_; - if ($class->can($method)) { - return $class->$method(@args); - } - my $function_ref; - { no strict 'refs'; $function_ref = \&{$method}; } - return $function_ref->($class, @args); + my ($class, $method, @args) = @_; + if ($class->can($method)) { + return $class->$method(@args); + } + my $function_ref; + { no strict 'refs'; $function_ref = \&{$method}; } + return $function_ref->($class, @args); } 1; diff --git a/Bugzilla/Field.pm b/Bugzilla/Field.pm index 761f7b94e..eb43eb633 100644 --- a/Bugzilla/Field.pm +++ b/Bugzilla/Field.pm @@ -81,82 +81,80 @@ use constant DB_TABLE => 'fielddefs'; use constant LIST_ORDER => 'sortkey, name'; use constant DB_COLUMNS => qw( - id - name - description - long_desc - type - custom - mailhead - sortkey - obsolete - enter_bug - buglist - visibility_field_id - value_field_id - reverse_desc - is_mandatory - is_numeric + id + name + description + long_desc + type + custom + mailhead + sortkey + obsolete + enter_bug + buglist + visibility_field_id + value_field_id + reverse_desc + is_mandatory + is_numeric ); use constant VALIDATORS => { - custom => \&_check_custom, - description => \&_check_description, - long_desc => \&_check_long_desc, - enter_bug => \&_check_enter_bug, - buglist => \&Bugzilla::Object::check_boolean, - mailhead => \&_check_mailhead, - name => \&_check_name, - obsolete => \&_check_obsolete, - reverse_desc => \&_check_reverse_desc, - sortkey => \&_check_sortkey, - type => \&_check_type, - value_field_id => \&_check_value_field_id, - visibility_field_id => \&_check_visibility_field_id, - visibility_values => \&_check_visibility_values, - is_mandatory => \&Bugzilla::Object::check_boolean, - is_numeric => \&_check_is_numeric, + custom => \&_check_custom, + description => \&_check_description, + long_desc => \&_check_long_desc, + enter_bug => \&_check_enter_bug, + buglist => \&Bugzilla::Object::check_boolean, + mailhead => \&_check_mailhead, + name => \&_check_name, + obsolete => \&_check_obsolete, + reverse_desc => \&_check_reverse_desc, + sortkey => \&_check_sortkey, + type => \&_check_type, + value_field_id => \&_check_value_field_id, + visibility_field_id => \&_check_visibility_field_id, + visibility_values => \&_check_visibility_values, + is_mandatory => \&Bugzilla::Object::check_boolean, + is_numeric => \&_check_is_numeric, }; use constant VALIDATOR_DEPENDENCIES => { - is_numeric => ['type'], - name => ['custom'], - type => ['custom'], - reverse_desc => ['type'], - value_field_id => ['type'], - visibility_values => ['visibility_field_id'], + is_numeric => ['type'], + name => ['custom'], + type => ['custom'], + reverse_desc => ['type'], + value_field_id => ['type'], + visibility_values => ['visibility_field_id'], }; use constant UPDATE_COLUMNS => qw( - description - long_desc - mailhead - sortkey - obsolete - enter_bug - buglist - visibility_field_id - value_field_id - reverse_desc - is_mandatory - is_numeric - type + description + long_desc + mailhead + sortkey + obsolete + enter_bug + buglist + visibility_field_id + value_field_id + reverse_desc + is_mandatory + is_numeric + type ); # How various field types translate into SQL data definitions. use constant SQL_DEFINITIONS => { - # Using commas because these are constants and they shouldn't - # be auto-quoted by the "=>" operator. - FIELD_TYPE_FREETEXT, { TYPE => 'varchar(255)', - NOTNULL => 1, DEFAULT => "''"}, - FIELD_TYPE_SINGLE_SELECT, { TYPE => 'varchar(64)', NOTNULL => 1, - DEFAULT => "'---'" }, - FIELD_TYPE_TEXTAREA, { TYPE => 'MEDIUMTEXT', - NOTNULL => 1, DEFAULT => "''"}, - FIELD_TYPE_DATETIME, { TYPE => 'DATETIME' }, - FIELD_TYPE_DATE, { TYPE => 'DATE' }, - FIELD_TYPE_BUG_ID, { TYPE => 'INT3' }, - FIELD_TYPE_INTEGER, { TYPE => 'INT4', NOTNULL => 1, DEFAULT => 0 }, + + # Using commas because these are constants and they shouldn't + # be auto-quoted by the "=>" operator. + FIELD_TYPE_FREETEXT, {TYPE => 'varchar(255)', NOTNULL => 1, DEFAULT => "''"}, + FIELD_TYPE_SINGLE_SELECT, + {TYPE => 'varchar(64)', NOTNULL => 1, DEFAULT => "'---'"}, FIELD_TYPE_TEXTAREA, + {TYPE => 'MEDIUMTEXT', NOTNULL => 1, DEFAULT => "''"}, FIELD_TYPE_DATETIME, + {TYPE => 'DATETIME'}, FIELD_TYPE_DATE, {TYPE => 'DATE'}, FIELD_TYPE_BUG_ID, + {TYPE => 'INT3'}, FIELD_TYPE_INTEGER, + {TYPE => 'INT4', NOTNULL => 1, DEFAULT => 0}, }; # Field definitions for the fields that ship with Bugzilla. @@ -164,110 +162,247 @@ use constant SQL_DEFINITIONS => { # the fielddefs table. # 'days_elapsed' is set in populate_field_definitions() itself. use constant DEFAULT_FIELDS => ( - {name => 'bug_id', desc => 'Bug #', in_new_bugmail => 1, - buglist => 1, is_numeric => 1}, - {name => 'short_desc', desc => 'Summary', in_new_bugmail => 1, - is_mandatory => 1, buglist => 1}, - {name => 'classification', desc => 'Classification', in_new_bugmail => 1, - type => FIELD_TYPE_SINGLE_SELECT, buglist => 1}, - {name => 'product', desc => 'Product', in_new_bugmail => 1, - is_mandatory => 1, - type => FIELD_TYPE_SINGLE_SELECT, buglist => 1}, - {name => 'version', desc => 'Version', in_new_bugmail => 1, - is_mandatory => 1, buglist => 1}, - {name => 'rep_platform', desc => 'Platform', in_new_bugmail => 1, - type => FIELD_TYPE_SINGLE_SELECT, buglist => 1}, - {name => 'bug_file_loc', desc => 'URL', in_new_bugmail => 1, - buglist => 1}, - {name => 'op_sys', desc => 'OS/Version', in_new_bugmail => 1, - type => FIELD_TYPE_SINGLE_SELECT, buglist => 1}, - {name => 'bug_status', desc => 'Status', in_new_bugmail => 1, - type => FIELD_TYPE_SINGLE_SELECT, buglist => 1}, - {name => 'status_whiteboard', desc => 'Status Whiteboard', - in_new_bugmail => 1, buglist => 1}, - {name => 'keywords', desc => 'Keywords', in_new_bugmail => 1, - type => FIELD_TYPE_KEYWORDS, buglist => 1}, - {name => 'resolution', desc => 'Resolution', - type => FIELD_TYPE_SINGLE_SELECT, buglist => 1}, - {name => 'bug_severity', desc => 'Severity', in_new_bugmail => 1, - type => FIELD_TYPE_SINGLE_SELECT, buglist => 1}, - {name => 'priority', desc => 'Priority', in_new_bugmail => 1, - type => FIELD_TYPE_SINGLE_SELECT, buglist => 1}, - {name => 'component', desc => 'Component', in_new_bugmail => 1, - is_mandatory => 1, - type => FIELD_TYPE_SINGLE_SELECT, buglist => 1}, - {name => 'assigned_to', desc => 'AssignedTo', in_new_bugmail => 1, - buglist => 1}, - {name => 'reporter', desc => 'ReportedBy', in_new_bugmail => 1, - buglist => 1}, - {name => 'qa_contact', desc => 'QAContact', in_new_bugmail => 1, - buglist => 1}, - {name => 'assigned_to_realname', desc => 'AssignedToName', - in_new_bugmail => 0, buglist => 1}, - {name => 'reporter_realname', desc => 'ReportedByName', - in_new_bugmail => 0, buglist => 1}, - {name => 'qa_contact_realname', desc => 'QAContactName', - in_new_bugmail => 0, buglist => 1}, - {name => 'cc', desc => 'CC', in_new_bugmail => 1}, - {name => 'dependson', desc => 'Depends on', in_new_bugmail => 1, - is_numeric => 1, buglist => 1}, - {name => 'blocked', desc => 'Blocks', in_new_bugmail => 1, - is_numeric => 1, buglist => 1}, - - {name => 'attachments.description', desc => 'Attachment description'}, - {name => 'attachments.filename', desc => 'Attachment filename'}, - {name => 'attachments.mimetype', desc => 'Attachment mime type'}, - {name => 'attachments.ispatch', desc => 'Attachment is patch', - is_numeric => 1}, - {name => 'attachments.isobsolete', desc => 'Attachment is obsolete', - is_numeric => 1}, - {name => 'attachments.isprivate', desc => 'Attachment is private', - is_numeric => 1}, - {name => 'attachments.submitter', desc => 'Attachment creator'}, - - {name => 'target_milestone', desc => 'Target Milestone', - in_new_bugmail => 1, buglist => 1}, - {name => 'creation_ts', desc => 'Creation date', - buglist => 1}, - {name => 'delta_ts', desc => 'Last changed date', - buglist => 1}, - {name => 'longdesc', desc => 'Comment'}, - {name => 'longdescs.isprivate', desc => 'Comment is private', - is_numeric => 1}, - {name => 'longdescs.count', desc => 'Number of Comments', - buglist => 1, is_numeric => 1}, - {name => 'alias', desc => 'Alias', buglist => 1}, - {name => 'everconfirmed', desc => 'Ever Confirmed', - is_numeric => 1}, - {name => 'reporter_accessible', desc => 'Reporter Accessible', - is_numeric => 1}, - {name => 'cclist_accessible', desc => 'CC Accessible', - is_numeric => 1}, - {name => 'bug_group', desc => 'Group', in_new_bugmail => 1}, - {name => 'estimated_time', desc => 'Estimated Hours', - in_new_bugmail => 1, buglist => 1, is_numeric => 1}, - {name => 'remaining_time', desc => 'Remaining Hours', buglist => 1, - is_numeric => 1}, - {name => 'deadline', desc => 'Deadline', - type => FIELD_TYPE_DATETIME, in_new_bugmail => 1, buglist => 1}, - {name => 'commenter', desc => 'Commenter'}, - {name => 'flagtypes.name', desc => 'Flags', buglist => 1}, - {name => 'requestees.login_name', desc => 'Flag Requestee'}, - {name => 'setters.login_name', desc => 'Flag Setter'}, - {name => 'work_time', desc => 'Hours Worked', buglist => 1, - is_numeric => 1}, - {name => 'percentage_complete', desc => 'Percentage Complete', - buglist => 1, is_numeric => 1}, - {name => 'content', desc => 'Content'}, - {name => 'attach_data.thedata', desc => 'Attachment data'}, - {name => "owner_idle_time", desc => "Time Since Assignee Touched"}, - {name => 'see_also', desc => "See Also", - type => FIELD_TYPE_BUG_URLS}, - {name => 'tag', desc => 'Personal Tags', buglist => 1, - type => FIELD_TYPE_KEYWORDS}, - {name => 'last_visit_ts', desc => 'Last Visit', buglist => 1, - type => FIELD_TYPE_DATETIME}, - {name => 'comment_tag', desc => 'Comment Tag'}, + { + name => 'bug_id', + desc => 'Bug #', + in_new_bugmail => 1, + buglist => 1, + is_numeric => 1 + }, + { + name => 'short_desc', + desc => 'Summary', + in_new_bugmail => 1, + is_mandatory => 1, + buglist => 1 + }, + { + name => 'classification', + desc => 'Classification', + in_new_bugmail => 1, + type => FIELD_TYPE_SINGLE_SELECT, + buglist => 1 + }, + { + name => 'product', + desc => 'Product', + in_new_bugmail => 1, + is_mandatory => 1, + type => FIELD_TYPE_SINGLE_SELECT, + buglist => 1 + }, + { + name => 'version', + desc => 'Version', + in_new_bugmail => 1, + is_mandatory => 1, + buglist => 1 + }, + { + name => 'rep_platform', + desc => 'Platform', + in_new_bugmail => 1, + type => FIELD_TYPE_SINGLE_SELECT, + buglist => 1 + }, + {name => 'bug_file_loc', desc => 'URL', in_new_bugmail => 1, buglist => 1}, + { + name => 'op_sys', + desc => 'OS/Version', + in_new_bugmail => 1, + type => FIELD_TYPE_SINGLE_SELECT, + buglist => 1 + }, + { + name => 'bug_status', + desc => 'Status', + in_new_bugmail => 1, + type => FIELD_TYPE_SINGLE_SELECT, + buglist => 1 + }, + { + name => 'status_whiteboard', + desc => 'Status Whiteboard', + in_new_bugmail => 1, + buglist => 1 + }, + { + name => 'keywords', + desc => 'Keywords', + in_new_bugmail => 1, + type => FIELD_TYPE_KEYWORDS, + buglist => 1 + }, + { + name => 'resolution', + desc => 'Resolution', + type => FIELD_TYPE_SINGLE_SELECT, + buglist => 1 + }, + { + name => 'bug_severity', + desc => 'Severity', + in_new_bugmail => 1, + type => FIELD_TYPE_SINGLE_SELECT, + buglist => 1 + }, + { + name => 'priority', + desc => 'Priority', + in_new_bugmail => 1, + type => FIELD_TYPE_SINGLE_SELECT, + buglist => 1 + }, + { + name => 'component', + desc => 'Component', + in_new_bugmail => 1, + is_mandatory => 1, + type => FIELD_TYPE_SINGLE_SELECT, + buglist => 1 + }, + { + name => 'assigned_to', + desc => 'AssignedTo', + in_new_bugmail => 1, + buglist => 1 + }, + {name => 'reporter', desc => 'ReportedBy', in_new_bugmail => 1, buglist => 1}, + {name => 'qa_contact', desc => 'QAContact', in_new_bugmail => 1, buglist => 1}, + { + name => 'assigned_to_realname', + desc => 'AssignedToName', + in_new_bugmail => 0, + buglist => 1 + }, + { + name => 'reporter_realname', + desc => 'ReportedByName', + in_new_bugmail => 0, + buglist => 1 + }, + { + name => 'qa_contact_realname', + desc => 'QAContactName', + in_new_bugmail => 0, + buglist => 1 + }, + ## REDHAT EXTENSION START 876015 + { + name => 'docs_contact', + desc => 'DocsContact', + in_new_bugmail => 1, + buglist => 1 + }, + { + name => 'docs_contact_realname', + desc => 'DocsContactName', + in_new_bugmail => 0, + buglist => 1 + }, + ## REDHAT EXTENSION END 876015 + {name => 'cc', desc => 'CC', in_new_bugmail => 1}, + { + name => 'dependson', + desc => 'Depends on', + in_new_bugmail => 1, + is_numeric => 1, + buglist => 1 + }, + { + name => 'blocked', + desc => 'Blocks', + in_new_bugmail => 1, + is_numeric => 1, + buglist => 1 + }, + + {name => 'attachments.description', desc => 'Attachment description'}, + {name => 'attachments.filename', desc => 'Attachment filename'}, + {name => 'attachments.mimetype', desc => 'Attachment mime type'}, + {name => 'attachments.ispatch', desc => 'Attachment is patch', is_numeric => 1}, + { + name => 'attachments.isobsolete', + desc => 'Attachment is obsolete', + is_numeric => 1 + }, + { + name => 'attachments.isprivate', + desc => 'Attachment is private', + is_numeric => 1 + }, + {name => 'attachments.submitter', desc => 'Attachment creator'}, + + { + name => 'target_milestone', + desc => 'Target Milestone', + in_new_bugmail => 1, + buglist => 1 + }, + {name => 'target_release', desc => 'Target Release', buglist => 1}, + {name => 'creation_ts', desc => 'Creation date', buglist => 1}, + {name => 'delta_ts', desc => 'Last changed date', buglist => 1}, + {name => 'longdesc', desc => 'Comment'}, + {name => 'longdescs.isprivate', desc => 'Comment is private', is_numeric => 1}, + { + name => 'longdescs.count', + desc => 'Number of Comments', + buglist => 1, + is_numeric => 1 + }, + {name => 'alias', desc => 'Alias', buglist => 1}, + {name => 'everconfirmed', desc => 'Ever Confirmed', is_numeric => 1}, + {name => 'reporter_accessible', desc => 'Reporter Accessible', is_numeric => 1}, + {name => 'cclist_accessible', desc => 'CC Accessible', is_numeric => 1}, + {name => 'bug_group', desc => 'Group', in_new_bugmail => 1}, + { + name => 'estimated_time', + desc => 'Estimated Hours', + in_new_bugmail => 1, + buglist => 1, + is_numeric => 1 + }, + { + name => 'remaining_time', + desc => 'Remaining Hours', + buglist => 1, + is_numeric => 1 + }, + { + name => 'deadline', + desc => 'Deadline', + type => FIELD_TYPE_DATETIME, + in_new_bugmail => 1, + buglist => 1 + }, + {name => 'commenter', desc => 'Commenter'}, + {name => 'flagtypes.name', desc => 'Flags', buglist => 1}, + {name => 'requestees.login_name', desc => 'Flag Requestee'}, + {name => 'setters.login_name', desc => 'Flag Setter'}, + {name => 'work_time', desc => 'Hours Worked', buglist => 1, is_numeric => 1}, + { + name => 'percentage_complete', + desc => 'Percentage Complete', + buglist => 1, + is_numeric => 1 + }, + {name => 'content', desc => 'Content'}, + {name => 'attach_data.thedata', desc => 'Attachment data'}, + {name => "owner_idle_time", desc => "Time Since Assignee Touched"}, + {name => 'see_also', desc => "See Also", type => FIELD_TYPE_BUG_URLS}, + { + name => 'tag', + desc => 'Personal Tags', + buglist => 1, + type => FIELD_TYPE_KEYWORDS + }, + { + name => 'last_visit_ts', + desc => 'Last Visit', + buglist => 1, + type => FIELD_TYPE_DATETIME + }, + {name => 'comment_tag', desc => 'Comment Tag'}, ); ################ @@ -276,12 +411,13 @@ use constant DEFAULT_FIELDS => ( # Override match to add is_select. sub match { - my $self = shift; - my ($params) = @_; - if (delete $params->{is_select}) { - $params->{type} = [FIELD_TYPE_SINGLE_SELECT, FIELD_TYPE_MULTI_SELECT]; - } - return $self->SUPER::match(@_); + my $self = shift; + my ($params) = @_; + if (delete $params->{is_select}) { + $params->{type} + = [FIELD_TYPE_SINGLE_SELECT, FIELD_TYPE_MULTI_SELECT, FIELD_TYPE_ONE_SELECT]; + } + return $self->SUPER::match(@_); } ############## @@ -291,155 +427,171 @@ sub match { sub _check_custom { return $_[1] ? 1 : 0; } sub _check_description { - my ($invocant, $desc) = @_; - $desc = clean_text($desc); - $desc || ThrowUserError('field_missing_description'); - return $desc; + my ($invocant, $desc) = @_; + $desc = clean_text($desc); + $desc || ThrowUserError('field_missing_description'); + return $desc; } sub _check_long_desc { - my ($invocant, $long_desc) = @_; - $long_desc = clean_text($long_desc || ''); - if (length($long_desc) > MAX_FIELD_LONG_DESC_LENGTH) { - ThrowUserError('field_long_desc_too_long'); - } - return $long_desc; + my ($invocant, $long_desc) = @_; + $long_desc = clean_text($long_desc || ''); + if (length($long_desc) > MAX_FIELD_LONG_DESC_LENGTH) { + ThrowUserError('field_long_desc_too_long'); + } + return $long_desc; } sub _check_enter_bug { return $_[1] ? 1 : 0; } sub _check_is_numeric { - my ($invocant, $value, undef, $params) = @_; - my $type = blessed($invocant) ? $invocant->type : $params->{type}; - return 1 if $type == FIELD_TYPE_BUG_ID; - return $value ? 1 : 0; + my ($invocant, $value, undef, $params) = @_; + my $type = blessed($invocant) ? $invocant->type : $params->{type}; + return 1 if $type == FIELD_TYPE_BUG_ID; + return $value ? 1 : 0; } sub _check_mailhead { return $_[1] ? 1 : 0; } sub _check_name { - my ($class, $name, undef, $params) = @_; - $name = lc(clean_text($name)); - $name || ThrowUserError('field_missing_name'); - - # Don't want to allow a name that might mess up SQL. - my $name_regex = qr/^[\w\.]+$/; - # Custom fields have more restrictive name requirements than - # standard fields. - $name_regex = qr/^[a-zA-Z0-9_]+$/ if $params->{custom}; - # Custom fields can't be named just "cf_", and there is no normal - # field named just "cf_". - ($name =~ $name_regex && $name ne "cf_") - || ThrowUserError('field_invalid_name', { name => $name }); - - # If it's custom, prepend cf_ to the custom field name to distinguish - # it from standard fields. - if ($name !~ /^cf_/ && $params->{custom}) { - $name = 'cf_' . $name; - } + my ($class, $name, undef, $params) = @_; + $name = lc(clean_text($name)); + $name || ThrowUserError('field_missing_name'); + + # Don't want to allow a name that might mess up SQL. + my $name_regex = qr/^[\w\.]+$/; + + # Custom fields have more restrictive name requirements than + # standard fields. + $name_regex = qr/^[a-zA-Z0-9_]+$/ if $params->{custom}; + + # Custom fields can't be named just "cf_", and there is no normal + # field named just "cf_". + ($name =~ $name_regex && $name ne "cf_") + || ThrowUserError('field_invalid_name', {name => $name}); + + # If it's custom, prepend cf_ to the custom field name to distinguish + # it from standard fields. + if ($name !~ /^cf_/ && $params->{custom}) { + $name = 'cf_' . $name; + } - # Assure the name is unique. Names can't be changed, so we don't have - # to worry about what to do on updates. - my $field = new Bugzilla::Field({ name => $name }); - ThrowUserError('field_already_exists', {'field' => $field }) if $field; + # Assure the name is unique. Names can't be changed, so we don't have + # to worry about what to do on updates. + my $field = new Bugzilla::Field({name => $name}); + ThrowUserError('field_already_exists', {'field' => $field}) if $field; - return $name; + return $name; } sub _check_obsolete { return $_[1] ? 1 : 0; } sub _check_sortkey { - my ($invocant, $sortkey) = @_; - my $skey = $sortkey; - if (!defined $skey || $skey eq '') { - ($sortkey) = Bugzilla->dbh->selectrow_array( - 'SELECT MAX(sortkey) + 100 FROM fielddefs') || 100; - } - detaint_natural($sortkey) - || ThrowUserError('field_invalid_sortkey', { sortkey => $skey }); - return $sortkey; + my ($invocant, $sortkey) = @_; + my $skey = $sortkey; + if (!defined $skey || $skey eq '') { + ($sortkey) + = Bugzilla->dbh->selectrow_array('SELECT MAX(sortkey) + 100 FROM fielddefs') + || 100; + } + detaint_natural($sortkey) + || ThrowUserError('field_invalid_sortkey', {sortkey => $skey}); + return $sortkey; } sub _check_type { - my ($invocant, $type, undef, $params) = @_; - my $saved_type = $type; - (detaint_natural($type) && $type < FIELD_TYPE_HIGHEST_PLUS_ONE) - || ThrowCodeError('invalid_customfield_type', { type => $saved_type }); - - my $custom = blessed($invocant) ? $invocant->custom : $params->{custom}; - if ($custom && !$type) { - ThrowCodeError('field_type_not_specified'); - } + my ($invocant, $type, undef, $params) = @_; + my $saved_type = $type; + (detaint_natural($type) && $type < FIELD_TYPE_HIGHEST_PLUS_ONE) + || ThrowCodeError('invalid_customfield_type', {type => $saved_type}); + + my $custom = blessed($invocant) ? $invocant->custom : $params->{custom}; + if ($custom && !$type) { + ThrowCodeError('field_type_not_specified'); + } - return $type; + return $type; } sub _check_value_field_id { - my ($invocant, $field_id, undef, $params) = @_; - my $is_select = $invocant->is_select($params); - if ($field_id && !$is_select) { - ThrowUserError('field_value_control_select_only'); - } - return $invocant->_check_visibility_field_id($field_id); + my ($invocant, $field_id, undef, $params) = @_; + my $is_select = $invocant->is_select($params); + if ($field_id && !$is_select) { + ThrowUserError('field_value_control_select_only'); + } + return $invocant->_check_visibility_field_id($field_id); } sub _check_visibility_field_id { - my ($invocant, $field_id) = @_; - $field_id = trim($field_id); - return undef if !$field_id; - my $field = Bugzilla::Field->check({ id => $field_id }); - if (blessed($invocant) && $field->id == $invocant->id) { - ThrowUserError('field_cant_control_self', { field => $field }); - } - if (!$field->is_select) { - ThrowUserError('field_control_must_be_select', - { field => $field }); - } - return $field->id; + my ($invocant, $field_id) = @_; + $field_id = trim($field_id); + return undef if !$field_id; + my $field = Bugzilla::Field->check({id => $field_id}); + if (blessed($invocant) && $field->id == $invocant->id) { + ThrowUserError('field_cant_control_self', {field => $field}); + } + if (!$field->is_select) { + ThrowUserError('field_control_must_be_select', {field => $field}); + } + return $field->id; } sub _check_visibility_values { - my ($invocant, $values, undef, $params) = @_; - my $field; - if (blessed $invocant) { - $field = $invocant->visibility_field; - } - elsif ($params->{visibility_field_id}) { - $field = $invocant->new($params->{visibility_field_id}); - } - # When no field is set, no values are set. - return [] if !$field; + my ($invocant, $values, undef, $params) = @_; + my $field; + if (blessed $invocant) { + $field = $invocant->visibility_field; + } + elsif ($params->{visibility_field_id}) { + $field = $invocant->new($params->{visibility_field_id}); + } - if (!scalar @$values) { - ThrowUserError('field_visibility_values_must_be_selected', - { field => $field }); - } + # When no field is set, no values are set. + return [] if !$field; - my @visibility_values; - my $choice = Bugzilla::Field::Choice->type($field); - foreach my $value (@$values) { - if (!blessed $value) { - $value = $choice->check({ id => $value }); - } - push(@visibility_values, $value); + if (!scalar @$values) { + ThrowUserError('field_visibility_values_must_be_selected', {field => $field}); + } + + my @visibility_values; + my $choice = Bugzilla::Field::Choice->type($field); + foreach my $value (@$values) { + if (!blessed $value) { + $value = $choice->check({id => $value}); } + push(@visibility_values, $value); + } - return \@visibility_values; + return \@visibility_values; } sub _check_reverse_desc { - my ($invocant, $reverse_desc, undef, $params) = @_; - my $type = blessed($invocant) ? $invocant->type : $params->{type}; - if ($type != FIELD_TYPE_BUG_ID) { - return undef; # store NULL for non-reversible field types - } - - $reverse_desc = clean_text($reverse_desc); - return $reverse_desc; + my ($invocant, $reverse_desc, undef, $params) = @_; + my $type = blessed($invocant) ? $invocant->type : $params->{type}; + if ($type != FIELD_TYPE_BUG_ID) { + return undef; # store NULL for non-reversible field types + } + + $reverse_desc = clean_text($reverse_desc); + return $reverse_desc; } sub _check_is_mandatory { return $_[1] ? 1 : 0; } +sub _check_groups { + my ($invocant, $group_names, undef, $params) = @_; + + my @add_groups; + + foreach my $name (@$group_names) { + my $group = Bugzilla::Group->check({name => $name}); + + push(@add_groups, $group->id) if ($group); + } + + return \@add_groups; +} + =pod =head2 Instance Properties @@ -571,7 +723,8 @@ sub buglist { return $_[0]->{buglist} } =item C True if this is a C or C -field. It is only safe to call L if this is true. +or C field. It is only safe to call L if +this is true. =item C @@ -583,11 +736,13 @@ objects. =cut sub is_select { - my ($invocant, $params) = @_; - # This allows this method to be called by create() validators. - my $type = blessed($invocant) ? $invocant->type : $params->{type}; - return ($type == FIELD_TYPE_SINGLE_SELECT - || $type == FIELD_TYPE_MULTI_SELECT) ? 1 : 0 + my ($invocant, $params) = @_; + + # This allows this method to be called by create() validators. + my $type = blessed($invocant) ? $invocant->type : $params->{type}; + return ($type == FIELD_TYPE_SINGLE_SELECT + || $type == FIELD_TYPE_MULTI_SELECT + || $type == FIELD_TYPE_ONE_SELECT) ? 1 : 0; } =over @@ -608,19 +763,20 @@ This method returns C<1> if the field is "abnormal", C<0> otherwise. =cut sub is_abnormal { - my $self = shift; - return ABNORMAL_SELECTS->{$self->name} ? 1 : 0; + my $self = shift; + return ABNORMAL_SELECTS->{$self->name} ? 1 : 0; } sub legal_values { - my $self = shift; + my $self = shift; - if (!defined $self->{'legal_values'}) { - require Bugzilla::Field::Choice; - my @values = Bugzilla::Field::Choice->type($self)->get_all(); - $self->{'legal_values'} = \@values; - } - return $self->{'legal_values'}; + if (!defined $self->{'legal_values'}) { + ## REDHAT EXTENSION 1039815: Filter partner values + my $values = Bugzilla->user->set_field_values($self); + + $self->{'legal_values'} = $values; + } + return $self->{'legal_values'}; } =pod @@ -637,8 +793,8 @@ in the C. =cut sub is_timetracking { - my ($self) = @_; - return grep($_ eq $self->name, TIMETRACKING_FIELDS) ? 1 : 0; + my ($self) = @_; + return grep($_ eq $self->name, TIMETRACKING_FIELDS) ? 1 : 0; } =pod @@ -657,12 +813,11 @@ Returns undef if there is no field that controls this field's visibility. =cut sub visibility_field { - my $self = shift; - if ($self->{visibility_field_id}) { - $self->{visibility_field} ||= - $self->new($self->{visibility_field_id}); - } - return $self->{visibility_field}; + my $self = shift; + if ($self->{visibility_field_id}) { + $self->{visibility_field} ||= $self->new($self->{visibility_field_id}); + } + return $self->{visibility_field}; } =pod @@ -680,22 +835,23 @@ or undef if there is no C set. =cut sub visibility_values { - my $self = shift; - my $dbh = Bugzilla->dbh; - - return [] if !$self->{visibility_field_id}; + my $self = shift; + my $dbh = Bugzilla->dbh; - if (!defined $self->{visibility_values}) { - my $visibility_value_ids = - $dbh->selectcol_arrayref("SELECT value_id FROM field_visibility - WHERE field_id = ?", undef, $self->id); + return [] if !$self->{visibility_field_id}; - $self->{visibility_values} = - Bugzilla::Field::Choice->type($self->visibility_field) - ->new_from_list($visibility_value_ids); - } + if (!defined $self->{visibility_values}) { + my $visibility_value_ids = $dbh->selectcol_arrayref( + "SELECT value_id FROM field_visibility + WHERE field_id = ?", undef, $self->id + ); - return $self->{visibility_values}; + $self->{visibility_values} + = Bugzilla::Field::Choice->type($self->visibility_field) + ->new_from_list($visibility_value_ids); + } + + return $self->{visibility_values}; } =pod @@ -712,10 +868,10 @@ field controls the visibility of. =cut sub controls_visibility_of { - my $self = shift; - $self->{controls_visibility_of} ||= - Bugzilla::Field->match({ visibility_field_id => $self->id }); - return $self->{controls_visibility_of}; + my $self = shift; + $self->{controls_visibility_of} + ||= Bugzilla::Field->match({visibility_field_id => $self->id}); + return $self->{controls_visibility_of}; } =pod @@ -733,11 +889,11 @@ Returns undef if there is no field that controls this field's visibility. =cut sub value_field { - my $self = shift; - if ($self->{value_field_id}) { - $self->{value_field} ||= $self->new($self->{value_field_id}); - } - return $self->{value_field}; + my $self = shift; + if ($self->{value_field_id}) { + $self->{value_field} ||= $self->new($self->{value_field_id}); + } + return $self->{value_field}; } =pod @@ -754,10 +910,10 @@ field controls the values of. =cut sub controls_values_of { - my $self = shift; - $self->{controls_values_of} ||= - Bugzilla::Field->match({ value_field_id => $self->id }); - return $self->{controls_values_of}; + my $self = shift; + $self->{controls_values_of} + ||= Bugzilla::Field->match({value_field_id => $self->id}); + return $self->{controls_values_of}; } =over @@ -771,15 +927,33 @@ See L. =cut sub is_visible_on_bug { - my ($self, $bug) = @_; + my ($self, $bug) = @_; - # Always return visible, if this field is not - # visibility controlled. - return 1 if !$self->{visibility_field_id}; + # Always return visible, if this field is not + # visibility controlled. + return 1 if !$self->{visibility_field_id}; - my $visibility_values = $self->visibility_values; + my $visibility_values = $self->visibility_values; - return (any { $_->is_set_on_bug($bug) } @$visibility_values) ? 1 : 0; + return (any { $_->is_set_on_bug($bug) } @$visibility_values) ? 1 : 0; +} + +=over + +=item C + +See L. + +=back + +=cut + +sub is_editable_on_bug { + my ($self, $bug, $user) = @_; + + return ($self->user_can_edit($user) + && ($user->in_group('editbugs', $bug->product_id)) + || ($self->name eq 'cf_partner' or $self->name eq 'cf_verified')); } =over @@ -795,13 +969,13 @@ dependency tree display, and similar functionality. =cut -sub is_relationship { - my $self = shift; - my $desc = $self->reverse_desc; - if (defined $desc && $desc ne "") { - return 1; - } - return 0; +sub is_relationship { + my $self = shift; + my $desc = $self->reverse_desc; + if (defined $desc && $desc ne "") { + return 1; + } + return 0; } =over @@ -847,6 +1021,135 @@ This is mostly used by L. sub is_numeric { return $_[0]->{is_numeric} } +=over + +=item C + +A hash of Groups, keyed on Group->id, of groups that can view a field. + +If this is empty then anyone can view the field. + +=item C + +A hash of Groups, keyed on Group->id, of groups that can edit a field. + +This right grants View access. + +If this and view_groups are empty then anyone can edit the field. + +=item C + +A hash of Groups, keyed on Group->id, of groups that can administrate a field. + +This right grants Edit access. + +If this empty then only teh admin group can administrate the field. + +=back + +=cut + +sub view_groups { $_[0]->_get_groups(ACCESS_TYPE_VIEW); } +sub edit_groups { $_[0]->_get_groups(ACCESS_TYPE_EDIT); } +sub admin_groups { $_[0]->_get_groups(ACCESS_TYPE_ADMIN); } + +sub _get_groups { + my ($self, $access) = @_; + + my $group_type = "group_type_$access"; + + unless ($self->{$group_type}) { + my $dbh = Bugzilla->dbh; + + my $SQL = <selectall_arrayref($SQL, undef, $self->id, $access)}) { + my $group = Bugzilla::Group->new($gid->[0]); + $groups{$group->id} = $group; + } + + $self->{$group_type} = \%groups if (%groups); + } + + return ($self->{$group_type}); +} + +=over + +=item C + +A boolean specifying whether or not a user can view this field. + +=item C + +A boolean specifying whether or not a user can edit this field. + +Edit access also grants View access. + +=item C + +A boolean specifying whether or not a user can administrate this field. + +Admin access also grants Edit access. + +=back + +=cut + +sub user_can_view { return $_[0]->_can_access($_[1], ACCESS_TYPE_VIEW); } +sub user_can_edit { return $_[0]->_can_access($_[1], ACCESS_TYPE_EDIT); } +sub user_can_admin { return $_[0]->_can_access($_[1], ACCESS_TYPE_ADMIN); } + +# can a user access this field with the given access level? +sub _can_access { + my ($self, $user, $access) = @_; + + # admin can do anything + return (1) if ($user->in_group('admin')); + + my $dbh = Bugzilla->dbh; + + my $SQL = <selectall_arrayref($SQL, undef, $self->id, $access); + + # If there are no rules >= the requested access then any user can view and edit + if ($has_rules->[0]->[0] == 0) { + return ($access <= ACCESS_TYPE_EDIT); + } + + + # If the user has no groups, then they can't view any CF with ACLs. + return (0) unless (@{$user->groups}); + + # Check group memberships + my $gids = join(', ', map { $_->id } @{$user->groups}); + + $SQL = <= ? +EOS + + my $can_access = $dbh->selectall_arrayref($SQL, undef, $self->id, $access); + + return ($can_access->[0]->[0]); +} + =pod =head2 Instance Mutators @@ -883,40 +1186,79 @@ They will throw an error if you try to set the values to something invalid. =item C +=item C + +=item C + +=item C =back =cut -sub set_description { $_[0]->set('description', $_[1]); } -sub set_long_desc { $_[0]->set('long_desc', $_[1]); } -sub set_enter_bug { $_[0]->set('enter_bug', $_[1]); } -sub set_is_numeric { $_[0]->set('is_numeric', $_[1]); } -sub set_obsolete { $_[0]->set('obsolete', $_[1]); } -sub set_sortkey { $_[0]->set('sortkey', $_[1]); } -sub set_in_new_bugmail { $_[0]->set('mailhead', $_[1]); } -sub set_buglist { $_[0]->set('buglist', $_[1]); } -sub set_reverse_desc { $_[0]->set('reverse_desc', $_[1]); } +sub set_description { $_[0]->set('description', $_[1]); } +sub set_long_desc { $_[0]->set('long_desc', $_[1]); } +sub set_enter_bug { $_[0]->set('enter_bug', $_[1]); } +sub set_is_numeric { $_[0]->set('is_numeric', $_[1]); } +sub set_obsolete { $_[0]->set('obsolete', $_[1]); } +sub set_sortkey { $_[0]->set('sortkey', $_[1]); } +sub set_in_new_bugmail { $_[0]->set('mailhead', $_[1]); } +sub set_buglist { $_[0]->set('buglist', $_[1]); } +sub set_reverse_desc { $_[0]->set('reverse_desc', $_[1]); } + sub set_visibility_field { - my ($self, $value) = @_; - $self->set('visibility_field_id', $value); - delete $self->{visibility_field}; - delete $self->{visibility_values}; + my ($self, $value) = @_; + $self->set('visibility_field_id', $value); + delete $self->{visibility_field}; + delete $self->{visibility_values}; } + sub set_visibility_values { - my ($self, $value_ids) = @_; - $self->set('visibility_values', $value_ids); + my ($self, $value_ids) = @_; + $self->set('visibility_values', $value_ids); } + sub set_value_field { - my ($self, $value) = @_; - $self->set('value_field_id', $value); - delete $self->{value_field}; + my ($self, $value) = @_; + $self->set('value_field_id', $value); + delete $self->{value_field}; } sub set_is_mandatory { $_[0]->set('is_mandatory', $_[1]); } # This is only used internally by upgrade code in Bugzilla::Field. sub _set_type { $_[0]->set('type', $_[1]); } +sub set_view_groups { $_[0]->_set_groups($_[1], ACCESS_TYPE_VIEW); } +sub set_edit_groups { $_[0]->_set_groups($_[1], ACCESS_TYPE_EDIT); } +sub set_admin_groups { $_[0]->_set_groups($_[1], ACCESS_TYPE_ADMIN); } + +sub _set_groups { + my ($self, $group_names, $access) = @_; + + my $group_type = "group_type_$access"; + + delete($self->{$group_type}); + + my $dbh = Bugzilla->dbh; + + $dbh->do( + "DELETE FROM fielddefs_access_groups WHERE field_id = ? AND access = ?", + undef, $self->id, $access); + + my $group_ids = $self->_check_groups($group_names); + + my $sth_insert + = $dbh->prepare( + 'INSERT INTO fielddefs_access_groups (field_id, group_id, access) VALUES (?,?, ?)' + ); + + foreach my $gid (@$group_ids) { + $sth_insert->execute($self->id, $gid, $access); + } + + return; +} + =pod =head2 Instance Method @@ -934,69 +1276,75 @@ there are no values specified (or EVER specified) for the field. =cut sub remove_from_db { - my $self = shift; - my $dbh = Bugzilla->dbh; + my $self = shift; + my $dbh = Bugzilla->dbh; - my $name = $self->name; + my $name = $self->name; - if (!$self->custom) { - ThrowCodeError('field_not_custom', {'name' => $name }); - } + if (!$self->custom) { + ThrowCodeError('field_not_custom', {'name' => $name}); + } - if (!$self->obsolete) { - ThrowUserError('customfield_not_obsolete', {'name' => $self->name }); - } + if (!$self->obsolete) { + ThrowUserError('customfield_not_obsolete', {'name' => $self->name}); + } - $dbh->bz_start_transaction(); + $dbh->bz_start_transaction(); - # 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 }); - } + # 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}); + } - # Check to see if bugs table has records (slow) - my $bugs_query = ""; + # 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"; - } - else { - $bugs_query = "SELECT COUNT(*) FROM bugs WHERE $name IS NOT NULL"; - if ($self->type != FIELD_TYPE_BUG_ID - && $self->type != FIELD_TYPE_DATE - && $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 != '---'"; - } + if ( $self->type == FIELD_TYPE_MULTI_SELECT + || $self->type == FIELD_TYPE_ONE_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_DATE + && $self->type != FIELD_TYPE_DATETIME) + { + $bugs_query .= " AND $name != ''"; } - my $has_bugs = $dbh->selectrow_array($bugs_query); - if ($has_bugs) { - ThrowUserError('customfield_has_contents', {'name' => $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}); + } - # Once we reach here, we should be OK to delete. - $self->SUPER::remove_from_db(); + # Once we reach here, we should be OK to delete. + $self->SUPER::remove_from_db(); - 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 && $type != FIELD_TYPE_ONE_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(); } =pod @@ -1042,90 +1390,95 @@ C - boolean - Whether this field is mandatory. Defaults to 0. =cut sub create { - my $class = shift; - my ($params) = @_; - my $dbh = Bugzilla->dbh; + my $class = shift; + 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(); + + $dbh->bz_commit_transaction(); + Bugzilla->memcached->clear_config(); + + 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}); + } - # 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(); - Bugzilla->memcached->clear_config(); + if ($field->is_select) { - 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 ($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; - - Bugzilla->memcached->clear({ table => 'fielddefs', id => $field->id }); - Bugzilla->memcached->clear_config(); + # Create the table that holds the legal values for this field. + $dbh->bz_add_field_tables($field); } - return $field; -} + if ($type == FIELD_TYPE_SINGLE_SELECT) { -sub update { - my $self = shift; - my $changes = $self->SUPER::update(@_); - my $dbh = Bugzilla->dbh; - if ($changes->{value_field_id} && $self->is_select) { - $dbh->do("UPDATE " . $self->name . " SET visibility_value_id = NULL"); + # Insert a default value of "---" into the legal values table. + $dbh->do("INSERT INTO $name (value) VALUES ('---')"); } - $self->_update_visibility_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; + + Bugzilla->memcached->clear({table => 'fielddefs', id => $field->id}); Bugzilla->memcached->clear_config(); - return $changes; + } + + return $field; } -sub _update_visibility_values { - my $self = shift; - my $dbh = Bugzilla->dbh; +sub update { + my $self = shift; + my $changes = $self->SUPER::update(@_); + my $dbh = Bugzilla->dbh; + if ($changes->{value_field_id} && $self->is_select) { + $dbh->do("UPDATE " . $self->name . " SET visibility_value_id = NULL"); + } + $self->_update_visibility_values(); + Bugzilla->memcached->clear_config(); + return $changes; +} - my @visibility_value_ids = map($_->id, @{$self->visibility_values}); - $self->_delete_visibility_values(); - for my $value_id (@visibility_value_ids) { - $dbh->do("INSERT INTO field_visibility (field_id, value_id) - VALUES (?, ?)", undef, $self->id, $value_id); - } +sub _update_visibility_values { + my $self = shift; + my $dbh = Bugzilla->dbh; + + my @visibility_value_ids = map($_->id, @{$self->visibility_values}); + $self->_delete_visibility_values(); + for my $value_id (@visibility_value_ids) { + $dbh->do( + "INSERT INTO field_visibility (field_id, value_id) + VALUES (?, ?)", undef, $self->id, $value_id + ); + } } sub _delete_visibility_values { - my ($self) = @_; - my $dbh = Bugzilla->dbh; - $dbh->do("DELETE FROM field_visibility WHERE field_id = ?", - undef, $self->id); - delete $self->{visibility_values}; + my ($self) = @_; + my $dbh = Bugzilla->dbh; + $dbh->do("DELETE FROM field_visibility WHERE field_id = ?", undef, $self->id); + delete $self->{visibility_values}; } =pod @@ -1148,13 +1501,14 @@ Returns: a reference to a list of valid values. =cut sub get_legal_field_values { - my ($field) = @_; - my $dbh = Bugzilla->dbh; - my $result_ref = $dbh->selectcol_arrayref( - "SELECT value FROM $field + my ($field) = @_; + my $dbh = Bugzilla->dbh; + my $result_ref = $dbh->selectcol_arrayref( + "SELECT value FROM $field WHERE isactive = ? - ORDER BY sortkey, value", undef, (1)); - return $result_ref; + ORDER BY sortkey, value", undef, (1) + ); + return $result_ref; } =over @@ -1173,107 +1527,115 @@ Returns: nothing =cut sub populate_field_definitions { - my $dbh = Bugzilla->dbh; + my $dbh = Bugzilla->dbh; + + # ADD and UPDATE field definitions + foreach my $def (DEFAULT_FIELDS) { + my $field = new Bugzilla::Field({name => $def->{name}}); + if ($field) { + $field->set_description($def->{desc}); + $field->set_in_new_bugmail($def->{in_new_bugmail}); + $field->set_buglist($def->{buglist}); + $field->_set_type($def->{type}) if $def->{type}; + $field->set_is_mandatory($def->{is_mandatory}); + $field->set_is_numeric($def->{is_numeric}); + $field->update(); + } + else { + if (exists $def->{in_new_bugmail}) { + $def->{mailhead} = $def->{in_new_bugmail}; + delete $def->{in_new_bugmail}; + } + $def->{description} = delete $def->{desc}; + Bugzilla::Field->create($def); + } + } - # ADD and UPDATE field definitions - foreach my $def (DEFAULT_FIELDS) { - my $field = new Bugzilla::Field({ name => $def->{name} }); - if ($field) { - $field->set_description($def->{desc}); - $field->set_in_new_bugmail($def->{in_new_bugmail}); - $field->set_buglist($def->{buglist}); - $field->_set_type($def->{type}) if $def->{type}; - $field->set_is_mandatory($def->{is_mandatory}); - $field->set_is_numeric($def->{is_numeric}); - $field->update(); - } - else { - if (exists $def->{in_new_bugmail}) { - $def->{mailhead} = $def->{in_new_bugmail}; - delete $def->{in_new_bugmail}; - } - $def->{description} = delete $def->{desc}; - Bugzilla::Field->create($def); - } + # DELETE fields which were added only accidentally, or which + # were never tracked in bugs_activity. Note that you can never + # delete fields which are used by bugs_activity. + + # Oops. Bug 163299 + $dbh->do("DELETE FROM fielddefs WHERE name='cc_accessible'"); + + # Oops. Bug 215319 + $dbh->do("DELETE FROM fielddefs WHERE name='requesters.login_name'"); + + # This field was never tracked in bugs_activity, so it's safe to delete. + $dbh->do("DELETE FROM fielddefs WHERE name='attachments.thedata'"); + + # MODIFY old field definitions + + # 2005-11-13 LpSolit@gmail.com - Bug 302599 + # One of the field names was a fragment of SQL code, which is DB dependent. + # We have to rename it to a real name, which is DB independent. + my $new_field_name = 'days_elapsed'; + my $field_description = 'Days since bug changed'; + + my ($old_field_id, $old_field_name) = $dbh->selectrow_array( + 'SELECT id, name FROM fielddefs + WHERE description = ?', undef, $field_description + ); + + if ($old_field_id && ($old_field_name ne $new_field_name)) { + say "SQL fragment found in the 'fielddefs' table..."; + say "Old field name: $old_field_name"; + + # We have to fix saved searches first. Queries have been escaped + # before being saved. We have to do the same here to find them. + $old_field_name = url_quote($old_field_name); + my $broken_named_queries = $dbh->selectall_arrayref( + 'SELECT userid, name, query + FROM namedqueries WHERE ' + . $dbh->sql_istrcmp('query', '?', 'LIKE'), undef, "%=$old_field_name%" + ); + + my $sth_UpdateQueries = $dbh->prepare( + 'UPDATE namedqueries SET query = ? + WHERE userid = ? AND name = ?' + ); + + print "Fixing saved searches...\n" if scalar(@$broken_named_queries); + foreach my $named_query (@$broken_named_queries) { + my ($userid, $name, $query) = @$named_query; + $query =~ s/=\Q$old_field_name\E(&|$)/=$new_field_name$1/gi; + $sth_UpdateQueries->execute($query, $userid, $name); } - # DELETE fields which were added only accidentally, or which - # were never tracked in bugs_activity. Note that you can never - # delete fields which are used by bugs_activity. - - # Oops. Bug 163299 - $dbh->do("DELETE FROM fielddefs WHERE name='cc_accessible'"); - # Oops. Bug 215319 - $dbh->do("DELETE FROM fielddefs WHERE name='requesters.login_name'"); - # This field was never tracked in bugs_activity, so it's safe to delete. - $dbh->do("DELETE FROM fielddefs WHERE name='attachments.thedata'"); - - # MODIFY old field definitions - - # 2005-11-13 LpSolit@gmail.com - Bug 302599 - # One of the field names was a fragment of SQL code, which is DB dependent. - # We have to rename it to a real name, which is DB independent. - my $new_field_name = 'days_elapsed'; - my $field_description = 'Days since bug changed'; - - my ($old_field_id, $old_field_name) = - $dbh->selectrow_array('SELECT id, name FROM fielddefs - WHERE description = ?', - undef, $field_description); - - if ($old_field_id && ($old_field_name ne $new_field_name)) { - say "SQL fragment found in the 'fielddefs' table..."; - say "Old field name: $old_field_name"; - # We have to fix saved searches first. Queries have been escaped - # before being saved. We have to do the same here to find them. - $old_field_name = url_quote($old_field_name); - my $broken_named_queries = - $dbh->selectall_arrayref('SELECT userid, name, query - FROM namedqueries WHERE ' . - $dbh->sql_istrcmp('query', '?', 'LIKE'), - undef, "%=$old_field_name%"); - - my $sth_UpdateQueries = $dbh->prepare('UPDATE namedqueries SET query = ? - WHERE userid = ? AND name = ?'); - - print "Fixing saved searches...\n" if scalar(@$broken_named_queries); - foreach my $named_query (@$broken_named_queries) { - my ($userid, $name, $query) = @$named_query; - $query =~ s/=\Q$old_field_name\E(&|$)/=$new_field_name$1/gi; - $sth_UpdateQueries->execute($query, $userid, $name); - } - - # We now do the same with saved chart series. - my $broken_series = - $dbh->selectall_arrayref('SELECT series_id, query - FROM series WHERE ' . - $dbh->sql_istrcmp('query', '?', 'LIKE'), - undef, "%=$old_field_name%"); - - my $sth_UpdateSeries = $dbh->prepare('UPDATE series SET query = ? - WHERE series_id = ?'); - - print "Fixing saved chart series...\n" if scalar(@$broken_series); - foreach my $series (@$broken_series) { - my ($series_id, $query) = @$series; - $query =~ s/=\Q$old_field_name\E(&|$)/=$new_field_name$1/gi; - $sth_UpdateSeries->execute($query, $series_id); - } - # Now that saved searches have been fixed, we can fix the field name. - say "Fixing the 'fielddefs' table..."; - say "New field name: $new_field_name"; - $dbh->do('UPDATE fielddefs SET name = ? WHERE id = ?', - undef, ($new_field_name, $old_field_id)); + # We now do the same with saved chart series. + my $broken_series = $dbh->selectall_arrayref( + 'SELECT series_id, query + FROM series WHERE ' + . $dbh->sql_istrcmp('query', '?', 'LIKE'), undef, "%=$old_field_name%" + ); + + my $sth_UpdateSeries = $dbh->prepare( + 'UPDATE series SET query = ? + WHERE series_id = ?' + ); + + print "Fixing saved chart series...\n" if scalar(@$broken_series); + foreach my $series (@$broken_series) { + my ($series_id, $query) = @$series; + $query =~ s/=\Q$old_field_name\E(&|$)/=$new_field_name$1/gi; + $sth_UpdateSeries->execute($query, $series_id); } - # This field has to be created separately, or the above upgrade code - # might not run properly. - Bugzilla::Field->create({ name => $new_field_name, - description => $field_description }) - unless new Bugzilla::Field({ name => $new_field_name }); + # Now that saved searches have been fixed, we can fix the field name. + say "Fixing the 'fielddefs' table..."; + say "New field name: $new_field_name"; + $dbh->do('UPDATE fielddefs SET name = ? WHERE id = ?', + undef, ($new_field_name, $old_field_id)); + } -} + # This field has to be created separately, or the above upgrade code + # might not run properly. + Bugzilla::Field->create({ + name => $new_field_name, description => $field_description + }) + unless new Bugzilla::Field({name => $new_field_name}); +} =head2 Data Validation @@ -1305,32 +1667,32 @@ Returns: 1 on success; 0 on failure if $no_warn is true (else an =cut sub check_field { - my ($name, $value, $legalsRef, $no_warn) = @_; - my $dbh = Bugzilla->dbh; - - # If $legalsRef is undefined, we use the default valid values. - # Valid values for this check are all possible values. - # Using get_legal_values would only return active values, but since - # some bugs may have inactive values set, we want to check them too. - unless (defined $legalsRef) { - $legalsRef = Bugzilla::Field->new({name => $name})->legal_values; - my @values = map($_->name, @$legalsRef); - $legalsRef = \@values; + my ($name, $value, $legalsRef, $no_warn) = @_; + my $dbh = Bugzilla->dbh; + + # If $legalsRef is undefined, we use the default valid values. + # Valid values for this check are all possible values. + # Using get_legal_values would only return active values, but since + # some bugs may have inactive values set, we want to check them too. + unless (defined $legalsRef) { + $legalsRef = Bugzilla::Field->new({name => $name})->legal_values; + my @values = map($_->name, @$legalsRef); + $legalsRef = \@values; - } + } - if (!defined($value) - or trim($value) eq "" - or !grep { $_ eq $value } @$legalsRef) - { - return 0 if $no_warn; # We don't want an error to be thrown; return. - trick_taint($name); + if ( !defined($value) + or trim($value) eq "" + or !grep { $_ eq $value } @$legalsRef) + { + return 0 if $no_warn; # We don't want an error to be thrown; return. + trick_taint($name); - my $field = new Bugzilla::Field({ name => $name }); - my $field_desc = $field ? $field->description : $name; - ThrowCodeError('illegal_field', { field => $field_desc }); - } - return 1; + my $field = new Bugzilla::Field({name => $name}); + my $field_desc = $field ? $field->description : $name; + ThrowCodeError('illegal_field', {field => $field_desc}); + } + return 1; } =pod @@ -1352,10 +1714,10 @@ Returns: the corresponding field ID or an error if the field name =cut sub get_field_id { - my $field = Bugzilla->fields({ by_name => 1 })->{$_[0]} - or ThrowCodeError('invalid_field_name', {field => $_[0]}); + my $field = Bugzilla->fields({by_name => 1})->{$_[0]} + or ThrowCodeError('invalid_field_name', {field => $_[0]}); - return $field->id; + return $field->id; } 1; diff --git a/Bugzilla/Field/Choice.pm b/Bugzilla/Field/Choice.pm index a66f69cee..9c18a1f5a 100644 --- a/Bugzilla/Field/Choice.pm +++ b/Bugzilla/Field/Choice.pm @@ -28,42 +28,42 @@ use Scalar::Util qw(blessed); use constant IS_CONFIG => 1; use constant DB_COLUMNS => qw( - id - value - sortkey - isactive - visibility_value_id + id + value + sortkey + isactive + visibility_value_id ); use constant UPDATE_COLUMNS => qw( - value - sortkey - isactive - visibility_value_id + value + sortkey + isactive + visibility_value_id ); use constant NAME_FIELD => 'value'; use constant LIST_ORDER => 'sortkey, value'; use constant VALIDATORS => { - value => \&_check_value, - sortkey => \&_check_sortkey, - visibility_value_id => \&_check_visibility_value_id, - isactive => \&_check_isactive, + value => \&_check_value, + sortkey => \&_check_sortkey, + visibility_value_id => \&_check_visibility_value_id, + isactive => \&_check_isactive, }; use constant CLASS_MAP => { - bug_status => 'Bugzilla::Status', - classification => 'Bugzilla::Classification', - component => 'Bugzilla::Component', - product => 'Bugzilla::Product', + bug_status => 'Bugzilla::Status', + classification => 'Bugzilla::Classification', + component => 'Bugzilla::Component', + product => 'Bugzilla::Product', }; use constant DEFAULT_MAP => { - op_sys => 'defaultopsys', - rep_platform => 'defaultplatform', - priority => 'defaultpriority', - bug_severity => 'defaultseverity', + op_sys => 'defaultopsys', + rep_platform => 'defaultplatform', + priority => 'defaultpriority', + bug_severity => 'defaultseverity', }; ################# @@ -76,49 +76,50 @@ use constant DEFAULT_MAP => { # are Bugzilla::Status objects. sub type { - my ($class, $field) = @_; - my $field_obj = blessed $field ? $field : Bugzilla::Field->check($field); - my $field_name = $field_obj->name; - - if (my $package = $class->CLASS_MAP->{$field_name}) { - # Callers expect the module to be already loaded. - eval "require $package"; - return $package; - } + my ($class, $field) = @_; + my $field_obj = blessed $field ? $field : Bugzilla::Field->check($field); + my $field_name = $field_obj->name; + + if (my $package = $class->CLASS_MAP->{$field_name}) { - # For generic classes, we use a lowercase class name, so as - # not to interfere with any real subclasses we might make some day. - my $package = "Bugzilla::Field::Choice::$field_name"; - Bugzilla->request_cache->{"field_$package"} = $field_obj; - - # This package only needs to be created once. We check if the DB_TABLE - # glob for this package already exists, which tells us whether or not - # we need to create the package (this works even under mod_perl, where - # this package definition will persist across requests)). - if (!defined *{"${package}::DB_TABLE"}) { - eval <request_cache->{"field_$package"} = $field_obj; + + # This package only needs to be created once. We check if the DB_TABLE + # glob for this package already exists, which tells us whether or not + # we need to create the package (this works even under mod_perl, where + # this package definition will persist across requests)). + if (!defined *{"${package}::DB_TABLE"}) { + eval < '$field_name'; EOC - } + } - return $package; + return $package; } ################ # Constructors # ################ -# We just make new() enforce this, which should give developers +# We just make new() enforce this, which should give developers # the understanding that you can't use Bugzilla::Field::Choice # without calling type(). sub new { - my $class = shift; - if ($class eq 'Bugzilla::Field::Choice') { - ThrowCodeError('field_choice_must_use_type'); - } - $class->SUPER::new(@_); + my $class = shift; + if ($class eq 'Bugzilla::Field::Choice') { + ThrowCodeError('field_choice_must_use_type'); + } + $class->SUPER::new(@_); } ######################### @@ -130,64 +131,68 @@ sub new { # columns. (Normally Bugzilla::Object dies if you pass arguments # that aren't valid columns.) sub create { - my $class = shift; - my ($params) = @_; - foreach my $key (keys %$params) { - if (!grep {$_ eq $key} $class->_get_db_columns) { - delete $params->{$key}; - } + my $class = shift; + my ($params) = @_; + foreach my $key (keys %$params) { + if (!grep { $_ eq $key } $class->_get_db_columns) { + delete $params->{$key}; } - return $class->SUPER::create(@_); + } + return $class->SUPER::create(@_); } sub update { - my $self = shift; - my $dbh = Bugzilla->dbh; - my $fname = $self->field->name; - - $dbh->bz_start_transaction(); - - my ($changes, $old_self) = $self->SUPER::update(@_); - if (exists $changes->{value}) { - my ($old, $new) = @{ $changes->{value} }; - if ($self->field->type == FIELD_TYPE_MULTI_SELECT) { - $dbh->do("UPDATE bug_$fname SET value = ? WHERE value = ?", - undef, $new, $old); - } - else { - $dbh->do("UPDATE bugs SET $fname = ? WHERE $fname = ?", - undef, $new, $old); - } + my $self = shift; + my $dbh = Bugzilla->dbh; + my $fname = $self->field->name; - if ($old_self->is_default) { - my $param = $self->DEFAULT_MAP->{$self->field->name}; - SetParam($param, $self->name); - write_params(); - } + $dbh->bz_start_transaction(); + + my ($changes, $old_self) = $self->SUPER::update(@_); + if (exists $changes->{value}) { + my ($old, $new) = @{$changes->{value}}; + if ( $self->field->type == FIELD_TYPE_MULTI_SELECT + || $self->field->type == FIELD_TYPE_ONE_SELECT) + { + $dbh->do("UPDATE bug_$fname SET value = ? WHERE value = ?", undef, $new, $old); } + else { + $dbh->do("UPDATE bugs SET $fname = ? WHERE $fname = ?", undef, $new, $old); + } + + if ($old_self->is_default) { + my $param = $self->DEFAULT_MAP->{$self->field->name}; + SetParam($param, $self->name); + write_params(); + } + } - $dbh->bz_commit_transaction(); - return wantarray ? ($changes, $old_self) : $changes; + $dbh->bz_commit_transaction(); + return wantarray ? ($changes, $old_self) : $changes; } sub remove_from_db { - my $self = shift; - if ($self->is_default) { - ThrowUserError('fieldvalue_is_default', - { field => $self->field, value => $self, - param_name => $self->DEFAULT_MAP->{$self->field->name}, - }); - } - if ($self->is_static) { - ThrowUserError('fieldvalue_not_deletable', - { field => $self->field, value => $self }); - } - if ($self->bug_count) { - ThrowUserError("fieldvalue_still_has_bugs", - { field => $self->field, value => $self }); - } - $self->_check_if_controller(); # From ChoiceInterface. - $self->SUPER::remove_from_db(); + my $self = shift; + if ($self->is_default) { + ThrowUserError( + 'fieldvalue_is_default', + { + field => $self->field, + value => $self, + param_name => $self->DEFAULT_MAP->{$self->field->name}, + } + ); + } + if ($self->is_static) { + ThrowUserError('fieldvalue_not_deletable', + {field => $self->field, value => $self}); + } + if ($self->bug_count) { + ThrowUserError("fieldvalue_still_has_bugs", + {field => $self->field, value => $self}); + } + $self->_check_if_controller(); # From ChoiceInterface. + $self->SUPER::remove_from_db(); } ############ @@ -195,12 +200,13 @@ sub remove_from_db { ############ sub set_is_active { $_[0]->set('isactive', $_[1]); } -sub set_name { $_[0]->set('value', $_[1]); } -sub set_sortkey { $_[0]->set('sortkey', $_[1]); } +sub set_name { $_[0]->set('value', $_[1]); } +sub set_sortkey { $_[0]->set('sortkey', $_[1]); } + sub set_visibility_value { - my ($self, $value) = @_; - $self->set('visibility_value_id', $value); - delete $self->{visibility_value}; + my ($self, $value) = @_; + $self->set('visibility_value_id', $value); + delete $self->{visibility_value}; } ############## @@ -208,73 +214,74 @@ sub set_visibility_value { ############## sub _check_isactive { - my ($invocant, $value) = @_; - $value = Bugzilla::Object::check_boolean($invocant, $value); - if (!$value and ref $invocant) { - if ($invocant->is_default) { - my $field = $invocant->field; - ThrowUserError('fieldvalue_is_default', - { value => $invocant, field => $field, - param_name => $invocant->DEFAULT_MAP->{$field->name} - }); - } - if ($invocant->is_static) { - ThrowUserError('fieldvalue_not_deletable', - { value => $invocant, field => $invocant->field }); + my ($invocant, $value) = @_; + $value = Bugzilla::Object::check_boolean($invocant, $value); + if (!$value and ref $invocant) { + if ($invocant->is_default) { + my $field = $invocant->field; + ThrowUserError( + 'fieldvalue_is_default', + { + value => $invocant, + field => $field, + param_name => $invocant->DEFAULT_MAP->{$field->name} } + ); + } + if ($invocant->is_static) { + ThrowUserError('fieldvalue_not_deletable', + {value => $invocant, field => $invocant->field}); } - return $value; + } + return $value; } sub _check_value { - my ($invocant, $value) = @_; + my ($invocant, $value) = @_; - my $field = $invocant->field; + my $field = $invocant->field; - $value = trim($value); + $value = trim($value); - # Make sure people don't rename static values - if (blessed($invocant) && $value ne $invocant->name - && $invocant->is_static) - { - ThrowUserError('fieldvalue_not_editable', - { field => $field, old_value => $invocant }); - } + # Make sure people don't rename static values + if (blessed($invocant) && $value ne $invocant->name && $invocant->is_static) { + ThrowUserError('fieldvalue_not_editable', + {field => $field, old_value => $invocant}); + } - ThrowUserError('fieldvalue_undefined') if !defined $value || $value eq ""; - ThrowUserError('fieldvalue_name_too_long', { value => $value }) - if length($value) > MAX_FIELD_VALUE_SIZE; + ThrowUserError('fieldvalue_undefined') if !defined $value || $value eq ""; + ThrowUserError('fieldvalue_name_too_long', {value => $value}) + if length($value) > MAX_FIELD_VALUE_SIZE; - my $exists = $invocant->type($field)->new({ name => $value }); - if ($exists && (!blessed($invocant) || $invocant->id != $exists->id)) { - ThrowUserError('fieldvalue_already_exists', - { field => $field, value => $exists }); - } + my $exists = $invocant->type($field)->new({name => $value}); + if ($exists && (!blessed($invocant) || $invocant->id != $exists->id)) { + ThrowUserError('fieldvalue_already_exists', + {field => $field, value => $exists}); + } - return $value; + return $value; } sub _check_sortkey { - my ($invocant, $value) = @_; - $value = trim($value); - return 0 if !$value; - # Store for the error message in case detaint_natural clears it. - my $orig_value = $value; - (detaint_natural($value) && $value <= MAX_SMALLINT) - || ThrowUserError('fieldvalue_sortkey_invalid', - { sortkey => $orig_value, - field => $invocant->field }); - return $value; + my ($invocant, $value) = @_; + $value = trim($value); + return 0 if !$value; + + # Store for the error message in case detaint_natural clears it. + my $orig_value = $value; + (detaint_natural($value) && $value <= MAX_SMALLINT) + || ThrowUserError('fieldvalue_sortkey_invalid', + {sortkey => $orig_value, field => $invocant->field}); + return $value; } sub _check_visibility_value_id { - my ($invocant, $value_id) = @_; - $value_id = trim($value_id); - my $field = $invocant->field->value_field; - return undef if !$field || !$value_id; - my $value_obj = Bugzilla::Field::Choice->type($field) - ->check({ id => $value_id }); - return $value_obj->id; + my ($invocant, $value_id) = @_; + $value_id = trim($value_id); + my $field = $invocant->field->value_field; + return undef if !$field || !$value_id; + my $value_obj = Bugzilla::Field::Choice->type($field)->check({id => $value_id}); + return $value_obj->id; } 1; diff --git a/Bugzilla/Field/ChoiceInterface.pm b/Bugzilla/Field/ChoiceInterface.pm index 634d36ad1..71ff039ed 100644 --- a/Bugzilla/Field/ChoiceInterface.pm +++ b/Bugzilla/Field/ChoiceInterface.pm @@ -26,14 +26,19 @@ sub FIELD_NAME { return $_[0]->DB_TABLE; } #################### sub _check_if_controller { - my $self = shift; - my $vis_fields = $self->controls_visibility_of_fields; - my $values = $self->controlled_values_array; - if (@$vis_fields || @$values) { - ThrowUserError('fieldvalue_is_controller', - { value => $self, fields => [map($_->name, @$vis_fields)], - vals => $self->controlled_values }); - } + my $self = shift; + my $vis_fields = $self->controls_visibility_of_fields; + my $values = $self->controlled_values_array; + if (@$vis_fields || @$values) { + ThrowUserError( + 'fieldvalue_is_controller', + { + value => $self, + fields => [map($_->name, @$vis_fields)], + vals => $self->controlled_values + } + ); + } } @@ -42,145 +47,151 @@ sub _check_if_controller { ############# sub is_active { return $_[0]->{'isactive'}; } -sub sortkey { return $_[0]->{'sortkey'}; } +sub sortkey { return $_[0]->{'sortkey'}; } sub bug_count { - my $self = shift; - return $self->{bug_count} if defined $self->{bug_count}; - my $dbh = Bugzilla->dbh; - my $fname = $self->field->name; - my $count; - if ($self->field->type == FIELD_TYPE_MULTI_SELECT) { - $count = $dbh->selectrow_array("SELECT COUNT(*) FROM bug_$fname - WHERE value = ?", undef, $self->name); - } - else { - $count = $dbh->selectrow_array("SELECT COUNT(*) FROM bugs - WHERE $fname = ?", - undef, $self->name); - } - $self->{bug_count} = $count; - return $count; + my $self = shift; + return $self->{bug_count} if defined $self->{bug_count}; + my $dbh = Bugzilla->dbh; + my $fname = $self->field->name; + my $count; + if ( $self->field->type == FIELD_TYPE_MULTI_SELECT + || $self->field->type == FIELD_TYPE_ONE_SELECT) + { + $count = $dbh->selectrow_array( + "SELECT COUNT(*) FROM bug_$fname + WHERE value = ?", undef, $self->name + ); + } + else { + $count = $dbh->selectrow_array( + "SELECT COUNT(*) FROM bugs + WHERE $fname = ?", undef, $self->name + ); + } + $self->{bug_count} = $count; + return $count; } sub field { - my $invocant = shift; - my $class = ref $invocant || $invocant; - my $cache = Bugzilla->request_cache; - # This is just to make life easier for subclasses. Our auto-generated - # subclasses from Bugzilla::Field::Choice->type() already have this set. - $cache->{"field_$class"} ||= - new Bugzilla::Field({ name => $class->FIELD_NAME }); - return $cache->{"field_$class"}; + my $invocant = shift; + my $class = ref $invocant || $invocant; + my $cache = Bugzilla->request_cache; + + # This is just to make life easier for subclasses. Our auto-generated + # subclasses from Bugzilla::Field::Choice->type() already have this set. + $cache->{"field_$class"} ||= new Bugzilla::Field({name => $class->FIELD_NAME}); + return $cache->{"field_$class"}; } sub is_default { - my $self = shift; - my $name = $self->DEFAULT_MAP->{$self->field->name}; - # If it doesn't exist in DEFAULT_MAP, then there is no parameter - # related to this field. - return 0 unless $name; - return ($self->name eq Bugzilla->params->{$name}) ? 1 : 0; + my $self = shift; + my $name = $self->DEFAULT_MAP->{$self->field->name}; + + # If it doesn't exist in DEFAULT_MAP, then there is no parameter + # related to this field. + return 0 unless $name; + return ($self->name eq Bugzilla->params->{$name}) ? 1 : 0; } sub is_static { - my $self = shift; - # If we need to special-case Resolution for *anything* else, it should - # get its own subclass. - if ($self->field->name eq 'resolution') { - return grep($_ eq $self->name, ('', 'FIXED', 'DUPLICATE')) - ? 1 : 0; - } - elsif ($self->field->custom) { - return $self->name eq '---' ? 1 : 0; - } - return 0; + my $self = shift; + + # If we need to special-case Resolution for *anything* else, it should + # get its own subclass. + if ($self->field->name eq 'resolution') { + return grep($_ eq $self->name, ('', 'FIXED', 'DUPLICATE')) ? 1 : 0; + } + elsif ($self->field->custom) { + return $self->name eq '---' ? 1 : 0; + } + return 0; } sub controls_visibility_of_fields { - my $self = shift; - my $dbh = Bugzilla->dbh; + my $self = shift; + my $dbh = Bugzilla->dbh; - if (!$self->{controls_visibility_of_fields}) { - my $ids = $dbh->selectcol_arrayref( - "SELECT id FROM fielddefs + if (!$self->{controls_visibility_of_fields}) { + my $ids = $dbh->selectcol_arrayref( + "SELECT id FROM fielddefs INNER JOIN field_visibility ON fielddefs.id = field_visibility.field_id - WHERE value_id = ? AND visibility_field_id = ?", undef, - $self->id, $self->field->id); + WHERE value_id = ? AND visibility_field_id = ?", undef, $self->id, + $self->field->id + ); - $self->{controls_visibility_of_fields} = - Bugzilla::Field->new_from_list($ids); - } + $self->{controls_visibility_of_fields} = Bugzilla::Field->new_from_list($ids); + } - return $self->{controls_visibility_of_fields}; + return $self->{controls_visibility_of_fields}; } sub visibility_value { - my $self = shift; - if ($self->{visibility_value_id}) { - require Bugzilla::Field::Choice; - $self->{visibility_value} ||= - Bugzilla::Field::Choice->type($self->field->value_field)->new( - $self->{visibility_value_id}); - } - return $self->{visibility_value}; + my $self = shift; + if ($self->{visibility_value_id}) { + require Bugzilla::Field::Choice; + $self->{visibility_value} + ||= Bugzilla::Field::Choice->type($self->field->value_field) + ->new($self->{visibility_value_id}); + } + return $self->{visibility_value}; } sub controlled_values { - my $self = shift; - return $self->{controlled_values} if defined $self->{controlled_values}; - my $fields = $self->field->controls_values_of; - my %controlled_values; - require Bugzilla::Field::Choice; - foreach my $field (@$fields) { - $controlled_values{$field->name} = - Bugzilla::Field::Choice->type($field) - ->match({ visibility_value_id => $self->id }); - } - $self->{controlled_values} = \%controlled_values; - return $self->{controlled_values}; + my $self = shift; + return $self->{controlled_values} if defined $self->{controlled_values}; + my $fields = $self->field->controls_values_of; + my %controlled_values; + require Bugzilla::Field::Choice; + foreach my $field (@$fields) { + $controlled_values{$field->name} = Bugzilla::Field::Choice->type($field) + ->match({visibility_value_id => $self->id}); + } + $self->{controlled_values} = \%controlled_values; + return $self->{controlled_values}; } sub controlled_values_array { - my ($self) = @_; - my $values = $self->controlled_values; - return [map { @{ $values->{$_} } } keys %$values]; + my ($self) = @_; + my $values = $self->controlled_values; + return [map { @{$values->{$_}} } keys %$values]; } sub is_visible_on_bug { - my ($self, $bug) = @_; + my ($self, $bug) = @_; - # Values currently set on the bug are always shown. - return 1 if $self->is_set_on_bug($bug); + # Values currently set on the bug are always shown. + return 1 if $self->is_set_on_bug($bug); - # Inactive values are, otherwise, never shown. - return 0 if !$self->is_active; + # Inactive values are, otherwise, never shown. + return 0 if !$self->is_active; - # Values without a visibility value are, otherwise, always shown. - my $visibility_value = $self->visibility_value; - return 1 if !$visibility_value; + # Values without a visibility value are, otherwise, always shown. + my $visibility_value = $self->visibility_value; + return 1 if !$visibility_value; - # Values with a visibility value are only shown if the visibility - # value is set on the bug. - return $visibility_value->is_set_on_bug($bug); + # Values with a visibility value are only shown if the visibility + # value is set on the bug. + return $visibility_value->is_set_on_bug($bug); } sub is_set_on_bug { - my ($self, $bug) = @_; - my $field_name = $self->FIELD_NAME; - # This allows bug/create/create.html.tmpl to pass in a hashref that - # looks like a bug object. - my $value = blessed($bug) ? $bug->$field_name : $bug->{$field_name}; - $value = $value->name if blessed($value); - return 0 if !defined $value; - - if ($self->field->type == FIELD_TYPE_BUG_URLS - or $self->field->type == FIELD_TYPE_MULTI_SELECT) - { - return grep($_ eq $self->name, @$value) ? 1 : 0; - } - return $value eq $self->name ? 1 : 0; + my ($self, $bug) = @_; + my $field_name = $self->FIELD_NAME; + + # This allows bug/create/create.html.tmpl to pass in a hashref that + # looks like a bug object. + my $value = blessed($bug) ? $bug->$field_name : $bug->{$field_name}; + $value = $value->name if blessed($value); + return 0 if !defined $value; + + if ( $self->field->type == FIELD_TYPE_BUG_URLS + or $self->field->type == FIELD_TYPE_MULTI_SELECT) + { + return grep($_ eq $self->name, @$value) ? 1 : 0; + } + return $value eq $self->name ? 1 : 0; } 1; diff --git a/Bugzilla/Flag.pm b/Bugzilla/Flag.pm index 50474b885..d1068fbfd 100644 --- a/Bugzilla/Flag.pm +++ b/Bugzilla/Flag.pm @@ -58,8 +58,9 @@ use parent qw(Bugzilla::Object Exporter); #### Initialization #### ############################### -use constant DB_TABLE => 'flags'; +use constant DB_TABLE => 'flags'; use constant LIST_ORDER => 'id'; + # Flags are tracked in bugs_activity. use constant AUDIT_CREATES => 0; use constant AUDIT_UPDATES => 0; @@ -68,35 +69,32 @@ use constant AUDIT_REMOVES => 0; use constant SKIP_REQUESTEE_ON_ERROR => 1; 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'; + 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 - setter_id - status - type_id + requestee_id + setter_id + status + type_id ); -use constant VALIDATORS => { -}; +use constant VALIDATORS => {}; -use constant UPDATE_VALIDATORS => { - setter => \&_check_setter, - status => \&_check_status, -}; +use constant UPDATE_VALIDATORS => + {setter => \&_check_setter, status => \&_check_status,}; ############################### #### Accessors ###### @@ -138,15 +136,15 @@ Returns the timestamp when the flag was last modified. =cut -sub id { return $_[0]->{'id'}; } -sub name { return $_[0]->type->name; } -sub type_id { return $_[0]->{'type_id'}; } -sub bug_id { return $_[0]->{'bug_id'}; } -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 id { return $_[0]->{'id'}; } +sub name { return $_[0]->type->name; } +sub type_id { return $_[0]->{'type_id'}; } +sub bug_id { return $_[0]->{'bug_id'}; } +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'}; } ############################### @@ -180,40 +178,42 @@ is an attachment flag, else undefined. =cut sub type { - my $self = shift; + my $self = shift; - return $self->{'type'} ||= new Bugzilla::FlagType($self->{'type_id'}); + return $self->{'type'} ||= new Bugzilla::FlagType($self->{'type_id'}); } sub setter { - my $self = shift; + my $self = shift; - return $self->{'setter'} ||= new Bugzilla::User({ id => $self->{'setter_id'}, cache => 1 }); + return $self->{'setter'} + ||= new Bugzilla::User({id => $self->{'setter_id'}, cache => 1}); } sub requestee { - my $self = shift; + my $self = shift; - if (!defined $self->{'requestee'} && $self->{'requestee_id'}) { - $self->{'requestee'} = new Bugzilla::User({ id => $self->{'requestee_id'}, cache => 1 }); - } - return $self->{'requestee'}; + if (!defined $self->{'requestee'} && $self->{'requestee_id'}) { + $self->{'requestee'} + = new Bugzilla::User({id => $self->{'requestee_id'}, cache => 1}); + } + return $self->{'requestee'}; } sub attachment { - my $self = shift; - return undef unless $self->attach_id; + my $self = shift; + return undef unless $self->attach_id; - require Bugzilla::Attachment; - return $self->{'attachment'} - ||= new Bugzilla::Attachment({ id => $self->attach_id, cache => 1 }); + require Bugzilla::Attachment; + return $self->{'attachment'} + ||= new Bugzilla::Attachment({id => $self->attach_id, cache => 1}); } sub bug { - my $self = shift; + my $self = shift; - require Bugzilla::Bug; - return $self->{'bug'} ||= new Bugzilla::Bug({ id => $self->bug_id, cache => 1 }); + require Bugzilla::Bug; + return $self->{'bug'} ||= new Bugzilla::Bug({id => $self->bug_id, cache => 1}); } ################################ @@ -235,26 +235,57 @@ and returns an array of matching records. =cut sub match { - my $class = shift; - my ($criteria) = @_; - - # If the caller specified only bug or attachment flags, - # limit the query to those kinds of flags. - if (my $type = delete $criteria->{'target_type'}) { - if ($type eq 'bug') { - $criteria->{'attach_id'} = IS_NULL; - } - elsif (!defined $criteria->{'attach_id'}) { - $criteria->{'attach_id'} = NOT_NULL; - } + my $class = shift; + my ($criteria) = @_; + + # If the caller specified only bug or attachment flags, + # limit the query to those kinds of flags. + if (my $type = delete $criteria->{'target_type'}) { + if ($type eq 'bug') { + $criteria->{'attach_id'} = IS_NULL; } - # Flag->snapshot() calls Flag->match() with bug_id and attach_id - # as hash keys, even if attach_id is undefined. - if (exists $criteria->{'attach_id'} && !defined $criteria->{'attach_id'}) { - $criteria->{'attach_id'} = IS_NULL; + elsif (!defined $criteria->{'attach_id'}) { + $criteria->{'attach_id'} = NOT_NULL; } - - return $class->SUPER::match(@_); + } + + # Flag->snapshot() calls Flag->match() with bug_id and attach_id + # as hash keys, even if attach_id is undefined. + if (exists $criteria->{'attach_id'} && !defined $criteria->{'attach_id'}) { + $criteria->{'attach_id'} = IS_NULL; + } + + ## REDHAT EXTENSION START 406121 + # Depending on the criteria, we may have to append additional tables. + my $tables = [DB_TABLE]; + my ($sql_criteria, $values) = sqlify_criteria($criteria, $tables); + + # Only return flags user is authorized to view + # Non-logged in users can only see public flags + # Users in 'editcomponents' and/or 'admin' can see all + push(@$tables, "flagtypes"); + push(@$sql_criteria, "flags.type_id = flagtypes.id"); + my $user = Bugzilla->user; + if (!$user->id) { + push(@$sql_criteria, "flagtypes.view_group_id IS NULL"); + } + elsif (!$user->in_group('editcomponents') && !$user->in_group('admin')) { + push(@$sql_criteria, + "(flagtypes.view_group_id IS NULL OR flagtypes.view_group_id IN (" + . $user->groups_as_string + . "))"); + } + + $tables = join(', ', @$tables); + my $where_criteria = join(' AND ', @$sql_criteria); + + my $dbh = Bugzilla->dbh; + + ## REDHAT EXTENSION START 1185156 + return Bugzilla::Flag->new_from_where( + "id IN (SELECT flags.id FROM $tables WHERE $where_criteria)", $values); + ## REDHAT EXTENSION END 1185156 + ## REDHAT EXTENSION END 406121 } =pod @@ -272,8 +303,8 @@ and returns an array of matching records. =cut sub count { - my $class = shift; - return scalar @{$class->match(@_)}; + my $class = shift; + return scalar @{$class->match(@_)}; } ###################################################################### @@ -281,144 +312,169 @@ sub count { ###################################################################### sub set_flag { - my ($class, $obj, $params) = @_; - - my ($bug, $attachment, $obj_flag, $requestee_changed); - if (blessed($obj) && $obj->isa('Bugzilla::Attachment')) { - $attachment = $obj; - $bug = $attachment->bug; - } - elsif (blessed($obj) && $obj->isa('Bugzilla::Bug')) { - $bug = $obj; - } - else { - ThrowCodeError('flag_unexpected_object', { 'caller' => ref $obj }); - } - - # Make sure the user can change flags - my $privs; - $bug->check_can_change_field('flagtypes.name', 0, 1, \$privs) - || ThrowUserError('illegal_change', - { field => 'flagtypes.name', privs => $privs }); - - # Update (or delete) an existing flag. - if ($params->{id}) { - my $flag = $class->check({ id => $params->{id} }); - - # Security check: make sure the flag belongs to the bug/attachment. - # We don't check that the user editing the flag can see - # the bug/attachment. That's the job of the caller. - ($attachment && $flag->attach_id && $attachment->id == $flag->attach_id) - || (!$attachment && !$flag->attach_id && $bug->id == $flag->bug_id) - || ThrowCodeError('invalid_flag_association', - { bug_id => $bug->id, - attach_id => $attachment ? $attachment->id : undef }); - - # Extract the current flag object from the object. - my ($obj_flagtype) = grep { $_->id == $flag->type_id } @{$obj->flag_types}; - # If no flagtype can be found for this flag, this means the bug is being - # moved into a product/component where the flag is no longer valid. - # So either we can attach the flag to another flagtype having the same - # name, or we remove the flag. - if (!$obj_flagtype) { - my $success = $flag->retarget($obj); - return unless $success; - - ($obj_flagtype) = grep { $_->id == $flag->type_id } @{$obj->flag_types}; - push(@{$obj_flagtype->{flags}}, $flag); - } - ($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; - - ($obj_flag, $requestee_changed) = - $class->_validate($obj_flag, $obj_flagtype, $params, $bug, $attachment); - } - # Create a new flag. - elsif ($params->{type_id}) { - # Don't bother validating types the user didn't touch. - return if $params->{status} eq 'X'; - - my $flagtype = Bugzilla::FlagType->check({ id => $params->{type_id} }); - # Security check: make sure the flag type belongs to the bug/attachment. - ($attachment && $flagtype->target_type eq 'attachment' - && scalar(grep { $_->id == $flagtype->id } @{$attachment->flag_types})) - || (!$attachment && $flagtype->target_type eq 'bug' - && scalar(grep { $_->id == $flagtype->id } @{$bug->flag_types})) - || ThrowCodeError('invalid_flag_association', - { bug_id => $bug->id, - attach_id => $attachment ? $attachment->id : undef }); - - # Make sure the flag type is active. - $flagtype->is_active - || ThrowCodeError('flag_type_inactive', { type => $flagtype->name }); - - # Extract the current flagtype object from the object. - my ($obj_flagtype) = grep { $_->id == $flagtype->id } @{$obj->flag_types}; - - # We cannot create a new flag if there is already one and this - # flag type is not multiplicable. - if (!$flagtype->is_multiplicable) { - if (scalar @{$obj_flagtype->{flags}}) { - ThrowUserError('flag_type_not_multiplicable', { type => $flagtype }); - } - } - - ($obj_flag, $requestee_changed) = - $class->_validate(undef, $obj_flagtype, $params, $bug, $attachment); + my ($class, $obj, $params) = @_; + + my ($bug, $attachment, $obj_flag, $requestee_changed); + if (blessed($obj) && $obj->isa('Bugzilla::Attachment')) { + $attachment = $obj; + $bug = $attachment->bug; + } + elsif (blessed($obj) && $obj->isa('Bugzilla::Bug')) { + $bug = $obj; + } + else { + ThrowCodeError('flag_unexpected_object', {'caller' => ref $obj}); + } + + # Make sure the user can change flags + my $privs; + $bug->check_can_change_field('flagtypes.name', 0, 1, \$privs) + || ThrowUserError('illegal_change', + {field => 'flagtypes.name', privs => $privs}); + + # Update (or delete) an existing flag. + if ($params->{id}) { + my $flag = $class->check({id => $params->{id}}); + + # Security check: make sure the flag belongs to the bug/attachment. + # We don't check that the user editing the flag can see + # the bug/attachment. That's the job of the caller. + ($attachment && $flag->attach_id && $attachment->id == $flag->attach_id) + || (!$attachment && !$flag->attach_id && $bug->id == $flag->bug_id) + || ThrowCodeError('invalid_flag_association', + {bug_id => $bug->id, attach_id => $attachment ? $attachment->id : undef}); + + # Extract the current flag object from the object. + my ($obj_flagtype) = grep { $_->id == $flag->type_id } @{$obj->flag_types}; + + # If no flagtype can be found for this flag, this means the bug is being + # moved into a product/component where the flag is no longer valid. + # So either we can attach the flag to another flagtype having the same + # name, or we remove the flag. + if (!$obj_flagtype) { + my $success = $flag->retarget($obj); + return unless $success; + + ($obj_flagtype) = grep { $_->id == $flag->type_id } @{$obj->flag_types}; + push(@{$obj_flagtype->{flags}}, $flag); } - else { - ThrowCodeError('param_required', { function => $class . '->set_flag', - param => 'id/type_id' }); + ($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; + + ($obj_flag, $requestee_changed) + = $class->_validate($obj_flag, $obj_flagtype, $params, $bug, $attachment); + } + + # Create a new flag. + elsif ($params->{type_id}) { + + # Don't bother validating types the user didn't touch. + return if $params->{status} eq 'X'; + + my $flagtype = Bugzilla::FlagType->check({id => $params->{type_id}}); + + # Security check: make sure the flag type belongs to the bug/attachment. + ( $attachment + && $flagtype->target_type eq 'attachment' + && scalar(grep { $_->id == $flagtype->id } @{$attachment->flag_types})) + || (!$attachment + && $flagtype->target_type eq 'bug' + && scalar(grep { $_->id == $flagtype->id } @{$bug->flag_types})) + || ThrowCodeError('invalid_flag_association', + {bug_id => $bug->id, attach_id => $attachment ? $attachment->id : undef}); + + # Make sure the flag type is active. + $flagtype->is_active + || ThrowCodeError('flag_type_inactive', {type => $flagtype->name}); + + # Extract the current flagtype object from the object. + my ($obj_flagtype) = grep { $_->id == $flagtype->id } @{$obj->flag_types}; + + # We cannot create a new flag if there is already one and this + # flag type is not multiplicable. + if (!$flagtype->is_multiplicable) { + if (scalar @{$obj_flagtype->{flags}}) { + ThrowUserError('flag_type_not_multiplicable', {type => $flagtype}); + } } - if ($obj_flag - && $requestee_changed - && $obj_flag->requestee_id - && $obj_flag->requestee->setting('requestee_cc') eq 'on') - { - $bug->add_cc($obj_flag->requestee); - } + ($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->reporter->id != $obj_flag->requestee->id) ## RED HAT EXTENSION 1667751 + { + $bug->add_cc($obj_flag->requestee); + } } sub _validate { - my ($class, $flag, $flag_type, $params, $bug, $attachment) = @_; - - # If it's a new flag, let's create it now. - my $obj_flag = $flag || bless({ type_id => $flag_type->id, - status => '', - bug_id => $bug->id, - attach_id => $attachment ? - $attachment->id : undef}, - $class); - - my $old_status = $obj_flag->status; - my $old_requestee_id = $obj_flag->requestee_id; - - $obj_flag->_set_status($params->{status}); - $obj_flag->_set_requestee($params->{requestee}, $bug, $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) || $requestee_changed) { - $obj_flag->_set_setter($params->{setter}); - } + my ($class, $flag, $flag_type, $params, $bug, $attachment) = @_; - # 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; + # If it's a new flag, let's create it now. + my $obj_flag = $flag || bless( + { + type_id => $flag_type->id, + status => '', + bug_id => $bug->id, + attach_id => $attachment ? $attachment->id : undef + }, + $class + ); + ## REDHAT EXTENSION START 798022 + # If the flag is 'requires_doc_text' and status is '+' and either + # it is a new flag or the status was something else and the change + # was made by someone not in the ecs group, give an error + if ( $flag_type->name eq 'requires_doc_text' + and $params->{status} eq '+' + and $obj_flag->status ne '+' + and not Bugzilla->user->in_group('ecs')) + { + ThrowUserError('flag_change_not_ecs'); + } + ## REDHAT EXTENSION END 798022 + + my $old_status = $obj_flag->status; + my $old_requestee_id = $obj_flag->requestee_id; + + $obj_flag->_set_status($params->{status}); + $obj_flag->_set_requestee($params->{requestee}, $bug, $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) || $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 @@ -434,143 +490,151 @@ Creates a flag record in the database. =cut sub create { - my ($class, $flag, $timestamp) = @_; - $timestamp ||= Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); + my ($class, $flag, $timestamp) = @_; + $timestamp ||= Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); - my $params = {}; - my @columns = grep { $_ ne 'id' } $class->_get_db_columns; + 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; + # Some columns use date formatting so use alias instead + @columns = map { /\s+AS\s+(.*)$/ ? $1 : $_ } @columns; - $params->{$_} = $flag->{$_} foreach @columns; + $params->{$_} = $flag->{$_} foreach @columns; - $params->{creation_date} = $params->{modification_date} = $timestamp; + $params->{creation_date} = $params->{modification_date} = $timestamp; - $flag = $class->SUPER::create($params); - return $flag; + $flag = $class->SUPER::create($params); + return $flag; } sub update { - my $self = shift; - my $dbh = Bugzilla->dbh; - my $timestamp = shift || $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); - - my $changes = $self->SUPER::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', Bugzilla->local_timezone); - Bugzilla->memcached->clear({ table => 'flags', id => $self->id }); - } - return $changes; + my $self = shift; + my $dbh = Bugzilla->dbh; + my $timestamp = shift || $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); + + my $changes = $self->SUPER::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', Bugzilla->local_timezone); + Bugzilla->memcached->clear({table => 'flags', id => $self->id}); + } + return $changes; } sub snapshot { - my ($class, $flags) = @_; - - my @summaries; - foreach my $flag (@$flags) { - my $summary = $flag->setter->nick . ':' . $flag->type->name . $flag->status; - $summary .= "(" . $flag->requestee->login . ")" if $flag->requestee; - push(@summaries, $summary); - } - return @summaries; + my ($class, $flags) = @_; + + my @summaries; + foreach my $flag (@$flags) { + my $summary = $flag->setter->nick . ':' . $flag->type->name . $flag->status; + $summary .= "(" . $flag->requestee->login . ")" if $flag->requestee; + push(@summaries, $summary); + } + return @summaries; } sub update_activity { - my ($class, $old_summaries, $new_summaries) = @_; + my ($class, $old_summaries, $new_summaries) = @_; - my ($removed, $added) = diff_arrays($old_summaries, $new_summaries); - if (scalar @$removed || scalar @$added) { - # Remove flag requester/setter information - foreach (@$removed, @$added) { s/^[^:]+:// } + my ($removed, $added) = diff_arrays($old_summaries, $new_summaries); + if (scalar @$removed || scalar @$added) { - $removed = join(", ", @$removed); - $added = join(", ", @$added); - return ($removed, $added); - } - return (); + # Remove flag requester/setter information + foreach (@$removed, @$added) {s/^[^:]+://} + + $removed = join(", ", @$removed); + $added = join(", ", @$added); + return ($removed, $added); + } + return (); } sub update_flags { - my ($class, $self, $old_self, $timestamp) = @_; + my ($class, $self, $old_self, $timestamp) = @_; - my @old_summaries = $class->snapshot($old_self->flags); - my %old_flags = map { $_->id => $_ } @{$old_self->flags}; + my @old_summaries = $class->snapshot($old_self->flags); + my %old_flags = map { $_->id => $_ } @{$old_self->flags}; - foreach my $new_flag (@{$self->flags}) { - if (!$new_flag->id) { - # This is a new flag. - my $flag = $class->create($new_flag, $timestamp); - $new_flag->{id} = $flag->id; - $class->notify($new_flag, undef, $self, $timestamp); - } - else { - my $changes = $new_flag->update($timestamp); - if (scalar(keys %$changes)) { - $class->notify($new_flag, $old_flags{$new_flag->id}, $self, $timestamp); - } - delete $old_flags{$new_flag->id}; - } + foreach my $new_flag (@{$self->flags}) { + if (!$new_flag->id) { + + # This is a new flag. + my $flag = $class->create($new_flag, $timestamp); + $new_flag->{id} = $flag->id; + $class->notify($new_flag, undef, $self, $timestamp); } - # These flags have been deleted. - foreach my $old_flag (values %old_flags) { - $class->notify(undef, $old_flag, $self, $timestamp); - $old_flag->remove_from_db(); + else { + my $changes = $new_flag->update($timestamp); + if (scalar(keys %$changes)) { + $class->notify($new_flag, $old_flags{$new_flag->id}, $self, $timestamp); + } + delete $old_flags{$new_flag->id}; } - - # If the bug has been moved into another product or component, - # we must also take care of attachment flags which are no longer valid, - # as well as all bug flags which haven't been forgotten above. - if ($self->isa('Bugzilla::Bug') - && ($self->{_old_product_name} || $self->{_old_component_name})) + } + + # These flags have been deleted. + foreach my $old_flag (values %old_flags) { + $class->notify(undef, $old_flag, $self, $timestamp); + $old_flag->remove_from_db(); + } + + # If the bug has been moved into another product or component, + # we must also take care of attachment flags which are no longer valid, + # as well as all bug flags which haven't been forgotten above. + if ($self->isa('Bugzilla::Bug') + && ($self->{_old_product_name} || $self->{_old_component_name})) + { + my @removed = $class->force_cleanup($self); + push(@old_summaries, @removed); + } + + my @new_summaries = $class->snapshot($self->flags); + my @changes = $class->update_activity(\@old_summaries, \@new_summaries); + + Bugzilla::Hook::process( + 'flag_end_of_update', { - my @removed = $class->force_cleanup($self); - push(@old_summaries, @removed); + object => $self, + timestamp => $timestamp, + old_flags => \@old_summaries, + new_flags => \@new_summaries, } - - my @new_summaries = $class->snapshot($self->flags); - my @changes = $class->update_activity(\@old_summaries, \@new_summaries); - - Bugzilla::Hook::process('flag_end_of_update', { object => $self, - timestamp => $timestamp, - old_flags => \@old_summaries, - new_flags => \@new_summaries, - }); - return @changes; + ); + return @changes; } sub retarget { - my ($self, $obj) = @_; - - my @flagtypes = grep { $_->name eq $self->type->name } @{$obj->flag_types}; - - my $success = 0; - foreach my $flagtype (@flagtypes) { - next if !$flagtype->is_active; - next if (!$flagtype->is_multiplicable && scalar @{$flagtype->{flags}}); - next unless (($self->status eq '?' && $self->setter->can_request_flag($flagtype)) - || $self->setter->can_set_flag($flagtype)); - - $self->{type_id} = $flagtype->id; - delete $self->{type}; - $success = 1; - last; - } - return $success; + my ($self, $obj) = @_; + + my @flagtypes = grep { $_->name eq $self->type->name } @{$obj->flag_types}; + + my $success = 0; + foreach my $flagtype (@flagtypes) { + next if !$flagtype->is_active; + next if (!$flagtype->is_multiplicable && scalar @{$flagtype->{flags}}); + next + unless (($self->status eq '?' && $self->setter->can_request_flag($flagtype)) + || $self->setter->can_set_flag($flagtype)); + + $self->{type_id} = $flagtype->id; + delete $self->{type}; + $success = 1; + last; + } + return $success; } # In case the bug's product/component has changed, clear flags that are # no longer valid. sub force_cleanup { - my ($class, $bug) = @_; - my $dbh = Bugzilla->dbh; + my ($class, $bug) = @_; + my $dbh = Bugzilla->dbh; - my $flag_ids = $dbh->selectcol_arrayref( - 'SELECT DISTINCT flags.id + my $flag_ids = $dbh->selectcol_arrayref( + 'SELECT DISTINCT flags.id FROM flags INNER JOIN bugs ON flags.bug_id = bugs.bug_id @@ -578,48 +642,50 @@ sub force_cleanup { ON flags.type_id = i.type_id AND (bugs.product_id = i.product_id OR i.product_id IS NULL) AND (bugs.component_id = i.component_id OR i.component_id IS NULL) - WHERE bugs.bug_id = ? AND i.type_id IS NULL', - undef, $bug->id); + WHERE bugs.bug_id = ? AND i.type_id IS NULL', undef, $bug->id + ); - my @removed = $class->force_retarget($flag_ids, $bug); + my @removed = $class->force_retarget($flag_ids, $bug); - $flag_ids = $dbh->selectcol_arrayref( - 'SELECT DISTINCT flags.id + $flag_ids = $dbh->selectcol_arrayref( + 'SELECT DISTINCT flags.id FROM flags, bugs, flagexclusions e WHERE bugs.bug_id = ? AND flags.bug_id = bugs.bug_id AND flags.type_id = e.type_id AND (bugs.product_id = e.product_id OR e.product_id IS NULL) AND (bugs.component_id = e.component_id OR e.component_id IS NULL)', - undef, $bug->id); + undef, $bug->id + ); - push(@removed , $class->force_retarget($flag_ids, $bug)); - return @removed; + push(@removed, $class->force_retarget($flag_ids, $bug)); + return @removed; } sub force_retarget { - my ($class, $flag_ids, $bug) = @_; - my $dbh = Bugzilla->dbh; - - my $flags = $class->new_from_list($flag_ids); - my @removed; - foreach my $flag (@$flags) { - # $bug is undefined when e.g. editing inclusion and exclusion lists. - my $obj = $flag->attachment || $bug || $flag->bug; - my $is_retargetted = $flag->retarget($obj); - if ($is_retargetted) { - $dbh->do('UPDATE flags SET type_id = ? WHERE id = ?', - undef, ($flag->type_id, $flag->id)); - Bugzilla->memcached->clear({ table => 'flags', id => $flag->id }); - } - else { - # Track deleted attachment flags. - push(@removed, $class->snapshot([$flag])) if $flag->attach_id; - $class->notify(undef, $flag, $bug || $flag->bug); - $flag->remove_from_db(); - } + my ($class, $flag_ids, $bug, $skip_mail) = @_; + my $dbh = Bugzilla->dbh; + + my $flags = $class->new_from_list($flag_ids); + my @removed; + foreach my $flag (@$flags) { + + # $bug is undefined when e.g. editing inclusion and exclusion lists. + my $obj = $flag->attachment || $bug || $flag->bug; + my $is_retargetted = $flag->retarget($obj); + if ($is_retargetted) { + $dbh->do('UPDATE flags SET type_id = ? WHERE id = ?', + undef, ($flag->type_id, $flag->id)); + Bugzilla->memcached->clear({table => 'flags', id => $flag->id}); } - return @removed; + else { + # Track deleted attachment flags. + push(@removed, $class->snapshot([$flag])) if $flag->attach_id; + $class->notify(undef, $flag, $bug || $flag->bug) unless ($skip_mail); + $flag->remove_from_db(); + } + } + return @removed; } ############################### @@ -627,164 +693,178 @@ sub force_retarget { ############################### sub _set_requestee { - my ($self, $requestee, $bug, $attachment, $skip_requestee_on_error) = @_; + my ($self, $requestee, $bug, $attachment, $skip_requestee_on_error) = @_; - $self->{requestee} = - $self->_check_requestee($requestee, $bug, $attachment, $skip_requestee_on_error); + $self->{requestee} = $self->_check_requestee($requestee, $bug, $attachment, + $skip_requestee_on_error); - $self->{requestee_id} = - $self->{requestee} ? $self->{requestee}->id : undef; + $self->{requestee_id} = $self->{requestee} ? $self->{requestee}->id : undef; } sub _set_setter { - my ($self, $setter) = @_; + my ($self, $setter) = @_; - $self->set('setter', $setter); - $self->{setter_id} = $self->setter->id; + $self->set('setter', $setter); + $self->{setter_id} = $self->setter->id; } sub _set_status { - my ($self, $status) = @_; + my ($self, $status) = @_; - # Store the old flag status. It's needed by _check_setter(). - $self->{_old_status} = $self->status; - $self->set('status', $status); + # Store the old flag status. It's needed by _check_setter(). + $self->{_old_status} = $self->status; + $self->set('status', $status); } sub _check_requestee { - my ($self, $requestee, $bug, $attachment, $skip_requestee_on_error) = @_; - - # If the flag status is not "?", then no requestee can be defined. - return undef if ($self->status ne '?'); - - # Store this value before updating the flag object. - my $old_requestee = $self->requestee ? $self->requestee->login : ''; - - if ($self->status eq '?' && $requestee) { - $requestee = Bugzilla::User->check($requestee); + my ($self, $requestee, $bug, $attachment, $skip_requestee_on_error) = @_; + + # If the flag status is not "?", then no requestee can be defined. + return undef if ($self->status ne '?'); + + # Store this value before updating the flag object. + my $old_requestee = $self->requestee ? $self->requestee->login : ''; + + if ($self->status eq '?' && $requestee) { + $requestee = Bugzilla::User->check($requestee); + } + else { + undef $requestee; + } + + if ($requestee && $requestee->login ne $old_requestee) { + + # Make sure the user didn't specify a requestee unless the flag + # is specifically requestable. For existing flags, if the requestee + # was set before the flag became specifically unrequestable, the + # user can either remove them or leave them alone. + ThrowUserError('flag_type_requestee_disabled', {type => $self->type}) + if !$self->type->is_requesteeble; + + # You can't ask a disabled account, as they don't have the ability to + # set the flag. + ThrowUserError('flag_requestee_disabled', {requestee => $requestee}) + if !$requestee->is_enabled; + + # Make sure the requestee can see the bug. + # Note that can_see_bug() will query the DB, so if the bug + # is being added/removed from some groups and these changes + # haven't been committed to the DB yet, they won't be taken + # into account here. In this case, old group restrictions matter. + # However, if the user has just been changed to the assignee, + # qa_contact, or added to the cc list of the bug and the bug + # is cclist_accessible, the requestee is allowed. + if ( + !$requestee->can_see_bug($self->bug_id) + && ( !$bug->cclist_accessible + || !grep($_->id == $requestee->id, @{$bug->cc_users}) + && $requestee->id != $bug->assigned_to->id + && (!$bug->qa_contact || $requestee->id != $bug->qa_contact->id)) + ) + { + if ($skip_requestee_on_error) { + undef $requestee; + } + else { + ThrowUserError( + 'flag_requestee_unauthorized', + { + flag_type => $self->type, + requestee => $requestee, + bug_id => $self->bug_id, + attach_id => $self->attach_id + } + ); + } } - else { + + # Make sure the requestee can see the private attachment. + elsif ($self->attach_id && $attachment->isprivate && !$requestee->is_insider) { + if ($skip_requestee_on_error) { undef $requestee; + } + else { + ThrowUserError( + 'flag_requestee_unauthorized_attachment', + { + flag_type => $self->type, + requestee => $requestee, + bug_id => $self->bug_id, + attach_id => $self->attach_id + } + ); + } } - if ($requestee && $requestee->login ne $old_requestee) { - # Make sure the user didn't specify a requestee unless the flag - # is specifically requestable. For existing flags, if the requestee - # was set before the flag became specifically unrequestable, the - # user can either remove them or leave them alone. - ThrowUserError('flag_type_requestee_disabled', { type => $self->type }) - if !$self->type->is_requesteeble; - - # You can't ask a disabled account, as they don't have the ability to - # set the flag. - ThrowUserError('flag_requestee_disabled', { requestee => $requestee }) - if !$requestee->is_enabled; - - # Make sure the requestee can see the bug. - # Note that can_see_bug() will query the DB, so if the bug - # is being added/removed from some groups and these changes - # haven't been committed to the DB yet, they won't be taken - # into account here. In this case, old group restrictions matter. - # However, if the user has just been changed to the assignee, - # qa_contact, or added to the cc list of the bug and the bug - # is cclist_accessible, the requestee is allowed. - if (!$requestee->can_see_bug($self->bug_id) - && (!$bug->cclist_accessible - || !grep($_->id == $requestee->id, @{ $bug->cc_users }) - && $requestee->id != $bug->assigned_to->id - && (!$bug->qa_contact || $requestee->id != $bug->qa_contact->id))) - { - if ($skip_requestee_on_error) { - undef $requestee; - } - else { - ThrowUserError('flag_requestee_unauthorized', - { flag_type => $self->type, - requestee => $requestee, - bug_id => $self->bug_id, - attach_id => $self->attach_id }); - } - } - # Make sure the requestee can see the private attachment. - elsif ($self->attach_id && $attachment->isprivate && !$requestee->is_insider) { - if ($skip_requestee_on_error) { - undef $requestee; - } - else { - ThrowUserError('flag_requestee_unauthorized_attachment', - { flag_type => $self->type, - requestee => $requestee, - bug_id => $self->bug_id, - attach_id => $self->attach_id }); - } - } - # Make sure the user is allowed to set the flag. - elsif (!$requestee->can_set_flag($self->type)) { - if ($skip_requestee_on_error) { - undef $requestee; - } - else { - ThrowUserError('flag_requestee_needs_privs', - {'requestee' => $requestee, - 'flagtype' => $self->type}); - } - } + # Make sure the user is allowed to set the flag. + elsif (!$requestee->can_set_flag($self->type)) { + if ($skip_requestee_on_error) { + undef $requestee; + } + else { + ThrowUserError('flag_requestee_needs_privs', + {'requestee' => $requestee, 'flagtype' => $self->type}); + } } - return $requestee; + } + return $requestee; } sub _check_setter { - my ($self, $setter) = @_; - - # By default, the currently logged in user is the setter. - $setter ||= Bugzilla->user; - (blessed($setter) && $setter->isa('Bugzilla::User') && $setter->id) - || ThrowUserError('invalid_user'); - - # set_status() has already been called. So this refers - # to the new flag status. - my $status = $self->status; - - # Make sure the user is authorized to modify flags, see bug 180879: - # - The flag exists and is unchanged. - # - The flag setter can unset flag. - # - Users in the request_group can clear pending requests and set flags - # and can rerequest set flags. - # - Users in the grant_group can set/clear flags, including "+" and "-". - unless (($status eq $self->{_old_status}) - || ($status eq 'X' && $setter->id == Bugzilla->user->id) - || (($status eq 'X' || $status eq '?') - && $setter->can_request_flag($self->type)) - || $setter->can_set_flag($self->type)) - { - ThrowUserError('flag_update_denied', - { name => $self->type->name, - status => $status, - old_status => $self->{_old_status} }); - } - - # If the request is being retargetted, we don't update - # the setter, so that the setter gets the notification. - if ($status eq '?' && $self->{_old_status} eq '?') { - return $self->setter; - } - return $setter; + my ($self, $setter) = @_; + + # By default, the currently logged in user is the setter. + $setter ||= Bugzilla->user; + (blessed($setter) && $setter->isa('Bugzilla::User') && $setter->id) + || ThrowUserError('invalid_user'); + + # set_status() has already been called. So this refers + # to the new flag status. + my $status = $self->status; + + # Make sure the user is authorized to modify flags, see bug 180879: + # - The flag exists and is unchanged. + # - The flag setter can unset flag. + # - Users in the request_group can clear pending requests and set flags + # and can rerequest set flags. + # - Users in the grant_group can set/clear flags, including "+" and "-". + unless (($status eq $self->{_old_status}) + || ($status eq 'X' && $setter->id == Bugzilla->user->id) + || (($status eq 'X' || $status eq '?') + && $setter->can_request_flag($self->type)) + || $setter->can_set_flag($self->type)) + { + ThrowUserError( + 'flag_update_denied', + { + name => $self->type->name, + status => $status, + old_status => $self->{_old_status} + } + ); + } + + # If the request is being retargetted, we don't update + # the setter, so that the setter gets the notification. + if ($status eq '?' && $self->{_old_status} eq '?') { + return $self->setter; + } + return $setter; } sub _check_status { - my ($self, $status) = @_; - - # - Make sure the status is valid. - # - Make sure the user didn't request the flag unless it's requestable. - # If the flag existed and was requested before it became unrequestable, - # leave it as is. - if (!grep($status eq $_ , qw(X + - ?)) - || ($status eq '?' && $self->status ne '?' && !$self->type->is_requestable)) - { - ThrowUserError('flag_status_invalid', { id => $self->id, - status => $status }); - } - return $status; + my ($self, $status) = @_; + + # - Make sure the status is valid. + # - Make sure the user didn't request the flag unless it's requestable. + # If the flag existed and was requested before it became unrequestable, + # leave it as is. + if (!grep($status eq $_, qw(X + - ?)) + || ($status eq '?' && $self->status ne '?' && !$self->type->is_requestable)) + { + ThrowUserError('flag_status_invalid', {id => $self->id, status => $status}); + } + return $status; } ###################################################################### @@ -795,138 +875,182 @@ sub _check_status { =over -=item C +=item C Checks whether or not there are new flags to create and returns an array of hashes. This array is then passed to Flag::create(). +$args is a hash contaning either a bug and optionally an attachment object +or a product_id and component_id + =back =cut sub extract_flags_from_cgi { - my ($class, $bug, $attachment, $vars, $skip) = @_; - my $cgi = Bugzilla->cgi; - - my $match_status = Bugzilla::User::match_field({ - '^requestee(_type)?-(\d+)$' => { 'type' => 'multi' }, - }, undef, $skip); - - $vars->{'match_field'} = 'requestee'; - if ($match_status == USER_MATCH_FAILED) { - $vars->{'message'} = 'user_match_failed'; - } - elsif ($match_status == USER_MATCH_MULTIPLE) { - $vars->{'message'} = 'user_match_multiple'; + my ($class, $vars, $skip, $args) = @_; + + my $cgi = Bugzilla->cgi; + + ## REDHAT EXTENSION 1188085 BEGIN + my ($bug, $attachment, $component_id, $product_id); + + if (defined($args->{bug})) { + $bug = $args->{bug}; + $component_id = $bug->component_id; + $product_id = $bug->product_id; + $attachment = $args->{attachment} if defined $args->{attachment}; + } + elsif (defined($args->{product_id})) { + $product_id = $args->{product_id}; + $component_id = $args->{component_id}; + } + ## REDHAT EXTENSION 1188085 END + + my $match_status + = Bugzilla::User::match_field( + {'^requestee(_type)?-(\d+)$' => {'type' => 'multi'},}, + undef, $skip); + + $vars->{'match_field'} = 'requestee'; + if ($match_status == USER_MATCH_FAILED) { + $vars->{'message'} = 'user_match_failed'; + } + elsif ($match_status == USER_MATCH_MULTIPLE) { + $vars->{'message'} = 'user_match_multiple'; + } + + # Extract a list of flag type IDs from field names. + my @flagtype_ids = map(/^flag_type-(\d+)$/ ? $1 : (), $cgi->param()); + @flagtype_ids = grep($cgi->param("flag_type-$_") ne 'X', @flagtype_ids); + + # Extract a list of existing flag IDs. + my @flag_ids = map(/^flag-(\d+)$/ ? $1 : (), $cgi->param()); + + return ([], []) unless (scalar(@flagtype_ids) || scalar(@flag_ids)); + + my (@new_flags, @flags); + foreach my $flag_id (@flag_ids) { + my $flag = $class->new($flag_id); + + # If the flag no longer exists, ignore it. + next unless $flag; + + my $status = $cgi->param("flag-$flag_id"); + + # If the user entered more than one name into the requestee field + # (i.e. they want more than one person to set the flag) we can reuse + # the existing flag for the first person (who may well be the existing + # requestee), but we have to create new flags for each additional requestee. + my @requestees = $cgi->param("requestee-$flag_id"); + my $requestee_email; + if ($status eq "?" && scalar(@requestees) > 1 && $flag->type->is_multiplicable) + { + # The first person, for which we'll reuse the existing flag. + $requestee_email = shift(@requestees); + + # Create new flags like the existing one for each additional person. + foreach my $login (@requestees) { + push( + @new_flags, + { + type_id => $flag->type_id, + status => "?", + requestee => $login, + skip_roe => $skip + } + ); + } } + elsif ($status eq "?" && scalar(@requestees)) { - # Extract a list of flag type IDs from field names. - my @flagtype_ids = map(/^flag_type-(\d+)$/ ? $1 : (), $cgi->param()); - @flagtype_ids = grep($cgi->param("flag_type-$_") ne 'X', @flagtype_ids); - - # Extract a list of existing flag IDs. - my @flag_ids = map(/^flag-(\d+)$/ ? $1 : (), $cgi->param()); - - return ([], []) unless (scalar(@flagtype_ids) || scalar(@flag_ids)); - - my (@new_flags, @flags); - foreach my $flag_id (@flag_ids) { - my $flag = $class->new($flag_id); - # If the flag no longer exists, ignore it. - next unless $flag; - - my $status = $cgi->param("flag-$flag_id"); - - # If the user entered more than one name into the requestee field - # (i.e. they want more than one person to set the flag) we can reuse - # the existing flag for the first person (who may well be the existing - # requestee), but we have to create new flags for each additional requestee. - my @requestees = $cgi->param("requestee-$flag_id"); - my $requestee_email; - if ($status eq "?" - && scalar(@requestees) > 1 - && $flag->type->is_multiplicable) - { - # The first person, for which we'll reuse the existing flag. - $requestee_email = shift(@requestees); - - # Create new flags like the existing one for each additional person. - foreach my $login (@requestees) { - push(@new_flags, { type_id => $flag->type_id, - status => "?", - requestee => $login, - skip_roe => $skip }); - } - } - elsif ($status eq "?" && scalar(@requestees)) { - # If there are several requestees and the flag type is not multiplicable, - # this will fail. But that's the job of the validator to complain. All - # we do here is to extract and convert data from the CGI. - $requestee_email = trim($cgi->param("requestee-$flag_id") || ''); - } - - push(@flags, { id => $flag_id, - status => $status, - requestee => $requestee_email, - skip_roe => $skip }); + # If there are several requestees and the flag type is not multiplicable, + # this will fail. But that's the job of the validator to complain. All + # we do here is to extract and convert data from the CGI. + $requestee_email = trim($cgi->param("requestee-$flag_id") || ''); } - # Get a list of active flag types available for this product/component. - my $flag_types = Bugzilla::FlagType::match( - { 'product_id' => $bug->{'product_id'}, - 'component_id' => $bug->{'component_id'}, - 'is_active' => 1 }); - - foreach my $flagtype_id (@flagtype_ids) { - # Checks if there are unexpected flags for the product/component. - if (!scalar(grep { $_->id == $flagtype_id } @$flag_types)) { - $vars->{'message'} = 'unexpected_flag_types'; - last; - } + push( + @flags, + { + id => $flag_id, + status => $status, + requestee => $requestee_email, + skip_roe => $skip + } + ); + } + + # Get a list of active flag types available for this product/component. + my $flag_types = Bugzilla::FlagType::match({ + 'product_id' => $product_id, + 'component_id' => $component_id, + 'is_active' => 1 + }); + + foreach my $flagtype_id (@flagtype_ids) { + + # Checks if there are unexpected flags for the product/component. + if (!scalar(grep { $_->id == $flagtype_id } @$flag_types)) { + $vars->{'message'} = 'unexpected_flag_types'; + last; } - - foreach my $flag_type (@$flag_types) { - my $type_id = $flag_type->id; - - # Bug flags are only valid for bugs, and attachment flags are - # only valid for attachments. So don't mix both. - next unless ($flag_type->target_type eq 'bug' xor $attachment); - - # We are only interested in flags the user tries to create. - next unless scalar(grep { $_ == $type_id } @flagtype_ids); - - # Get the number of flags of this type already set for this target. - my $has_flags = $class->count( - { 'type_id' => $type_id, - 'target_type' => $attachment ? 'attachment' : 'bug', - 'bug_id' => $bug->bug_id, - 'attach_id' => $attachment ? $attachment->id : undef }); - - # Do not create a new flag of this type if this flag type is - # not multiplicable and already has a flag set. - next if (!$flag_type->is_multiplicable && $has_flags); - - my $status = $cgi->param("flag_type-$type_id"); - trick_taint($status); - - my @logins = $cgi->param("requestee_type-$type_id"); - if ($status eq "?" && scalar(@logins)) { - foreach my $login (@logins) { - push (@new_flags, { type_id => $type_id, - status => $status, - requestee => $login, - skip_roe => $skip }); - last unless $flag_type->is_multiplicable; - } - } - else { - push (@new_flags, { type_id => $type_id, - status => $status }); - } + } + + foreach my $flag_type (@$flag_types) { + my $type_id = $flag_type->id; + + # Bug flags are only valid for bugs, and attachment flags are + # only valid for attachments. So don't mix both. + next unless ($flag_type->target_type eq 'bug' xor $attachment); + + # We are only interested in flags the user tries to create. + next unless scalar(grep { $_ == $type_id } @flagtype_ids); + + ## REDHAT EXTENSION 1188085 BEGIN + # If $bug is not defined, then we are creating a flag for an as + # yet uncreated bug. + if (defined $bug) { + + # Get the number of flags of this type already set for this target. + my $has_flags = $class->count({ + 'type_id' => $type_id, + 'target_type' => $attachment ? 'attachment' : 'bug', + 'bug_id' => $bug->bug_id, + 'attach_id' => $attachment ? $attachment->id : undef + }); + + # Do not create a new flag of this type if this flag type is + # not multiplicable and already has a flag set. + next if (!$flag_type->is_multiplicable && $has_flags); + } + ## REDHAT EXTENSION 1188085 END + + my $status = $cgi->param("flag_type-$type_id"); + trick_taint($status); + + my @logins = $cgi->param("requestee_type-$type_id"); + if ($status eq "?" && scalar(@logins)) { + foreach my $login (@logins) { + push( + @new_flags, + { + type_id => $type_id, + status => $status, + requestee => $login, + skip_roe => $skip + } + ); + last unless $flag_type->is_multiplicable; + } } + else { + push(@new_flags, {type_id => $type_id, status => $status}); + } + } - # Return the list of flags to update and/or to create. - return (\@flags, \@new_flags); + # Return the list of flags to update and/or to create. + return (\@flags, \@new_flags); } =pod @@ -944,100 +1068,111 @@ from the previous sub-routine as it is called for changing multiple bugs =cut sub multi_extract_flags_from_cgi { - my ($class, $bug, $vars, $skip) = @_; - my $cgi = Bugzilla->cgi; - - my $match_status = Bugzilla::User::match_field({ - '^requestee(_type)?-(\d+)$' => { 'type' => 'multi' }, - }, undef, $skip); - - $vars->{'match_field'} = 'requestee'; - if ($match_status == USER_MATCH_FAILED) { - $vars->{'message'} = 'user_match_failed'; - } - elsif ($match_status == USER_MATCH_MULTIPLE) { - $vars->{'message'} = 'user_match_multiple'; - } - - # Extract a list of flag type IDs from field names. - my @flagtype_ids = map(/^flag_type-(\d+)$/ ? $1 : (), $cgi->param()); - - my (@new_flags, @flags); - - # Get a list of active flag types available for this product/component. - my $flag_types = Bugzilla::FlagType::match( - { 'product_id' => $bug->{'product_id'}, - 'component_id' => $bug->{'component_id'}, - 'is_active' => 1 }); - - foreach my $flagtype_id (@flagtype_ids) { - # Checks if there are unexpected flags for the product/component. - if (!scalar(grep { $_->id == $flagtype_id } @$flag_types)) { - $vars->{'message'} = 'unexpected_flag_types'; - last; - } + my ($class, $bug, $vars, $skip) = @_; + my $cgi = Bugzilla->cgi; + + my $match_status + = Bugzilla::User::match_field( + {'^requestee(_type)?-(\d+)$' => {'type' => 'multi'},}, + undef, $skip); + + $vars->{'match_field'} = 'requestee'; + if ($match_status == USER_MATCH_FAILED) { + $vars->{'message'} = 'user_match_failed'; + } + elsif ($match_status == USER_MATCH_MULTIPLE) { + $vars->{'message'} = 'user_match_multiple'; + } + + # Extract a list of flag type IDs from field names. + my @flagtype_ids = map(/^flag_type-(\d+)$/ ? $1 : (), $cgi->param()); + + my (@new_flags, @flags); + + # Get a list of active flag types available for this product/component. + my $flag_types = Bugzilla::FlagType::match({ + 'product_id' => $bug->{'product_id'}, + 'component_id' => $bug->{'component_id'}, + 'is_active' => 1 + }); + + foreach my $flagtype_id (@flagtype_ids) { + + # Checks if there are unexpected flags for the product/component. + if (!scalar(grep { $_->id == $flagtype_id } @$flag_types)) { + $vars->{'message'} = 'unexpected_flag_types'; + last; } - - foreach my $flag_type (@$flag_types) { - my $type_id = $flag_type->id; - - # Bug flags are only valid for bugs - next unless ($flag_type->target_type eq 'bug'); - - # We are only interested in flags the user tries to create. - next unless scalar(grep { $_ == $type_id } @flagtype_ids); - - # Get the flags of this type already set for this bug. - my $current_flags = $class->match( - { 'type_id' => $type_id, - 'target_type' => 'bug', - 'bug_id' => $bug->bug_id }); - - # We will update existing flags (instead of creating new ones) - # if the flag exists and the user has not chosen the 'always add' - # option - my $update = scalar(@$current_flags) && ! $cgi->param("flags_add-$type_id"); - - my $status = $cgi->param("flag_type-$type_id"); - trick_taint($status); - - my @logins = $cgi->param("requestee_type-$type_id"); - if ($status eq "?" && scalar(@logins)) { - foreach my $login (@logins) { - if ($update) { - foreach my $current_flag (@$current_flags) { - push (@flags, { id => $current_flag->id, - status => $status, - requestee => $login, - skip_roe => $skip }); - } - } - else { - push (@new_flags, { type_id => $type_id, - status => $status, - requestee => $login, - skip_roe => $skip }); - } - - last unless $flag_type->is_multiplicable; - } + } + + foreach my $flag_type (@$flag_types) { + my $type_id = $flag_type->id; + + # Bug flags are only valid for bugs + next unless ($flag_type->target_type eq 'bug'); + + # We are only interested in flags the user tries to create. + next unless scalar(grep { $_ == $type_id } @flagtype_ids); + + # Get the flags of this type already set for this bug. + my $current_flags + = $class->match({ + 'type_id' => $type_id, 'target_type' => 'bug', 'bug_id' => $bug->bug_id + }); + + # We will update existing flags (instead of creating new ones) + # if the flag exists and the user has not chosen the 'always add' + # option + my $update = scalar(@$current_flags) && !$cgi->param("flags_add-$type_id"); + + my $status = $cgi->param("flag_type-$type_id"); + trick_taint($status); + + my @logins = $cgi->param("requestee_type-$type_id"); + if ($status eq "?" && scalar(@logins)) { + foreach my $login (@logins) { + if ($update) { + foreach my $current_flag (@$current_flags) { + push( + @flags, + { + id => $current_flag->id, + status => $status, + requestee => $login, + skip_roe => $skip + } + ); + } } else { - if ($update) { - foreach my $current_flag (@$current_flags) { - push (@flags, { id => $current_flag->id, - status => $status }); - } - } - else { - push (@new_flags, { type_id => $type_id, - status => $status }); + push( + @new_flags, + { + type_id => $type_id, + status => $status, + requestee => $login, + skip_roe => $skip } + ); + } + + last unless $flag_type->is_multiplicable; + } + } + else { + if ($update) { + foreach my $current_flag (@$current_flags) { + push(@flags, {id => $current_flag->id, status => $status}); } + } + else { + push(@new_flags, {type_id => $type_id, status => $status}); + } } + } - # Return the list of flags to update and/or to create. - return (\@flags, \@new_flags); + # Return the list of flags to update and/or to create. + return (\@flags, \@new_flags); } =pod @@ -1054,113 +1189,124 @@ or deleted. =cut sub notify { - my ($class, $flag, $old_flag, $obj, $timestamp) = @_; - - my ($bug, $attachment); - if (blessed($obj) && $obj->isa('Bugzilla::Attachment')) { - $attachment = $obj; - $bug = $attachment->bug; - } - elsif (blessed($obj) && $obj->isa('Bugzilla::Bug')) { - $bug = $obj; + my ($class, $flag, $old_flag, $obj, $timestamp) = @_; + + my ($bug, $attachment); + if (blessed($obj) && $obj->isa('Bugzilla::Attachment')) { + $attachment = $obj; + $bug = $attachment->bug; + } + elsif (blessed($obj) && $obj->isa('Bugzilla::Bug')) { + $bug = $obj; + } + else { + # Not a good time to throw an error. + return; + } + + my $addressee; + + # If the flag is set to '?', maybe the requestee wants a notification. + if ( $flag + && $flag->requestee_id + && (!$old_flag || ($old_flag->requestee_id || 0) != $flag->requestee_id)) + { + if ($flag->requestee->wants_mail([EVT_FLAG_REQUESTED])) { + $addressee = $flag->requestee; } - else { - # Not a good time to throw an error. - return; - } - - my $addressee; - # If the flag is set to '?', maybe the requestee wants a notification. - if ($flag && $flag->requestee_id - && (!$old_flag || ($old_flag->requestee_id || 0) != $flag->requestee_id)) - { - if ($flag->requestee->wants_mail([EVT_FLAG_REQUESTED])) { - $addressee = $flag->requestee; - } - } - elsif ($old_flag && $old_flag->status eq '?' - && (!$flag || $flag->status ne '?')) - { - if ($old_flag->setter->wants_mail([EVT_REQUESTED_FLAG])) { - $addressee = $old_flag->setter; - } - } - - my $cc_list = $flag ? $flag->type->cc_list : $old_flag->type->cc_list; - # Is there someone to notify? - return unless ($addressee || $cc_list); - - # The email client will display the Date: header in the desired timezone, - # so we can always use UTC here. - $timestamp ||= Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); - $timestamp = format_time($timestamp, '%a, %d %b %Y %T %z', 'UTC'); - - # If the target bug is restricted to one or more groups, then we need - # to make sure we don't send email about it to unauthorized users - # on the request type's CC: list, so we have to trawl the list for users - # not in those groups or email addresses that don't have an account. - my @bug_in_groups = grep {$_->{'ison'} || $_->{'mandatory'}} @{$bug->groups}; - my $attachment_is_private = $attachment ? $attachment->isprivate : undef; - - my %recipients; - foreach my $cc (split(/[, ]+/, $cc_list)) { - my $ccuser = new Bugzilla::User({ name => $cc }); - next if (scalar(@bug_in_groups) && (!$ccuser || !$ccuser->can_see_bug($bug->bug_id))); - next if $attachment_is_private && (!$ccuser || !$ccuser->is_insider); - # Prevent duplicated entries due to case sensitivity. - $cc = $ccuser ? $ccuser->email : $cc; - $recipients{$cc} = $ccuser; - } - - # Only notify if the addressee is allowed to receive the email. - if ($addressee && $addressee->email_enabled) { - $recipients{$addressee->email} = $addressee; - } - # Process and send notification for each recipient. - # If there are users in the CC list who don't have an account, - # use the default language for email notifications. - my $default_lang; - if (grep { !$_ } values %recipients) { - $default_lang = Bugzilla::User->new()->setting('lang'); - } - - # Get comments on the bug - my $all_comments = $bug->comments({ after => $bug->lastdiffed }); - @$all_comments = grep { $_->type || $_->body =~ /\S/ } @$all_comments; - - # Get public only comments - my $public_comments = [ grep { !$_->is_private } @$all_comments ]; - - foreach my $to (keys %recipients) { - # Add threadingmarker to allow flag notification emails to be the - # threaded similar to normal bug change emails. - my $thread_user_id = $recipients{$to} ? $recipients{$to}->id : 0; - - # We only want to show private comments to users in the is_insider group - my $comments = $recipients{$to} && $recipients{$to}->is_insider - ? $all_comments : $public_comments; - - my $vars = { - flag => $flag, - old_flag => $old_flag, - to => $to, - date => $timestamp, - bug => $bug, - attachment => $attachment, - threadingmarker => build_thread_marker($bug->id, $thread_user_id), - new_comments => $comments, - }; - - my $lang = $recipients{$to} ? - $recipients{$to}->setting('lang') : $default_lang; - - my $template = Bugzilla->template_inner($lang); - my $message; - $template->process("email/flagmail.txt.tmpl", $vars, \$message) - || ThrowTemplateError($template->error()); - - MessageToMTA($message); + } + elsif ($old_flag + && $old_flag->status eq '?' + && (!$flag || $flag->status ne '?')) + { + if ($old_flag->setter->wants_mail([EVT_REQUESTED_FLAG])) { + $addressee = $old_flag->setter; } + } + + my $cc_list = $flag ? $flag->type->cc_list : $old_flag->type->cc_list; + + # Is there someone to notify? + return unless ($addressee || $cc_list); + + # The email client will display the Date: header in the desired timezone, + # so we can always use UTC here. + $timestamp ||= Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); + $timestamp = format_time($timestamp, '%a, %d %b %Y %T %z', 'UTC'); + + # If the target bug is restricted to one or more groups, then we need + # to make sure we don't send email about it to unauthorized users + # on the request type's CC: list, so we have to trawl the list for users + # not in those groups or email addresses that don't have an account. + # Also filter recipients if the flag is set to private. + my @bug_in_groups = grep { $_->{'ison'} || $_->{'mandatory'} } @{$bug->groups}; + my $attachment_is_private = $attachment ? $attachment->isprivate : undef; + + my %recipients; + foreach my $cc (split(/[, ]+/, $cc_list)) { + my $ccuser = new Bugzilla::User({name => $cc}); + next + if (scalar(@bug_in_groups) + && (!$ccuser || !$ccuser->can_see_bug($bug->bug_id))); + next if $attachment_is_private && (!$ccuser || !$ccuser->is_insider); + next if (!$ccuser || ($flag && !$ccuser->can_see_flag($flag->type))); + + # Prevent duplicated entries due to case sensitivity. + $cc = $ccuser ? $ccuser->email : $cc; + $recipients{$cc} = $ccuser; + } + + # Only notify if the addressee is allowed to receive the email. + if ($addressee && $addressee->email_enabled) { + $recipients{$addressee->email} = $addressee; + } + + # Process and send notification for each recipient. + # If there are users in the CC list who don't have an account, + # use the default language for email notifications. + my $default_lang; + if (grep { !$_ } values %recipients) { + $default_lang = Bugzilla::User->new()->setting('lang'); + } + + # Get comments on the bug + my $all_comments = $bug->comments({after => $bug->lastdiffed}); + @$all_comments = grep { $_->type || $_->body =~ /\S/ } @$all_comments; + + # Get public only comments + my $public_comments = [grep { !$_->is_private } @$all_comments]; + + foreach my $to (keys %recipients) { + + # Add threadingmarker to allow flag notification emails to be the + # threaded similar to normal bug change emails. + my $thread_user_id = $recipients{$to} ? $recipients{$to}->id : 0; + + # We only want to show private comments to users in the is_insider group + my $comments = $recipients{$to} + && $recipients{$to}->is_insider ? $all_comments : $public_comments; + + my $vars = { + flag => $flag, + old_flag => $old_flag, + to => $to, + date => $timestamp, + bug => $bug, + attachment => $attachment, + threadingmarker => build_thread_marker($bug->id, $thread_user_id), + new_comments => $comments, + }; + + my $lang = $recipients{$to} ? $recipients{$to}->setting('lang') : $default_lang; + + my $template = Bugzilla->template_inner($lang); + my $message; + $template->process("email/flagmail.txt.tmpl", $vars, \$message) + || ThrowTemplateError($template->error()); + + MessageToMTA($message); + } + ## REDHAT EXTENSION END 458431 } # This is an internal function used by $bug->flag_types @@ -1168,39 +1314,47 @@ sub notify { # flag types and existing flags set on them. You should never # call this function directly. sub _flag_types { - my ($class, $vars) = @_; - - my $target_type = $vars->{target_type}; - my $flags; - - # Retrieve all existing flags for this bug/attachment. - if ($target_type eq 'bug') { - my $bug_id = delete $vars->{bug_id}; - $flags = $class->match({target_type => 'bug', bug_id => $bug_id}); - } - elsif ($target_type eq 'attachment') { - my $attach_id = delete $vars->{attach_id}; - $flags = $class->match({attach_id => $attach_id}); - } - else { - ThrowCodeError('bad_arg', {argument => 'target_type', - function => $class . '->_flag_types'}); - } - - # Get all available flag types for the given product and component. - my $cache = Bugzilla->request_cache->{flag_types_per_component}->{$vars->{target_type}} ||= {}; - my $flag_data = $cache->{$vars->{component_id}} ||= Bugzilla::FlagType::match($vars); - my $flag_types = dclone($flag_data); - - $_->{flags} = [] foreach @$flag_types; - my %flagtypes = map { $_->id => $_ } @$flag_types; - - # Group existing flags per type, and skip those becoming invalid - # (which can happen when a bug is being moved into a new product - # or component). - @$flags = grep { exists $flagtypes{$_->type_id} } @$flags; - push(@{$flagtypes{$_->type_id}->{flags}}, $_) foreach @$flags; - return $flag_types; + my ($class, $vars) = @_; + + my $target_type = $vars->{target_type}; + my $flags; + + # Retrieve all existing flags for this bug/attachment. + if ($target_type eq 'bug') { + my $bug_id = delete $vars->{bug_id}; + $flags = $class->match({target_type => 'bug', bug_id => $bug_id}); + } + elsif ($target_type eq 'attachment') { + my $attach_id = delete $vars->{attach_id}; + + ## REDHAT EXTENSION START 1317777 +# This can be an array or a string, which are fine, or an empty array which causes BugViewPLus to fail + $flags = $class->match({attach_id => $attach_id}) + unless ((ref($attach_id) eq 'ARRAY') && (!grep { defined($_) } @$attach_id)); + ## REDHAT EXTENSION END 1317777 + } + else { + ThrowCodeError('bad_arg', + {argument => 'target_type', function => $class . '->_flag_types'}); + } + + # Get all available flag types for the given product and component. + my $cache + = Bugzilla->request_cache->{flag_types_per_component}->{$vars->{target_type}} + ||= {}; + my $flag_data = $cache->{$vars->{component_id}} + ||= Bugzilla::FlagType::match($vars); + my $flag_types = dclone($flag_data); + + $_->{flags} = [] foreach @$flag_types; + my %flagtypes = map { $_->id => $_ } @$flag_types; + + # Group existing flags per type, and skip those becoming invalid + # (which can happen when a bug is being moved into a new product + # or component). + @$flags = grep { exists $flagtypes{$_->type_id} } @$flags; + push(@{$flagtypes{$_->type_id}->{flags}}, $_) foreach @$flags; + return $flag_types; } 1; @@ -1229,4 +1383,51 @@ sub _flag_types { =item update +## REDHAT EXTENSION START 406121 + +=item C + +Converts a hash of criteria into a list of SQL criteria. +$criteria is a reference to the criteria (field => value), +$tables is a reference to an array of tables being accessed +by the query. + =back + +=cut + +sub sqlify_criteria { + my ($criteria, $tables) = @_; + my $dbh = Bugzilla->dbh; + my (@values, @criteria); + + foreach my $field (keys %$criteria) { + my $value = $criteria->{$field}; + if (ref $value eq 'ARRAY') { + + # IN () is invalid SQL, and if we have an empty list + # to match against, we're just returning an empty + # array anyhow. + return [] if !scalar @$value; + + my @qmarks = ("?") x @$value; + push(@criteria, $dbh->sql_in($field, \@qmarks)); + push(@values, @$value); + } + elsif ($value eq NOT_NULL) { + push(@criteria, "$field IS NOT NULL"); + } + elsif ($value eq IS_NULL) { + push(@criteria, "$field IS NULL"); + } + else { + push(@criteria, "$field = ?"); + push(@values, $value); + } + } + + return (\@criteria, \@values); +} + +## REDHAT EXTENSION END 406121 + diff --git a/Bugzilla/FlagType.pm b/Bugzilla/FlagType.pm index 72b3f64c1..f9ab96467 100644 --- a/Bugzilla/FlagType.pm +++ b/Bugzilla/FlagType.pm @@ -49,113 +49,142 @@ use parent qw(Bugzilla::Object); #### Initialization #### ############################### -use constant DB_TABLE => 'flagtypes'; +use constant DB_TABLE => 'flagtypes'; use constant LIST_ORDER => 'sortkey, name'; +## REDHAT EXTENSION BEGIN 711031 +# Last two values only use constant DB_COLUMNS => qw( - id - name - description - cc_list - target_type - sortkey - is_active - is_requestable - is_requesteeble - is_multiplicable - grant_group_id - request_group_id + id + name + description + cc_list + target_type + sortkey + is_active + is_requestable + is_requesteeble + is_multiplicable + grant_group_id + request_group_id + view_group_id + category ); +## REDHAT EXTENSION END 711031 use constant UPDATE_COLUMNS => qw( - name - description - cc_list - sortkey - is_active - is_requestable - is_requesteeble - is_multiplicable - grant_group_id - request_group_id + name + description + cc_list + sortkey + is_active + is_requestable + is_requesteeble + is_multiplicable + grant_group_id + request_group_id + view_group_id + category ); use constant VALIDATORS => { - name => \&_check_name, - description => \&_check_description, - cc_list => \&_check_cc_list, - target_type => \&_check_target_type, - sortkey => \&_check_sortkey, - is_active => \&Bugzilla::Object::check_boolean, - is_requestable => \&Bugzilla::Object::check_boolean, - is_requesteeble => \&Bugzilla::Object::check_boolean, - is_multiplicable => \&Bugzilla::Object::check_boolean, - grant_group => \&_check_group, - request_group => \&_check_group, + name => \&_check_name, + description => \&_check_description, + cc_list => \&_check_cc_list, + target_type => \&_check_target_type, + sortkey => \&_check_sortkey, + is_active => \&Bugzilla::Object::check_boolean, + is_requestable => \&Bugzilla::Object::check_boolean, + is_requesteeble => \&Bugzilla::Object::check_boolean, + is_multiplicable => \&Bugzilla::Object::check_boolean, + grant_group => \&_check_group, + request_group => \&_check_group, + view_group => \&_check_group, + category => \&_check_category, }; use constant UPDATE_VALIDATORS => { - grant_group_id => \&_check_group, - request_group_id => \&_check_group, + grant_group_id => \&_check_group, + request_group_id => \&_check_group, + view_group_id => \&_check_group, }; ############################### sub create { - my $class = shift; - my $dbh = Bugzilla->dbh; + my $class = shift; + my $dbh = Bugzilla->dbh; - $dbh->bz_start_transaction(); + $dbh->bz_start_transaction(); - $class->check_required_create_fields(@_); - my $params = $class->run_create_validators(@_); - # In the DB, only the first character of the target type is stored. - $params->{target_type} = substr($params->{target_type}, 0, 1); + $class->check_required_create_fields(@_); + my $params = $class->run_create_validators(@_); - # Extract everything which is not a valid column name. - $params->{grant_group_id} = delete $params->{grant_group}; - $params->{request_group_id} = delete $params->{request_group}; - my $inclusions = delete $params->{inclusions}; - my $exclusions = delete $params->{exclusions}; + # In the DB, only the first character of the target type is stored. + $params->{target_type} = substr($params->{target_type}, 0, 1); - my $flagtype = $class->insert_create_data($params); + # Extract everything which is not a valid column name. + $params->{grant_group_id} = delete $params->{grant_group}; + $params->{request_group_id} = delete $params->{request_group}; + $params->{view_group_id} = delete $params->{view_group}; + my $inclusions = delete $params->{inclusions}; + my $exclusions = delete $params->{exclusions}; - $flagtype->set_clusions({ inclusions => $inclusions, - exclusions => $exclusions }); - $flagtype->update(); + my $flagtype = $class->insert_create_data($params); - $dbh->bz_commit_transaction(); - return $flagtype; + $flagtype->set_clusions({inclusions => $inclusions, exclusions => $exclusions}); + $flagtype->update(); + + $dbh->bz_commit_transaction(); + return $flagtype; } sub update { - my $self = shift; - my $dbh = Bugzilla->dbh; - my $flag_id = $self->id; + my $self = shift; + my $dbh = Bugzilla->dbh; + my $flag_id = $self->id; - $dbh->bz_start_transaction(); - my $changes = $self->SUPER::update(@_); + $dbh->bz_start_transaction(); + my $changes = $self->SUPER::update(@_); - # Update the flaginclusions and flagexclusions tables. - foreach my $category ('inclusions', 'exclusions') { - next unless delete $self->{"_update_$category"}; + # Update the flaginclusions and flagexclusions tables. + foreach my $category ('inclusions', 'exclusions') { + next unless delete $self->{"_update_$category"}; - $dbh->do("DELETE FROM flag$category WHERE type_id = ?", undef, $flag_id); + ## REDHAT EXTENSION BEGIN 1315542 + if ($self->AUDIT_UPDATES) { - my $sth = $dbh->prepare("INSERT INTO flag$category - (type_id, product_id, component_id) VALUES (?, ?, ?)"); + # Fetch the current inclusions and exclusions + my ($clu) = get_clusions($self->id, substr($category, 0, 2)); - foreach my $prod_comp (values %{$self->{$category}}) { - my ($prod_id, $comp_id) = split(':', $prod_comp); - $prod_id ||= undef; - $comp_id ||= undef; - $sth->execute($flag_id, $prod_id, $comp_id); - } - $changes->{$category} = [0, 1]; + my ($removed, $added) = diff_arrays([keys %$clu], [keys %{$self->{$category}}]); + + # If there's been any changes, log them in the audit_log table. + if (scalar(@$removed) || scalar(@$added)) { + $self->audit_log({$category => [$removed, $added]}); + } + } + ## REDHAT EXTENSION END 1315542 + + $dbh->do("DELETE FROM flag$category WHERE type_id = ?", undef, $flag_id); + + my $sth = $dbh->prepare( + "INSERT INTO flag$category + (type_id, product_id, component_id) VALUES (?, ?, ?)" + ); + + foreach my $prod_comp (values %{$self->{$category}}) { + my ($prod_id, $comp_id) = split(':', $prod_comp); + $prod_id ||= undef; + $comp_id ||= undef; + $sth->execute($flag_id, $prod_id, $comp_id); } + $changes->{$category} = [0, 1]; + } - # Clear existing flags for bugs/attachments in categories no longer on - # the list of inclusions or that have been added to the list of exclusions. - my $flag_ids = $dbh->selectcol_arrayref('SELECT DISTINCT flags.id + # Clear existing flags for bugs/attachments in categories no longer on + # the list of inclusions or that have been added to the list of exclusions. + my $flag_ids = $dbh->selectcol_arrayref( + 'SELECT DISTINCT flags.id FROM flags INNER JOIN bugs ON flags.bug_id = bugs.bug_id @@ -166,11 +195,13 @@ sub update { AND (bugs.component_id = i.component_id OR i.component_id IS NULL)) WHERE flags.type_id = ? - AND i.type_id IS NULL', - undef, $self->id); - Bugzilla::Flag->force_retarget($flag_ids); + AND i.type_id IS NULL', undef, + $self->id + ); + Bugzilla::Flag->force_retarget($flag_ids, undef, 1); - $flag_ids = $dbh->selectcol_arrayref('SELECT DISTINCT flags.id + $flag_ids = $dbh->selectcol_arrayref( + 'SELECT DISTINCT flags.id FROM flags INNER JOIN bugs ON flags.bug_id = bugs.bug_id @@ -181,26 +212,29 @@ sub update { OR e.product_id IS NULL) AND (bugs.component_id = e.component_id OR e.component_id IS NULL)', - undef, $self->id); - Bugzilla::Flag->force_retarget($flag_ids); - - # Silently remove requestees from flags which are no longer - # specifically requestable. - if (!$self->is_requesteeble) { - my $ids = $dbh->selectcol_arrayref( - 'SELECT id FROM flags WHERE type_id = ? AND requestee_id IS NOT NULL', - undef, $self->id); - - if (@$ids) { - $dbh->do('UPDATE flags SET requestee_id = NULL WHERE ' . $dbh->sql_in('id', $ids)); - foreach my $id (@$ids) { - Bugzilla->memcached->clear({ table => 'flags', id => $id }); - } - } + undef, $self->id + ); + Bugzilla::Flag->force_retarget($flag_ids, undef, 1); + + # Silently remove requestees from flags which are no longer + # specifically requestable. + if (!$self->is_requesteeble) { + my $ids + = $dbh->selectcol_arrayref( + 'SELECT id FROM flags WHERE type_id = ? AND requestee_id IS NOT NULL', + undef, $self->id); + + if (@$ids) { + $dbh->do( + 'UPDATE flags SET requestee_id = NULL WHERE ' . $dbh->sql_in('id', $ids)); + foreach my $id (@$ids) { + Bugzilla->memcached->clear({table => 'flags', id => $id}); + } } + } - $dbh->bz_commit_transaction(); - return $changes; + $dbh->bz_commit_transaction(); + return $changes; } ############################### @@ -255,176 +289,208 @@ flagtype in a given bug/attachment. Returns the sortkey of the flagtype. +=item C B + +Returns the category of the flagtype. + =back =cut -sub id { return $_[0]->{'id'}; } -sub name { return $_[0]->{'name'}; } -sub description { return $_[0]->{'description'}; } -sub cc_list { return $_[0]->{'cc_list'}; } -sub target_type { return $_[0]->{'target_type'} eq 'b' ? 'bug' : 'attachment'; } -sub is_active { return $_[0]->{'is_active'}; } -sub is_requestable { return $_[0]->{'is_requestable'}; } -sub is_requesteeble { return $_[0]->{'is_requesteeble'}; } +sub id { return $_[0]->{'id'}; } +sub name { return $_[0]->{'name'}; } +sub description { return $_[0]->{'description'}; } +sub cc_list { return $_[0]->{'cc_list'}; } +sub target_type { return $_[0]->{'target_type'} eq 'b' ? 'bug' : 'attachment'; } +sub is_active { return $_[0]->{'is_active'}; } +sub is_requestable { return $_[0]->{'is_requestable'}; } +sub is_requesteeble { return $_[0]->{'is_requesteeble'}; } sub is_multiplicable { return $_[0]->{'is_multiplicable'}; } -sub sortkey { return $_[0]->{'sortkey'}; } +sub sortkey { return $_[0]->{'sortkey'}; } sub request_group_id { return $_[0]->{'request_group_id'}; } -sub grant_group_id { return $_[0]->{'grant_group_id'}; } +sub grant_group_id { return $_[0]->{'grant_group_id'}; } +sub view_group_id { return $_[0]->{'view_group_id'}; } +## REDHAT EXTENSION BEGIN 711031 +sub category { return $_[0]->{'category'}; } +## REDHAT EXTENSION END 711031 ################################ # Validators ################################ sub _check_name { - my ($invocant, $name) = @_; + my ($invocant, $name) = @_; - $name = trim($name); - ($name && $name !~ /[\s,]/ && length($name) <= 50) - || ThrowUserError('flag_type_name_invalid', { name => $name }); - return $name; + $name = trim($name); + ($name && $name !~ /[\s,]/ && length($name) <= 50) + || ThrowUserError('flag_type_name_invalid', {name => $name}); + return $name; } sub _check_description { - my ($invocant, $desc) = @_; + my ($invocant, $desc) = @_; - $desc = trim($desc); - $desc || ThrowUserError('flag_type_description_invalid'); - return $desc; + $desc = trim($desc); + $desc || ThrowUserError('flag_type_description_invalid'); + return $desc; } sub _check_cc_list { - my ($invocant, $cc_list) = @_; - - length($cc_list) <= 200 - || ThrowUserError('flag_type_cc_list_invalid', { cc_list => $cc_list }); - - my @addresses = split(/[,\s]+/, $cc_list); - my $addr_spec = $Email::Address::addr_spec; - # We do not call check_email_syntax() because these addresses do not - # require to match 'emailregexp' and do not depend on 'emailsuffix'. - foreach my $address (@addresses) { - ($address !~ /\P{ASCII}/ && $address =~ /^$addr_spec$/) - || ThrowUserError('illegal_email_address', - {addr => $address, default => 1}); - } - return $cc_list; + my ($invocant, $cc_list) = @_; + + length($cc_list) <= 200 + || ThrowUserError('flag_type_cc_list_invalid', {cc_list => $cc_list}); + + my @addresses = split(/[,\s]+/, $cc_list); + my $addr_spec = $Email::Address::addr_spec; + + # We do not call check_email_syntax() because these addresses do not + # require to match 'emailregexp' and do not depend on 'emailsuffix'. + foreach my $address (@addresses) { + ($address !~ /\P{ASCII}/ && $address =~ /^$addr_spec$/) + || ThrowUserError('illegal_email_address', {addr => $address, default => 1}); + } + return $cc_list; } sub _check_target_type { - my ($invocant, $target_type) = @_; + my ($invocant, $target_type) = @_; - ($target_type eq 'bug' || $target_type eq 'attachment') - || ThrowCodeError('flag_type_target_type_invalid', { target_type => $target_type }); - return $target_type; + ($target_type eq 'bug' || $target_type eq 'attachment') + || ThrowCodeError('flag_type_target_type_invalid', + {target_type => $target_type}); + return $target_type; } sub _check_sortkey { - my ($invocant, $sortkey) = @_; + my ($invocant, $sortkey) = @_; - (detaint_natural($sortkey) && $sortkey <= MAX_SMALLINT) - || ThrowUserError('flag_type_sortkey_invalid', { sortkey => $sortkey }); - return $sortkey; + (detaint_natural($sortkey) && $sortkey <= MAX_SMALLINT) + || ThrowUserError('flag_type_sortkey_invalid', {sortkey => $sortkey}); + return $sortkey; } sub _check_group { - my ($invocant, $group) = @_; - return unless $group; + my ($invocant, $group) = @_; + return unless $group; - trick_taint($group); - $group = Bugzilla::Group->check($group); - return $group->id; + trick_taint($group); + $group = Bugzilla::Group->check($group); + return $group->id; } +## REDHAT EXTENSION BEGIN 711031 +sub _check_category { + my ($invocant, $category) = @_; + + # Get a list of valid categories + my @categories = split /[\s,]+/, Bugzilla->params->{flag_type_categories}; + + # If there are no, don't check + if (scalar(@categories) == 0) { + return; + } + + # Check that the category is valid. If no category is valid, make sure the params + # value ends or begins with a comma + grep($category eq $_, @categories) + || ThrowCodeError("flag_category_invalid", {category => $category}); + return $category; +} +## REDHAT EXTENSION BEGIN 711031 + ############################### #### Methods #### ############################### -sub set_name { $_[0]->set('name', $_[1]); } -sub set_description { $_[0]->set('description', $_[1]); } -sub set_cc_list { $_[0]->set('cc_list', $_[1]); } -sub set_sortkey { $_[0]->set('sortkey', $_[1]); } -sub set_is_active { $_[0]->set('is_active', $_[1]); } -sub set_is_requestable { $_[0]->set('is_requestable', $_[1]); } -sub set_is_specifically_requestable { $_[0]->set('is_requesteeble', $_[1]); } -sub set_is_multiplicable { $_[0]->set('is_multiplicable', $_[1]); } -sub set_grant_group { $_[0]->set('grant_group_id', $_[1]); } -sub set_request_group { $_[0]->set('request_group_id', $_[1]); } +sub set_name { $_[0]->set('name', $_[1]); } +sub set_description { $_[0]->set('description', $_[1]); } +sub set_cc_list { $_[0]->set('cc_list', $_[1]); } +sub set_sortkey { $_[0]->set('sortkey', $_[1]); } +sub set_is_active { $_[0]->set('is_active', $_[1]); } +sub set_is_requestable { $_[0]->set('is_requestable', $_[1]); } +sub set_is_specifically_requestable { $_[0]->set('is_requesteeble', $_[1]); } +sub set_is_multiplicable { $_[0]->set('is_multiplicable', $_[1]); } +sub set_grant_group { $_[0]->set('grant_group_id', $_[1]); } +sub set_request_group { $_[0]->set('request_group_id', $_[1]); } +sub set_view_group { $_[0]->set('view_group_id', $_[1]); } +sub set_category { $_[0]->set('category', $_[1]); } sub set_clusions { - my ($self, $list) = @_; - my $user = Bugzilla->user; - my %products; - my $params = {}; - - # If the user has editcomponents privs, then we only need to make sure - # that the product exists. - if ($user->in_group('editcomponents')) { - $params->{allow_inaccessible} = 1; - } - - foreach my $category (keys %$list) { - my %clusions; - my %clusions_as_hash; - - foreach my $prod_comp (@{$list->{$category} || []}) { - my ($prod_id, $comp_id) = split(':', $prod_comp); - my $prod_name = '__Any__'; - my $comp_name = '__Any__'; - # Does the product exist? - if ($prod_id) { - detaint_natural($prod_id) - || ThrowCodeError('param_must_be_numeric', - { function => 'Bugzilla::FlagType::set_clusions' }); - - if (!$products{$prod_id}) { - $params->{id} = $prod_id; - $products{$prod_id} = Bugzilla::Product->check($params); - } - $prod_name = $products{$prod_id}->name; - - # Does the component belong to this product? - if ($comp_id) { - detaint_natural($comp_id) - || ThrowCodeError('param_must_be_numeric', - { function => 'Bugzilla::FlagType::set_clusions' }); - - my ($component) = grep { $_->id == $comp_id } @{$products{$prod_id}->components} - or ThrowUserError('product_unknown_component', - { product => $prod_name, comp_id => $comp_id }); - $comp_name = $component->name; - } - else { - $comp_id = 0; - } - } - else { - $prod_id = 0; - $comp_id = 0; - } - $clusions{"$prod_name:$comp_name"} = "$prod_id:$comp_id"; - $clusions_as_hash{$prod_id}->{$comp_id} = 1; + my ($self, $list) = @_; + my $user = Bugzilla->user; + my %products; + my $params = {}; + + # If the user has editcomponents privs, then we only need to make sure + # that the product exists. + if ($user->in_group('editcomponents')) { + $params->{allow_inaccessible} = 1; + } + + foreach my $category (keys %$list) { + my %clusions; + my %clusions_as_hash; + + foreach my $prod_comp (@{$list->{$category} || []}) { + my ($prod_id, $comp_id) = split(':', $prod_comp); + my $prod_name = '__Any__'; + my $comp_name = '__Any__'; + + # Does the product exist? + if ($prod_id) { + detaint_natural($prod_id) + || ThrowCodeError('param_must_be_numeric', + {function => 'Bugzilla::FlagType::set_clusions'}); + + if (!$products{$prod_id}) { + $params->{id} = $prod_id; + $products{$prod_id} = Bugzilla::Product->check($params); } - - # Check the user has the editcomponent permission on products that are changing - if (! $user->in_group('editcomponents')) { - my $current_clusions = $self->$category; - my ($removed, $added) - = diff_arrays([ values %$current_clusions ], [ values %clusions ]); - my @changed_product_ids - = uniq map { substr($_, 0, index($_, ':')) } @$removed, @$added; - foreach my $product_id (@changed_product_ids) { - $user->in_group('editcomponents', $product_id) - || ThrowUserError('product_access_denied', - { name => $products{$product_id}->name }); - } + $prod_name = $products{$prod_id}->name; + + # Does the component belong to this product? + if ($comp_id) { + detaint_natural($comp_id) + || ThrowCodeError('param_must_be_numeric', + {function => 'Bugzilla::FlagType::set_clusions'}); + + my ($component) = grep { $_->id == $comp_id } @{$products{$prod_id}->components} + or ThrowUserError('product_unknown_component', + {product => $prod_name, comp_id => $comp_id}); + $comp_name = $component->name; + } + else { + $comp_id = 0; } + } + else { + $prod_id = 0; + $comp_id = 0; + } + $clusions{"$prod_name:$comp_name"} = "$prod_id:$comp_id"; + $clusions_as_hash{$prod_id}->{$comp_id} = 1; + } - # Set the changes - $self->{$category} = \%clusions; - $self->{"${category}_as_hash"} = \%clusions_as_hash; - $self->{"_update_$category"} = 1; + # Check the user has the editcomponent permission on products that are changing + if (!$user->in_group('editcomponents')) { + my $current_clusions = $self->$category; + my ($removed, $added) + = diff_arrays([values %$current_clusions], [values %clusions]); + my @changed_product_ids = uniq map { substr($_, 0, index($_, ':')) } @$removed, + @$added; + foreach my $product_id (@changed_product_ids) { + $user->in_group('editcomponents', $product_id) + || ThrowUserError('product_access_denied', + {name => $products{$product_id}->name}); + } } + + # Set the changes + $self->{$category} = \%clusions; + $self->{"${category}_as_hash"} = \%clusions_as_hash; + $self->{"_update_$category"} = 1; + } } =pod @@ -436,6 +502,11 @@ sub set_clusions { Returns a reference to an array of users who have permission to grant this flag type. The arrays are populated with hashrefs containing the login, identity and visibility of users. +=item C + +Returns the group (as a Bugzilla::Group object) in which a user +must be in order to see a specific flag. + =item C Returns the group (as a Bugzilla::Group object) in which a user @@ -465,76 +536,88 @@ explicitly excluded from the flagtype. =cut sub grant_list { - my $self = shift; - require Bugzilla::User; - my @custusers; - my @allusers = @{Bugzilla->user->get_userlist}; - foreach my $user (@allusers) { - my $user_obj = new Bugzilla::User({name => $user->{login}}); - push(@custusers, $user) if $user_obj->can_set_flag($self); - } - return \@custusers; + my $self = shift; + require Bugzilla::User; + my @custusers; + my @allusers = @{Bugzilla->user->get_userlist}; + foreach my $user (@allusers) { + my $user_obj = new Bugzilla::User({name => $user->{login}}); + push(@custusers, $user) if $user_obj->can_set_flag($self); + } + return \@custusers; +} + +sub view_group { + my $self = shift; + + if (!defined $self->{'view_group'} && $self->{'view_group_id'}) { + $self->{'view_group'} = new Bugzilla::Group($self->{'view_group_id'}); + } + return $self->{'view_group'}; } sub grant_group { - my $self = shift; + my $self = shift; - if (!defined $self->{'grant_group'} && $self->{'grant_group_id'}) { - $self->{'grant_group'} = new Bugzilla::Group($self->{'grant_group_id'}); - } - return $self->{'grant_group'}; + if (!defined $self->{'grant_group'} && $self->{'grant_group_id'}) { + $self->{'grant_group'} = new Bugzilla::Group($self->{'grant_group_id'}); + } + return $self->{'grant_group'}; } sub request_group { - my $self = shift; + my $self = shift; - if (!defined $self->{'request_group'} && $self->{'request_group_id'}) { - $self->{'request_group'} = new Bugzilla::Group($self->{'request_group_id'}); - } - return $self->{'request_group'}; + if (!defined $self->{'request_group'} && $self->{'request_group_id'}) { + $self->{'request_group'} = new Bugzilla::Group($self->{'request_group_id'}); + } + return $self->{'request_group'}; } sub flag_count { - my $self = shift; - - if (!defined $self->{'flag_count'}) { - $self->{'flag_count'} = - Bugzilla->dbh->selectrow_array('SELECT COUNT(*) FROM flags - WHERE type_id = ?', undef, $self->{'id'}); - } - return $self->{'flag_count'}; + my $self = shift; + + if (!defined $self->{'flag_count'}) { + $self->{'flag_count'} = Bugzilla->dbh->selectrow_array( + 'SELECT COUNT(*) FROM flags + WHERE type_id = ?', undef, $self->{'id'} + ); + } + return $self->{'flag_count'}; } sub inclusions { - my $self = shift; + my $self = shift; - if (!defined $self->{inclusions}) { - ($self->{inclusions}, $self->{inclusions_as_hash}) = get_clusions($self->id, 'in'); - } - return $self->{inclusions}; + if (!defined $self->{inclusions}) { + ($self->{inclusions}, $self->{inclusions_as_hash}) + = get_clusions($self->id, 'in'); + } + return $self->{inclusions}; } sub inclusions_as_hash { - my $self = shift; + my $self = shift; - $self->inclusions unless defined $self->{inclusions_as_hash}; - return $self->{inclusions_as_hash}; + $self->inclusions unless defined $self->{inclusions_as_hash}; + return $self->{inclusions_as_hash}; } sub exclusions { - my $self = shift; + my $self = shift; - if (!defined $self->{exclusions}) { - ($self->{exclusions}, $self->{exclusions_as_hash}) = get_clusions($self->id, 'ex'); - } - return $self->{exclusions}; + if (!defined $self->{exclusions}) { + ($self->{exclusions}, $self->{exclusions_as_hash}) + = get_clusions($self->id, 'ex'); + } + return $self->{exclusions}; } sub exclusions_as_hash { - my $self = shift; + my $self = shift; - $self->exclusions unless defined $self->{exclusions_as_hash}; - return $self->{exclusions_as_hash}; + $self->exclusions unless defined $self->{exclusions_as_hash}; + return $self->{exclusions_as_hash}; } ###################################################################### @@ -558,11 +641,11 @@ $clusions{'product_name:component_name'} = "product_ID:component_ID" =cut sub get_clusions { - my ($id, $type) = @_; - my $dbh = Bugzilla->dbh; + my ($id, $type) = @_; + my $dbh = Bugzilla->dbh; - my $list = - $dbh->selectall_arrayref("SELECT products.id, products.name, + my $list = $dbh->selectall_arrayref( + "SELECT products.id, products.name, components.id, components.name FROM flagtypes INNER JOIN flag${type}clusions @@ -571,19 +654,19 @@ sub get_clusions { ON flag${type}clusions.product_id = products.id LEFT JOIN components ON flag${type}clusions.component_id = components.id - WHERE flagtypes.id = ?", - undef, $id); - my (%clusions, %clusions_as_hash); - foreach my $data (@$list) { - my ($product_id, $product_name, $component_id, $component_name) = @$data; - $product_id ||= 0; - $product_name ||= "__Any__"; - $component_id ||= 0; - $component_name ||= "__Any__"; - $clusions{"$product_name:$component_name"} = "$product_id:$component_id"; - $clusions_as_hash{$product_id}->{$component_id} = 1; - } - return (\%clusions, \%clusions_as_hash); + WHERE flagtypes.id = ?", undef, $id + ); + my (%clusions, %clusions_as_hash); + foreach my $data (@$list) { + my ($product_id, $product_name, $component_id, $component_name) = @$data; + $product_id ||= 0; + $product_name ||= "__Any__"; + $component_id ||= 0; + $component_name ||= "__Any__"; + $clusions{"$product_name:$component_name"} = "$product_id:$component_id"; + $clusions_as_hash{$product_id}->{$component_id} = 1; + } + return (\%clusions, \%clusions_as_hash); } =pod @@ -600,18 +683,36 @@ and returns a list of matching flagtype objects. =cut sub match { - my ($criteria) = @_; - my $dbh = Bugzilla->dbh; + my ($criteria) = @_; + my $dbh = Bugzilla->dbh; + + # Depending on the criteria, we may have to append additional tables. + my $tables = [DB_TABLE]; + my @criteria = sqlify_criteria($criteria, $tables); + + # Only return flags user is authorized to view + # Non-logged in users can only see public flags + # Users in 'editcomponents' and/or 'admin' can see all + my $user = Bugzilla->user; + if (not $user->id) { + push(@criteria, "flagtypes.view_group_id IS NULL"); + } + else { + if (not $user->in_group('editcomponents') and not $user->in_group('admin')) { + push(@criteria, + "(flagtypes.view_group_id IS NULL OR flagtypes.view_group_id IN (" + . $user->groups_as_string + . "))"); + } + } - # Depending on the criteria, we may have to append additional tables. - my $tables = [DB_TABLE]; - my @criteria = sqlify_criteria($criteria, $tables); - $tables = join(' ', @$tables); - $criteria = join(' AND ', @criteria); + $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 id FROM $tables WHERE $criteria"); - return Bugzilla::FlagType->new_from_list($flagtype_ids); + return Bugzilla::FlagType->new_from_list($flagtype_ids); } =pod @@ -627,18 +728,20 @@ Returns the total number of flag types matching the given criteria. =cut sub count { - my ($criteria) = @_; - my $dbh = Bugzilla->dbh; - - # Depending on the criteria, we may have to append additional tables. - my $tables = [DB_TABLE]; - my @criteria = sqlify_criteria($criteria, $tables); - $tables = join(' ', @$tables); - $criteria = join(' AND ', @criteria); - - my $count = $dbh->selectrow_array("SELECT COUNT(flagtypes.id) - FROM $tables WHERE $criteria"); - return $count; + my ($criteria) = @_; + my $dbh = Bugzilla->dbh; + + # Depending on the criteria, we may have to append additional tables. + my $tables = [DB_TABLE]; + my @criteria = sqlify_criteria($criteria, $tables); + $tables = join(' ', @$tables); + $criteria = join(' AND ', @criteria); + + my $count = $dbh->selectrow_array( + "SELECT COUNT(flagtypes.id) + FROM $tables WHERE $criteria" + ); + return $count; } ###################################################################### @@ -646,93 +749,100 @@ sub count { ###################################################################### # Converts a hash of criteria into a list of SQL criteria. -# $criteria is a reference to the criteria (field => value), -# $tables is a reference to an array of tables being accessed +# $criteria is a reference to the criteria (field => value), +# $tables is a reference to an array of tables being accessed # by the query. sub sqlify_criteria { - my ($criteria, $tables) = @_; - my $dbh = Bugzilla->dbh; - - # the generated list of SQL criteria; "1=1" is a clever way of making sure - # there's something in the list so calling code doesn't have to check list - # size before building a WHERE clause out of it - my @criteria = ("1=1"); - - if ($criteria->{name}) { - if (ref($criteria->{name}) eq 'ARRAY') { - my @names = map { $dbh->quote($_) } @{$criteria->{name}}; - # Detaint data as we have quoted it. - foreach my $name (@names) { - trick_taint($name); - } - push @criteria, $dbh->sql_in('flagtypes.name', \@names); - } - else { - my $name = $dbh->quote($criteria->{name}); - trick_taint($name); # Detaint data as we have quoted it. - push(@criteria, "flagtypes.name = $name"); - } + my ($criteria, $tables) = @_; + my $dbh = Bugzilla->dbh; + + # the generated list of SQL criteria; "1=1" is a clever way of making sure + # there's something in the list so calling code doesn't have to check list + # size before building a WHERE clause out of it + my @criteria = ("1=1"); + + if ($criteria->{name}) { + if (ref($criteria->{name}) eq 'ARRAY') { + my @names = map { $dbh->quote($_) } @{$criteria->{name}}; + + # Detaint data as we have quoted it. + foreach my $name (@names) { + trick_taint($name); + } + push @criteria, $dbh->sql_in('flagtypes.name', \@names); } - if ($criteria->{target_type}) { - # The target type is stored in the database as a one-character string - # ("a" for attachment and "b" for bug), but this function takes complete - # names ("attachment" and "bug") for clarity, so we must convert them. - my $target_type = $criteria->{target_type} eq 'bug'? 'b' : 'a'; - push(@criteria, "flagtypes.target_type = '$target_type'"); + else { + my $name = $dbh->quote($criteria->{name}); + trick_taint($name); # Detaint data as we have quoted it. + push(@criteria, "flagtypes.name = $name"); } - if (exists($criteria->{is_active})) { - my $is_active = $criteria->{is_active} ? "1" : "0"; - push(@criteria, "flagtypes.is_active = $is_active"); + } + if ($criteria->{target_type}) { + + # The target type is stored in the database as a one-character string + # ("a" for attachment and "b" for bug), but this function takes complete + # names ("attachment" and "bug") for clarity, so we must convert them. + my $target_type = $criteria->{target_type} eq 'bug' ? 'b' : 'a'; + push(@criteria, "flagtypes.target_type = '$target_type'"); + } + if (exists($criteria->{is_active})) { + my $is_active = $criteria->{is_active} ? "1" : "0"; + push(@criteria, "flagtypes.is_active = $is_active"); + } + if ($criteria->{product_id}) { + my $product_id = $criteria->{product_id}; + detaint_natural($product_id) + || ThrowCodeError('bad_arg', + {argument => 'product_id', function => 'Bugzilla::FlagType::sqlify_criteria'}); + + # Add inclusions to the query, which simply involves joining the table + # by flag type ID and target product/component. + push(@$tables, "INNER JOIN flaginclusions AS i ON flagtypes.id = i.type_id"); + push(@criteria, "(i.product_id = $product_id OR i.product_id IS NULL)"); + + # Add exclusions to the query, which is more complicated. First of all, + # we do a LEFT JOIN so we don't miss flag types with no exclusions. + # Then, as with inclusions, we join on flag type ID and target product/ + # component. However, since we want flag types that *aren't* on the + # exclusions list, we add a WHERE criteria to use only records with + # NULL exclusion type, i.e. without any exclusions. + my $join_clause = "flagtypes.id = e.type_id "; + + my $addl_join_clause = ""; + if ($criteria->{component_id}) { + my $component_id = $criteria->{component_id}; + detaint_natural($component_id) || ThrowCodeError('bad_arg', + {argument => 'component_id', function => 'Bugzilla::FlagType::sqlify_criteria'} + ); + + push(@criteria, "(i.component_id = $component_id OR i.component_id IS NULL)"); + $join_clause + .= "AND (e.component_id = $component_id OR e.component_id IS NULL) "; } - if ($criteria->{product_id}) { - my $product_id = $criteria->{product_id}; - detaint_natural($product_id) - || ThrowCodeError('bad_arg', { argument => 'product_id', - function => 'Bugzilla::FlagType::sqlify_criteria' }); - - # Add inclusions to the query, which simply involves joining the table - # by flag type ID and target product/component. - push(@$tables, "INNER JOIN flaginclusions AS i ON flagtypes.id = i.type_id"); - push(@criteria, "(i.product_id = $product_id OR i.product_id IS NULL)"); - - # Add exclusions to the query, which is more complicated. First of all, - # we do a LEFT JOIN so we don't miss flag types with no exclusions. - # Then, as with inclusions, we join on flag type ID and target product/ - # component. However, since we want flag types that *aren't* on the - # exclusions list, we add a WHERE criteria to use only records with - # NULL exclusion type, i.e. without any exclusions. - my $join_clause = "flagtypes.id = e.type_id "; - - my $addl_join_clause = ""; - if ($criteria->{component_id}) { - my $component_id = $criteria->{component_id}; - detaint_natural($component_id) - || ThrowCodeError('bad_arg', { argument => 'component_id', - function => 'Bugzilla::FlagType::sqlify_criteria' }); - - push(@criteria, "(i.component_id = $component_id OR i.component_id IS NULL)"); - $join_clause .= "AND (e.component_id = $component_id OR e.component_id IS NULL) "; - } - else { - $addl_join_clause = "AND e.component_id IS NULL OR (i.component_id = e.component_id) "; - } - $join_clause .= "AND ((e.product_id = $product_id $addl_join_clause) OR e.product_id IS NULL)"; - - push(@$tables, "LEFT JOIN flagexclusions AS e ON ($join_clause)"); - push(@criteria, "e.type_id IS NULL"); - } - if ($criteria->{group}) { - my $gid = $criteria->{group}; - detaint_natural($gid) - || ThrowCodeError('bad_arg', { argument => 'group', - function => 'Bugzilla::FlagType::sqlify_criteria' }); - - push(@criteria, "(flagtypes.grant_group_id = $gid " . - " OR flagtypes.request_group_id = $gid)"); + else { + $addl_join_clause + = "AND e.component_id IS NULL OR (i.component_id = e.component_id) "; } - - return @criteria; + $join_clause + .= "AND ((e.product_id = $product_id $addl_join_clause) OR e.product_id IS NULL)"; + + push(@$tables, "LEFT JOIN flagexclusions AS e ON ($join_clause)"); + push(@criteria, "e.type_id IS NULL"); + } + if ($criteria->{group}) { + my $gid = $criteria->{group}; + detaint_natural($gid) + || ThrowCodeError('bad_arg', + {argument => 'group', function => 'Bugzilla::FlagType::sqlify_criteria'}); + + push(@criteria, + "(flagtypes.grant_group_id = $gid " + . " OR flagtypes.view_group_id = $gid " + . " OR flagtypes.request_group_id = $gid)"); + } + + return @criteria; } 1; @@ -775,4 +885,10 @@ sub sqlify_criteria { =item update +=item set_view_group + +=item set_category + +=item view_group_id + =back diff --git a/Bugzilla/Group.pm b/Bugzilla/Group.pm index f7a50f7f1..029141f74 100644 --- a/Bugzilla/Group.pm +++ b/Bugzilla/Group.pm @@ -22,153 +22,162 @@ use Bugzilla::Config qw(:admin); ##### Module Initialization ### ############################### -use constant IS_CONFIG => 1; +## REDHAT EXTENSION BEGIN 751352 +# Last value only use constant DB_COLUMNS => qw( - groups.id - groups.name - groups.description - groups.isbuggroup - groups.userregexp - groups.isactive - groups.icon_url + groups.id + groups.name + groups.description + groups.isbuggroup + groups.userregexp + groups.isactive + groups.icon_url + groups.category ); +## REDHAT EXTENSION END 751352 use constant DB_TABLE => 'groups'; use constant LIST_ORDER => 'isbuggroup, name'; use constant VALIDATORS => { - name => \&_check_name, - description => \&_check_description, - userregexp => \&_check_user_regexp, - isactive => \&_check_is_active, - isbuggroup => \&_check_is_bug_group, - icon_url => \&_check_icon_url, + name => \&_check_name, + description => \&_check_description, + userregexp => \&_check_user_regexp, + isactive => \&_check_is_active, + isbuggroup => \&_check_is_bug_group, + icon_url => \&_check_icon_url, + category => \&_check_category, }; use constant UPDATE_COLUMNS => qw( - name - description - userregexp - isactive - icon_url + name + description + userregexp + isactive + icon_url + category ); # Parameters that are lists of groups. use constant GROUP_PARAMS => qw( - chartgroup comment_taggers_group debug_group insidergroup - querysharegroup timetrackinggroup + chartgroup comment_taggers_group debug_group insidergroup + querysharegroup timetrackinggroup ); ############################### #### Accessors ###### ############################### -sub description { return $_[0]->{'description'}; } -sub is_bug_group { return $_[0]->{'isbuggroup'}; } -sub user_regexp { return $_[0]->{'userregexp'}; } -sub is_active { return $_[0]->{'isactive'}; } -sub icon_url { return $_[0]->{'icon_url'}; } +sub description { return $_[0]->{'description'}; } +sub is_bug_group { return $_[0]->{'isbuggroup'}; } +sub user_regexp { return $_[0]->{'userregexp'}; } +sub is_active { return $_[0]->{'isactive'}; } +sub icon_url { return $_[0]->{'icon_url'}; } +## REDHAT EXTENSION BEGIN 751352 +sub category { return $_[0]->{'category'}; } +## REDHAT EXTENSION END 751352 sub bugs { - my $self = shift; - return $self->{bugs} if exists $self->{bugs}; - my $bug_ids = Bugzilla->dbh->selectcol_arrayref( - 'SELECT bug_id FROM bug_group_map WHERE group_id = ?', - undef, $self->id); - require Bugzilla::Bug; - $self->{bugs} = Bugzilla::Bug->new_from_list($bug_ids); - return $self->{bugs}; + my $self = shift; + return $self->{bugs} if exists $self->{bugs}; +## REDHAT EXTENSION BEGIN 1376439 + require Bugzilla::Bug; + $self->{bugs} + = Bugzilla::Bug->new_from_where( + 'bug_id in (SELECT bug_id FROM bug_group_map WHERE group_id = ?)', + [$self->id]); +## REDHAT EXTENSION END 1376439 + return $self->{bugs}; } sub members_direct { - my ($self) = @_; - $self->{members_direct} ||= $self->_get_members(GRANT_DIRECT); - return $self->{members_direct}; + my ($self) = @_; + $self->{members_direct} ||= $self->_get_members(GRANT_DIRECT); + return $self->{members_direct}; } sub members_non_inherited { - my ($self) = @_; - $self->{members_non_inherited} ||= $self->_get_members(); - return $self->{members_non_inherited}; + my ($self) = @_; + $self->{members_non_inherited} ||= $self->_get_members(); + return $self->{members_non_inherited}; } # A helper for members_direct and members_non_inherited sub _get_members { - my ($self, $grant_type) = @_; - my $dbh = Bugzilla->dbh; - my $grant_clause = defined($grant_type) ? "AND grant_type = $grant_type" - : ""; - my $user_ids = $dbh->selectcol_arrayref( - "SELECT DISTINCT user_id + my ($self, $grant_type) = @_; + my $dbh = Bugzilla->dbh; + my $grant_clause = defined($grant_type) ? "AND grant_type = $grant_type" : ""; + my $user_ids = $dbh->selectcol_arrayref( + "SELECT DISTINCT user_id FROM user_group_map - WHERE isbless = 0 $grant_clause AND group_id = ?", undef, $self->id); - require Bugzilla::User; - return Bugzilla::User->new_from_list($user_ids); + WHERE isbless = 0 $grant_clause AND group_id = ?", undef, $self->id + ); + require Bugzilla::User; + return Bugzilla::User->new_from_list($user_ids); } sub flag_types { - my $self = shift; - require Bugzilla::FlagType; - $self->{flag_types} ||= Bugzilla::FlagType::match({ group => $self->id }); - return $self->{flag_types}; + my $self = shift; + require Bugzilla::FlagType; + $self->{flag_types} ||= Bugzilla::FlagType::match({group => $self->id}); + return $self->{flag_types}; } sub grant_direct { - my ($self, $type) = @_; - $self->{grant_direct} ||= {}; - return $self->{grant_direct}->{$type} - if defined $self->{grant_direct}->{$type}; - my $dbh = Bugzilla->dbh; - - my $ids = $dbh->selectcol_arrayref( - "SELECT member_id FROM group_group_map - WHERE grantor_id = ? AND grant_type = $type", - undef, $self->id) || []; - - $self->{grant_direct}->{$type} = $self->new_from_list($ids); - return $self->{grant_direct}->{$type}; + my ($self, $type) = @_; + $self->{grant_direct} ||= {}; + return $self->{grant_direct}->{$type} if defined $self->{grant_direct}->{$type}; + my $dbh = Bugzilla->dbh; + + my $ids = $dbh->selectcol_arrayref( + "SELECT member_id FROM group_group_map + WHERE grantor_id = ? AND grant_type = $type", undef, $self->id + ) || []; + + $self->{grant_direct}->{$type} = $self->new_from_list($ids); + return $self->{grant_direct}->{$type}; } sub granted_by_direct { - my ($self, $type) = @_; - $self->{granted_by_direct} ||= {}; - return $self->{granted_by_direct}->{$type} - if defined $self->{granted_by_direct}->{$type}; - my $dbh = Bugzilla->dbh; - - my $ids = $dbh->selectcol_arrayref( - "SELECT grantor_id FROM group_group_map - WHERE member_id = ? AND grant_type = $type", - undef, $self->id) || []; - - $self->{granted_by_direct}->{$type} = $self->new_from_list($ids); - return $self->{granted_by_direct}->{$type}; + my ($self, $type) = @_; + $self->{granted_by_direct} ||= {}; + return $self->{granted_by_direct}->{$type} + if defined $self->{granted_by_direct}->{$type}; + my $dbh = Bugzilla->dbh; + + my $ids = $dbh->selectcol_arrayref( + "SELECT grantor_id FROM group_group_map + WHERE member_id = ? AND grant_type = $type", undef, $self->id + ) || []; + + $self->{granted_by_direct}->{$type} = $self->new_from_list($ids); + return $self->{granted_by_direct}->{$type}; } sub products { - my $self = shift; - return $self->{products} if exists $self->{products}; - my $product_data = Bugzilla->dbh->selectall_arrayref( - 'SELECT product_id, entry, membercontrol, othercontrol, + my $self = shift; + return $self->{products} if exists $self->{products}; + my $product_data = Bugzilla->dbh->selectall_arrayref( + 'SELECT product_id, entry, membercontrol, othercontrol, canedit, editcomponents, editbugs, canconfirm - FROM group_control_map WHERE group_id = ?', {Slice=>{}}, - $self->id); - my @ids = map { $_->{product_id} } @$product_data; - require Bugzilla::Product; - my $products = Bugzilla::Product->new_from_list(\@ids); - my %data_map = map { $_->{product_id} => $_ } @$product_data; - my @retval; - foreach my $product (@$products) { - # Data doesn't need to contain product_id--we already have - # the product object. - delete $data_map{$product->id}->{product_id}; - push(@retval, { controls => $data_map{$product->id}, - product => $product }); - } - $self->{products} = \@retval; - return $self->{products}; + FROM group_control_map WHERE group_id = ?', {Slice => {}}, $self->id + ); + my @ids = map { $_->{product_id} } @$product_data; + require Bugzilla::Product; + my $products = Bugzilla::Product->new_from_list(\@ids); + my %data_map = map { $_->{product_id} => $_ } @$product_data; + my @retval; + foreach my $product (@$products) { + + # Data doesn't need to contain product_id--we already have + # the product object. + delete $data_map{$product->id}->{product_id}; + push(@retval, {controls => $data_map{$product->id}, product => $product}); + } + $self->{products} = \@retval; + return $self->{products}; } ############################### @@ -176,126 +185,169 @@ sub products { ############################### sub check_members_are_visible { - my $self = shift; - my $user = Bugzilla->user; - return if !Bugzilla->params->{'usevisibilitygroups'}; - - my $group_id = $self->id; - my $is_visible = grep { $_ == $group_id } @{ $user->visible_groups_inherited }; - if (!$is_visible) { - ThrowUserError('group_not_visible', { group => $self }); - } + my $self = shift; + my $user = Bugzilla->user; + return if !Bugzilla->params->{'usevisibilitygroups'}; + + my $group_id = $self->id; + my $is_visible = grep { $_ == $group_id } @{$user->visible_groups_inherited}; + if (!$is_visible) { + ThrowUserError('group_not_visible', {group => $self}); + } } sub set_description { $_[0]->set('description', $_[1]); } -sub set_is_active { $_[0]->set('isactive', $_[1]); } -sub set_name { $_[0]->set('name', $_[1]); } -sub set_user_regexp { $_[0]->set('userregexp', $_[1]); } -sub set_icon_url { $_[0]->set('icon_url', $_[1]); } - -sub update { - my $self = shift; - my $dbh = Bugzilla->dbh; - $dbh->bz_start_transaction(); - my $changes = $self->SUPER::update(@_); - - if (exists $changes->{name}) { - my ($old_name, $new_name) = @{$changes->{name}}; - my $update_params; - foreach my $group (GROUP_PARAMS) { - if ($old_name eq Bugzilla->params->{$group}) { - SetParam($group, $new_name); - $update_params = 1; - } - } - write_params() if $update_params; +sub set_is_active { $_[0]->set('isactive', $_[1]); } +sub set_name { $_[0]->set('name', $_[1]); } +sub set_user_regexp { $_[0]->set('userregexp', $_[1]); } +sub set_icon_url { $_[0]->set('icon_url', $_[1]); } +## REDHAT EXTENSION BEGIN 751352 +sub set_category { + my ($self, $new_category) = @_; + + my $current_cat = $self->category; + + if ( $current_cat + && $current_cat =~ m/^(Red Hat|Partner|Admin)$/ + && $new_category !~ m/^(Red Hat|Partner|Admin|Deprecated)$/) + { + my $bugs = $self->bugs; + if (scalar(@$bugs)) { + ThrowUserError('cannot_unprotect_active_groups', {group => $self}); } + } - # If we've changed this group to be active, fix any Mandatory groups. - $self->_enforce_mandatory if (exists $changes->{isactive} - && $changes->{isactive}->[1]); - - $self->_rederive_regexp() if exists $changes->{userregexp}; - Bugzilla::Hook::process('group_end_of_update', - { group => $self, changes => $changes }); - $dbh->bz_commit_transaction(); - Bugzilla->memcached->clear_config(); - return $changes; + $self->set('category', $new_category); } +## REDHAT EXTENSION END 751352 -sub check_remove { - my ($self, $params) = @_; - - # System groups cannot be deleted! - if (!$self->is_bug_group) { - ThrowUserError("system_group_not_deletable", { name => $self->name }); - } - - # Groups having a special role cannot be deleted. - my @special_groups; - foreach my $special_group (GROUP_PARAMS) { - if ($self->name eq Bugzilla->params->{$special_group}) { - push(@special_groups, $special_group); - } - } - if (scalar(@special_groups)) { - ThrowUserError('group_has_special_role', - { name => $self->name, - groups => \@special_groups }); +sub update { + my $self = shift; + my $dbh = Bugzilla->dbh; + $dbh->bz_start_transaction(); + my $changes = $self->SUPER::update(@_); + + if (exists $changes->{name}) { + my ($old_name, $new_name) = @{$changes->{name}}; + my $update_params; + foreach my $group (GROUP_PARAMS) { + if ($old_name eq Bugzilla->params->{$group}) { + SetParam($group, $new_name); + $update_params = 1; + } } + write_params() if $update_params; + } - return if $params->{'test_only'}; - - my $cantdelete = 0; + # If we've changed this group to be active, fix any Mandatory groups. + $self->_enforce_mandatory + if (exists $changes->{isactive} && $changes->{isactive}->[1]); - my $users = $self->members_non_inherited; - if (scalar(@$users) && !$params->{'remove_from_users'}) { - $cantdelete = 1; - } + $self->_rederive_regexp() if exists $changes->{userregexp}; - my $bugs = $self->bugs; - if (scalar(@$bugs) && !$params->{'remove_from_bugs'}) { - $cantdelete = 1; - } - - my $products = $self->products; - if (scalar(@$products) && !$params->{'remove_from_products'}) { - $cantdelete = 1; - } + Bugzilla::Hook::process('group_end_of_update', + {group => $self, changes => $changes}); + $dbh->bz_commit_transaction(); + Bugzilla->memcached->clear_config(); + return $changes; +} - my $flag_types = $self->flag_types; - if (scalar(@$flag_types) && !$params->{'remove_from_flags'}) { - $cantdelete = 1; +sub check_remove { + my ($self, $params) = @_; + + # System groups cannot be deleted! + if (!$self->is_bug_group) { + ThrowUserError("system_group_not_deletable", {name => $self->name}); + } + + # Groups having a special role cannot be deleted. + my @special_groups; + foreach my $special_group (GROUP_PARAMS) { + if ($self->name eq Bugzilla->params->{$special_group}) { + push(@special_groups, $special_group); } - - ThrowUserError('group_cannot_delete', { group => $self }) if $cantdelete; + } + if (scalar(@special_groups)) { + ThrowUserError('group_has_special_role', + {name => $self->name, groups => \@special_groups}); + } + + return if $params->{'test_only'}; + + my $cantdelete = 0; + my $refs = ''; + + my $users = $self->members_non_inherited; + if (scalar(@$users) && !$params->{'remove_from_users'}) { + $cantdelete = 1; + $refs .= 'users '; + } + + my $bugs = $self->bugs; + if (scalar(@$bugs) && !$params->{'remove_from_bugs'}) { + $cantdelete = 1; + $refs .= 'bugs '; + } + + my $products = $self->products; + if (scalar(@$products) && !$params->{'remove_from_products'}) { + $cantdelete = 1; + $refs .= 'products '; + } + + my $flag_types = $self->flag_types; + if (scalar(@$flag_types) && !$params->{'remove_from_flags'}) { + $cantdelete = 1; + $refs .= 'flags: ' . join(', ', map { $_->name } @$flag_types) . ' '; + } + + ThrowUserError('group_cannot_delete', {group => $self, refs => $refs}) + if $cantdelete; } sub remove_from_db { - my $self = shift; - my $dbh = Bugzilla->dbh; - $self->check_remove(@_); - $dbh->bz_start_transaction(); - Bugzilla::Hook::process('group_before_delete', { group => $self }); - $dbh->do('DELETE FROM whine_schedules - WHERE mailto_type = ? AND mailto = ?', - undef, MAILTO_GROUP, $self->id); - # All the other tables will be handled by foreign keys when we - # drop the main "groups" row. - $self->SUPER::remove_from_db(@_); - $dbh->bz_commit_transaction(); + my $self = shift; + my $dbh = Bugzilla->dbh; + $self->check_remove(@_); + $dbh->bz_start_transaction(); + Bugzilla::Hook::process('group_before_delete', {group => $self}); + $dbh->do( + 'DELETE FROM whine_schedules + WHERE mailto_type = ? AND mailto = ?', undef, MAILTO_GROUP, $self->id + ); + + # Record removing user from group. + my $id = $self->id; + my $name = $self->name; + require Bugzilla::User; + my $user = Bugzilla->user; + my $uid = $user->id; + my $sql = <do($sql, undef, $uid, $name, $id); + + # All the other tables will be handled by foreign keys when we + # drop the main "groups" row. + $self->SUPER::remove_from_db(@_); + $dbh->bz_commit_transaction(); } # Add missing entries in bug_group_map for bugs created while # a mandatory group was disabled and which is now enabled again. sub _enforce_mandatory { - my ($self) = @_; - my $dbh = Bugzilla->dbh; - my $gid = $self->id; + my ($self) = @_; + my $dbh = Bugzilla->dbh; + my $gid = $self->id; - my $bug_ids = - $dbh->selectcol_arrayref('SELECT bugs.bug_id + my $bug_ids = $dbh->selectcol_arrayref( + 'SELECT bugs.bug_id FROM bugs INNER JOIN group_control_map ON group_control_map.product_id = bugs.product_id @@ -304,156 +356,173 @@ sub _enforce_mandatory { AND bug_group_map.group_id = group_control_map.group_id WHERE group_control_map.group_id = ? AND group_control_map.membercontrol = ? - AND bug_group_map.group_id IS NULL', - undef, ($gid, CONTROLMAPMANDATORY)); - - my $sth = $dbh->prepare('INSERT INTO bug_group_map (bug_id, group_id) VALUES (?, ?)'); - foreach my $bug_id (@$bug_ids) { - $sth->execute($bug_id, $gid); - } + AND bug_group_map.group_id IS NULL', undef, + ($gid, CONTROLMAPMANDATORY) + ); + + my $sth + = $dbh->prepare('INSERT INTO bug_group_map (bug_id, group_id) VALUES (?, ?)'); + foreach my $bug_id (@$bug_ids) { + $sth->execute($bug_id, $gid); + } } sub is_active_bug_group { - my $self = shift; - return $self->is_active && $self->is_bug_group; + my $self = shift; + return $self->is_active && $self->is_bug_group; } sub _rederive_regexp { - my ($self) = @_; + my ($self) = @_; - my $dbh = Bugzilla->dbh; - my $sth = $dbh->prepare("SELECT userid, login_name, group_id + my $dbh = Bugzilla->dbh; + my $sth = $dbh->prepare( + "SELECT userid, login_name, group_id FROM profiles LEFT JOIN user_group_map ON user_group_map.user_id = profiles.userid AND group_id = ? AND grant_type = ? - AND isbless = 0"); - my $sthadd = $dbh->prepare("INSERT INTO user_group_map + AND isbless = 0" + ); + my $sthadd = $dbh->prepare( + "INSERT INTO user_group_map (user_id, group_id, grant_type, isbless) - VALUES (?, ?, ?, 0)"); - my $sthdel = $dbh->prepare("DELETE FROM user_group_map + VALUES (?, ?, ?, 0)" + ); + my $sthdel = $dbh->prepare( + "DELETE FROM user_group_map WHERE user_id = ? AND group_id = ? - AND grant_type = ? and isbless = 0"); - $sth->execute($self->id, GRANT_REGEXP); - my $regexp = $self->user_regexp; - while (my ($uid, $login, $present) = $sth->fetchrow_array) { - if ($regexp ne '' and $login =~ /$regexp/i) { - $sthadd->execute($uid, $self->id, GRANT_REGEXP) unless $present; - } else { - $sthdel->execute($uid, $self->id, GRANT_REGEXP) if $present; - } + AND grant_type = ? and isbless = 0" + ); + $sth->execute($self->id, GRANT_REGEXP); + my $regexp = $self->user_regexp; + + while (my ($uid, $login, $present) = $sth->fetchrow_array) { + if ($regexp ne '' and $login =~ /$regexp/i) { + $sthadd->execute($uid, $self->id, GRANT_REGEXP) unless $present; + } + else { + $sthdel->execute($uid, $self->id, GRANT_REGEXP) if $present; } + } } sub flatten_group_membership { - my ($self, @groups) = @_; - - my $dbh = Bugzilla->dbh; - my $sth; - my @groupidstocheck = @groups; - my %groupidschecked = (); - $sth = $dbh->prepare("SELECT member_id FROM group_group_map + my ($self, @groups) = @_; + + my $dbh = Bugzilla->dbh; + my $sth; + my @groupidstocheck = @groups; + my %groupidschecked = (); + $sth = $dbh->prepare( + "SELECT member_id FROM group_group_map WHERE grantor_id = ? - AND grant_type = " . GROUP_MEMBERSHIP); - while (my $node = shift @groupidstocheck) { - $sth->execute($node); - my $member; - while (($member) = $sth->fetchrow_array) { - if (!$groupidschecked{$member}) { - $groupidschecked{$member} = 1; - push @groupidstocheck, $member; - push @groups, $member unless grep $_ == $member, @groups; - } - } + AND grant_type = " . GROUP_MEMBERSHIP + ); + while (my $node = shift @groupidstocheck) { + $sth->execute($node); + my $member; + while (($member) = $sth->fetchrow_array) { + if (!$groupidschecked{$member}) { + $groupidschecked{$member} = 1; + push @groupidstocheck, $member; + push @groups, $member unless grep $_ == $member, @groups; + } } - return \@groups; + } + return \@groups; } - - ################################ ##### Module Subroutines ### ################################ sub create { - my $class = shift; - my ($params) = @_; - my $dbh = Bugzilla->dbh; - - my $silently = delete $params->{silently}; - my $use_in_all_products = delete $params->{use_in_all_products}; - if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE and !$silently) { - print get_text('install_group_create', { name => $params->{name} }), - "\n"; - } + my $class = shift; + my ($params) = @_; + my $dbh = Bugzilla->dbh; - $dbh->bz_start_transaction(); + my $silently = delete $params->{silently}; + my $use_in_all_products = delete $params->{use_in_all_products}; + if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE and !$silently) { + print get_text('install_group_create', {name => $params->{name}}), "\n"; + } - my $group = $class->SUPER::create(@_); + $dbh->bz_start_transaction(); - # Since we created a new group, give the "admin" group all privileges - # initially. - my $admin = new Bugzilla::Group({name => 'admin'}); - # This function is also used to create the "admin" group itself, - # so there's a chance it won't exist yet. - if ($admin) { - my $sth = $dbh->prepare('INSERT INTO group_group_map - (member_id, grantor_id, grant_type) - VALUES (?, ?, ?)'); - $sth->execute($admin->id, $group->id, GROUP_MEMBERSHIP); - $sth->execute($admin->id, $group->id, GROUP_BLESS); - $sth->execute($admin->id, $group->id, GROUP_VISIBLE); - } + my $group = $class->SUPER::create(@_); - # Permit all existing products to use the new group if requested. - if ($use_in_all_products) { - $dbh->do('INSERT INTO group_control_map + # Since we created a new group, give the "admin" group all privileges + # initially. + my $admin = new Bugzilla::Group({name => 'admin'}); + + # This function is also used to create the "admin" group itself, + # so there's a chance it won't exist yet. + if ($admin) { + my $sth = $dbh->prepare( + 'INSERT INTO group_group_map + (member_id, grantor_id, grant_type) + VALUES (?, ?, ?)' + ); + $sth->execute($admin->id, $group->id, GROUP_MEMBERSHIP) + if ($group->category() && $group->category() eq 'Admin'); + +# $sth->execute($admin->id, $group->id, GROUP_BLESS); + $sth->execute($admin->id, $group->id, GROUP_VISIBLE); + } + + # Permit all existing products to use the new group if requested. + if ($use_in_all_products) { + $dbh->do( + 'INSERT INTO group_control_map (group_id, product_id, membercontrol, othercontrol) - SELECT ?, products.id, ?, ? FROM products', - undef, ($group->id, CONTROLMAPSHOWN, CONTROLMAPNA)); - } + SELECT ?, products.id, ?, ? FROM products', undef, + ($group->id, CONTROLMAPSHOWN, CONTROLMAPNA) + ); + } - $group->_rederive_regexp() if $group->user_regexp; + $group->_rederive_regexp() if $group->user_regexp; - Bugzilla::Hook::process('group_end_of_create', { group => $group }); - $dbh->bz_commit_transaction(); - return $group; + Bugzilla::Hook::process('group_end_of_create', {group => $group}); + $dbh->bz_commit_transaction(); + return $group; } sub ValidateGroupName { - my ($name, @users) = (@_); - my $dbh = Bugzilla->dbh; - my $query = "SELECT id FROM groups " . - "WHERE name = ?"; - if (Bugzilla->params->{'usevisibilitygroups'}) { - my @visible = (-1); - foreach my $user (@users) { - $user && push @visible, @{$user->visible_groups_direct}; - } - my $visible = join(', ', @visible); - $query .= " AND id IN($visible)"; + my ($name, @users) = (@_); + my $dbh = Bugzilla->dbh; + my $query = "SELECT id FROM groups " . "WHERE name = ?"; + if (Bugzilla->params->{'usevisibilitygroups'}) { + my @visible = (-1); + foreach my $user (@users) { + $user && push @visible, @{$user->visible_groups_direct}; } - my $sth = $dbh->prepare($query); - $sth->execute($name); - my ($ret) = $sth->fetchrow_array(); - return $ret; + my $visible = join(', ', @visible); + $query .= " AND id IN($visible)"; + } + my $sth = $dbh->prepare($query); + $sth->execute($name); + my ($ret) = $sth->fetchrow_array(); + return $ret; } sub check_no_disclose { - my ($class, $params) = @_; - my $action = delete $params->{action}; + my ($class, $params) = @_; + my $action = delete $params->{action}; - $action =~ /^(?:add|remove)$/ - or ThrowCodeError('bad_arg', { argument => $action, - function => "${class}::check_no_disclose" }); + $action =~ /^(?:add|remove)$/ + or ThrowCodeError('bad_arg', + {argument => $action, function => "${class}::check_no_disclose"}); - $params->{_error} = ($action eq 'add') ? 'group_restriction_not_allowed' - : 'group_invalid_removal'; + $params->{_error} + = ($action eq 'add') + ? 'group_restriction_not_allowed' + : 'group_invalid_removal'; - my $group = $class->check($params); - return $group; + my $group = $class->check($params); + return $group; } ############################### @@ -461,38 +530,60 @@ sub check_no_disclose { ############################### sub _check_name { - my ($invocant, $name) = @_; - $name = trim($name); - $name || ThrowUserError("empty_group_name"); - # If we're creating a Group or changing the name... - if (!ref($invocant) || lc($invocant->name) ne lc($name)) { - my $exists = new Bugzilla::Group({name => $name }); - ThrowUserError("group_exists", { name => $name }) if $exists; - } - return $name; + my ($invocant, $name) = @_; + $name = trim($name); + $name || ThrowUserError("empty_group_name"); + + # If we're creating a Group or changing the name... + if (!ref($invocant) || lc($invocant->name) ne lc($name)) { + my $exists = new Bugzilla::Group({name => $name}); + ThrowUserError("group_exists", {name => $name}) if $exists; + } + return $name; } sub _check_description { - my ($invocant, $desc) = @_; - $desc = trim($desc); - $desc || ThrowUserError("empty_group_description"); - return $desc; + my ($invocant, $desc) = @_; + $desc = trim($desc); + $desc || ThrowUserError("empty_group_description"); + return $desc; } sub _check_user_regexp { - my ($invocant, $regex) = @_; - $regex = trim($regex) || ''; - ThrowUserError("invalid_regexp") unless (eval {qr/$regex/}); - return $regex; + my ($invocant, $regex) = @_; + $regex = trim($regex) || ''; + ThrowUserError("invalid_regexp") unless (eval {qr/$regex/}); + return $regex; } sub _check_is_active { return $_[1] ? 1 : 0; } + sub _check_is_bug_group { - return $_[1] ? 1 : 0; + return $_[1] ? 1 : 0; } sub _check_icon_url { return $_[1] ? clean_text($_[1]) : undef; } +## REDHAT EXTENSION BEGIN 751352 +sub _check_category { + my ($invocant, $category) = @_; + + # Get a list of valid categories + my @categories = split /\,\s*/, Bugzilla->params->{group_categories}; + + # If there are none, don't check + if (scalar(@categories) == 0) { + return $category; + } + + # Check that the category is valid. If no category is valid, make sure the params + # value ends or begins with a comma + grep($category eq $_, @categories) + || ThrowCodeError("group_category_invalid", {category => $category}); + return $category; +} +## REDHAT EXTENSION END 751352 + 1; __END__ @@ -514,6 +605,7 @@ Bugzilla::Group - Bugzilla group class. my $user_reg_exp = $group->user_reg_exp; my $is_active = $group->is_active; my $icon_url = $group->icon_url; + my $category = $group->category; my $is_active_bug_group = $group->is_active_bug_group; my $group_id = Bugzilla::Group::ValidateGroupName('admin', @users); @@ -710,4 +802,8 @@ of groups returned. =item update +=item set_category + +=item category + =back diff --git a/Bugzilla/Hook.pm b/Bugzilla/Hook.pm index d6ba5e1d0..af58cce79 100644 --- a/Bugzilla/Hook.pm +++ b/Bugzilla/Hook.pm @@ -12,33 +12,33 @@ use strict; use warnings; sub process { - my ($name, $args) = @_; + my ($name, $args) = @_; - _entering($name); + _entering($name); - foreach my $extension (@{ Bugzilla->extensions }) { - if ($extension->can($name)) { - $extension->$name($args); - } + foreach my $extension (@{Bugzilla->extensions}) { + if ($extension->can($name)) { + $extension->$name($args); } + } - _leaving($name); + _leaving($name); } sub in { - my $hook_name = shift; - my $currently_in = Bugzilla->request_cache->{hook_stack}->[-1] || ''; - return $hook_name eq $currently_in ? 1 : 0; + my $hook_name = shift; + my $currently_in = Bugzilla->request_cache->{hook_stack}->[-1] || ''; + return $hook_name eq $currently_in ? 1 : 0; } sub _entering { - my ($hook_name) = @_; - my $hook_stack = Bugzilla->request_cache->{hook_stack} ||= []; - push(@$hook_stack, $hook_name); + my ($hook_name) = @_; + my $hook_stack = Bugzilla->request_cache->{hook_stack} ||= []; + push(@$hook_stack, $hook_name); } sub _leaving { - pop @{ Bugzilla->request_cache->{hook_stack} }; + pop @{Bugzilla->request_cache->{hook_stack}}; } 1; @@ -213,6 +213,19 @@ your column name(s) onto the array. =back +=head2 bug_before_create + +This happens at the beginning of L before other changes +are made to the database. This occurs inside a database transaction. + +Params: + +=over + +=item C - The hash of parameters passed into L. + +=back + =head2 bug_end_of_create This happens at the end of L, after all other changes are @@ -1479,6 +1492,21 @@ look at the code for C in L.) =back +=head2 template_after_create + +This hook allows you to manipulate the Template object before it is used. +You can use this to define new vmethods or filters in extensions. + +Params: + +=over + +=item C